Skip to content

[MOD-109][Feature] Reviews, Volume I: Moderation#7807

Merged
sloria merged 64 commits into
developfrom
feature/reviews
Oct 23, 2017
Merged

[MOD-109][Feature] Reviews, Volume I: Moderation#7807
sloria merged 64 commits into
developfrom
feature/reviews

Conversation

@aaxelb

@aaxelb aaxelb commented Oct 12, 2017

Copy link
Copy Markdown
Collaborator

Replaces #7708

https://openscience.atlassian.net/browse/MOD-109

Purpose

All the backend changes required for OSF Reviews. Adds workflows for moderators to control which preprints are publicly available in their branded interface.

Changes

New changes since #7708 was approved

  • Allow including a top-level meta object in API responses
    • override get_renderer_context in the view, something like:
      def get_renderer_context(self):
          context = super(...).get_renderer_context()
          context['meta'] = { ... }
          return context
    • At /preprint_providers/<id>/preprints/, add meta[reviews_state_counts]=true in the query string to get counts of that provider's preprints grouped by reviews_state to top-level meta
  • Make all a user's preprints visible on their "My Projects -> All my preprints" page, regardless whether they've been approved/rejected by a moderator
    • Set _has_abandoned_preprint=False on submission to providers using a reviews workflow
  • Send email notifications when a preprint transitions states
  • Mint DOIs with EZID when a preprint first transitions into a public state (should match existing behavior on unreviewed providers)
  • Create a "preprint initiated" NodeLog when a preprint is submitted for review
  • Keep previously published preprints public when a provider first sets up a reviews workflow
    • Migration to set all preprints for unreviewed providers to "accepted" state
    • When a preprint is published to an unreviewed provider, set "accepted" state
  • Allow filtering preprint lists with filter[node_is_public], so we can exclude preprints with private nodes from the reviews interface
  • Various bugfixes in the new reviews code

Original changes

  • Add Action top-level object
    • Each action represents a transition, a state change on an object
    • e.g. "Abram submitted preprint X for review at time Y, changing its state from initial to pending"
    • Once created, actions are meant to be immutable
    • New API endpoints:
      • /actions/
        • Write-only endpoint for creating actions
        • GET returns an empty list for the sake of the browsable API
      • /actions/<id>/
        • Read-only action detail
      • /preprints/<guid>/actions/
        • Read-only list of actions which target the given preprint
      • /users/me/actions/
        • Read-only list of actions from providers for which the user is a moderator
  • Add ReviewableMixin for objects which can be reviewed
    • Right now just PreprintService
    • Fields:
      • reviews_state: Current state of the preprint
    • Includes methods to trigger transitions and create corresponding Actions
  • Add ReviewProviderMixin for objects which own Reviewables and can choose a workflow for them
    • Right now just PreprintProvider
    • Fields:
      • reviews_workflow: Provider's chosen workflow (see below)
      • reviews_comments_private: Whether comments made by moderators are visible to the submitter/contributors
      • reviews_comments_anonymous: Whether the name/ID of the moderators who accept/reject/act on a preprint are visible to the submitter/contributors
    • Modified API endpoints:
      • /preprint_providers/<id>/ is once-writable
        • Provider admins can set the three reviews fields exactly once to set up reviews
        • If reviews_workflow is non-null, the endpoint is read-only
  • Add workflows that define valid transitions
    • Workflows:
      • pre-moderation: Preprints are publicly visible as soon as they're submitted, but can later be removed by moderators
      • post-moderation: Preprints are visible only after approval by a moderator
    • Valid states:
      • initial: Default state, created but not yet submitted
      • pending: Submitted for and awaiting review, may be publicly visible depending on the workflow
      • accepted: Has explicit moderator approval, publicly visible under the provider
      • rejected: Has explicit moderator disapproval, only visible to contributors and moderators
    • Valid action triggers
      • submit
        • Move from inital to pending, or from rejected to pending if resubmission is allowed by the workflow
        • Node admins and moderators can submit
      • accept
        • Move from pending or rejected to accepted
        • Only moderators can accept
      • reject
        • Move from pending or accepted to rejected
        • Only moderators can reject
      • edit_comment
        • Modify the comment visible to moderators (and possibly contributors) without changing the preprint's state
        • Only moderators can edit_comment
  • Use django guardian for permissions
    • Create admin and moderator groups for each provider, assign group various object permissions on that provider
    • Add users to groups to make them admin and/or moderator

Side Effects

Preprints workflow

If a provider has non-null reviews_workflow, authors can no longer publish their preprints directly (by setting is_published). Instead, they must create a submit action at /actions/. The corresponding changes to ember-preprints are here: CenterForOpenScience/ember-osf-preprints#465

If a provider has null reviews_workflow, the existing behavior is unchanged. Workflows are entirely opt-in per provider, for now.

Ember OSF

ember-osf changes: CenterForOpenScience/ember-osf#272

