diff --git a/scipost_django/colleges/forms.py b/scipost_django/colleges/forms.py index dada1f7b77c89f572c43fbde6b61374ffba91b56..9940082abc0380791f4e78c391c10f7f92ce49d8 100644 --- a/scipost_django/colleges/forms.py +++ b/scipost_django/colleges/forms.py @@ -13,6 +13,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.urls import reverse from django.utils import timezone from ontology.models import Specialty @@ -908,6 +909,75 @@ class FellowshipNominationVotingRoundSearchForm(forms.Form): return rounds +from datetime import date + + +class FellowshipNominationVotingRoundStartForm(forms.ModelForm): + class Meta: + model = FellowshipNominationVotingRound + fields = ["voting_opens", "voting_deadline"] + + widgets = { + "voting_opens": forms.DateInput( + attrs={"type": "date", "min": date.today()} + ), + "voting_deadline": forms.DateInput( + attrs={"type": "date", "min": date.today()} + ), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.helper = FormHelper() + self.helper.attrs = { + "hx-target": f"nomination-{self.instance.nomination.id}-round-tab-holder", + "hx-swap": "outerHTML", + "hx-post": reverse( + "colleges:_hx_nomination_voting_rounds_tab", + kwargs={ + "nomination_id": self.instance.nomination.id, + "round_id": self.instance.id, + }, + ), + } + self.helper.layout = Layout( + Div( + Div(Field("voting_opens"), css_class="col"), + Div(Field("voting_deadline"), css_class="col"), + Div( + ButtonHolder(Submit("submit", "Start")), + css_class="col-auto align-self-end mb-3", + ), + css_class="row mb-0", + ) + ) + + def clean(self): + if self.is_valid(): + # Check that the voting deadline is after the voting opens + if ( + self.cleaned_data["voting_deadline"] + <= self.cleaned_data["voting_opens"] + ): + self.add_error( + "voting_deadline", + "The voting deadline must be after the voting opens.", + ) + + # Check that the voting opens is after today + if self.cleaned_data["voting_opens"] < date.today(): + self.add_error( + "voting_opens", "The voting opens date must be after today." + ) + + def save(self): + voting_round = super().save(commit=False) + voting_round.save() + print(self.fields["voting_opens"]) + return voting_round + + ############### # Invitations # ############### diff --git a/scipost_django/colleges/migrations/0042_auto_20230914_1057.py b/scipost_django/colleges/migrations/0042_auto_20230914_1057.py new file mode 100644 index 0000000000000000000000000000000000000000..8d541a2576032e89376c65fd43bfa3ffe535317b --- /dev/null +++ b/scipost_django/colleges/migrations/0042_auto_20230914_1057.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.18 on 2023-09-14 08:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('colleges', '0041_auto_20230720_1608'), + ] + + operations = [ + migrations.AlterField( + model_name='fellowshipnominationvotinground', + name='voting_deadline', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='fellowshipnominationvotinground', + name='voting_opens', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/scipost_django/colleges/models/nomination.py b/scipost_django/colleges/models/nomination.py index 4adfaf87fb36feb5043bdc29de878065381934e7..23b7dbda143bb54f5c009e95a3be8316bb616c48 100644 --- a/scipost_django/colleges/models/nomination.py +++ b/scipost_django/colleges/models/nomination.py @@ -190,9 +190,9 @@ class FellowshipNominationVotingRound(models.Model): blank=True, ) - voting_opens = models.DateTimeField(blank=True) + voting_opens = models.DateTimeField(blank=True, null=True) - voting_deadline = models.DateTimeField(blank=True) + voting_deadline = models.DateTimeField(blank=True, null=True) objects = FellowshipNominationVotingRoundQuerySet.as_manager() @@ -204,6 +204,8 @@ class FellowshipNominationVotingRound(models.Model): verbose_name_plural = "Fellowship Nomination Voting Rounds" def __str__(self): + if self.voting_deadline is None or self.voting_opens is None: + return f"Unscheduled voting round for {self.nomination}" return ( f'Voting round ({self.voting_opens.strftime("%Y-%m-%d")} -' f' {self.voting_deadline.strftime("%Y-%m-%d")}) for {self.nomination}' @@ -215,14 +217,44 @@ class FellowshipNominationVotingRound(models.Model): return vote.vote return None + def add_voter(self, fellow): + self.eligible_to_vote.add(fellow) + self.save() + + def set_eligible_voters(self): + """ + Set the eligible voters for this voting round. + Eligible voters are Senior Fellows with at least one specialty in common, or, in the event + that there are fewer than 5 such Senior Fellows, all Senior Fellows of the same college. + """ + specialties_slug_list = [ + s.slug for s in self.nomination.profile.specialties.all() + ] + self.eligible_to_vote.set( + Fellowship.objects.active() + .senior() + .specialties_overlap(specialties_slug_list) + ) + if self.eligible_to_vote.count() <= 5: + # add Senior Fellows from all specialties + self.eligible_to_vote.set( + Fellowship.objects.active() + .senior() + .filter(college=self.nomination.college) + ) + + self.save() + @property def is_open(self): + if self.voting_deadline is None or self.voting_opens is None: + return False return self.voting_opens <= timezone.now() <= self.voting_deadline @property def is_scheduled(self): return self.voting_deadline is not None and self.voting_opens > timezone.now() - + @property def is_unscheduled(self): return self.voting_opens is None or self.voting_deadline is None diff --git a/scipost_django/colleges/templates/colleges/_hx_nomination_decision_form.html b/scipost_django/colleges/templates/colleges/_hx_nomination_decision_form.html index 1cb5375750713f5fa81ca56638fc2662f6c6fcc1..78cb9ec659023aa753f02899dcafdb6c489e4f5d 100644 --- a/scipost_django/colleges/templates/colleges/_hx_nomination_decision_form.html +++ b/scipost_django/colleges/templates/colleges/_hx_nomination_decision_form.html @@ -20,22 +20,25 @@ {% if "edadmin" in user_roles %} - {% if not voting_round.is_open %} + {% if voting_round.is_open %} + <p class="text-warning">The voting round is still open. You many not draft a decision yet.</p> + {% elif voting_round.is_scheduled %} + <p class="text-warning">The voting round is scheduled but has not yet started.</p> + {% elif voting_round.is_unscheduled %} + <p class="text-warning">The voting round is not yet scheduled.</p> + {% else %} {% with blocks=voting_round.decision_blocks %} - {% if blocks %} - <p>The decision cannot be fixed at this moment: {{ blocks }}</p> - {% else %} - <form hx-post="{% url 'colleges:_hx_nomination_decision_form' round_id=voting_round.id %}" - hx-target="#nomination-{{ voting_round.id }}-decision"> - {% crispy decision_form %} - </form> - {% endif %} + {% if blocks %} + <p>The decision cannot be fixed at this moment: {{ blocks }}</p> + {% else %} + <form hx-post="{% url 'colleges:_hx_nomination_decision_form' round_id=voting_round.id %}" + hx-target="#nomination-{{ voting_round.id }}-decision"> + {% crispy decision_form %} + </form> + {% endif %} - {% endwith %} - {% else %} - <p class="text-warning">The voting round is still open. You many not draft a decision yet.</p> {% endif %} {% else %} diff --git a/scipost_django/colleges/templates/colleges/_hx_nomination_voter_table.html b/scipost_django/colleges/templates/colleges/_hx_nomination_voter_table.html index 27f9622f36cbd151df34ca917e1e9ba99569b697..06e377f0dc40d7b2df3070d2179459a1bdb6def0 100644 --- a/scipost_django/colleges/templates/colleges/_hx_nomination_voter_table.html +++ b/scipost_django/colleges/templates/colleges/_hx_nomination_voter_table.html @@ -1,5 +1,5 @@ {% if voters %} - <table class="table mb-0"> + <table class="table mb-0 border"> <thead class="table-light"> <tr> <th>Fellow</th> diff --git a/scipost_django/colleges/templates/colleges/_hx_nomination_voting_rounds_tab.html b/scipost_django/colleges/templates/colleges/_hx_nomination_voting_rounds_tab.html index cb09c04f0549e0477d02b48cb5a25e02cb805442..d0eb327ec17931a892315ef34738ca8e74fae005 100644 --- a/scipost_django/colleges/templates/colleges/_hx_nomination_voting_rounds_tab.html +++ b/scipost_django/colleges/templates/colleges/_hx_nomination_voting_rounds_tab.html @@ -1,12 +1,14 @@ <div id="nomination-{{ nomination_id }}-round-tab-holder"> <nav class="nav nav-pills m-2 overflow-scroll"> - <div type="button" class="me-2 px-2 nav-link {% if new_round %}bg-success{% else %}border border-success{% endif %}" - hx-get="{% url 'colleges:_hx_nomination_voting_rounds_tab' nomination_id=nomination_id round_id=0 %}" - hx-target="#nomination-{{ nomination_id }}-round-tab-holder" - hx-swap="outerHTML"> - <span class="fs-1 align-items-center text-{% if new_round %}white{% else %}success{% endif %}">+</span> - </div> + {% if "edadmin" in user_roles %} + <div type="button" class="me-2 px-2 nav-link {% if new_round %}bg-success{% else %}border border-success{% endif %}" + hx-get="{% url 'colleges:_hx_nomination_voting_rounds_create' nomination_id=nomination_id %}" + hx-target="#nomination-{{ nomination_id }}-round-tab-holder" + hx-swap="outerHTML"> + <span class="fs-1 align-items-center text-{% if new_round %}white{% else %}success{% endif %}">+</span> + </div> + {% endif %} {% for voting_round in voting_rounds %} <div type="button" class="me-2 nav-link {% if selected_round and selected_round.id == voting_round.id %}active{% endif %} {% if voting_round.id in inaccessible_round_ids %}disabled opacity-50{% endif %}" @@ -14,29 +16,33 @@ hx-target="#nomination-{{ nomination_id }}-round-tab-holder" hx-swap="outerHTML"> <span class="d-block text-nowrap"> - <span class="d-flex justify-content-between"> - <span>Round #{{ forloop.counter0|add:1 }}</span> - {% if voting_round.is_unscheduled %} - <span class="text-danger">Uncheduled</span> - {% elif voting_round.is_scheduled %} - <span class="text-warning">Scheduled</span> + {% if voting_round.voting_opens and voting_round.voting_deadline %} + <small>{{ voting_round.voting_opens|date:"d M Y" }} - {{ voting_round.voting_deadline|date:"d M Y" }}</small> + {% else %} + <span class="badge bg-danger">Uncheduled</span> + {% endif %} + <span class="d-flex justify-content-between align-items-center"> + <span>Round #{{ forloop.revcounter }}</span> + {% if voting_round.is_scheduled %} + <span class="badge bg-warning">Scheduled</span> {% elif voting_round.is_open %} - <span class="text-success">Open</span> + <span class="badge bg-success">Open</span> {% endif %} </span> - <small>{{ voting_round.voting_opens|date:"d M Y" }} - {{ voting_round.voting_deadline|date:"d M Y" }}</small> </span> </div> {% endfor %} </nav> - <div id="nomination-{{ nomination_id }}-round-{{ selected_round.id }}-tab-content-holder" class="mt-3 px-3"> - {% if new_round %} - Newrounds! - {% else %} - {% include "colleges/_hx_voting_round_results.html" with voting_round=selected_round %} - {% endif %} - </div> + {% if selected_round %} + <div id="nomination-{{ nomination_id }}-round-{{ selected_round.id }}-tab-content-holder" class="mt-3 px-3"> + {% if selected_round.is_unscheduled %} + {% include "colleges/_hx_voting_round_creation.html" with voting_round=selected_round round_start_form=round_start_form %} + {% else %} + {% include "colleges/_hx_voting_round_results.html" with voting_round=selected_round %} + {% endif %} + </div> + {% endif %} </div> diff --git a/scipost_django/colleges/templates/colleges/_hx_voting_round_creation.html b/scipost_django/colleges/templates/colleges/_hx_voting_round_creation.html new file mode 100644 index 0000000000000000000000000000000000000000..70f6e2881a252968bcca32f72d420642a0810819 --- /dev/null +++ b/scipost_django/colleges/templates/colleges/_hx_voting_round_creation.html @@ -0,0 +1,17 @@ +{% load crispy_forms_tags %} + +<div class="row"> + <div class="col-8"> + <h4>Eligible voters</h4> + <div id="nomination-{{ nomination.id }}-round-{{ voting_round.id }}-votes" + hx-get="{% url 'colleges:_hx_nomination_voter_table' round_id=voting_round.id %}" + hx-trigger="intersect once"></div> + </div> + <div class="col-4"> + <h4>Add new voter</h4> + </div> + <div class="col-12"> + <h4>Start round</h4> + {% crispy round_start_form %} + </div> +</div> diff --git a/scipost_django/colleges/urls.py b/scipost_django/colleges/urls.py index 573d0916f90e436d03fc6735b10e29414ea96f5d..d982355533a56806c686d436a95d4e308c5a569d 100644 --- a/scipost_django/colleges/urls.py +++ b/scipost_django/colleges/urls.py @@ -218,6 +218,11 @@ urlpatterns = [ views._hx_nomination_voting_rounds_tab, name="_hx_nomination_voting_rounds_tab", ), + path( + "_hx_nomination_voting_rounds_create", + views._hx_nomination_voting_rounds_create, + name="_hx_nomination_voting_rounds_create", + ), ] ), ), diff --git a/scipost_django/colleges/views.py b/scipost_django/colleges/views.py index 02d765985221a3036dc90897818d8f043c98e72d..1fe2aaf9d927c98d7d439aee3fd80115fb8cc9d4 100644 --- a/scipost_django/colleges/views.py +++ b/scipost_django/colleges/views.py @@ -43,6 +43,7 @@ from .constants import ( from .forms import ( CollegeChoiceForm, FellowshipNominationSearchForm, + FellowshipNominationVotingRoundStartForm, FellowshipSearchForm, FellowshipDynSelForm, FellowshipForm, @@ -903,7 +904,7 @@ def _hx_voting_round_results(request, round_id): def _hx_nomination_voting_rounds_tab(request, nomination_id, round_id): """Render the selected voting round contents and display the others as tabs.""" nomination = get_object_or_404(FellowshipNomination, pk=nomination_id) - voting_rounds = nomination.voting_rounds.all() + voting_rounds = nomination.voting_rounds.all().order_by("-voting_opens") inaccessible_round_ids = [ round.id for round in voting_rounds if not round.can_view(request.user) @@ -916,14 +917,48 @@ def _hx_nomination_voting_rounds_tab(request, nomination_id, round_id): "new_round": False, } - if round_id == 0: - context["new_round"] = True - else: - context["selected_round"] = voting_rounds.get(id=round_id) + if round_id != 0: + selected_round = voting_rounds.get(id=round_id) + context["selected_round"] = selected_round + + print(request.POST) + + if selected_round.voting_opens is None: + today = datetime.date.today() + round_start_form = FellowshipNominationVotingRoundStartForm( + request.POST or None, + instance=selected_round, + initial={ + "voting_opens": today, + "voting_deadline": today + datetime.timedelta(days=14), + } + if (request.POST is None) and (selected_round.voting_opens is None) + else None, + ) + if round_start_form.is_valid(): + round_start_form.save() + messages.success( + request, + f"Voting round for {nomination.profile} started from now until {selected_round.voting_deadline}.", + ) + context["round_start_form"] = round_start_form return render(request, "colleges/_hx_nomination_voting_rounds_tab.html", context) +@login_required +@user_passes_test(is_edadmin) +def _hx_nomination_voting_rounds_create(request, nomination_id): + nomination = get_object_or_404(FellowshipNomination, pk=nomination_id) + new_round = FellowshipNominationVotingRound( + nomination=nomination, voting_opens=None, voting_deadline=None + ) + new_round.save() + new_round.set_eligible_voters() + + return _hx_nomination_voting_rounds_tab(request, nomination_id, new_round.id) + + def _hx_voting_round_li_contents(request, round_id): """For (re)loading the details if modified.""" round = get_object_or_404(FellowshipNominationVotingRound, pk=round_id) diff --git a/scipost_django/production/forms.py b/scipost_django/production/forms.py index 34c1cb0dbc8a9b45fcfc16c3bd8c086888cf284d..bf3a70f7d8ac22f39382b8a1c0bdef72ebea2672 100644 --- a/scipost_django/production/forms.py +++ b/scipost_django/production/forms.py @@ -376,7 +376,6 @@ class ProductionStreamSearchForm(forms.Form): for field in self.fields: if field in session: self.fields[field].initial = session[field] - print(field, session[field]) self.helper = FormHelper() self.helper.layout = Layout(