diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_action_buttons.html b/scipost_django/production/templates/production/_hx_productionstream_change_action_buttons.html new file mode 100644 index 0000000000000000000000000000000000000000..70891420566ae5899ea220c8a87dc783b27b25f0 --- /dev/null +++ b/scipost_django/production/templates/production/_hx_productionstream_change_action_buttons.html @@ -0,0 +1,18 @@ +{% comment %} Only show something if the option is about to change {% endcomment %} +{% if not current_option == new_option %} + {% comment %} If selected option is None, it is being removed {% endcomment %} + {% if new_option == "None" %} + <button type="submit" class="btn btn-danger"> + Remove + </button> + {% comment %} Otherwise differentiate between changing or adding based on current option {% endcomment %} + {% else %} + <button type="submit" class="btn btn-primary"> + {% if current_option == "None" %} + Add + {% else %} + Change + {% endif %} + </button> + {% endif %} +{% endif %} diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_invofficer.html b/scipost_django/production/templates/production/_hx_productionstream_change_invofficer.html new file mode 100644 index 0000000000000000000000000000000000000000..e97de052f2746ec458ea099f770e9ea6d95b494d --- /dev/null +++ b/scipost_django/production/templates/production/_hx_productionstream_change_invofficer.html @@ -0,0 +1,15 @@ +{% load bootstrap %} + +{% if perms.scipost.can_take_decisions_related_to_proofs and form.fields.invitations_officer.choices|length > 0 %} +<div id="productionstream-update-invitations_officer"> + <form hx-post="{% url 'production:update_invitations_officer' stream.id %}" hx-target="#productionstream-update-invitations_officer" hx-swap="outerHTML" class="row"> + {% csrf_token %} + <div class="col"> + {{form|bootstrap_purely_inline}} + </div> + <div class="col-auto h-100" hx-post="{% url 'production:render_action_buttons' stream.id 'invitations_officer' %}" hx-swap="innerHTML" class="col-auto" hx-trigger="change from:select#id_invitations_officer" hx-target="this"> + </div> + <div class="text-primary">{{message}}</div> + </form> +</div> +{% endif %} \ No newline at end of file diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_prodofficer.html b/scipost_django/production/templates/production/_hx_productionstream_change_prodofficer.html new file mode 100644 index 0000000000000000000000000000000000000000..ea1d130cf2c66ee49778729f172e0ed167a80a47 --- /dev/null +++ b/scipost_django/production/templates/production/_hx_productionstream_change_prodofficer.html @@ -0,0 +1,15 @@ +{% load bootstrap %} + +{% if perms.scipost.can_take_decisions_related_to_proofs and form.fields.officer.choices|length > 0 %} +<div id="productionstream-update-officer"> + <form hx-post="{% url 'production:update_officer' stream.id %}" hx-target="#productionstream-update-officer" hx-swap="outerHTML" class="row"> + {% csrf_token %} + <div class="col"> + {{form|bootstrap_purely_inline}} + </div> + <div class="col-auto h-100" hx-post="{% url 'production:render_action_buttons' stream.id 'officer' %}" hx-swap="innerHTML" class="col-auto" hx-trigger="change from:select#id_officer" hx-target="this"> + </div> + <div class="text-primary">{{message}}</div> + </form> +</div> +{% endif %} \ No newline at end of file diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_status.html b/scipost_django/production/templates/production/_hx_productionstream_change_status.html new file mode 100644 index 0000000000000000000000000000000000000000..4458c29f034cee7d5ce024f49b2c5440fb6fc834 --- /dev/null +++ b/scipost_django/production/templates/production/_hx_productionstream_change_status.html @@ -0,0 +1,15 @@ +{% load bootstrap %} + +{% if perms.scipost.can_take_decisions_related_to_proofs and form.fields.status.choices|length > 0 %} +<div id="productionstream-update-status"> + <form hx-post="{% url 'production:update_status' stream.id %}" hx-target="#productionstream-update-status" hx-swap="outerHTML" class="row"> + {% csrf_token %} + <div class="col"> + {{form|bootstrap_purely_inline}} + </div> + <div class="col-auto h-100" hx-post="{% url 'production:render_action_buttons' stream.id 'status' %}" hx-swap="innerHTML" class="col-auto" hx-trigger="load, change from:select#id_status" hx-target="this"> + </div> + <div class="text-primary">{{message}}</div> + </form> +</div> +{% endif %} \ No newline at end of file diff --git a/scipost_django/production/templates/production/_hx_productionstream_change_supervisor.html b/scipost_django/production/templates/production/_hx_productionstream_change_supervisor.html new file mode 100644 index 0000000000000000000000000000000000000000..91a9c8cb37219aae03b9de10d283a05752484d32 --- /dev/null +++ b/scipost_django/production/templates/production/_hx_productionstream_change_supervisor.html @@ -0,0 +1,15 @@ +{% load bootstrap %} + +{% if perms.scipost.can_take_decisions_related_to_proofs and form.fields.supervisor.choices|length > 0 %} +<div id="productionstream-update-supervisor"> + <form hx-post="{% url 'production:update_supervisor' stream.id %}" hx-target="#productionstream-update-supervisor" hx-swap="outerHTML" class="row"> + {% csrf_token %} + <div class="col"> + {{form|bootstrap_purely_inline}} + </div> + <div class="col-auto h-100" hx-post="{% url 'production:render_action_buttons' stream.id 'supervisor' %}" hx-swap="innerHTML" class="col-auto" hx-trigger="load, change from:select#id_supervisor" hx-target="this"> + </div> + <div class="text-primary">{{message}}</div> + </form> +</div> +{% endif %} \ No newline at end of file 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 29b10ba51dd279f89fd129e020e02f7f99c06e58..4000c331b15c6871dc89a120bb928484045cb915 100644 --- a/scipost_django/production/templates/production/_hx_productionstream_details_contents.html +++ b/scipost_django/production/templates/production/_hx_productionstream_details_contents.html @@ -1,18 +1,43 @@ +{% load guardian_tags %} + +{% get_obj_perms request.user for productionstream as "sub_perms" %} <p> <strong class="text-warning">While the new production page is being built</strong>: go the the <a href="{{ productionstream.get_absolute_url }}" target="_blank">(old) stream detail page</a> to manage this stream. </p> +<div class="row overflow-hidden" style="height: min(50vh, 40em)"> + <div class="col-lg-6"> + <h3>Actions</h3> + {% if "can_perform_supervisory_actions" in sub_perms %} + <div class="container"> + <h4>Change:</h4> + <div class="border-primary border-start ps-2"> + {% 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_prodofficer.html" with form=prod_officer_form stream=productionstream %} + {% include "production/_hx_productionstream_change_invofficer.html" with form=inv_officer_form stream=productionstream %} + </div> + </div> + {% endif %} + <ul> + <li>Worklog</li> + <li>Completed</li> + <li>Upload proofs</li> + <li>Accessibility</li> + <li>Send Proofs</li> + </ul> + </div> -<h3>Events</h3> -{% include 'production/_productionstream_events.html' with productionstream=productionstream events=productionstream.events.all_without_duration %} - - -<button - hx-get="{% url 'production:_hx_event_form' productionstream_id=productionstream.id %}" - hx-target="#productionstream-{{ productionstream.id }}-event-form" - hx-trigger="click" -> - Add a comment to this stream -</button> -<div id="productionstream-{{ productionstream.id }}-event-form"></div> + <div id="productionstream-{{ productionstream.id }}-event-container" + class="col-lg-6 h-100 overflow-scroll"> + <h3>Events</h3> + {% include "production/_productionstream_events.html" with productionstream=productionstream events=productionstream.events.all_without_duration %} + <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" + hx-trigger="click" + hx-swap="outerHTML">Add a comment to this stream</button> + </div> + </div> +</div> diff --git a/scipost_django/production/templates/production/_stream_status_changes.html b/scipost_django/production/templates/production/_stream_status_changes.html index 07efba92e800094e7e5bfe7ed3821df0ca0b6f98..832317d264936565a4a55187503e9ca2a9cac0c8 100644 --- a/scipost_django/production/templates/production/_stream_status_changes.html +++ b/scipost_django/production/templates/production/_stream_status_changes.html @@ -1,14 +1,11 @@ {% load bootstrap %} {% if perms.scipost.can_take_decisions_related_to_proofs and form.fields.status.choices|length > 0 %} - <h3>Change current stream status:</h3> - <form method="post" action="{% url 'production:update_status' stream.id %}" class="form-inline"> + <form method="post" action="{% url 'production:update_status' stream.id %}" class="row"> {% csrf_token %} - {{ form|bootstrap_inline }} - <div class="form-group row"> - <div class="col-form-label col ms-2"> - <button type="submit" class="btn btn-primary">Change</button> - </div> + {{ form|bootstrap_purely_inline }} + <div class="col-auto"> + <button type="submit" class="btn btn-primary">Change</button> </div> </form> {% endif %} diff --git a/scipost_django/production/urls.py b/scipost_django/production/urls.py index dd60eae857f5d1226349dffd72e640a3bd082f26..2ce5894c34e2e62441f273a5c8682d208734adbe 100644 --- a/scipost_django/production/urls.py +++ b/scipost_django/production/urls.py @@ -71,7 +71,6 @@ urlpatterns = [ production_views.delete_officer, name="delete_officer", ), - # streams path( "streams/<int:stream_id>/", include( @@ -122,37 +121,82 @@ urlpatterns = [ ), path("events/add", production_views.add_event, name="add_event"), path("logs/add", production_views.add_work_log, name="add_work_log"), - path("officer/add", production_views.add_officer, name="add_officer"), path( - "officer/<int:officer_id>/remove", - production_views.remove_officer, - name="remove_officer", - ), - path( - "invitations_officer/add", - production_views.add_invitations_officer, - name="add_invitations_officer", - ), - path( - "invitations_officer/<int:officer_id>/remove", - production_views.remove_invitations_officer, - name="remove_invitations_officer", + "officer", + include( + [ + path( + "add", + production_views.add_officer, + name="add_officer", + ), + path( + "<int:officer_id>/remove", + production_views.remove_officer, + name="remove_officer", + ), + path( + "update", + production_views.update_officer, + name="update_officer", + ), + ] + ), ), path( - "supervisor/add", - production_views.add_supervisor, - name="add_supervisor", + "invitations_officer", + include( + [ + path( + "add", + production_views.add_invitations_officer, + name="add_invitations_officer", + ), + path( + "<int:officer_id>/remove", + production_views.remove_invitations_officer, + name="remove_invitations_officer", + ), + path( + "update", + production_views.update_invitations_officer, + name="update_invitations_officer", + ), + ] + ), ), path( - "supervisor/<int:officer_id>/remove", - production_views.remove_supervisor, - name="remove_supervisor", + "supervisor", + include( + [ + path( + "add", + production_views.add_supervisor, + name="add_supervisor", + ), + path( + "<int:officer_id>/remove", + production_views.remove_supervisor, + name="remove_supervisor", + ), + path( + "update", + production_views.update_supervisor, + name="update_supervisor", + ), + ] + ), ), path( "mark_completed", production_views.mark_as_completed, name="mark_as_completed", ), + path( + "render_action_buttons/<str:key>", + production_views.render_action_buttons, + name="render_action_buttons", + ), ] ), ), diff --git a/scipost_django/production/views.py b/scipost_django/production/views.py index f546f01b61bf9db50f28d14164be0b9634e6f25f..8d4cb0fb8402123bd75581c6c5e71138316ff31d 100644 --- a/scipost_django/production/views.py +++ b/scipost_django/production/views.py @@ -55,7 +55,9 @@ from .utils import proofs_slug_to_id, ProductionUtils @permission_required("scipost.can_view_production", raise_exception=True) def production_new(request): form = ProductionStreamSearchForm(user=request.user) - context = {"search_productionstreams_form": form,} + context = { + "search_productionstreams_form": form, + } return render(request, "production/production_new.html", context) @@ -72,7 +74,11 @@ def _hx_productionstream_list(request): page_obj = paginator.get_page(page_nr) count = paginator.count start_index = page_obj.start_index - context = {"count": count, "page_obj": page_obj, "start_index": start_index,} + context = { + "count": count, + "page_obj": page_obj, + "start_index": start_index, + } return render(request, "production/_hx_productionstream_list.html", context) @@ -80,8 +86,25 @@ def _hx_productionstream_list(request): @permission_required("scipost.can_view_production", raise_exception=True) def _hx_productionstream_details_contents(request, productionstream_id): productionstream = get_object_or_404(ProductionStream, pk=productionstream_id) + status_form = StreamStatusForm( + instance=productionstream, production_user=request.user.production_user + ) + supervisor_form = AssignSupervisorForm( + instance=productionstream, + ) + inv_officer_form = AssignInvitationsOfficerForm( + instance=productionstream, + ) + prod_officer_form = AssignOfficerForm( + instance=productionstream, + ) + context = { - "productionstream": productionstream, + "" "productionstream": productionstream, + "status_form": status_form, + "supervisor_form": supervisor_form, + "inv_officer_form": inv_officer_form, + "prod_officer_form": prod_officer_form, } return render( request, @@ -105,10 +128,14 @@ def _hx_event_form(request, productionstream_id, event_id=None): form = ProductionEventForm(request.POST, instance=productionevent) if form.is_valid(): form.save() - return redirect(reverse( - "production:_hx_productionstream_details_contents", - kwargs={"productionstream_id": productionstream.id,}, - )) + return redirect( + reverse( + "production:_hx_productionstream_details_contents", + kwargs={ + "productionstream_id": productionstream.id, + }, + ) + ) elif productionevent: form = ProductionEventForm(instance=productionevent) else: @@ -130,10 +157,14 @@ def _hx_event_form(request, productionstream_id, event_id=None): def _hx_event_delete(request, productionstream_id, event_id): productionstream = get_object_or_404(ProductionStream, pk=productionstream_id) ProductionEvent.objects.filter(pk=event_id).delete() - return redirect(reverse( - "production:_hx_productionstream_details_contents", - kwargs={"productionstream_id": productionstream.id,}, - )) + return redirect( + reverse( + "production:_hx_productionstream_details_contents", + kwargs={ + "productionstream_id": productionstream.id, + }, + ) + ) ################################ @@ -215,7 +246,7 @@ def stream(request, stream_id): if not request.user.has_perm("scipost.can_view_all_production_streams"): # Restrict stream queryset if user is not supervisor streams = streams.filter_for_user(request.user.production_user) - stream = get_object_or_404(streams, id=stream_id) + productionstream = get_object_or_404(streams, id=stream_id) prodevent_form = ProductionEventForm_deprec() assign_officer_form = AssignOfficerForm() assign_invitiations_officer_form = AssignInvitationsOfficerForm() @@ -228,11 +259,13 @@ def stream(request, stream_id): types = constants.PRODUCTION_OFFICERS_WORK_LOG_TYPES work_log_form = WorkLogForm(log_types=types) status_form = StreamStatusForm( - instance=stream, production_user=request.user.production_user + instance=productionstream, + production_user=request.user.production_user, + auto_id=f"productionstream_{productionstream.id}_id_%s", ) context = { - "stream": stream, + "stream": productionstream, "prodevent_form": prodevent_form, "assign_officer_form": assign_officer_form, "assign_supervisor_form": assign_supervisor_form, @@ -264,44 +297,6 @@ def user_to_officer(request): return redirect(reverse("production:production")) -@is_production_user() -@permission_required("scipost.can_promote_user_to_production_officer") -def delete_officer(request, officer_id): - production_user = get_object_or_404(ProductionUser.objects.active(), id=officer_id) - production_user.name = "{first_name} {last_name}".format( - first_name=production_user.user.first_name, - last_name=production_user.user.last_name, - ) - production_user.user = None - production_user.save() - - messages.success( - request, "{user} removed as Production Officer".format(user=production_user) - ) - return redirect(reverse("production:production")) - - -@is_production_user() -@permission_required( - "scipost.can_take_decisions_related_to_proofs", raise_exception=True -) -def update_status(request, stream_id): - stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) - checker = ObjectPermissionChecker(request.user) - if not checker.has_perm("can_perform_supervisory_actions", stream): - return redirect(reverse("production:production", args=(stream.id,))) - - p = request.user.production_user - form = StreamStatusForm(request.POST or None, instance=stream, production_user=p) - - if form.is_valid(): - stream = form.save() - messages.warning(request, "Production Stream succesfully changed status.") - else: - messages.warning(request, "The status change was invalid.") - return redirect(stream.get_absolute_url()) - - @is_production_user() @permission_required("scipost.can_view_production", raise_exception=True) def add_event(request, stream_id): @@ -347,6 +342,47 @@ def add_work_log(request, stream_id): return redirect(stream.get_absolute_url()) +@is_production_user() +@permission_required( + "scipost.can_take_decisions_related_to_proofs", raise_exception=True +) +def update_status(request, stream_id): + productionstream = get_object_or_404( + ProductionStream.objects.ongoing(), pk=stream_id + ) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm("can_perform_supervisory_actions", productionstream): + context = { + "stream": productionstream, + "message": "You do not have permission to perform this action. ", + } + else: + status_form = StreamStatusForm( + request.POST or None, + instance=productionstream, + production_user=request.user.production_user, + ) + + if status_form.is_valid(): + stream = status_form.save() + status_form.fields["status"].choices = status_form.get_available_statuses() + message = "Production Stream succesfully changed status." + else: + message = "\\n".join(status_form.errors.values()) + + context = { + "stream": productionstream, + "form": status_form, + "message": message, + } + + return render( + request, + "production/_hx_productionstream_change_status.html", + context, + ) + + @is_production_user() @permission_required("scipost.can_assign_production_officer", raise_exception=True) @transaction.atomic @@ -384,6 +420,109 @@ def add_officer(request, stream_id): return redirect(reverse("production:production", args=(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) + if not checker.has_perm("can_perform_supervisory_actions", stream): + return redirect(reverse("production:production", args=(stream.id,))) + + if getattr(stream.officer, "id", 0) == int(officer_id): + officer = stream.officer + stream.officer = None + stream.save() + if officer not in [stream.invitations_officer, stream.supervisor]: + # Remove Officer from stream if not assigned anymore + remove_perm("can_work_for_stream", officer.user, stream) + messages.success( + request, "Officer {officer} has been removed.".format(officer=officer) + ) + + return redirect(reverse("production:production", args=(stream.id,))) + + +@is_production_user() +@permission_required("scipost.can_assign_production_officer", raise_exception=True) +@transaction.atomic +def update_officer(request, stream_id): + productionstream = get_object_or_404( + ProductionStream.objects.ongoing(), pk=stream_id + ) + prev_officer = productionstream.officer + checker = ObjectPermissionChecker(request.user) + + if not checker.has_perm("can_perform_supervisory_actions", productionstream): + context = { + "stream": productionstream, + "message": "You do not have permission to perform this action. ", + } + else: + prod_officer_form = AssignOfficerForm( + request.POST or None, instance=productionstream + ) + if prod_officer_form.is_valid(): + prod_officer_form.save() + officer = prod_officer_form.cleaned_data.get("officer") + + # Add officer to stream if they exist. + if officer is not None: + assign_perm("can_work_for_stream", officer.user, productionstream) + message = f"Officer {officer} has been assigned." + + event = ProductionEvent( + stream=productionstream, + event="assignment", + comments=" tasked Production Officer with proofs production:", + noted_to=officer, + noted_by=request.user.production_user, + ) + event.save() + + # Temp fix. + # TODO: Implement proper email + ProductionUtils.load({"request": request, "stream": productionstream}) + ProductionUtils.email_assigned_production_officer() + + # Remove old officer. + else: + remove_perm("can_work_for_stream", prev_officer.user, productionstream) + message = f"Officer {prev_officer} has been removed." + + else: + message = "\\n".join(prod_officer_form.errors.values()) + + context = { + "stream": productionstream, + "form": prod_officer_form, + "message": message, + } + + return render( + request, + "production/_hx_productionstream_change_prodofficer.html", + context, + ) + + +@is_production_user() +@permission_required("scipost.can_promote_user_to_production_officer") +def delete_officer(request, officer_id): + production_user = get_object_or_404(ProductionUser.objects.active(), id=officer_id) + production_user.name = "{first_name} {last_name}".format( + first_name=production_user.user.first_name, + last_name=production_user.user.last_name, + ) + production_user.user = None + production_user.save() + + messages.success( + request, "{user} removed as Production Officer".format(user=production_user) + ) + return redirect(reverse("production:production")) + + @is_production_user() @permission_required("scipost.can_assign_production_officer", raise_exception=True) @transaction.atomic @@ -424,21 +563,22 @@ def add_invitations_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): +def remove_invitations_officer(request, stream_id, officer_id): stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) checker = ObjectPermissionChecker(request.user) if not checker.has_perm("can_perform_supervisory_actions", stream): return redirect(reverse("production:production", args=(stream.id,))) - if getattr(stream.officer, "id", 0) == int(officer_id): - officer = stream.officer - stream.officer = None + if getattr(stream.invitations_officer, "id", 0) == int(officer_id): + officer = stream.invitations_officer + stream.invitations_officer = None stream.save() - if officer not in [stream.invitations_officer, stream.supervisor]: + if officer not in [stream.officer, stream.supervisor]: # Remove Officer from stream if not assigned anymore remove_perm("can_work_for_stream", officer.user, stream) messages.success( - request, "Officer {officer} has been removed.".format(officer=officer) + request, + "Invitations Officer {officer} has been removed.".format(officer=officer), ) return redirect(reverse("production:production", args=(stream.id,))) @@ -447,25 +587,66 @@ def remove_officer(request, stream_id, officer_id): @is_production_user() @permission_required("scipost.can_assign_production_officer", raise_exception=True) @transaction.atomic -def remove_invitations_officer(request, stream_id, officer_id): - stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) +def update_invitations_officer(request, stream_id): + productionstream = get_object_or_404( + ProductionStream.objects.ongoing(), pk=stream_id + ) + prev_inv_officer = productionstream.invitations_officer checker = ObjectPermissionChecker(request.user) - if not checker.has_perm("can_perform_supervisory_actions", stream): - return redirect(reverse("production:production", args=(stream.id,))) - if getattr(stream.invitations_officer, "id", 0) == int(officer_id): - officer = stream.invitations_officer - stream.invitations_officer = None - stream.save() - if officer not in [stream.officer, stream.supervisor]: - # Remove Officer from stream if not assigned anymore - remove_perm("can_work_for_stream", officer.user, stream) - messages.success( - request, - "Invitations Officer {officer} has been removed.".format(officer=officer), + if not checker.has_perm("can_perform_supervisory_actions", productionstream): + context = { + "stream": productionstream, + "message": "You do not have permission to perform this action. ", + } + else: + inv_officer_form = AssignInvitationsOfficerForm( + request.POST or None, instance=productionstream ) + if inv_officer_form.is_valid(): + inv_officer_form.save() + inv_officer = inv_officer_form.cleaned_data.get("invitations_officer") + + # Add invitations officer to stream if they exist. + if inv_officer is not None: + assign_perm("can_work_for_stream", inv_officer.user, productionstream) + message = f"Invitations Officer {inv_officer} has been assigned." + + event = ProductionEvent( + stream=productionstream, + event="assignment", + comments=" tasked Invitations Officer with invitations:", + noted_to=inv_officer, + noted_by=request.user.production_user, + ) + event.save() + + # Temp fix. + # TODO: Implement proper email + ProductionUtils.load({"request": request, "stream": productionstream}) + ProductionUtils.email_assigned_invitation_officer() + + # Remove old invitations officer. + else: + remove_perm( + "can_work_for_stream", prev_inv_officer.user, productionstream + ) + message = f"Invitations Officer {prev_inv_officer} has been removed." - return redirect(reverse("production:production", args=(stream.id,))) + else: + message = "\\n".join(inv_officer_form.errors.values()) + + context = { + "stream": productionstream, + "form": inv_officer_form, + "message": message, + } + + return render( + request, + "production/_hx_productionstream_change_invofficer.html", + context, + ) @is_production_user() @@ -542,6 +723,71 @@ class UpdateEventView(UpdateView): return super().form_valid(form) +@is_production_user() +@permission_required("scipost.can_assign_production_supervisor", raise_exception=True) +@transaction.atomic +def update_supervisor(request, stream_id): + productionstream = get_object_or_404( + ProductionStream.objects.ongoing(), pk=stream_id + ) + supervisor_form = AssignSupervisorForm( + request.POST or None, instance=productionstream + ) + prev_supervisor = productionstream.supervisor + + if supervisor_form.is_valid(): + supervisor_form.save() + supervisor = supervisor_form.cleaned_data.get("supervisor") + + # Add supervisor to stream if they exist. + if supervisor is not None: + message = f"Supervisor {supervisor} has been assigned." + + assign_perm("can_work_for_stream", supervisor.user, productionstream) + assign_perm( + "can_perform_supervisory_actions", supervisor.user, productionstream + ) + + event = ProductionEvent( + stream=productionstream, + event="assignment", + comments=" assigned Production Supervisor:", + noted_to=supervisor, + noted_by=request.user.production_user, + ) + event.save() + + # Temp fix. + # TODO: Implement proper email + ProductionUtils.load({"request": request, "stream": productionstream}) + ProductionUtils.email_assigned_supervisor() + + # Remove old supervisor. + else: + remove_perm("can_work_for_stream", prev_supervisor.user, productionstream) + remove_perm( + "can_perform_supervisory_actions", + prev_supervisor.user, + productionstream, + ) + message = f"Supervisor {supervisor} has been removed." + + else: + message = "\\n".join(supervisor_form.errors.values()) + + context = { + "stream": productionstream, + "form": supervisor_form, + "message": message, + } + + return render( + request, + "production/_hx_productionstream_change_supervisor.html", + context, + ) + + @method_decorator(is_production_user(), name="dispatch") @method_decorator( permission_required("scipost.can_view_production", raise_exception=True), @@ -848,3 +1094,27 @@ def send_proofs(request, stream_id, version): messages.success(request, "Proofs have been sent.") return redirect(stream.get_absolute_url()) + + +def render_action_buttons(request, stream_id, key): + productionstream = get_object_or_404(ProductionStream, pk=stream_id) + + # Get either the id, or the id of the object and convert it to a string + # If this fails, set to "None" + current_option = getattr(productionstream, key, None) + current_option = getattr(current_option, "id", current_option) + current_option_str = str(current_option) + + # Get the new option from the POST request, which is a string + # Set to "None" if the string is empty + new_option = request.POST.get(key, None) + new_option_str = str(new_option) or "None" + + return render( + request, + "production/_hx_productionstream_change_action_buttons.html", + { + "current_option": current_option_str, + "new_option": new_option_str, + }, + ) diff --git a/scipost_django/scipost/templatetags/bootstrap.py b/scipost_django/scipost/templatetags/bootstrap.py index f255ebd9d8061546d689dc421151155d7156347c..ac204b325c8822d5a52b1ca623d1c9b3d1cb9943 100644 --- a/scipost_django/scipost/templatetags/bootstrap.py +++ b/scipost_django/scipost/templatetags/bootstrap.py @@ -60,6 +60,19 @@ def bootstrap_inline(element, args="2,10"): return render(element, markup_classes) +@register.filter +def bootstrap_purely_inline(element, args="2,10"): + args = [arg.strip() for arg in args.split(",")] + markup_classes = { + "label": "col-auto fs-6", + "value": "col", + "single_value": "", + } + markup_classes["form_control"] = "" + + return render(element, markup_classes) + + @register.filter def bootstrap_grouped(element, args="2,10"): return bootstrap(element, args, "grouped")