TomBaxter and others added 30 commits October 3, 2017 17:30
[#OSF-8498]
Return relationship data for those relations of the form...
proto://host:port/v2/<namespace>/<namespace_id>/
* [MOD-20][Feature] Add PreprintService.state field

* [MOD-21][Feature] Add ReviewProviderMixin

* Responding to review

* Use Enum

* Isolate transitions machine

* comments_public -> comments_private, for clarity
* [MOD-12] Setup ember-osf-reviews app
*Update docker setup
*Add osf-reviews campaign
*Add reviews route
*Add osf-reviews logo
*Update registration templates
*Update osf navbar
*Add mako landing page

* fix
* Add reviews fields to preprint/provider endpoints

* Add permissions...

* Cleaner permissions

* Apply permissions to ReviewLog endpoints

* Preprint permissions...

* Use reviews permissions on all preprint endpoints

* Filter preprint providers by reviews permission

* Add provider setup endpoint

* Allow node admins to see logs on their submissions

Assuming the provider settings allow it

* Hide log creators if comments are anonymous

* Add script to generate fake ReviewLogs

* Replace log state fields with action

* Creating review logs to trigger state transitions

* Allow resubmitting

* Fix admin

* Filter permissions correctly

* Responding to review

* Fix reviews migration

* Responding to review

* Fix stupid mistakes

* Cleaner PermissionHelper

* Fix provider log list view

* Add preprint list permissions tests

* Fix migrations after rebase

* Fix a couple things...

* No reviews workflow is None instead of 'none'

* Create review logs at /v2/reviews/review_logs/

* Add read-only title and contributors to preprints

* Hide preprints with private nodes from moderators

* Add date_last_transitioned field to preprints

* Expose live reload port for reviews

* Better permission names

* Tests for creating review logs, transitions
* Add related counts for reviewables

* Fixes

* CR
* More reviews tests

* plac8 flake8

* Filter providers by multiple permissions with OR

* Tests and cleanup...

* More cleanup

* Responding to review
* ReviewLog => Action

* Move Action model to osf.models

* Fix up migrations

* Fix failing tests

* Fix up migrations again

* Make /actions/ useful in the browsable API
aaxelb and others added 2 commits October 17, 2017 11:06
target = validated_data.pop('target')
comment = validated_data.pop('comment', '')
try:
if trigger == Triggers.ACCEPT.value:

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 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.


this.nextLink = link ?
this.nextLink = link ?
link + '&version=2.2' :

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.

It wouldn't be a bad idea to upgrade to 2.6 for this, but I don't think it would hold anything up leaving it at 2.2.

@sloria sloria left a comment

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.

Pass finished.

<div style="margin: 40px;">
<p>Hello ${user.fullname},</p>
% if workflow == 'pre-moderation':
<p>Your submission "${reviewable.node.title}", submitted to ${reviewable.provider.name} has ${'not been accepted. You may edit the '+ reviewable.provider.preprint_word+ ' and resubmit, at which time it will becoming pending moderation.' if is_rejected else 'been accepted by the moderator and is now discoverable to others.'}</p>

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.

Almost always prefer str.format to string concatenation, even in templates.

http://cosdev.readthedocs.io/en/latest/style_guides/python.html#string-formatting

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-

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.

Minor: Can you rename this file to something more descriptive? Custom migrations should not use the autogenerated names. Something like 0065_set_abandoned_false_for_all_submitted_preprints.py

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-

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.

Same here: Can you rename this file to something more descriptive?

]

