Skip to content
This repository was archived by the owner on Nov 21, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion amivapi/events/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,8 @@

'data_relation': {
'resource': 'events',
'embeddable': True
'embeddable': True,
'cascade_delete': True,
},
'not_patchable': True,
'required': True,
Expand Down
18 changes: 12 additions & 6 deletions amivapi/studydocs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
Contains settings for eve resource, special validation and email_confirmation
logic needed for signup of non members to events.
"""
from amivapi.studydocs.authorization import (
add_uploader_on_bulk_insert,
add_uploader_on_insert
)
from amivapi.studydocs.authorization import add_uploader_on_insert
from amivapi.studydocs.summary import add_summary
from amivapi.studydocs.rating import (
init_rating, update_rating_post, update_rating_patch, update_rating_delete)
from amivapi.studydocs.model import studydocdomain, StudyDocValidator
from amivapi.utils import register_domain, register_validator

Expand All @@ -22,7 +21,14 @@ def init_app(app):
register_domain(app, studydocdomain)
register_validator(app, StudyDocValidator)

app.on_insert_item_studydocuments += add_uploader_on_insert
app.on_insert_studydocuments += add_uploader_on_bulk_insert
# Uploader
app.on_insert_studydocuments += add_uploader_on_insert

# Rating
app.on_insert_studydocuments += init_rating
app.on_inserted_studydocumentratings += update_rating_post
app.on_updated_studydocumentratings += update_rating_patch
app.on_deleted_item_studydocumentratings += update_rating_delete

# Meta summary
app.on_fetched_resource_studydocuments += add_summary
20 changes: 15 additions & 5 deletions amivapi/studydocs/authorization.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,22 @@ def has_resource_write_permission(self, user_id):
return True


def add_uploader_on_insert(item):
"""Add the _author field before inserting studydocs"""
item['uploader'] = g.get('current_user')
class StudydocratingsAuth(AmivTokenAuth):
def has_item_write_permission(self, user_id, item):
"""Allow users to modify only their own ratings."""
# item['user'] is Objectid, convert to str
return user_id == str(get_id(item['user']))

def create_user_lookup_filter(self, user_id):
"""Allow users to only see their own ratings."""
return {'user': user_id}

def has_resource_write_permission(self, user_id):
# All users can rate studydocs
return True


def add_uploader_on_bulk_insert(items):
def add_uploader_on_insert(items):
"""Add the _author field before inserting studydocs"""
for item in items:
add_uploader_on_insert(item)
item['uploader'] = g.get('current_user')
123 changes: 113 additions & 10 deletions amivapi/studydocs/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
# you to buy us beer if we meet and you like the software.
"""Model for studydocuments."""

from amivapi.settings import DEPARTMENT_LIST
from flask import g

from .authorization import StudydocsAuth
from amivapi.settings import DEPARTMENT_LIST
from .authorization import StudydocsAuth, StudydocratingsAuth


description = ("""
Expand All @@ -23,16 +24,18 @@

<br />

## Security
## Ratings

