Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
cc6ba73
Update API to return ...{relationship: {data: {type: , id:}...
TomBaxter Sep 13, 2017
8165183
[MOD-29] Reviews Initial Commit
chrisseto Jul 13, 2017
0251b49
[MOD-18] Add reviews to EXTERNAL_EMBER_APPS
chrisseto Jul 13, 2017
8cda742
[MOD-20][MOD-21][Feature] Add reviews mixins (#21)
aaxelb Jul 24, 2017
cb8a053
Log model and read-only endpoints (#24)
aaxelb Aug 1, 2017
5b97814
[MOD-12][MOD-17][MOD-39]Setup ember-osf-reviews app (OSF-side) (#22)
sheriefvt Aug 3, 2017
1d854e4
[MOD-22][MOD-24][MOD-25][MOD-48][Feature] Reviews API (#26)
aaxelb Aug 24, 2017
7fa6611
Expose date_last_transitioned in API (#27)
aaxelb Aug 24, 2017
919295a
[MOD-61] Set reviews settings by PATCHing provider (#28)
aaxelb Aug 25, 2017
ac81090
Nullable provider settings, set all at once (#29)
aaxelb Aug 25, 2017
262367f
Add related counts for reviewables (#30)
chrisseto Sep 11, 2017
75eba5c
[Fix] Hack around GuidQuerySet (#31)
chrisseto Sep 11, 2017
97a9548
[Feature] More API tests and some cleanup (#32)
aaxelb Sep 12, 2017
30c7872
Post-rebase cleanup
aaxelb Sep 13, 2017
3582395
Make travis happy
aaxelb Sep 12, 2017
6382c2e
Remove osf-reviews campaign
aaxelb Sep 13, 2017
b5a85d6
Make travis happier...
aaxelb Sep 13, 2017
b059ee9
Add can_view_reviews to /users/me/ (#1)
chrisseto Sep 14, 2017
c103cb6
[Feature][MOD-73] Rename log to action (#2)
aaxelb Sep 14, 2017
e194199
Cleanup for consistency
aaxelb Sep 14, 2017
9d609db
Add `action` relationship to users serializer
aaxelb Sep 15, 2017
481ed25
Comment out reviews-specific dev docker stuff
aaxelb Sep 15, 2017
38b28fa
Responding to review
aaxelb Sep 15, 2017
bc68494
Remove OSFReviews from navbar (#3)
laurenbarker Sep 15, 2017
20421be
Fix provider detail endpoint
aaxelb Sep 15, 2017
c73320e
Responding to review...
aaxelb Sep 18, 2017
3d6eb79
Keep permissions in permissions classes
aaxelb Sep 18, 2017
7275977
Move scripts to management commands
aaxelb Sep 18, 2017
2e040d0
Revert "Keep permissions in permissions classes"
aaxelb Sep 18, 2017
7c15538
Reenable sorting tests
aaxelb Sep 18, 2017
4d6538f
Permissions in permission classes, mark 2
aaxelb Sep 18, 2017
f41dde4
Remove Reviews from the nav bar
aaxelb Sep 19, 2017
c543c22
[Improvement][MOD-82] Move state counts to top-level meta (#5)
aaxelb Sep 21, 2017
39cfb6c
[MOD-104][Fix] Exclude deleted preprints from moderation list counts …
aaxelb Sep 26, 2017
b981ecc
[MOD-93][Feature] Show all preprints on my preprints page (#7)
laurenbarker Sep 27, 2017
2b47259
[Feature][MOD-53][MOD-100] Add email notifications (#4)
sheriefvt Sep 28, 2017
b25cb89
Remove accidentally added comma (#9)
sheriefvt Oct 2, 2017
3e89674
[MOD-70][Feature] Update DOI mint behavior for moderation submissions…
sheriefvt Oct 3, 2017
5244eaa
[MOD-53][Feature] Add migration for reviews subscriptions (#10)
laurenbarker Oct 3, 2017
5b3558e
Fix bower dependency issue
aaxelb Oct 4, 2017
fbd43d1
Merge branch 'develop' of github.com:centerforopenscience/osf.io into…
binoculars Oct 4, 2017
ab1bc19
pin keen-tracking
binoculars Oct 4, 2017
72157a7
[MOD-121][Improvement] Add html templates for moderation notification…
laurenbarker Oct 6, 2017
33fe2ee
Remove the state machine property from the preprint (#7783)
laurenbarker Oct 6, 2017
9eb7c61
[MOD-84][Improvement] Put all published preprints in `accepted` state…
aaxelb Oct 10, 2017
3a45af9
[Improvement][MOD-132] Update email html templates (#7787)
sheriefvt Oct 11, 2017
71f9c08
[MOD-110][Fix] Allow filtering preprints by the node's is_public (#7793)
aaxelb Oct 12, 2017
3eb6e5b
[Feature][MOD-128] Update email notifications for new submissions (#7…
sheriefvt Oct 12, 2017
0110e8b
Merge branch 'develop' into feature/reviews
aaxelb Oct 12, 2017
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: 3 additions & 0 deletions admin/base/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,11 @@
'webpack_loader',
'django_nose',
'password_reset',
'guardian',

# OSF
'osf',
'reviews',

# Addons
'addons.osfstorage',
Expand All @@ -114,6 +116,7 @@

MIGRATION_MODULES = {
'osf': None,
'reviews': None,
'addons_osfstorage': None,
'addons_wiki': None,
'addons_twofactor': None,
Expand Down
Empty file added api/actions/__init__.py
Empty file.
142 changes: 142 additions & 0 deletions api/actions/serializers.py
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'
8 changes: 8 additions & 0 deletions api/actions/urls.py
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),
]
184 changes: 184 additions & 0 deletions api/actions/views.py
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*

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think we've discussed, but more important than this is documentation at http://github.com/centerforopenscience/developer.osf.io

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I agree, I'll update my WIP PR there next week.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we expect this API to be stable? If nah, it shouldn't go to developer.osf.io quite yet.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good point, though it probably shouldn't go here, either. @aaxelb you'll be getting an invite to an osf project which will be a better place to hold this info until it's stable.


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()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rather than returning an empty queryset, would it make more sense to return 405 Method not allowed? @aaxelb @brianjgeiger

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I think I did that at first (used CreateAPIView instead of ListCreateAPIView), but it hid the browsable API, which felt wrong.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We really aren't relying on the browseable API anymore, and once we are able to link to specific sections of the dev docs, all the BAPI docs will just be links. I'm good with this being CreateAPIView.

20 changes: 19 additions & 1 deletion api/base/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re

import pytz
from guardian.shortcuts import get_objects_for_user
from api.base import utils
from api.base.exceptions import (InvalidFilterComparisonType,
InvalidFilterError, InvalidFilterFieldError,
Expand All @@ -17,7 +18,7 @@
from modularodm.query import queryset as modularodm_queryset
from rest_framework import serializers as ser
from rest_framework.filters import OrderingFilter
from osf.models import Subject
from osf.models import Subject, PreprintProvider
from osf.models.base import GuidMixin


Expand Down Expand Up @@ -494,3 +495,20 @@ def postprocess_query_param(self, key, field_name, operation):
except Subject.DoesNotExist:
operation['source_field_name'] = 'subjects__text'
operation['op'] = 'iexact'

def preprints_queryset(self, base_queryset, auth_user, allow_contribs=True):
default_query = Q(node__isnull=False, node__is_deleted=False)
no_user_query = Q(is_published=True, node__is_public=True)

if auth_user:
admin_user_query = Q(node__contributor__user_id=auth_user.id, node__contributor__admin=True)
reviews_user_query = Q(node__is_public=True, provider__in=get_objects_for_user(auth_user, 'view_submissions', PreprintProvider))
if allow_contribs:
contrib_user_query = Q(is_published=True, node__contributor__user_id=auth_user.id, node__contributor__read=True)
query = default_query & (no_user_query | contrib_user_query | admin_user_query | reviews_user_query)
else:
query = default_query & (no_user_query | admin_user_query | reviews_user_query)
else:
query = default_query & no_user_query

return base_queryset.filter(query)
Loading