operations = [
migrations.RunPython(add_reviews_notification_setting),

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.

This is a very slow migration--after 30 minutes, I'm still in the users whose GUIDS start with "a". And since there are schema changes in later migrations, the app would need to be down until this migration finished.

One workaround would be to make this a mgmt command instead of a migration. Just be clear document in the ticket that the command needs to be run when this is deployed.

Comment thread osf/models/notifications.py Outdated
send_type = models.CharField(max_length=50, db_index=True, validators=[validate_subscription_type, ])
event = models.CharField(max_length=50)
message = models.CharField(max_length=2048)
message = models.CharField(max_length=10000)

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.

I think this field should just be changed to a TextField, which doesn't have a db-level limit on character length.

Besides, there's no performance difference between the vachar and text columns in Postgres: https://www.postgresql.org/docs/9.0/static/datatype-character.html

Comment thread reviews/models/mixins.py Outdated
for user_id in context['email_recipients']:
user = OSFUser.load(user_id)
context['is_creator'] = user == context.get('reviewable').node.creator
email = mails.Mail(template, subject='Confirmation of your submission to {provider}'.format(provider=context.get('reviewable').provider.name))

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.

This should be a constant in website.mails.mails.

title = ser.CharField(source='node.title', required=False)
description = ser.CharField(required=False, allow_blank=True, allow_null=True, source='node.description')
tags = JSONAPIListField(child=NodeTagField(), required=False, source='node.tags')
node_is_public = ser.BooleanField(read_only=True, source='node__is_public')

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 actually need this field? If you're just using it for filtering, can you do ?filter[node__is_public]=1 instead?

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.

If there's a way to make that work, that'd be lovely, but I get a 400:

{
    "errors": [
        {
            "source": {
                "parameter": "filter"
            },
            "meta": {},
            "detail": "'node__is_public' is not a valid field for this endpoint."
        }
    ]
}

even if I add node__is_public to filterable_fields

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 was about to be very surprised and pleased and possibly a little concerned if that worked.

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 don't think it would take a lot to update the filtering to handle this; it's probably just the verification that a field is named what you think it is, but it would be a bit to do.

@sloria sloria Oct 18, 2017

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.

Ah, it looks like we check if the passed filter name is one of the serializer fields here:

    def _get_field_or_error(self, field_name):
        """
        Check that the attempted filter field is valid

        :raises InvalidFilterError: If the filter field is not valid
        """
        serializer_class = self.serializer_class
        if field_name not in serializer_class._declared_fields:
            raise InvalidFilterError(detail="'{0}' is not a valid field for this endpoint.".format(field_name))
        if field_name not in getattr(serializer_class, 'filterable_fields', set()):
            raise InvalidFilterFieldError(parameter='filter', value=field_name)
        return serializer_class._declared_fields[field_name]

Does DRF support hidden fields? Maybe if read_only=True and write_only=True?

@sloria sloria Oct 18, 2017

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.

No need for this to block merging this PR, but it's worth a few minutes to see if we can avoid adding the extra field, especially if we intend to ever support nested filtering (node__is_public), which I think we should.

Comment thread reviews/models/mixins.py Outdated
def save_changes(self, ev):
now = self.action.date_created if self.action is not None else timezone.now()
should_publish = self.reviewable.in_public_reviews_state
self.reviewable.node._has_abandoned_preprint = False

@sloria sloria Oct 18, 2017

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.

Minor: to avoid potentially repeating queries, save self.reviewable.node to a variable.

node = self.reviewable.node
node._has_abandoned_preprint = False

....
node.save()


Your ${reviewable.provider.name} and OSF teams

Center for Open Science

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.

You need to put one empty line between 'Center for Open Science' and '210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083' or gmail render will just think they are at the same line.


Center for Open Science
210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083

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.

You need to add a link to the privacy police here. An example will be https://github.com/CenterForOpenScience/osf.io/pull/7613/files#diff-dfa2a5104ecad07e986bd711b1d9464fR10


Your ${reviewable.provider.name} and OSF teams

Center for Open Science

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.

One line space.


Center for Open Science
210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083

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.

A link to privacy policy.


Your ${reviewable.provider.name} and OSF teams

Center for Open Science

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.

One line space.


Center for Open Science
210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083

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.

A link to privacy policy

Your ${reviewable.provider.name} and OSF teams
<p>
Center for Open Science<br>
210 Ridge McIntire Road, Suite 500, Charlottesville, VA 22903-5083

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.

This is fine for now. But after #7613 got merged in. There will be duplicate in html email templates for COS physical address and privacy policy. Create a ticket for the later on clean up. https://openscience.atlassian.net/browse/OSF-8826

@@ -0,0 +1,31 @@
Hello ${user.fullname},

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.

Product team has specified all emails to be in html email template instead of txt. There is a ticket to change all current existing txt email to html, which is https://openscience.atlassian.net/browse/OSF-8770. This is fine for now. But we should stop writing any more txt email template.

laurenbarker and others added 6 commits October 19, 2017 14:54
Rename migrations

Use variable for related node

Use constant in mails.py

Change message field on NotificationDigest from char to text

Add management command for adding subscriptions to users
* Update templates and mixins

* Remove .txt templates and Apply other requested changes

* Apply requested changes
* * Add new re-submission template
* Add new method notify_resubmit
* Update workflow

* Remove zipcode

* Apply requested changes

* Apply requested changes
Comment thread reviews/models/mixins.py

# Handle email notifications including: update comment, accept, and reject of submission.
@reviews_signals.reviews_email.connect
def reviews_notification(self, context):

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.

Note: This will be refactored in the 0.1.0 release to use a mails.notify-like function instead of checking subscriptions and creating NotificationDigests manually.
https://openscience.atlassian.net/browse/MOD-150

Comment thread website/mails/mails.py Outdated

REVIEWS_SUBMISSION_CONFIRMATION = lambda provider_name: Mail(
'reviews_submission_confirmation',
subject='Confirmation of your submission to {}'.format(provider_name)

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.

@laurenbarker Did subject='Confirmation of your submission to ${provider_name}' not work?

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.

Woops, disregard. Didn't see this one done in a future commit.

Comment thread reviews/models/mixins.py Outdated
mails.send_mail(user.username, email, mimetype='html', user=user, **context)
mails.send_mail(
user.username,
getattr(mails, 'REVIEWS_SUBMISSION_CONFIRMATION')(context.get('reviewable').provider.name),

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.

Why is getattr necessary?

@sloria

sloria commented Oct 23, 2017

Copy link
Copy Markdown
Contributor

🚢 🇮🇹 !

@sloria
sloria merged commit b7f857f into develop Oct 23, 2017
@binoculars
binoculars deleted the feature/reviews branch October 30, 2017 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants