Skip to content

Commit de47daf

Browse files
Soorya Narayanhvr
Soorya Narayan
authored andcommitted
Extend voting interface to allow score
This extends the package voting mechanism from boolean "stars" to integral "lambdas", and using the statistical method outlined in http://www.evanmiller.org/how-not-to-sort-by-average-rating.html to compute an overall score. This has been factored out of the HSoC work haskell#514 by @sooryan The code has been refactored & cleaned up by Duncan, Gershom and myself. NOTE: Detecting non-anonymous users to display the user's current vote doesn't work yet. This will be fixed at some later point.
1 parent dd7a55f commit de47daf

File tree

8 files changed

+327
-38
lines changed

8 files changed

+327
-38
lines changed

Distribution/Server/Features.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ initHackageFeatures env@ServerEnv{serverVerbosity = verbosity} = do
257257
coreFeature
258258
-- [reverse index disabled] reverseFeature
259259
downloadFeature
260+
votesFeature
260261
tagsFeature
261262
versionsFeature
262263

Distribution/Server/Features/Html.hs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ mkHtmlCore :: ServerEnv
463463
-> HtmlCore
464464
mkHtmlCore ServerEnv{serverBaseURI}
465465
utilities@HtmlUtilities{..}
466-
UserFeature{queryGetUserDb}
466+
UserFeature{queryGetUserDb, checkAuthenticated}
467467
CoreFeature{coreResource}
468468
VersionsFeature{ versionsResource
469469
, queryGetDeprecatedFor
@@ -544,7 +544,10 @@ mkHtmlCore ServerEnv{serverBaseURI}
544544
distributions <- queryPackageStatus pkgname
545545
totalDown <- cmFind pkgname `liftM` totalPackageDownloads
546546
recentDown <- cmFind pkgname `liftM` recentPackageDownloads
547-
pkgVotesHtml <- renderVotesHtml pkgname
547+
pkgVotes <- pkgNumVotes pkgname
548+
pkgScore <- fmap (/2) $ pkgNumScore pkgname
549+
auth <- checkAuthenticated
550+
userRating <- case auth of Just (uid,_) -> pkgUserVote pkgname uid; _ -> return Nothing
548551
mdoctarblob <- queryDocumentation realpkg
549552
tags <- queryTagsForPackage pkgname
550553
deprs <- queryGetDeprecatedFor pkgname
@@ -571,7 +574,9 @@ mkHtmlCore ServerEnv{serverBaseURI}
571574
(classifyVersions prefInfo $ map packageVersion pkgs) infoUrl)
572575
, "totalDownloads" $= totalDown
573576
, "recentDownloads" $= recentDown
574-
, "votesSection" $= pkgVotesHtml
577+
, "votes" $= pkgVotes
578+
, "userRating" $= userRating
579+
, "score" $= pkgScore
575580
, "buildStatus" $= buildStatus
576581
] ++
577582
-- Items not related to IO (mostly pure functions)

Distribution/Server/Features/PackageList.hs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Distribution.Server.Framework
1010

1111
import Distribution.Server.Features.Core
1212
-- [reverse index disabled] import Distribution.Server.Features.ReverseDependencies
13+
import Distribution.Server.Features.Votes
1314
import Distribution.Server.Features.DownloadCount
1415
import Distribution.Server.Features.Tags
1516
import Distribution.Server.Features.PreferredVersions
@@ -89,6 +90,7 @@ initListFeature :: ServerEnv
8990
-> IO (CoreFeature
9091
-- [reverse index disabled] -> ReverseFeature
9192
-> DownloadFeature
93+
-> VotesFeature
9294
-> TagsFeature
9395
-> VersionsFeature
9496
-> IO ListFeature)
@@ -99,17 +101,19 @@ initListFeature _env = do
99101
return $ \core@CoreFeature{..}
100102
-- [reverse index disabled] revs
101103
download
104+
votesf@VotesFeature{..}
102105
tagsf@TagsFeature{..}
103106
versions@VersionsFeature{..} -> do
104107

105108
let (feature, modifyItem, updateDesc) =
106-
listFeature core download tagsf versions
109+
listFeature core download votesf tagsf versions
107110
itemCache itemUpdate
108111

109112
registerHookJust packageChangeHook isPackageChangeAny $ \(pkgid, _) ->
110113
updateDesc (packageName pkgid)
111114

112115
{- [reverse index disabled]
116+
votesf@VotesFeature{..}
113117
registerHook (reverseUpdateHook revs) $ \mrev -> do
114118
let pkgs = Map.keys mrev
115119
forM_ pkgs $ \pkgname -> do
@@ -131,6 +135,7 @@ initListFeature _env = do
131135

132136
listFeature :: CoreFeature
133137
-> DownloadFeature
138+
-> VotesFeature
134139
-> TagsFeature
135140
-> VersionsFeature
136141
-> MemState (Map PackageName PackageItem)
@@ -140,7 +145,10 @@ listFeature :: CoreFeature
140145
PackageName -> IO ())
141146

142147
listFeature CoreFeature{..}
143-
DownloadFeature{..} TagsFeature{..} VersionsFeature{..}
148+
DownloadFeature{..}
149+
VotesFeature{..}
150+
TagsFeature{..}
151+
VersionsFeature{..}
144152
itemCache itemUpdate
145153
= (ListFeature{..}, modifyItem, updateDesc)
146154
where

Distribution/Server/Features/Users.hs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,11 @@ data UserFeature = UserFeature {
6161
guardAuthorised_ :: [PrivilegeCondition] -> ServerPartE (),
6262
-- | Require any of a set of privileges, giving the id of the current user.
6363
guardAuthorised :: [PrivilegeCondition] -> ServerPartE UserId,
64+
guardAuthorised' :: [PrivilegeCondition] -> ServerPartE Bool,
6465
-- | Require being logged in, giving the id of the current user.
6566
guardAuthenticated :: ServerPartE UserId,
67+
-- | Gets the authentication if it exists.
68+
checkAuthenticated :: ServerPartE (Maybe (UserId, UserInfo)),
6669
-- | A hook to override the default authentication error in particular
6770
-- circumstances.
6871
authFailHook :: Hook Auth.AuthError (Maybe ErrorResponse),
@@ -389,6 +392,13 @@ userFeature templates usersState adminsState
389392
Auth.guardPriviledged users uid privconds
390393
return uid
391394

395+
guardAuthorised' :: [PrivilegeCondition] -> ServerPartE Bool
396+
guardAuthorised' privconds = do
397+
users <- queryGetUserDb
398+
uid <- guardAuthenticatedWithErrHook users
399+
valid <- Auth.checkPriviledged users uid privconds
400+
return valid
401+
392402
-- Simply check if the user is authenticated as some user, without any
393403
-- check that they have any particular priveledges. Only useful as a
394404
-- building block.
@@ -412,6 +422,11 @@ userFeature templates usersState adminsState
412422
overrideResponse <- msum <$> runHook authFailHook err
413423
throwError (fromMaybe defaultResponse overrideResponse)
414424

425+
-- Check if there is an authenticated userid, and return info, if so.
426+
checkAuthenticated :: ServerPartE (Maybe (UserId, UserInfo))
427+
checkAuthenticated = do
428+
users <- queryGetUserDb
429+
either (const Nothing) Just `fmap` Auth.checkAuthenticated Auth.hackageRealm users
415430

416431
-- | Resources representing the collection of known users.
417432
--

Distribution/Server/Features/Votes.hs

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import Distribution.Server.Framework.BackupRestore
1616
import Distribution.Server.Features.Core
1717
import Distribution.Server.Features.Users
1818
import Distribution.Server.Users.Types (UserId(..))
19-
import Distribution.Server.Users.UserIdSet as UserIdSet
2019

2120
import Distribution.Package
2221
import Distribution.Text
@@ -26,6 +25,7 @@ import qualified Data.Map as Map
2625
import qualified Data.Text as T
2726
import qualified Data.HashMap.Strict as HashMap
2827

28+
import Control.Monad (when)
2929
import Control.Arrow (first)
3030
import qualified Text.XHtml.Strict as X
3131

@@ -35,6 +35,9 @@ data VotesFeature = VotesFeature {
3535
votesFeatureInterface :: HackageFeature
3636
, didUserVote :: forall m. MonadIO m => PackageName -> UserId -> m Bool
3737
, pkgNumVotes :: forall m. MonadIO m => PackageName -> m Int
38+
, pkgNumScore :: forall m. MonadIO m => PackageName -> m Float
39+
, pkgUserVote :: forall m. MonadIO m => PackageName -> UserId -> m (Maybe Score)
40+
, votesUpdated :: Hook (PackageName, Float) ()
3841
, renderVotesHtml :: PackageName -> ServerPartE X.Html
3942
}
4043

@@ -49,11 +52,13 @@ initVotesFeature :: ServerEnv
4952
-> IO VotesFeature)
5053
initVotesFeature env@ServerEnv{serverStateDir} = do
5154
dbVotesState <- votesStateComponent serverStateDir
55+
updateVotes <- newHook
5256