In addition to the usual
[permissions](#section/Authentication-and-Authorization/Authorization),
file uploaders have additional permissions:
Every **User** can rate study documents by either up- or downvoting, using the
[Study Document Ratings](#tag/Study-Document-Rating) endpoint.

- **Users** can modify all items they uploaded themselves (identified by the
user id in the `uploader` field).
Ratings are not simply averages, but take the number of votes into account.
Concretely, the rating is the lower bound of the wilson confidence interval.

[You can read more here][1] if you are interested!

[1]: http://www.evanmiller.org/how-not-to-sort-by-average-rating.html

- **Admins** can modify items for all users.

<br />

Expand All @@ -58,6 +61,26 @@
The summary is only computed for documents matching the current `where` query,
e.g. when searching for ITET documents, only professors related to ITET
documents will show up in the summary.

<br />

## Security

In addition to the usual
[permissions](#section/Authentication-and-Authorization/Authorization),
file uploaders have additional permissions, and rating access is restricted:

- **Users** can modify all items they uploaded themselves (identified by the
user id in the `uploader` field). They can give ratings (only for themselves,
not for other users) and see their own ratings.

- **Admins** can see and modify items and ratings for all users.

""")


description_rating = ("""

""")


Expand All @@ -67,6 +90,20 @@ class StudyDocValidator(object):
def _validate_allow_summary(self, *args, **kwargs):
"""{'type': 'boolean'}"""

def _validate_only_self(self, enabled, field, value):
"""Validate if the id can be used for a rating.

Users can only sign up themselves
Moderators and admins can sign up everyone

The rule's arguments are validated against this schema:
{'type': 'boolean'}
"""
user = g.get('current_user')
if enabled and not g.get('resource_admin') and (str(value) != user):
self._error(field, "You can only rate with your own id."
"(Your id: %s)" % (user))


studydocdomain = {
'studydocuments': {
Expand Down Expand Up @@ -196,7 +233,73 @@ def _validate_allow_summary(self, *args, **kwargs):
'description': 'The year in which the course *was taken*, '
'to separate older from newer files.',
'allow_summary': True,
}
},

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this empty line is inconsequenctial with the rest of the schema.

'rating': {
'title': 'Study Document Rating',
'description': 'The study document rating as a fraction of '
'upvotes divided by all votes. Computed using '
'a confidence interval. Null if no votes have '
'been cast.',
'example': '0.9',

'type': 'float',
'readonly': True,
},
},
},

'studydocumentratings': {
'resource_title': "Study Document Ratings",
'item_title': "Study Document Rating",

'description': "A rating for a [Study Document](#tag/Study-Document).",

'resource_methods': ['GET', 'POST'],
'item_methods': ['GET', 'PATCH', 'DELETE'],

'authentication': StudydocratingsAuth,

'schema': {
'user': {
'description': 'The rating user. You can only use your own id.',
'example': '679ff66720812cdc2da4fb4a',

'type': 'objectid',
'data_relation': {
'resource': 'users',
'embeddable': True,
'cascade_delete': True,
},
'not_patchable': True,
'required': True,
'only_self': True,
'unique_combination': ['studydocument'],
},

'studydocument': {
'title': 'Study Document',
'description': 'The rated study document.',
'example': '10d8e50e303049ecb856ae9b',

'data_relation': {
'resource': 'studydocuments',
'embeddable': True,
'cascade_delete': True,
},
'not_patchable': True,
'required': True,
'type': 'objectid',
},
'rating': {
'description': 'The given rating, can be an up- or downvote.',
'example': 'up',

'type': 'string',
'allowed': ['up', 'down'],
'required': True
},
},
}

}
81 changes: 81 additions & 0 deletions amivapi/studydocs/rating.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
#
# license: AGPLv3, see LICENSE for details. In addition we strongly encourage
# you to buy us beer if we meet and you like the software.
"""Studydoc rating computation.
For the rating, we use the lower bound of a confidence interval around
the 'naive' average vote, which accounts for the number of votes.
The rating for a study document is updated each time a study doc rating is
for the respective document is POSTed or PATCHed. The value is then written
to the database to allow sorting of study documents by rating.
"""

from flask import current_app
from math import sqrt

from amivapi.utils import get_id


def compute_rating(upvotes, downvotes, z=1.28):
"""Compute the rating.
Concretely, the lower bound of the wilson confidence interval is returned,
which takes the number of votes into account. [1]
We use z = 1.28 by default, which corresponds to a 80% confidence interval
(As there are only a few votes, we do not want to be overly cautious).
[1]: http://www.evanmiller.org/how-not-to-sort-by-average-rating.html
"""
total = upvotes + downvotes
if not total:
return

p = upvotes / total
bound = ((p + (z**2)/(2*total) -
z * sqrt((p * (1-p) + (z**2)/(4*total)) / total)) /
(1 + (z**2)/total))

# Ensure that the bound is not below 0
return max(bound, 0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can bound mathematically even be lower than 0?



def _update_rating(studydoc_id):
"""Computes the rating for a study document."""
docs = current_app.data.driver.db['studydocuments']
ratings = current_app.data.driver.db['studydocumentratings']
lookup = {'studydocument': studydoc_id}

# Check votes
upvotes = ratings.count_documents({'rating': 'up', **lookup})
downvotes = ratings.count_documents({'rating': 'down', **lookup})

# Compute rating and write to database
rating = compute_rating(upvotes, downvotes)
docs.update_one({'_id': studydoc_id}, {'$set': {'rating': rating}})


def init_rating(items):
"""On creating of a study-document, set the rating to None."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On creation

for item in items:
item['rating'] = None


def update_rating_post(items):
"""Rating update hook for POST requests."""
for item in items:
_update_rating(get_id(item['studydocument']))


def update_rating_patch(updates, original):
"""Rating update hook for PATCH requests."""
_update_rating(get_id(updates['studydocument'])
if 'studydocument' in updates else
get_id(original['studydocument']))


def update_rating_delete(item):
"""Rating update hook for DELETE requests."""
_update_rating(get_id(item['studydocument']))
Loading