-
Notifications
You must be signed in to change notification settings - Fork 359
[MOD-109][Feature] Reviews, Volume I: Moderation #7807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
64 commits
Select commit
Hold shift + click to select a range
cc6ba73
Update API to return ...{relationship: {data: {type: , id:}...
TomBaxter 8165183
[MOD-29] Reviews Initial Commit
chrisseto 0251b49
[MOD-18] Add reviews to EXTERNAL_EMBER_APPS
chrisseto 8cda742
[MOD-20][MOD-21][Feature] Add reviews mixins (#21)
aaxelb cb8a053
Log model and read-only endpoints (#24)
aaxelb 5b97814
[MOD-12][MOD-17][MOD-39]Setup ember-osf-reviews app (OSF-side) (#22)
sheriefvt 1d854e4
[MOD-22][MOD-24][MOD-25][MOD-48][Feature] Reviews API (#26)
aaxelb 7fa6611
Expose date_last_transitioned in API (#27)
aaxelb 919295a
[MOD-61] Set reviews settings by PATCHing provider (#28)
aaxelb ac81090
Nullable provider settings, set all at once (#29)
aaxelb 262367f
Add related counts for reviewables (#30)
chrisseto 75eba5c
[Fix] Hack around GuidQuerySet (#31)
chrisseto 97a9548
[Feature] More API tests and some cleanup (#32)
aaxelb 30c7872
Post-rebase cleanup
aaxelb 3582395
Make travis happy
aaxelb 6382c2e
Remove osf-reviews campaign
aaxelb b5a85d6
Make travis happier...
aaxelb b059ee9
Add can_view_reviews to /users/me/ (#1)
chrisseto c103cb6
[Feature][MOD-73] Rename log to action (#2)
aaxelb e194199
Cleanup for consistency
aaxelb 9d609db
Add `action` relationship to users serializer
aaxelb 481ed25
Comment out reviews-specific dev docker stuff
aaxelb 38b28fa
Responding to review
aaxelb bc68494
Remove OSFReviews from navbar (#3)
laurenbarker 20421be
Fix provider detail endpoint
aaxelb c73320e
Responding to review...
aaxelb 3d6eb79
Keep permissions in permissions classes
aaxelb 7275977
Move scripts to management commands
aaxelb 2e040d0
Revert "Keep permissions in permissions classes"
aaxelb 7c15538
Reenable sorting tests
aaxelb 4d6538f
Permissions in permission classes, mark 2
aaxelb f41dde4
Remove Reviews from the nav bar
aaxelb c543c22
[Improvement][MOD-82] Move state counts to top-level meta (#5)
aaxelb 39cfb6c
[MOD-104][Fix] Exclude deleted preprints from moderation list counts …
aaxelb b981ecc
[MOD-93][Feature] Show all preprints on my preprints page (#7)
laurenbarker 2b47259
[Feature][MOD-53][MOD-100] Add email notifications (#4)
sheriefvt b25cb89
Remove accidentally added comma (#9)
sheriefvt 3e89674
[MOD-70][Feature] Update DOI mint behavior for moderation submissions…
sheriefvt 5244eaa
[MOD-53][Feature] Add migration for reviews subscriptions (#10)
laurenbarker 5b3558e
Fix bower dependency issue
aaxelb fbd43d1
Merge branch 'develop' of github.com:centerforopenscience/osf.io into…
binoculars ab1bc19
pin keen-tracking
binoculars 72157a7
[MOD-121][Improvement] Add html templates for moderation notification…
laurenbarker 33fe2ee
Remove the state machine property from the preprint (#7783)
laurenbarker 9eb7c61
[MOD-84][Improvement] Put all published preprints in `accepted` state…
aaxelb 3a45af9
[Improvement][MOD-132] Update email html templates (#7787)
sheriefvt 71f9c08
[MOD-110][Fix] Allow filtering preprints by the node's is_public (#7793)
aaxelb 3eb6e5b
[Feature][MOD-128] Update email notifications for new submissions (#7…
sheriefvt 0110e8b
Merge branch 'develop' into feature/reviews
aaxelb ea4d153
Add merge migration
aaxelb bb936f5
Send emails in HTML instead of plaintext (#7806)
sheriefvt 69a9d5b
[MOD-138][Improvement] Allow contributors to see unpublished preprint…
laurenbarker b5cbc04
[MOD-153][Fix] Update status change email so notify comment is always…
laurenbarker 9bd39aa
Fix email template
aaxelb 34e0f67
[MOD-147][Improvement] Set abandoned to False on submission (#7816)
laurenbarker 3e17965
Include no_future_emails in submission confirmation email (#7823)
laurenbarker 1e82ff7
Update global_reviews notification display text
laurenbarker 7472a82
Add provider_name to the context
laurenbarker 25e9014
Clean up the migrations
laurenbarker dbfed68
[Improvement][MOD-154] Update wording in emails (#7820)
sheriefvt 0e7089f
Merge pull request #7825 from laurenbarker/improvement/respond-cr
sloria 8323a94
[Improvement][MOD-152] Update re-submisison email notificaiton (#7821)
sheriefvt 63872d1
Don't say user will get email if no_future_emails (#7833)
laurenbarker 1bc9d27
Fix rejected wording (#7834)
laurenbarker File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
|
|
||
| from rest_framework import generics | ||
| from rest_framework import serializers as ser | ||
|
|
||
| from api.base import utils | ||
| from api.base.exceptions import Conflict | ||
| from api.base.exceptions import JSONAPIAttributeException | ||
| from api.base.serializers import JSONAPISerializer | ||
| from api.base.serializers import LinksField | ||
| from api.base.serializers import RelationshipField | ||
| from api.base.serializers import HideIfProviderCommentsAnonymous | ||
| from api.base.serializers import HideIfProviderCommentsPrivate | ||
|
|
||
| from osf.models import PreprintService | ||
|
|
||
| from reviews.exceptions import InvalidTriggerError | ||
| from reviews.workflow import Triggers | ||
| from reviews.workflow import States | ||
|
|
||
|
|
||
| class ReviewableCountsRelationshipField(RelationshipField): | ||
|
|
||
| def __init__(self, *args, **kwargs): | ||
| kwargs['related_meta'] = kwargs.get('related_meta') or {} | ||
| if 'include_state_counts' not in kwargs['related_meta']: | ||
| kwargs['related_meta']['include_state_counts'] = True | ||
| super(ReviewableCountsRelationshipField, self).__init__(*args, **kwargs) | ||
|
|
||
| def get_meta_information(self, metadata, provider): | ||
| # Clone metadata because its mutability is questionable | ||
| metadata = dict(metadata or {}) | ||
|
|
||
| # Make counts opt-in | ||
| show_counts = utils.is_truthy(self.context['request'].query_params.get('related_counts', False)) | ||
| # Only include counts on detail routes | ||
| is_detail = self.context.get('view') and not isinstance(self.context['view'], generics.ListAPIView) | ||
| # Weird hack to avoid being called twice | ||
| # get_meta_information is called with both self.related_meta and self.self_meta. | ||
| # `is` could probably be used here but this seems more comprehensive. | ||
| is_related_meta = metadata.pop('include_state_counts', False) | ||
|
|
||
| if show_counts and is_detail and is_related_meta: | ||
| # Finally, require users to have view_actions permissions | ||
| auth = utils.get_user_auth(self.context['request']) | ||
| if auth and auth.logged_in and auth.user.has_perm('view_actions', provider): | ||
| metadata.update(provider.get_reviewable_state_counts()) | ||
|
|
||
| return super(ReviewableCountsRelationshipField, self).get_meta_information(metadata, provider) | ||
|
|
||
|
|
||
| class TargetRelationshipField(RelationshipField): | ||
| def get_object(self, preprint_id): | ||
| return PreprintService.objects.get(guids___id=preprint_id) | ||
|
|
||
| def to_internal_value(self, data): | ||
| preprint = self.get_object(data) | ||
| return {'target': preprint} | ||
|
|
||
|
|
||
| class ActionSerializer(JSONAPISerializer): | ||
| filterable_fields = frozenset([ | ||
| 'id', | ||
| 'trigger', | ||
| 'from_state', | ||
| 'to_state', | ||
| 'date_created', | ||
| 'date_modified', | ||
| 'provider', | ||
| 'target', | ||
| ]) | ||
|
|
||
| id = ser.CharField(source='_id', read_only=True) | ||
|
|
||
| trigger = ser.ChoiceField(choices=Triggers.choices()) | ||
|
|
||
| comment = HideIfProviderCommentsPrivate(ser.CharField(max_length=65535, required=False)) | ||
|
|
||
| from_state = ser.ChoiceField(choices=States.choices(), read_only=True) | ||
| to_state = ser.ChoiceField(choices=States.choices(), read_only=True) | ||
|
|
||
| date_created = ser.DateTimeField(read_only=True) | ||
| date_modified = ser.DateTimeField(read_only=True) | ||
|
|
||
| provider = RelationshipField( | ||
| read_only=True, | ||
| related_view='preprint_providers:preprint_provider-detail', | ||
| related_view_kwargs={'provider_id': '<target.provider._id>'}, | ||
| filter_key='target__provider___id', | ||
| ) | ||
|
|
||
| target = TargetRelationshipField( | ||
| read_only=False, | ||
| required=True, | ||
| related_view='preprints:preprint-detail', | ||
| related_view_kwargs={'preprint_id': '<target._id>'}, | ||
| filter_key='target__guids___id', | ||
| ) | ||
|
|
||
| creator = HideIfProviderCommentsAnonymous(RelationshipField( | ||
| read_only=True, | ||
| related_view='users:user-detail', | ||
| related_view_kwargs={'user_id': '<creator._id>'}, | ||
| filter_key='creator__guids___id', | ||
| always_embed=True, | ||
| )) | ||
|
|
||
| links = LinksField( | ||
| { | ||
| 'self': 'get_action_url', | ||
| } | ||
| ) | ||
|
|
||
| def get_absolute_url(self, obj): | ||
| return self.get_action_url(obj) | ||
|
|
||
| def get_action_url(self, obj): | ||
| return utils.absolute_reverse('actions:action-detail', kwargs={'action_id': obj._id, 'version': self.context['request'].parser_context['kwargs']['version']}) | ||
|
|
||
| def create(self, validated_data): | ||
| trigger = validated_data.pop('trigger') | ||
| user = validated_data.pop('user') | ||
| target = validated_data.pop('target') | ||
| comment = validated_data.pop('comment', '') | ||
| try: | ||
| if trigger == Triggers.ACCEPT.value: | ||
| return target.reviews_accept(user, comment) | ||
| if trigger == Triggers.REJECT.value: | ||
| return target.reviews_reject(user, comment) | ||
| if trigger == Triggers.EDIT_COMMENT.value: | ||
| return target.reviews_edit_comment(user, comment) | ||
| if trigger == Triggers.SUBMIT.value: | ||
| return target.reviews_submit(user) | ||
| except InvalidTriggerError as e: | ||
| # Invalid transition from the current state | ||
| raise Conflict(e.message) | ||
| else: | ||
| raise JSONAPIAttributeException(attribute='trigger', detail='Invalid trigger.') | ||
|
|
||
| class Meta: | ||
| type_ = 'actions' | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| from django.conf.urls import url | ||
|
|
||
| from . import views | ||
|
|
||
| urlpatterns = [ | ||
| url(r'^$', views.CreateAction.as_view(), name=views.CreateAction.view_name), | ||
| url(r'^(?P<action_id>\w+)/$', views.ActionDetail.as_view(), name=views.ActionDetail.view_name), | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
|
|
||
| from django.shortcuts import get_object_or_404 | ||
| from rest_framework import generics | ||
| from rest_framework import permissions | ||
|
|
||
| from framework.auth.oauth_scopes import CoreScopes | ||
| from osf.models import Action | ||
| from reviews import permissions as reviews_permissions | ||
|
|
||
| from api.actions.serializers import ActionSerializer | ||
| from api.base.exceptions import Conflict | ||
| from api.base.parsers import ( | ||
| JSONAPIMultipleRelationshipsParser, | ||
| JSONAPIMultipleRelationshipsParserForRegularJSON, | ||
| ) | ||
| from api.base.utils import absolute_reverse | ||
| from api.base.views import JSONAPIBaseView | ||
| from api.base import permissions as base_permissions | ||
|
|
||
|
|
||
| def get_actions_queryset(): | ||
| return Action.objects.include( | ||
| 'creator', | ||
| 'creator__guids', | ||
| 'target', | ||
| 'target__guids', | ||
| 'target__provider', | ||
| ).filter(is_deleted=False) | ||
|
|
||
|
|
||
| class ActionDetail(JSONAPIBaseView, generics.RetrieveAPIView): | ||
| """Action Detail | ||
|
|
||
| Actions represent state changes and/or comments on a reviewable object (e.g. a preprint) | ||
|
|
||
| ##Action Attributes | ||
|
|
||
| name type description | ||
| ==================================================================================== | ||
| date_created iso8601 timestamp timestamp that the action was created | ||
| date_modified iso8601 timestamp timestamp that the action was last modified | ||
| from_state string state of the reviewable before this action was created | ||
| to_state string state of the reviewable after this action was created | ||
| comment string comment explaining the state change | ||
| trigger string name of the trigger for this action | ||
|
|
||
| ##Relationships | ||
|
|
||
| ###Target | ||
| Link to the object (e.g. preprint) this action acts on | ||
|
|
||
| ###Provider | ||
| Link to detail for the target object's provider | ||
|
|
||
| ###Creator | ||
| Link to the user that created this action | ||
|
|
||
| ##Links | ||
| - `self` -- Detail page for the current action | ||
| """ | ||
| permission_classes = ( | ||
| permissions.IsAuthenticatedOrReadOnly, | ||
| base_permissions.TokenHasScope, | ||
| reviews_permissions.ActionPermission, | ||
| ) | ||
|
|
||
| required_read_scopes = [CoreScopes.ACTIONS_READ] | ||
| required_write_scopes = [CoreScopes.ACTIONS_WRITE] | ||
|
|
||
| serializer_class = ActionSerializer | ||
| view_category = 'actions' | ||
| view_name = 'action-detail' | ||
|
|
||
| def get_object(self): | ||
| action = get_object_or_404(get_actions_queryset(), _id=self.kwargs['action_id']) | ||
| self.check_object_permissions(self.request, action) | ||
| return action | ||
|
|
||
|
|
||
| class CreateAction(JSONAPIBaseView, generics.ListCreateAPIView): | ||
| """Create Actions *Write-only* | ||
|
|
||
| Use this endpoint to create a new Action and thereby trigger a state change on a preprint. | ||
|
|
||
| GETting from this endpoint will always return an empty list. | ||
| Use `/user/me/actions/` or `/preprints/<guid>/actions/` to read lists of actions. | ||
|
|
||
| ##Action Attributes | ||
|
|
||
| name type description | ||
| ==================================================================================== | ||
| date_created iso8601 timestamp timestamp that the action was created | ||
| date_modified iso8601 timestamp timestamp that the action was last modified | ||
| from_state string state of the reviewable before this action was created | ||
| to_state string state of the reviewable after this action was created | ||
| comment string comment explaining the state change | ||
| trigger string name of the trigger for this action | ||
|
|
||
| ##Relationships | ||
|
|
||
| ###Target | ||
| Link to the object (e.g. preprint) this action acts on | ||
|
|
||
| ###Provider | ||
| Link to detail for the target object's provider | ||
|
|
||
| ###Creator | ||
| Link to the user that created this action | ||
|
|
||
| ##Links | ||
| - `self` -- Detail page for the current action | ||
|
|
||
| ##Query Params | ||
|
|
||
| + `page=<Int>` -- page number of results to view, default 1 | ||
|
|
||
| + `filter[<fieldname>]=<Str>` -- fields and values to filter the search results on. | ||
|
|
||
| Actions may be filtered by their `id`, `from_state`, `to_state`, `date_created`, `date_modified`, `creator`, `provider`, `target` | ||
|
|
||
| ###Creating New Actions | ||
|
|
||
| Create a new Action by POSTing to `/actions/`, including the target preprint and the action trigger. | ||
|
|
||
| Valid triggers are: `submit`, `accept`, `reject`, and `edit_comment` | ||
|
|
||
| Method: POST | ||
| URL: /actions/ | ||
| Query Params: <none> | ||
| Body (JSON): { | ||
| "data": { | ||
| "attributes": { | ||
| "trigger": {trigger}, # required | ||
| "comment": {comment}, | ||
| }, | ||
| "relationships": { | ||
| "target": { # required | ||
| "data": { | ||
| "type": "preprints", | ||
| "id": {preprint_id} | ||
| } | ||
| }, | ||
| } | ||
| } | ||
| } | ||
| Success: 201 CREATED + action representation | ||
| """ | ||
| permission_classes = ( | ||
| permissions.IsAuthenticatedOrReadOnly, | ||
| base_permissions.TokenHasScope, | ||
| reviews_permissions.ActionPermission, | ||
| ) | ||
|
|
||
| required_read_scopes = [CoreScopes.NULL] | ||
| required_write_scopes = [CoreScopes.ACTIONS_WRITE] | ||
|
|
||
| parser_classes = (JSONAPIMultipleRelationshipsParser, JSONAPIMultipleRelationshipsParserForRegularJSON,) | ||
|
|
||
| serializer_class = ActionSerializer | ||
|
|
||
| view_category = 'actions' | ||
| view_name = 'create-action' | ||
|
|
||
| # overrides ListCreateAPIView | ||
| def perform_create(self, serializer): | ||
| target = serializer.validated_data['target'] | ||
| self.check_object_permissions(self.request, target) | ||
|
|
||
| if not target.provider.is_reviewed: | ||
| raise Conflict('{} is an unmoderated provider. If you are an admin, set up moderation by setting `reviews_workflow` at {}'.format( | ||
| target.provider.name, | ||
| absolute_reverse('preprint_providers:preprint_provider-detail', kwargs={ | ||
| 'provider_id': target.provider._id, | ||
| 'version': self.request.parser_context['kwargs']['version'] | ||
| }) | ||
| )) | ||
|
|
||
| serializer.save(user=self.request.user) | ||
|
|
||
| # overrides ListCreateAPIView | ||
| def get_queryset(self): | ||
| return Action.objects.none() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like this will be simplified one day, as it seems like it's having to maintain a redundant list. But not a big deal right now.