diff --git a/production/forms.py b/production/forms.py index ec9c435974e458bc860fa345f45638fd20417924..4e01e4d2ba0f4ad750808e26d3a11ab472fae22a 100644 --- a/production/forms.py +++ b/production/forms.py @@ -159,3 +159,41 @@ class ProofUploadForm(forms.ModelForm): class Meta: model = Proof fields = ('attachment',) + + +class ProofDecisionForm(forms.ModelForm): + decision = forms.ChoiceField(choices=[(True, 'Accept Proofs for publication'), + (False, 'Decline Proofs for publication')]) + comments = forms.CharField(required=False, widget=forms.Textarea) + + class Meta: + model = Proof + fields = () + + def save(self, commit=True): + proof = self.instance + decision = self.cleaned_data['decision'] + comments = self.cleaned_data['comments'] + if decision: + proof.status = constants.PROOF_ACCEPTED + if proof.stream.status in [constants.PROOFS_PRODUCED, + constants.PROOF_CHECKED, + constants.PROOFS_SENT, + constants.PROOFS_CORRECTED]: + # Force status change on Stream if appropriate + proof.stream.status = constants.PROOFS_ACCEPTED + else: + proof.status = constants.PROOF_DECLINED + + if commit: + proof.save() + proof.stream.save() + + prodevent = ProductionEvent( + stream=proof.stream, + event='status', + comments='Received comments: {comments}'.format(comments=comments), + noted_by=proof.stream.supervisor + ) + prodevent.save() + return proof diff --git a/production/managers.py b/production/managers.py index acaf63b521b5028802748afd881564f4d0a787e8..707df554a068d5401807a5ccba7c6c741a41abd3 100644 --- a/production/managers.py +++ b/production/managers.py @@ -17,3 +17,8 @@ class ProductionStreamQuerySet(models.QuerySet): class ProductionEventManager(models.Manager): def get_my_events(self, production_user): return self.filter(noted_by=production_user) + + +class ProofsQuerySet(models.QuerySet): + def for_authors(self): + return self.filter(accessible_for_authors=True) diff --git a/production/models.py b/production/models.py index 868c4a4166d42b2720e2001fa6fb6e0edc3b7fdc..84f7ea4a614362e06b10348a7d2bfe940493acc0 100644 --- a/production/models.py +++ b/production/models.py @@ -8,7 +8,7 @@ from django.utils.functional import cached_property from .constants import PRODUCTION_STREAM_STATUS, PRODUCTION_STREAM_INITIATED, PRODUCTION_EVENTS,\ EVENT_MESSAGE, EVENT_HOUR_REGISTRATION, PRODUCTION_STREAM_COMPLETED,\ PROOF_STATUSES, PROOF_UPLOADED -from .managers import ProductionStreamQuerySet, ProductionEventManager +from .managers import ProductionStreamQuerySet, ProductionEventManager, ProofsQuerySet from .utils import proof_id_to_slug from scipost.storage import SecureFileStorage @@ -114,12 +114,13 @@ class Proof(models.Model): status = models.CharField(max_length=16, choices=PROOF_STATUSES, default=PROOF_UPLOADED) accessible_for_authors = models.BooleanField(default=False) + objects = ProofsQuerySet.as_manager() + class Meta: ordering = ['version'] def get_absolute_url(self): - return reverse('production:proof', - kwargs={'stream_id': self.stream.id, 'version': self.version}) + return reverse('production:proof_pdf', kwargs={'slug': self.slug}) def save(self, *args, **kwargs): # Control Report count per Submission. diff --git a/production/urls.py b/production/urls.py index 2b244223a76d8f17a7c24ccffda6b7d76ef3aca2..7b5f44d72c6294a95c5d186535a955f1f8c879f2 100644 --- a/production/urls.py +++ b/production/urls.py @@ -38,4 +38,6 @@ urlpatterns = [ production_views.DeleteEventView.as_view(), name='delete_event'), url(r'^proofs/(?P<slug>[0-9]+)$', production_views.proof_pdf, name='proof_pdf'), + url(r'^proofs/(?P<slug>[0-9]+)/decision$', + production_views.author_decision, name='author_decision'), ] diff --git a/production/views.py b/production/views.py index 30a20ee78a1e9f6a8e1d6716911a038c063e3671..559f14c85c7f2badb761b5eb8f55eefe19e6b237 100644 --- a/production/views.py +++ b/production/views.py @@ -18,7 +18,7 @@ from guardian.shortcuts import assign_perm, remove_perm from . import constants from .models import ProductionUser, ProductionStream, ProductionEvent, Proof from .forms import ProductionEventForm, AssignOfficerForm, UserToOfficerForm,\ - AssignSupervisorForm, StreamStatusForm, ProofUploadForm + AssignSupervisorForm, StreamStatusForm, ProofUploadForm, ProofDecisionForm from .permissions import is_production_user from .signals import notify_stream_status_change, notify_new_stream_assignment from .utils import proof_slug_to_id @@ -159,6 +159,7 @@ def add_event(request, stream_id): @is_production_user() @permission_required('scipost.can_assign_production_officer', raise_exception=True) +@transaction.atomic def add_officer(request, stream_id): stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) checker = ObjectPermissionChecker(request.user) @@ -187,6 +188,7 @@ def add_officer(request, stream_id): @is_production_user() @permission_required('scipost.can_assign_production_officer', raise_exception=True) +@transaction.atomic def remove_officer(request, stream_id, officer_id): stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) checker = ObjectPermissionChecker(request.user) @@ -233,6 +235,7 @@ def add_supervisor(request, stream_id): @is_production_user() @permission_required('scipost.can_assign_production_supervisor', raise_exception=True) +@transaction.atomic def remove_supervisor(request, stream_id, officer_id): stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) if getattr(stream.supervisor, 'id', 0) == int(officer_id): @@ -285,6 +288,7 @@ class DeleteEventView(DeleteView): @is_production_user() @permission_required('scipost.can_publish_accepted_submission', raise_exception=True) +@transaction.atomic def mark_as_completed(request, stream_id): stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) stream.status = constants.PRODUCTION_STREAM_COMPLETED @@ -298,13 +302,14 @@ def mark_as_completed(request, stream_id): noted_by=request.user.production_user ) prodevent.save() - notify_stream_status_change(request.user, stream) + notify_stream_status_change(request.user, stream, True) messages.success(request, 'Stream marked as completed.') return redirect(reverse('production:production')) @is_production_user() @permission_required('scipost.can_upload_proofs', raise_exception=True) +@transaction.atomic def upload_proofs(request, stream_id): """ Called by a member of the Production Team. @@ -386,8 +391,8 @@ def proof_pdf(request, slug): # Check if user has access! checker = ObjectPermissionChecker(request.user) access = checker.has_perm('can_work_for_stream', stream) and request.user.has_perm('scipost.can_view_production') - if not access: - access = request.user in proof.stream.submission.authors.all() + if not access and request.user.contributor: + access = request.user.contributor in proof.stream.submission.authors.all() if not access: raise Http404 @@ -399,6 +404,29 @@ def proof_pdf(request, slug): return response +@login_required +@transaction.atomic +def author_decision(request, slug): + """ + The authors of a Submission/Proof are asked for their decision on the proof. + Accept or Decline? This will be asked if proof status is `ACCEPTED_SUP` and + will be handled in this view. + """ + proof = Proof.objects.get(id=proof_slug_to_id(slug)) + stream = proof.stream + + # Check if user has access! + if request.user.contributor not in proof.stream.submission.authors.all(): + raise Http404 + + form = ProofDecisionForm(request.POST or None, instance=proof) + if form.is_valid(): + proof = form.save() + messages.success(request, 'Your decision has been sent.') + + return redirect(stream.submission.get_absolute_url()) + + @is_production_user() @permission_required('scipost.can_run_proofs_by_authors', raise_exception=True) def toggle_accessibility(request, stream_id, version): @@ -423,6 +451,7 @@ def toggle_accessibility(request, stream_id, version): @is_production_user() @permission_required('scipost.can_run_proofs_by_authors', raise_exception=True) +@transaction.atomic def decision(request, stream_id, version, decision): """ Send/open proofs to the authors. @@ -462,6 +491,7 @@ def decision(request, stream_id, version, decision): @is_production_user() @permission_required('scipost.can_run_proofs_by_authors', raise_exception=True) +@transaction.atomic def send_proofs(request, stream_id, version): """ Send/open proofs to the authors. diff --git a/submissions/templates/submissions/submission_detail.html b/submissions/templates/submissions/submission_detail.html index e471bbd5e786995fc619aa745daba7e181f754a1..95d99674d508869036de848e75412f16996b0fcd 100644 --- a/submissions/templates/submissions/submission_detail.html +++ b/submissions/templates/submissions/submission_detail.html @@ -2,6 +2,7 @@ {% load scipost_extras %} {% load submissions_extras %} +{% load bootstrap %} {% block pagetitle %}: submission detail{% endblock pagetitle %} @@ -116,6 +117,29 @@ </div> {% endif %} +{% if is_author %} + {% if submission.production_stream.proofs.for_authors.exists %} + <div class="mb-4" id="proofsslist"> + <h2>Proofs</h2> + <ul> + {% for proof in submission.production_stream.proofs.for_authors %} + <li> + <a href="{{ proof.get_absolute_url }}" target="_blank">Download version {{ proof.version }}</a> · uploaded: {{ proof.created|date:"DATE_FORMAT" }} · + status: <span class="label label-secondary label-sm">{{ proof.get_status_display }}</span> + {% if proof.status == 'accepted_sup' and proof_decision_form %} + <h3 class="mb-0 mt-2">Please advise the Production Team on your findings on Proofs version {{ proof.version }}</h3> + <form method="post" action="{% url 'production:author_decision' proof.slug %}" class="my-2"> + {% csrf_token %} + {{ proof_decision_form|bootstrap }} + <input class="btn btn-primary btn-sm" type="submit" value="Submit"> + </form> + {% endif %} + </li> + {% endfor %} + </ul> + </div> + {% endif %} +{% endif %} {% if user.is_authenticated and user|is_in_group:'Registered Contributors' %} <div class="row"> diff --git a/submissions/views.py b/submissions/views.py index 7007d5d231c2a9af3164314afbd0f38d241bee4b..60443b71abd0f8937e2c09daa609124dd896aacd 100644 --- a/submissions/views.py +++ b/submissions/views.py @@ -41,6 +41,7 @@ from scipost.utils import Utils from scipost.permissions import is_tester from comments.forms import CommentForm +from production.forms import ProofDecisionForm from production.models import ProductionStream import strings @@ -177,6 +178,7 @@ def submission_detail_wo_vn_nr(request, arxiv_identifier_wo_vn_nr): def submission_detail(request, arxiv_identifier_w_vn_nr): submission = get_object_or_404(Submission, arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr) + context = {} try: is_author = request.user.contributor in submission.authors.all() is_author_unchecked = (not is_author and @@ -187,6 +189,8 @@ def submission_detail(request, arxiv_identifier_w_vn_nr): .get(author=request.user.contributor)) except Report.DoesNotExist: unfinished_report_for_user = None + + context['proof_decision_form'] = ProofDecisionForm() except AttributeError: is_author = False is_author_unchecked = False @@ -209,16 +213,18 @@ def submission_detail(request, arxiv_identifier_w_vn_nr): recommendations = submission.eicrecommendations.all() - context = {'submission': submission, - 'recommendations': recommendations, - 'comments': comments, - 'invited_reports': invited_reports, - 'contributed_reports': contributed_reports, - 'unfinished_report_for_user': unfinished_report_for_user, - 'author_replies': author_replies, - 'form': form, - 'is_author': is_author, - 'is_author_unchecked': is_author_unchecked} + context.update({ + 'submission': submission, + 'recommendations': recommendations, + 'comments': comments, + 'invited_reports': invited_reports, + 'contributed_reports': contributed_reports, + 'unfinished_report_for_user': unfinished_report_for_user, + 'author_replies': author_replies, + 'form': form, + 'is_author': is_author, + 'is_author_unchecked': is_author_unchecked, + }) return render(request, 'submissions/submission_detail.html', context)