5357
return $ \coref@CoreFeature{..} userf@UserFeature{..} -> do
5458
let feature = votesFeature env
5559
dbVotesState
56-
coref userf
60+
coref userf updateVotes
61+
5762
return feature
5863

5964
-- | Define the backing store (i.e. database component)
@@ -79,12 +84,14 @@ votesFeature :: ServerEnv
7984
-> StateComponent AcidState VotesState
8085
-> CoreFeature -- To get site package list
8186
-> UserFeature -- To authenticate users
87+
-> Hook (PackageName, Float) ()
8288
-> VotesFeature
8389

8490
votesFeature ServerEnv{..}
8591
votesState
8692
CoreFeature { coreResource = CoreResource{..} }
8793
UserFeature{..}
94+
votesUpdated
8895
= VotesFeature{..}
8996
where
9097
votesFeatureInterface = (emptyHackageFeature "votes") {
@@ -111,7 +118,7 @@ votesFeature ServerEnv{..}
111118
, (DELETE, "Remove a user's vote from this package")
112119
]
113120
, resourceGet = [("json", servePackageNumVotesGet)]
114-
, resourcePut = [("", servePackageVotePut)]
121+
, resourcePost = [("", servePackageVotePut)]
115122
, resourceDelete = [("", servePackageVoteDelete)]
116123
}
117124

@@ -123,8 +130,8 @@ votesFeature ServerEnv{..}
123130
cacheControlWithoutETag [Public, maxAgeMinutes 10]
124131
votesMap <- queryState votesState GetAllPackageVoteSets
125132
ok . toResponse $ objectL
126-
[ (display pkgname, toJSON (UserIdSet.size voterset))
127-
| (pkgname, voterset) <- Map.toList votesMap ]
133+
[ (display pkgname, toJSON (votesScore pkgMap))
134+
| (pkgname, pkgMap) <- Map.toList votesMap ]
128135

129136
-- Get the number of votes a package has. If the package
130137
-- has never been voted for, returns 0.
@@ -133,7 +140,7 @@ votesFeature ServerEnv{..}
133140
pkgname <- packageInPath dpath
134141
guardValidPackageName pkgname
135142
cacheControlWithoutETag [Public, maxAgeMinutes 10]
136-
voteCount <- queryState votesState (GetPackageVoteCount pkgname)
143+
voteCount <- pkgNumVotes pkgname
137144
let obj = objectL
138145
[ ("packageName", string $ display pkgname)
139146
, ("numVotes", toJSON voteCount)
@@ -146,12 +153,17 @@ votesFeature ServerEnv{..}
146153
uid <- guardAuthorised [AnyKnownUser]
147154
pkgname <- packageInPath dpath
148155
guardValidPackageName pkgname
149-
150-
success <- updateState votesState (AddVote pkgname uid)
151-
if success
152-
then ok . toResponse $ Render.voteConfirmationPage pkgname
153-
"Package voted for successfully"
154-
else ok . toResponse $ Render.alreadyVotedPage pkgname
156+
scoreStr <- look "score"
157+
-- very simple input validation; we accept only three literals
158+
score <- case scoreStr of
159+
"1" -> pure 1
160+
"2" -> pure 2
161+
"3" -> pure 3
162+
_ -> fail "invalid score value received"
163+
_ <- updateState votesState (AddVote pkgname uid score)
164+
pkgScore <- pkgNumScore pkgname
165+
runHook_ votesUpdated (pkgname, pkgScore)
166+
ok . toResponse $ "Package voted for successfully"
155167

156168
-- Removes a user's vote from a package. If the user has not voted
157169
-- for this package, does nothing.
@@ -162,11 +174,12 @@ votesFeature ServerEnv{..}
162174
guardValidPackageName pkgname
163175

164176
success <- updateState votesState (RemoveVote pkgname uid)
177+
pkgScore <- pkgNumScore pkgname
178+
when success $ runHook_ votesUpdated (pkgname, pkgScore)
165179

166180
let responseMsg | success = "Package vote removed successfully."
167181
| otherwise = "User has not voted for this package."
168-
ok . toResponse $ Render.voteConfirmationPage
169-
pkgname responseMsg
182+
ok . toResponse $ responseMsg
170183

171184
-- Helper Functions (Used outside of responses, e.g. by other features.)
172185

@@ -181,6 +194,14 @@ votesFeature ServerEnv{..}
181194
pkgNumVotes pkgname =
182195
queryState votesState (GetPackageVoteCount pkgname)
183196

197+
pkgNumScore :: MonadIO m => PackageName -> m Float
198+
pkgNumScore pkgname =
199+
queryState votesState (GetPackageVoteScore pkgname)
200+
201+
pkgUserVote :: MonadIO m => PackageName -> UserId -> m (Maybe Score)
202+
pkgUserVote pkgname uid =
203+
queryState votesState (GetPackageUserVote pkgname uid)
204+
184205
-- Renders the HTML for the "Votes:" section on package pages.
185206
renderVotesHtml :: PackageName -> ServerPartE X.Html
186207
renderVotesHtml pkgname = do

Distribution/Server/Features/Votes/State.hs

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,47 +15,70 @@ import Distribution.Server.Users.State ()
1515
import Data.Typeable
1616
import Data.Map (Map)
1717
import qualified Data.Map as Map
18+
import Data.List
1819
import Data.Maybe (fromMaybe)
19-
20+
import Control.Arrow ((&&&))
2021
import Data.Acid (Query, Update, makeAcidic)
21-
import Data.SafeCopy (base, deriveSafeCopy)
22+
import Data.SafeCopy (base, extension, deriveSafeCopy, Migrate(..))
2223

2324
import qualified Control.Monad.State as State
2425
import Control.Monad.Reader.Class (ask)
2526

27+
type Score = Int
28+
29+
newtype VotesState_v0 = VotesState_v0 { votesMap :: Map PackageName UserIdSet }
2630

27-
newtype VotesState = VotesState { votesMap :: Map PackageName UserIdSet }
31+
newtype VotesState = VotesState (Map PackageName (Map UserId Score))
2832
deriving (Show, Eq, Typeable, MemSize)
2933

30-
$(deriveSafeCopy 0 'base ''VotesState)
34+
-- SafeCopy instances
35+
deriveSafeCopy 1 'extension ''VotesState
36+
37+
deriveSafeCopy 0 'base ''VotesState_v0
38+
39+
instance Migrate VotesState where
40+
type MigrateFrom VotesState = VotesState_v0
41+
42+
migrate (VotesState_v0 m) = VotesState (Map.map go m)
43+
where
44+
go :: UserIdSet -> Map UserId Score
45+
go = Map.fromList . map (\x->(x,3)) . UserIdSet.toList
46+
47+
--
3148

3249
initialVotesState :: VotesState
3350
initialVotesState = VotesState Map.empty
3451

3552
-- helper function
36-
userVotedForPackage :: PackageName -> UserId -> Map PackageName UserIdSet -> Bool
53+
userVotedForPackage :: PackageName -> UserId -> Map PackageName (Map UserId Score) -> Bool
3754
userVotedForPackage pkgname uid votes =
3855
case Map.lookup pkgname votes of
3956
Nothing -> False
40-
Just uidset -> UserIdSet.member uid uidset
57+
Just m -> case Map.lookup uid m of
58+
Nothing -> False
59+
Just _ -> True
60+
61+
-- Using La Placian rule of succession to calculate scoring
62+
votesScore :: Map UserId Score -> Float
63+
votesScore m =
64+
let grouping = map (head &&& length) . group . sort . Map.elems $ m
65+
score = fromIntegral (sum $ map (uncurry (*)) grouping) / fromIntegral (4 + foldr (\(_,b) -> (+b) ) 0 grouping)
66+
in score*10
4167

4268
-- All the acid state transactions
4369

44-
addVote :: PackageName -> UserId -> Update VotesState Bool
45-
addVote pkgname uid = do
70+
addVote :: PackageName -> UserId -> Score -> Update VotesState Float
71+
addVote pkgname uid score = do
4672
VotesState votes <- State.get
47-
if userVotedForPackage pkgname uid votes
48-
then return False
49-
else do let votes' = Map.alter insert pkgname votes
50-
insert = Just . UserIdSet.insert uid . fromMaybe UserIdSet.empty
51-
State.put $! VotesState votes'
52-
return True
73+
let votes' = Map.insertWith Map.union pkgname (Map.singleton uid score) votes
74+
State.put $! VotesState votes'
75+
return $ votesScore $ fromMaybe Map.empty $ Map.lookup pkgname votes'
5376

5477
removeVote :: PackageName -> UserId -> Update VotesState Bool
5578
removeVote pkgname uid = do
5679
VotesState votes <- State.get
5780
if userVotedForPackage pkgname uid votes
58-
then do let votes' = Map.adjust (UserIdSet.delete uid) pkgname votes
81+
then do let votes' = Map.adjust (Map.delete uid) pkgname votes
5982
State.put $! VotesState votes'
6083
return True
6184
else return False
@@ -65,14 +88,26 @@ getPackageVoteCount pkgname = do
6588
VotesState votes <- ask
6689
case Map.lookup pkgname votes of
6790
Nothing -> return 0
68-
Just uidset -> return $! UserIdSet.size uidset
91+
Just m -> return $! Map.size m
92+
93+
getPackageVoteScore :: PackageName -> Query VotesState Float
94+
getPackageVoteScore pkgname = do
95+
VotesState votes <- ask
96+
case Map.lookup pkgname votes of
97+
Nothing -> return 0
98+
Just m -> return $! votesScore m
6999

70100
getPackageUserVoted :: PackageName -> UserId -> Query VotesState Bool
71101
getPackageUserVoted pkgname uid = do
72102
VotesState votes <- ask
73103
return $! userVotedForPackage pkgname uid votes
74104

75-
getAllPackageVoteSets :: Query VotesState (Map PackageName UserIdSet)
105+
getPackageUserVote :: PackageName -> UserId -> Query VotesState (Maybe Score)
106+
getPackageUserVote pkgname uid = do
107+
VotesState votes <- ask
108+
return $! Map.lookup uid =<< Map.lookup pkgname votes
109+
110+
getAllPackageVoteSets :: Query VotesState (Map PackageName (Map UserId Score))
76111
getAllPackageVoteSets = do
77112
VotesState votes <- ask
78113
return votes
@@ -91,7 +126,9 @@ makeAcidic
91126
[ 'addVote
92127
, 'removeVote
93128
, 'getPackageVoteCount
129+
, 'getPackageVoteScore
94130
, 'getPackageUserVoted
131+
, 'getPackageUserVote
95132
, 'getAllPackageVoteSets
96133
, 'getVotesState
97134
, 'replaceVotesState

0 commit comments

Comments
 (0)