From 29273420050afb215c2f4f0927dccb6209801ab8 Mon Sep 17 00:00:00 2001 From: George Katsikas <giorgakis.katsikas@gmail.com> Date: Tue, 20 Jun 2023 17:38:44 +0200 Subject: [PATCH] reformat proofs views to use htmx fix htmx mailer subview add appropriate permissions to stream view and proofs --- .../mails/templates/mails/_hx_mail_form.html | 5 +- scipost_django/production/permissions.py | 27 ++ ...ctionstream_actions_change_properties.html | 17 +- ..._productionstream_actions_proofs_item.html | 8 +- ...tionstream_change_invitations_officer.html | 2 +- ..._hx_productionstream_details_contents.html | 286 +++++++++--------- scipost_django/production/urls.py | 15 + scipost_django/production/views.py | 220 +++++++++++++- scipost_django/scipost/views.py | 8 +- 9 files changed, 413 insertions(+), 175 deletions(-) diff --git a/scipost_django/mails/templates/mails/_hx_mail_form.html b/scipost_django/mails/templates/mails/_hx_mail_form.html index f2f1bfa25..6e9ae47ee 100644 --- a/scipost_django/mails/templates/mails/_hx_mail_form.html +++ b/scipost_django/mails/templates/mails/_hx_mail_form.html @@ -1,9 +1,6 @@ {% load bootstrap %} -<h2 class="text-danger">HTMX send mail view is not working ATM.</h2> -<form enctype="multipart/form-data" - method="post" - class="border border-danger p-2"> +<form hx-post="{{ view_url }}"> {% csrf_token %} {% if transfer_data_form %}{{ transfer_data_form }}{% endif %} {{ form|bootstrap }} diff --git a/scipost_django/production/permissions.py b/scipost_django/production/permissions.py index 841dd9451..909d5b9a5 100644 --- a/scipost_django/production/permissions.py +++ b/scipost_django/production/permissions.py @@ -3,6 +3,9 @@ __license__ = "AGPL v3" from django.contrib.auth.decorators import user_passes_test +from scipost.views import HTMXPermissionsDenied +from functools import wraps +from django.contrib import messages def is_production_user(): @@ -15,3 +18,27 @@ def is_production_user(): return False return user_passes_test(test) + + +def permission_required_htmx( + perm, + message="You do not have the required permissions.", + **message_kwargs, +): + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + if isinstance(perm, str): + perms = (perm,) + else: + perms = perm + + if request.user.has_perms(perms): + return view_func(request, *args, **kwargs) + else: + messages.error(request, message) + return HTMXPermissionsDenied(message, **message_kwargs) + + return _wrapped_view + + return decorator diff --git a/scipost_django/production/templates/production/_hx_productionstream_actions_change_properties.html b/scipost_django/production/templates/production/_hx_productionstream_actions_change_properties.html index 7afb65a41..d55ce237b 100644 --- a/scipost_django/production/templates/production/_hx_productionstream_actions_change_properties.html +++ b/scipost_django/production/templates/production/_hx_productionstream_actions_change_properties.html @@ -1,7 +1,16 @@ {% load bootstrap %} {% load scipost_extras %} +{% load guardian_tags %} -{% include "production/_hx_productionstream_change_status.html" with form=status_form stream=productionstream %} -{% include "production/_hx_productionstream_change_supervisor.html" with form=supervisor_form stream=productionstream %} -{% include "production/_hx_productionstream_change_officer.html" with form=officer_form stream=productionstream %} -{% include "production/_hx_productionstream_change_invitations_officer.html" with form=invitations_officer_form stream=productionstream %} +{% get_obj_perms request.user for productionstream as "sub_perms" %} + +{% if "can_work_for_stream" in sub_perms and perms.scipost.can_take_decisions_related_to_proofs %} + {% include "production/_hx_productionstream_change_status.html" with form=status_form stream=productionstream %} +{% endif %} +{% if perms.scipost.can_assign_production_supervisor %} + {% include "production/_hx_productionstream_change_supervisor.html" with form=supervisor_form stream=productionstream %} +{% endif %} +{% if "can_work_for_stream" in sub_perms and perms.scipost.can_assign_production_officer %} + {% include "production/_hx_productionstream_change_officer.html" with form=officer_form stream=productionstream %} + {% include "production/_hx_productionstream_change_invitations_officer.html" with form=invitations_officer_form stream=productionstream %} +{% endif %} diff --git a/scipost_django/production/templates/production/_hx_productionstream_actions_proofs_item.html b/scipost_django/production/templates/production/_hx_productionstream_actions_proofs_item.html index 6848c94ea..f9d1916ae 100644 --- a/scipost_django/production/templates/production/_hx_productionstream_actions_proofs_item.html +++ b/scipost_django/production/templates/production/_hx_productionstream_actions_proofs_item.html @@ -42,7 +42,7 @@ {% if proofs.status == 'uploaded' %} <div class="col-6 col-sm-3 col-md-6 col-lg-3 h-100 d-none-empty"> <div class="row m-0 d-none-empty"> - <button hx-get="{% url 'production:decision' proofs.stream.id proofs.version 'accept' %}" + <button hx-get="{% url 'production:_hx_proofs_decision' proofs.stream.id proofs.version 'accept' %}" hx-target="#productionstream-{{ stream.id }}-proofs-list-accordion-proofs-{{ proofs.version }}-item" hx-swap="outerHTML" class="btn btn-sm btn-primary proof-action-button">Accept</button> @@ -50,7 +50,7 @@ </div> <div class="col-6 col-sm-3 col-md-6 col-lg-3 h-100 d-none-empty"> <div class="row m-0 d-none-empty"> - <button hx-get="{% url 'production:decision' proofs.stream.id proofs.version 'decline' %}" + <button hx-get="{% url 'production:_hx_proofs_decision' proofs.stream.id proofs.version 'decline' %}" hx-target="#productionstream-{{ stream.id }}-proofs-list-accordion-proofs-{{ proofs.version }}-item" hx-swap="outerHTML" class="btn btn-sm btn-danger proof-action-button">Decline</button> @@ -59,7 +59,7 @@ {% elif proofs.status == 'accepted_sup' %} <div class="col-12 col-sm-6 col-md-12 col-lg-6 h-100 d-none-empty"> <div class="row m-0 d-none-empty"> - <button hx-get="{% url 'production:send_proofs' proofs.stream.id proofs.version %}" + <button hx-get="{% url 'production:_hx_send_proofs' proofs.stream.id proofs.version %}" hx-target="#productionstream-{{ stream.id }}-proofs-list-accordion-proofs-{{ proofs.version }}-action-row" hx-swap="afterend" class="btn btn-sm btn-warning">Send proofs to authors</button> @@ -68,7 +68,7 @@ {% else %} <div class="col-12 col-sm-6 col-md-12 col-lg-6 h-100 d-none-empty"> <div class="row m-0 d-none-empty"> - <button hx-get="{% url 'production:toggle_accessibility' proofs.stream.id proofs.version %}" + <button hx-get="{% url 'production:_hx_toggle_accessibility' proofs.stream.id proofs.version %}" hx-target="#productionstream-{{ stream.id }}-proofs-list-accordion-proofs-{{ proofs.version }}-item" hx-swap="outerHTML" class="btn btn-sm btn-primary"> diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_invitations_officer.html b/scipost_django/production/templates/production/_hx_productionstream_change_invitations_officer.html index 7f6e4df72..f4c7284ff 100644 --- a/scipost_django/production/templates/production/_hx_productionstream_change_invitations_officer.html +++ b/scipost_django/production/templates/production/_hx_productionstream_change_invitations_officer.html @@ -5,7 +5,7 @@ <form hx-post="{% url 'production:update_invitations_officer' stream.id %}" hx-target="#productionstream-{{ stream.id }}-update-invitations_officer" hx-swap="outerHTML" - class="row"> + class="row mb-0"> {% csrf_token %} <div class="col">{{ form|bootstrap_purely_inline }}</div> <div class="col-12 col-sm-auto col-md-12 col-lg-auto h-100 d-none-empty" diff --git a/scipost_django/production/templates/production/_hx_productionstream_details_contents.html b/scipost_django/production/templates/production/_hx_productionstream_details_contents.html index 4d58bc797..0e4c81441 100644 --- a/scipost_django/production/templates/production/_hx_productionstream_details_contents.html +++ b/scipost_django/production/templates/production/_hx_productionstream_details_contents.html @@ -9,30 +9,30 @@ </p> <div class="row"> - <div class="col-12 col-md-6 d-flex flex-column"> + {% if "can_work_for_stream" in sub_perms or perms.scipost.can_assign_production_supervisor %} + <div class="col-12 col-md d-flex flex-column"> - <div class="accordion px-2 mb-2" - id="productionstream-{{ productionstream.id }}-actions-accordion"> - <h3>Actions</h3> + <div class="accordion px-2 mb-2" + id="productionstream-{{ productionstream.id }}-actions-accordion"> + <h3>Actions</h3> - {% if perms.scipost.can_take_decisions_related_to_proofs %} - <div class="accordion-item"> - <h2 class="accordion-header" - id="productionstream-{{ productionstream.id }}-change-properties-header"> - <button class="accordion-button fs-6 {% if accordion_default_open == '' or accordion_default_open != 'change-properties' %}collapsed{% endif %}" - type="button" - data-bs-toggle="collapse" - data-bs-target="#productionstream-{{ productionstream.id }}-change-properties" - aria-expanded="true" - aria-controls="productionstream-{{ productionstream.id }}-change-properties"> - Change Properties - </button> - </h2> - <div id="productionstream-{{ productionstream.id }}-change-properties" - class="accordion-collapse collapse {% if accordion_default_open == 'change-properties' %}show{% endif %}" - aria-labelledby="productionstream-{{ productionstream.id }}-change-properties-header" - data-bs-parent="#productionstream-{{ productionstream.id }}-actions-accordion"> - <div class="accordion-body"> + {% if perms.scipost.can_take_decisions_related_to_proofs %} + <div class="accordion-item"> + <h2 class="accordion-header" + id="productionstream-{{ productionstream.id }}-change-properties-header"> + <button class="accordion-button fs-6 {% if accordion_default_open == '' or accordion_default_open != 'change-properties' %}collapsed{% endif %}" + type="button" + data-bs-toggle="collapse" + data-bs-target="#productionstream-{{ productionstream.id }}-change-properties" + aria-expanded="true" + aria-controls="productionstream-{{ productionstream.id }}-change-properties"> + Change Properties + </button> + </h2> + <div id="productionstream-{{ productionstream.id }}-change-properties" + class="accordion-collapse collapse {% if accordion_default_open == 'change-properties' %}show{% endif %}" + aria-labelledby="productionstream-{{ productionstream.id }}-change-properties-header" + data-bs-parent="#productionstream-{{ productionstream.id }}-actions-accordion"> <div id="productionstream-{{ productionstream.id }}-change-properties-body" class="accordion-body" hx-get="{% url 'production:_hx_productionstream_actions_change_properties' productionstream_id=productionstream.id %}" @@ -57,157 +57,156 @@ {% comment %} End placeholder {% endcomment %} </div> - <div id="productionstream-{{ productionstream.id }}-actions-message"></div> </div> </div> - </div> - {% endif %} + {% endif %} - {% if "can_work_for_stream" in sub_perms %} - <div class="accordion-item"> - <h2 class="accordion-header" - id="productionstream-{{ productionstream.id }}-upload-proofs-header"> - <button class="accordion-button fs-6 {% if accordion_default_open == '' or accordion_default_open != 'upload_proofs' %}collapsed{% endif %}" - type="button" - data-bs-toggle="collapse" - data-bs-target="#productionstream-{{ productionstream.id }}-upload-proofs" - aria-expanded="false" - aria-controls="productionstream-{{ productionstream.id }}-upload-proofs">Upload Proofs</button> - </h2> - <div id="productionstream-{{ productionstream.id }}-upload-proofs" - class="accordion-collapse collapse {% if accordion_default_open == 'upload-proofs' %}show{% endif %}" - aria-labelledby="productionstream-{{ productionstream.id }}-upload-proofs-header" - data-bs-parent="#productionstream-{{ productionstream.id }}-actions-accordion"> - <div id="productionstream-{{ productionstream.id }}-upload-proofs-body" - class="accordion-body" - hx-get="{% url 'production:upload_proofs' stream_id=productionstream.id %}" - hx-trigger="intersect once"> + {% if "can_work_for_stream" in sub_perms %} + <div class="accordion-item"> + <h2 class="accordion-header" + id="productionstream-{{ productionstream.id }}-upload-proofs-header"> + <button class="accordion-button fs-6 {% if accordion_default_open == '' or accordion_default_open != 'upload-proofs' %}collapsed{% endif %}" + type="button" + data-bs-toggle="collapse" + data-bs-target="#productionstream-{{ productionstream.id }}-upload-proofs" + aria-expanded="false" + aria-controls="productionstream-{{ productionstream.id }}-upload-proofs">Upload Proofs</button> + </h2> + <div id="productionstream-{{ productionstream.id }}-upload-proofs" + class="accordion-collapse collapse {% if accordion_default_open == 'upload-proofs' %}show{% endif %}" + aria-labelledby="productionstream-{{ productionstream.id }}-upload-proofs-header" + data-bs-parent="#productionstream-{{ productionstream.id }}-actions-accordion"> + <div id="productionstream-{{ productionstream.id }}-upload-proofs-body" + class="accordion-body" + hx-get="{% url 'production:upload_proofs' stream_id=productionstream.id %}" + hx-trigger="intersect once"> - {% comment %} Placeholder before HTMX content loads {% endcomment %} - <div class="placeholder-glow"> - <h3> - <span class="placeholder">Proofs</span> - </h3> - <div class="w-100 mb-2 py-4 placeholder"></div> - <div class="w-100 mb-2 py-4 placeholder"></div> - <div class="row"> - <div class="col-3"> - <div class="w-100 py-3 placeholder"></div> - </div> - <div class="col-9"> - <div class="w-100 py-3 placeholder"></div> + {% comment %} Placeholder before HTMX content loads {% endcomment %} + <div class="placeholder-glow"> + <h3> + <span class="placeholder">Proofs</span> + </h3> + <div class="w-100 mb-2 py-4 placeholder"></div> + <div class="w-100 mb-2 py-4 placeholder"></div> + <div class="row"> + <div class="col-3"> + <div class="w-100 py-3 placeholder"></div> + </div> + <div class="col-9"> + <div class="w-100 py-3 placeholder"></div> + </div> </div> + <div class="w-25 px-1 py-3 placeholder"></div> </div> - <div class="w-25 px-1 py-3 placeholder"></div> - </div> - {% comment %} End placeholder {% endcomment %} + {% comment %} End placeholder {% endcomment %} + </div> </div> </div> - </div> - {% endif %} - <div class="accordion-item"> - <h2 class="accordion-header" - id="productionstream-{{ productionstream.id }}-work-log-header"> - <button class="accordion-button fs-6 {% if accordion_default_open == '' or accordion_default_open != 'work-log' %}collapsed{% endif %}" - type="button" - data-bs-toggle="collapse" - data-bs-target="#productionstream-{{ productionstream.id }}-work-log" - aria-expanded="false" - aria-controls="productionstream-{{ productionstream.id }}-work-log">Work Log</button> - </h2> - <div id="productionstream-{{ productionstream.id }}-work-log" - class="accordion-collapse collapse {% if accordion_default_open == 'work-log' %}show{% endif %}" - aria-labelledby="productionstream-{{ productionstream.id }}-work-log-header" - data-bs-parent="#productionstream-{{ productionstream.id }}-actions-accordion"> - <div id="productionstream-{{ productionstream.id }}-work-log-body" - class="accordion-body" - hx-get="{% url 'production:_hx_productionstream_actions_work_log' productionstream_id=productionstream.id %}" - hx-trigger="intersect once, htmx:trigger from:this target:a.work_log_delete_btn delay:1000"> + <div class="accordion-item"> + <h2 class="accordion-header" + id="productionstream-{{ productionstream.id }}-work-log-header"> + <button class="accordion-button fs-6 {% if accordion_default_open == '' or accordion_default_open != 'work-log' %}collapsed{% endif %}" + type="button" + data-bs-toggle="collapse" + data-bs-target="#productionstream-{{ productionstream.id }}-work-log" + aria-expanded="false" + aria-controls="productionstream-{{ productionstream.id }}-work-log">Work Log</button> + </h2> + <div id="productionstream-{{ productionstream.id }}-work-log" + class="accordion-collapse collapse {% if accordion_default_open == 'work-log' %}show{% endif %}" + aria-labelledby="productionstream-{{ productionstream.id }}-work-log-header" + data-bs-parent="#productionstream-{{ productionstream.id }}-actions-accordion"> + <div id="productionstream-{{ productionstream.id }}-work-log-body" + class="accordion-body" + hx-get="{% url 'production:_hx_productionstream_actions_work_log' productionstream_id=productionstream.id %}" + hx-trigger="intersect once, htmx:trigger from:this target:a.work_log_delete_btn delay:1000"> - {% comment %} Placeholder before HTMX content loads {% endcomment %} - <div class="placeholder-glow"> - <div class="row"> - <div class="col-3"> - <div class="w-100 placeholder"></div> - </div> - </div> - - {% for i_repeat in '12' %} - <div class="row"> - <div class="col-9"> - <div class="g-2"> - <div class="col-6 placeholder"></div> - <div class="col-8 placeholder"></div> + {% comment %} Placeholder before HTMX content loads {% endcomment %} + <div class="placeholder-glow"> + <div class="row"> + <div class="col-3"> + <div class="w-100 placeholder"></div> </div> </div> - <div class="col-3"> - <div class="g-2"> - <div class="col-12 placeholder"></div> - <div class="offset-4 col-8 placeholder"></div> + + {% for i_repeat in '12' %} + <div class="row"> + <div class="col-9"> + <div class="g-2"> + <div class="col-6 placeholder"></div> + <div class="col-8 placeholder"></div> + </div> + </div> + <div class="col-3"> + <div class="g-2"> + <div class="col-12 placeholder"></div> + <div class="offset-4 col-8 placeholder"></div> + </div> + </div> </div> - </div> - </div> - {% endfor %} + {% endfor %} - <div class="w-75 py-1 mb-3 placeholder"></div> + <div class="w-75 py-1 mb-3 placeholder"></div> - {% for i_repeat in '123' %} - <div class="row"> - <div class="col-2"> - <div class="w-100 py-1 placeholder"></div> - </div> - <div class="col-10"> - <div class="w-100 pb-4 placeholder"></div> - </div> + {% for i_repeat in '123' %} + <div class="row"> + <div class="col-2"> + <div class="w-100 py-1 placeholder"></div> + </div> + <div class="col-10"> + <div class="w-100 pb-4 placeholder"></div> + </div> + </div> + {% endfor %} </div> - {% endfor %} - </div> - {% comment %} End placeholder {% endcomment %} + {% comment %} End placeholder {% endcomment %} + </div> + </div> </div> - </div> + {% endif %} + </div> - </div> - {% if perms.scipost.can_draft_publication or perms.scipost.can_publish_accepted_submission %} - <div class="mb-2 mb-md-0 mt-md-auto px-2"> + {% if perms.scipost.can_draft_publication or perms.scipost.can_publish_accepted_submission %} + <div class="mb-2 mb-md-0 mt-md-auto px-2"> - <div class="row mb-0 g-2"> - {% if perms.scipost.can_publish_accepted_submission %} - <div class="col-12 col-sm-auto col-md-12 col-lg-auto h-100 d-none-empty"> - <div class="row m-0 d-none-empty"> - <button class="btn btn-warning text-white" - hx-get="{% url 'production:mark_as_completed' stream_id=productionstream.id %}" - {% if stream.status != 'published' %}hx-confirm="Are you sure you want to mark this unpublished stream as completed?"{% endif %} - hx-target="#productionstream-{{ productionstream.id }}-details"> - Mark this stream as completed - </button> + <div class="row mb-0 g-2"> + {% if perms.scipost.can_publish_accepted_submission %} + <div class="col-12 col-sm-auto col-md-12 col-lg-auto h-100 d-none-empty"> + <div class="row m-0 d-none-empty"> + <button class="btn btn-warning text-white" + hx-get="{% url 'production:mark_as_completed' stream_id=productionstream.id %}" + {% if stream.status != 'published' %}hx-confirm="Are you sure you want to mark this unpublished stream as completed?"{% endif %} + hx-target="#productionstream-{{ productionstream.id }}-details"> + Mark this stream as completed + </button> + </div> </div> - </div> - {% endif %} + {% endif %} - {% if perms.scipost.can_draft_publication and stream.status == 'accepted' %} - <div class="col-12 col-sm-auto col-md-12 col-lg-auto h-100 d-none-empty"> - <div class="row m-0 d-none-empty"> - <a class="btn btn-primary text-white" - href="{% url 'journals:create_publication' productionstream.submission.preprint.identifier_w_vn_nr %}"> - Draft publication - </a> + {% if perms.scipost.can_draft_publication and stream.status == 'accepted' %} + <div class="col-12 col-sm-auto col-md-12 col-lg-auto h-100 d-none-empty"> + <div class="row m-0 d-none-empty"> + <a class="btn btn-primary text-white" + href="{% url 'journals:create_publication' productionstream.submission.preprint.identifier_w_vn_nr %}"> + Draft publication + </a> + </div> </div> - </div> - {% endif %} - </div> + {% endif %} + </div> - </div> + </div> + {% endif %} {% endif %} - </div> <div id="productionstream-{{ productionstream.id }}-event-container" - class="col-12 col-md-6 d-flex flex-column px-3"> + class="col-12 col-md d-flex flex-column px-3"> {% comment %} This might be better to refactor with an OOB response on each event addition {% endcomment %} <h3>Events</h3> <div id="productionstream-{{ productionstream.id }}-event-list" @@ -251,7 +250,7 @@ {% comment %} End placeholder {% endcomment %} </div> - + <div id="productionstream-{{ productionstream.id }}-event-new-comment-form"> <button hx-get="{% url 'production:_hx_event_form' productionstream_id=productionstream.id %}" hx-target="#productionstream-{{ productionstream.id }}-event-new-comment-form" @@ -260,5 +259,4 @@ class="btn btn-primary">Add a comment to this stream</button> </div> </div> - </div> diff --git a/scipost_django/production/urls.py b/scipost_django/production/urls.py index ce83c7f03..23963b08e 100644 --- a/scipost_django/production/urls.py +++ b/scipost_django/production/urls.py @@ -140,16 +140,31 @@ urlpatterns = [ production_views.decision, name="decision", ), + re_path( + "_hx_proofs_decision/(?P<decision>accept|decline)$", + production_views._hx_proofs_decision, + name="_hx_proofs_decision", + ), path( "send_to_authors", production_views.send_proofs, name="send_proofs", ), + path( + "_hx_send_to_authors", + production_views._hx_send_proofs, + name="_hx_send_proofs", + ), path( "toggle_access", production_views.toggle_accessibility, name="toggle_accessibility", ), + path( + "_hx_toggle_access", + production_views._hx_toggle_accessibility, + name="_hx_toggle_accessibility", + ), ] ), ), diff --git a/scipost_django/production/views.py b/scipost_django/production/views.py index 99777f932..e32e322b9 100644 --- a/scipost_django/production/views.py +++ b/scipost_django/production/views.py @@ -44,7 +44,7 @@ from .forms import ( ProofsDecisionForm, AssignInvitationsOfficerForm, ) -from .permissions import is_production_user +from .permissions import is_production_user, permission_required_htmx from .utils import proofs_slug_to_id, ProductionUtils @@ -93,6 +93,10 @@ def _hx_productionstream_details_contents(request, productionstream_id): # Determine which accordion tab to open by default accordion_default_open = "" + + if request.user.has_perm("scipost.can_assign_production_supervisor"): + accordion_default_open = "change-properties" + if request.user.production_user == productionstream.supervisor: if productionstream.status in [ constants.PROOFS_ACCEPTED, @@ -408,7 +412,10 @@ def add_work_log(request, stream_id): @is_production_user() -@permission_required("scipost.can_view_production", raise_exception=True) +@permission_required_htmx( + ("scipost.can_view_production",), + message="You cannot view production.", +) def _hx_productionstream_actions_work_log(request, productionstream_id): productionstream = get_object_or_404(ProductionStream, pk=productionstream_id) checker = ObjectPermissionChecker(request.user) @@ -441,8 +448,13 @@ def _hx_productionstream_actions_work_log(request, productionstream_id): @is_production_user() -@permission_required( - "scipost.can_take_decisions_related_to_proofs", raise_exception=True +@permission_required_htmx( + ( + "scipost.can_view_production", + "scipost.can_take_decisions_related_to_proofs", + ), + message="You do not have permission to update the status of this stream.", + css_class="row", ) def update_status(request, stream_id): productionstream = get_object_or_404( @@ -543,7 +555,14 @@ def remove_officer(request, stream_id, officer_id): @is_production_user() -@permission_required("scipost.can_assign_production_officer", raise_exception=True) +@permission_required_htmx( + ( + "scipost.can_view_production", + "scipost.can_assign_production_officer", + ), + message="You do not have permission to update the officer of this stream.", + css_class="row", +) @transaction.atomic def update_officer(request, stream_id): productionstream = get_object_or_404( @@ -706,7 +725,14 @@ def remove_invitations_officer(request, stream_id, officer_id): @is_production_user() -@permission_required("scipost.can_assign_production_officer", raise_exception=True) +@permission_required_htmx( + ( + "scipost.can_view_production", + "scipost.can_assign_production_officer", + ), + message="You do not have permission to update the invitations officer of this stream.", + css_class="row", +) @transaction.atomic def update_invitations_officer(request, stream_id): productionstream = get_object_or_404( @@ -846,7 +872,11 @@ class UpdateEventView(UpdateView): @is_production_user() -@permission_required("scipost.can_assign_production_supervisor", raise_exception=True) +@permission_required_htmx( + ("scipost.can_view_production", "scipost.can_assign_production_supervisor"), + message="You do not have permission to update the supervisor of this stream.", + css_class="row", +) @transaction.atomic def update_supervisor(request, stream_id): productionstream = get_object_or_404( @@ -964,7 +994,13 @@ def mark_as_completed(request, stream_id): @is_production_user() -@permission_required("scipost.can_upload_proofs", raise_exception=True) +@permission_required_htmx( + ( + "scipost.can_view_production", + "scipost.can_upload_proofs", + ), + message="You cannot upload proofs for this stream.", +) @transaction.atomic def upload_proofs(request, stream_id): """ @@ -1028,6 +1064,7 @@ def proofs(request, stream_id, version): return render(request, "production/proofs.html", context) +@permission_required("scipost.can_view_production", raise_exception=True) def proofs_pdf(request, slug): """Open Proofs pdf.""" if not request.user.is_authenticated: @@ -1038,14 +1075,12 @@ def proofs_pdf(request, slug): proofs = get_object_or_404(Proofs, id=proofs_slug_to_id(slug)) stream = proofs.stream - # Check if user has access! + # 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 and request.user.contributor: - access = request.user.contributor in proofs.stream.submission.authors.all() - if not access: + can_work_for_stream = checker.has_perm("can_work_for_stream", stream) + is_submission_author = request.user.contributor in stream.submission.authors.all() + + if not (can_work_for_stream or is_submission_author): raise Http404 # Passed the test! The user may see the file! @@ -1130,6 +1165,37 @@ def toggle_accessibility(request, stream_id, version): except Proofs.DoesNotExist: raise Http404 + proofs.accessible_for_authors = not proofs.accessible_for_authors + proofs.save() + messages.success(request, "Proofs accessibility updated.") + return redirect(reverse("production:proofs", args=(stream.id, proofs.version))) + + +@is_production_user() +@permission_required_htmx( + ( + "scipost.can_view_production", + "scipost.can_take_decisions_related_to_proofs", + "scipost.can_run_proofs_by_authors", + ), + message="You cannot make proofs accessible to the authors.", +) +def _hx_toggle_accessibility(request, stream_id, version): + """ + Open/close accessibility of proofs to the authors. + """ + stream = get_object_or_404(ProductionStream.objects.all(), pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm("can_work_for_stream", stream): + return HTMXPermissionsDenied("You cannot work in this stream.") + + try: + proofs = stream.proofs.exclude(status=constants.PROOFS_UPLOADED).get( + version=version + ) + except Proofs.DoesNotExist: + return HTMXResponse("Proofs do not exist.", tag="danger") + proofs.accessible_for_authors = not proofs.accessible_for_authors proofs.save() messages.success(request, "Proofs accessibility updated.") @@ -1184,6 +1250,56 @@ def decision(request, stream_id, version, decision): ) prodevent.save() messages.success(request, "Proofs have been {decision}.".format(decision=decision)) + return redirect(reverse("production:proofs", args=(stream.id, proofs.version))) + + +@is_production_user() +@permission_required_htmx( + ( + "scipost.can_view_production", + "scipost.can_take_decisions_related_to_proofs", + "scipost.can_run_proofs_by_authors", + ), + message="You cannot accept or decline proofs.", +) +@transaction.atomic +def _hx_proofs_decision(request, stream_id, version, decision): + """ + Send/open proofs to the authors. This decision is taken by the supervisor. + """ + stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm("can_work_for_stream", stream): + return HTMXPermissionsDenied("You cannot work in this stream.") + + try: + proofs = stream.proofs.get(version=version, status=constants.PROOFS_UPLOADED) + except Proofs.DoesNotExist: + return HTMXResponse("Proofs do not exist.", tag="danger") + + if decision == "accept": + stream.status = constants.PROOFS_CHECKED + proofs.status = constants.PROOFS_ACCEPTED_SUP + decision = "accepted" + else: + stream.status = constants.PROOFS_TASKED + proofs.status = constants.PROOFS_DECLINED_SUP + proofs.accessible_for_authors = False + decision = "declined" + + stream.save() + proofs.save() + + prodevent = ProductionEvent( + stream=stream, + event="status", + comments="Proofs version {version} are {decision}.".format( + version=proofs.version, decision=decision + ), + noted_by=request.user.production_user, + ) + prodevent.save() + messages.success(request, f"Proofs have been {decision}.") return render( request, "production/_hx_productionstream_actions_proofs_item.html", @@ -1249,6 +1365,80 @@ def send_proofs(request, stream_id, version): return redirect(stream.get_absolute_url()) +@is_production_user() +@permission_required_htmx( + ( + "scipost.can_view_production", + "scipost.can_take_decisions_related_to_proofs", + "scipost.can_run_proofs_by_authors", + ), + message="You cannot send proofs to the authors.", +) +@transaction.atomic +def _hx_send_proofs(request, stream_id, version): + """ + Send/open proofs to the authors. + """ + stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm("can_work_for_stream", stream): + return HTMXPermissionsDenied("You cannot work in this stream.") + + try: + proofs = stream.proofs.can_be_send().get(version=version) + except Proofs.DoesNotExist: + return HTMXResponse("Proofs do not exist.", tag="danger") + + proofs.status = constants.PROOFS_SENT + proofs.accessible_for_authors = True + + if stream.status not in [ + constants.PROOFS_PUBLISHED, + constants.PROOFS_CITED, + constants.PRODUCTION_STREAM_COMPLETED, + ]: + stream.status = constants.PROOFS_SENT + stream.save() + + mail_request = MailEditorSubviewHTMX( + request, + "production_send_proofs", + proofs=proofs, + context={ + "view_url": reverse("production:_hx_send_proofs", args=[stream_id, version]) + }, + ) + + print(request) + + if request.method == "GET": + return mail_request.interrupt() + + if mail_request.is_valid(): + proofs.save() + stream.save() + + mail_request.send_mail() + + prodevent = ProductionEvent( + stream=stream, + event="status", + comments="Proofs version {version} sent to authors.".format( + version=proofs.version + ), + noted_by=request.user.production_user, + ) + prodevent.save() + + messages.success(request, "Proofs have been sent.") + return HTMXResponse("Proofs have been sent to the authors.", tag="success") + else: + messages.error(request, "Proofs have not been sent.") + return HTMXResponse( + "Proofs have not been sent. Please check the form.", tag="danger" + ) + + def render_action_buttons(request, stream_id, key): productionstream = get_object_or_404(ProductionStream, pk=stream_id) diff --git a/scipost_django/scipost/views.py b/scipost_django/scipost/views.py index 40b977a14..923d154c9 100644 --- a/scipost_django/scipost/views.py +++ b/scipost_django/scipost/views.py @@ -185,14 +185,16 @@ def _hx_messages(request): class HTMXResponse(HttpResponse): - tag = None - message = None + tag = "primary" + message = "" + css_class = "" def __init__(self, *args, **kwargs): tag = kwargs.pop("tag", self.tag) message = args[0] if args else kwargs.pop("message", self.message) + css_class = kwargs.pop("css_class", self.css_class) - alert_html = f"""<div class="text-{tag} border border-{tag} p-3"> + alert_html = f"""<div class="text-{tag} border border-{tag} p-3 {css_class}"> {message} </div>""" -- GitLab