diff --git a/scipost_django/colleges/forms.py b/scipost_django/colleges/forms.py index 467335b79c110a08123ea72239400b2bec772680..5e921e727bdb8e47fb9c2272330d795677383796 100644 --- a/scipost_django/colleges/forms.py +++ b/scipost_django/colleges/forms.py @@ -11,6 +11,7 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Div, Field, ButtonHolder, Submit from crispy_bootstrap5.bootstrap5 import FloatingField from dal import autocomplete +from django.utils import timezone from ontology.models import Specialty from proceedings.models import Proceedings @@ -593,20 +594,63 @@ class FellowshipInvitationResponseForm(forms.ModelForm): self.helper.layout = Layout( Field("nomination", type="hidden"), Div( - Div(Field("response"), css_class="col-lg-5"), - Div(Field("postpone_start_to"), css_class="col-lg-5"), - css_class="row", - ), - Div( + Div( + Div( + Div(Field("response"), css_class="col-12"), + Div(Field("postpone_start_to"), css_class="col-12"), + css_class="row mb-0", + ), + css_class="col-12 col-md-5", + ), Div( Field( "comments", placeholder="Add a comment (visible to EdAdmin)", - rows=2, + rows=4, ), - css_class="col-lg-10", + css_class="col-12 col-md-7", ), - Div(ButtonHolder(Submit("submit", "Submit")), css_class="col-lg-2"), - css_class="row mt-0", + Div(ButtonHolder(Submit("submit", "Update")), css_class="col-auto"), + css_class="row mb-0", ), ) + + def clean(self): + has_contributor = hasattr( + self.cleaned_data["nomination"].profile, "contributor" + ) + invitation_accepted = self.cleaned_data["response"] == ( + FellowshipInvitation.RESPONSE_ACCEPTED + ) + invitation_postponed = self.cleaned_data["response"] == ( + FellowshipInvitation.RESPONSE_POSTPONED + ) + postponed_date = self.cleaned_data["postpone_start_to"] + + if (invitation_accepted or invitation_postponed) and not has_contributor: + self.add_error( + "response", + "This profile does not have a Contributor account to create a Fellowship with. Please create one before updating the invitation response to a positive answer.", + ) + + if postponed_date and (timezone.now().date() > postponed_date): + self.add_error( + "postpone_start_to", + "You cannot set a postponed start date in the past.", + ) + + if ( + invitation_accepted + and (postponed_date is not None) + and (postponed_date != timezone.now().date()) + ): + self.add_error( + "postpone_start_to", + "If the invitation is accepted for immediate start, you cannot postpone its start date.", + ) + + if invitation_postponed and not postponed_date: + self.add_error( + "postpone_start_to", + "If the invitation is postponed, you must set a start date in the future.", + ) diff --git a/scipost_django/colleges/templates/colleges/_hx_nomination_invitation_update_response.html b/scipost_django/colleges/templates/colleges/_hx_nomination_invitation_update_response.html index 93e3b3a96fdf875d148449139ba2a9e0ac558463..8969da1766f0485a125de5e4c3052cf658992c95 100644 --- a/scipost_django/colleges/templates/colleges/_hx_nomination_invitation_update_response.html +++ b/scipost_django/colleges/templates/colleges/_hx_nomination_invitation_update_response.html @@ -1,8 +1,7 @@ {% load crispy_forms_tags %} <div class="m-2 mt-4"> <form hx-post="{% url 'colleges:_hx_fellowship_invitation_update_response' invitation_id=invitation.id %}" - hx-target="#invitations_tablist" - > + hx-target="#invitation-{{ invitation.id }}-update-response"> {% crispy form %} </form> </div> diff --git a/scipost_django/colleges/templates/colleges/_hx_nominations_invitations.html b/scipost_django/colleges/templates/colleges/_hx_nominations_invitations.html index 7b4a51157171e752c5ab980bcbd7eb696c96e8af..1cdfb939981a5b9e8102e215d10cb06bf6f96d8f 100644 --- a/scipost_django/colleges/templates/colleges/_hx_nominations_invitations.html +++ b/scipost_django/colleges/templates/colleges/_hx_nominations_invitations.html @@ -4,52 +4,28 @@ <details id="invitation-{{ invitation.id }}-details" class="my-2 border border-2"> <summary class="bg-light p-2">{{ invitation }}</summary> - <details class="m-2 mt-4 border"> - <summary class="p-2 bg-light">Events for this nomination</summary> - {% include 'colleges/_nomination_events_table.html' with nomination=invitation.nomination %} - </details> - <div class="p-2"> - <h4>Checklist</h4> - <ul> - - {% if not invitation.nomination.profile.contributor %} - - <li class="text-danger">N.B.: this nominee is not yet registered as a Contributor</li> - - {% else %} - - <li> - <span class="text-success">{% include 'bi/check-square-fill.html' %}</span> This nominee has a Contributor account - </li> - - {% endif %} - - {% if selected == 'notyetinvited' %} - - <li> - For named or elected, but not yet invited: - <a class="btn btn-primary" - href="{% url 'colleges:fellowship_invitation_email_initial' pk=invitation.id %}">prepare and send initial email</a> - - </li> - - {% elif selected == 'accepted' %} - - <li> - Accepted to serve as Fellow but not currently active in a College? <a href="{% url 'colleges:fellowship_create' contributor_id=invitation.nomination.profile.contributor.id %}" - target="_blank">Set up a Fellowship</a> - </li> - - {% endif %} - - </ul> - - <hr /> + <div class="row"> + <div class="col-12 col-md"> + <details class="m-2 mt-3 border"> + <summary class="p-2 bg-light list-triangle">Events</summary> + {% include 'colleges/_nomination_events_table.html' with nomination=invitation.nomination %} + </details> + </div> + <div class="col-12 col-md"> + <details open class="m-2 mt-3 border"> + <summary class="p-2 bg-light list-triangle">Checklist</summary> + {% include 'colleges/_nominations_invitation_checklist.html' with invitation=invitation %} + </details> + </div> + </div> + + <div class="p-2"> <h4>Update the response to this invitation:</h4> <div id="invitation-{{ invitation.id }}-update-response" hx-get="{% url 'colleges:_hx_fellowship_invitation_update_response' invitation_id=invitation.id %}" - hx-trigger="toggle from:#invitation-{{ invitation.id }}-details"></div> + hx-trigger="toggle from:#invitation-{{ invitation.id }}-details" + hx-target="this"></div> </div> </details> {% empty %} diff --git a/scipost_django/colleges/templates/colleges/_nominations_invitation_checklist.html b/scipost_django/colleges/templates/colleges/_nominations_invitation_checklist.html new file mode 100644 index 0000000000000000000000000000000000000000..20e752af4aab24394ea17577cd129adff0a8527d --- /dev/null +++ b/scipost_django/colleges/templates/colleges/_nominations_invitation_checklist.html @@ -0,0 +1,32 @@ +<div class="p-2"> + <ul class="mb-0"> + + {% if not invitation.nomination.profile.contributor %} + + <li class="text-danger">N.B.: this nominee is not yet registered as a Contributor</li> + + {% else %} + + <li> + <span class="text-success">{% include 'bi/check-square-fill.html' %}</span> This nominee has a Contributor account + </li> + + {% endif %} + + {% if selected == 'notyetinvited' %} + + <li class="text-danger">This nominee is elected, but not yet invited.</li> + <a class="btn btn-primary" + href="{% url 'colleges:fellowship_invitation_email_initial' pk=invitation.id %}">Invite nominee</a> + + {% elif selected == 'accepted' %} + + <li> + Accepted to serve as Fellow but not currently active in a College? <a href="{% url 'colleges:fellowship_create' contributor_id=invitation.nomination.profile.contributor.id %}" + target="_blank">Set up a Fellowship</a> + </li> + + {% endif %} + + </ul> +</div> diff --git a/scipost_django/colleges/templates/colleges/nominations.html b/scipost_django/colleges/templates/colleges/nominations.html index 4eaecfc68dbc04384293c47cf7bad35da8343809..f24bdaad945150e85daf5f95e90cb95986d6953c 100644 --- a/scipost_django/colleges/templates/colleges/nominations.html +++ b/scipost_django/colleges/templates/colleges/nominations.html @@ -138,8 +138,9 @@ </summary> <div class="p-2"> - <div id="voting_tablist" hx-trigger="toggle from:#voting-details, click from:body target:.nomination-start-round-btn" - hx-get="{% url 'colleges:_hx_voting_rounds' %}?tab={% if 'edadmin' in user_roles %}ongoing{% else %}ongoing-vote_required{% endif %}" ></div> + <div id="voting_tablist" hx-trigger="toggle from:#voting-details, click from:body target:.nomination-start-round-btn" hx-get="{% url 'colleges:_hx_voting_rounds' %}?tab= + {% if 'edadmin' in user_roles %}ongoing{% else %}ongoing-vote_required{% endif %} + "></div> </div> </details> @@ -152,7 +153,8 @@ <div class="p-2 mt-2"> <div id="invitations_tablist" hx-get="{% url 'colleges:_hx_nominations_invitations' %}?response=notyetinvited" - hx-trigger="toggle from:#invitations-details"></div> + hx-trigger="toggle from:#invitations-details" + hx-target="#invitations_tablist"></div> </div> </details> {% endif %} diff --git a/scipost_django/colleges/views.py b/scipost_django/colleges/views.py index 8cc95efe419afb8fbf56a4f09fe71bcee18fc324..1593db62ed801b0d94682a3c8fca01d360e862f8 100644 --- a/scipost_django/colleges/views.py +++ b/scipost_django/colleges/views.py @@ -1033,12 +1033,69 @@ def _hx_fellowship_invitation_update_response(request, invitation_id): description=f"Response updated to: {invitation.get_response_display()}", by=request.user.contributor, ) - return redirect( - "%s?response=%s" - % ( - reverse("colleges:_hx_nominations_invitations"), - form.cleaned_data["response"], + + nonexpired_fellowship = ( + Fellowship.objects.exclude( + until_date__lte=timezone.now().date(), + ) + .filter( + college=invitation.nomination.college, + contributor=invitation.nomination.profile.contributor, ) + .order_by("-start_date") + .first() + ) + + # If the invitation is accepted or postponed, create a Fellowship + if invitation.response in [ + FellowshipInvitation.RESPONSE_ACCEPTED, + FellowshipInvitation.RESPONSE_POSTPONED, + ]: + # Create a new Fellowship if no object exists + if not nonexpired_fellowship: + fellowship = Fellowship.objects.create( + college=invitation.nomination.college, + contributor=invitation.nomination.profile.contributor, + start_date=timezone.now() + if invitation.response == FellowshipInvitation.RESPONSE_ACCEPTED + else invitation.postpone_start_to, + until_date=None, + ) + + invitation.nomination.add_event( + description=f"Fellowship created (start: {fellowship.start_date.strftime('%Y-%m-%d')})", + by=request.user.contributor, + ) + else: + # Update the start date of the Fellowship if an object already exists + nonexpired_fellowship.start_date = ( + timezone.now() + if invitation.response == FellowshipInvitation.RESPONSE_ACCEPTED + else invitation.postpone_start_to + ) + nonexpired_fellowship.until_date = None + invitation.nomination.add_event( + description=f"Fellowship start date updated (start: {nonexpired_fellowship.start_date.strftime('%Y-%m-%d')})", + by=request.user.contributor, + ) + nonexpired_fellowship.save() + # Terminate the Fellowship if the invitation is declined + elif invitation.response == FellowshipInvitation.RESPONSE_DECLINED: + if nonexpired_fellowship: + nonexpired_fellowship.until_date = ( + timezone.now().date() + if nonexpired_fellowship.is_active() + else nonexpired_fellowship.start_date + ) + invitation.nomination.add_event( + description=f"Fellowship ended (end: {nonexpired_fellowship.until_date.strftime('%Y-%m-%d')})", + by=request.user.contributor, + ) + nonexpired_fellowship.save() + + return HTMXResponse( + f"Response updated to: {invitation.get_response_display()}", + tag="success", ) context = { "invitation": invitation,