diff --git a/scipost_django/colleges/models/nomination.py b/scipost_django/colleges/models/nomination.py index e42eb87f4d97e7db235c670054cbb9819c0172f4..6487d2a841550277eeda7752d74b85c4b2a48bf1 100644 --- a/scipost_django/colleges/models/nomination.py +++ b/scipost_django/colleges/models/nomination.py @@ -4,6 +4,7 @@ __license__ = "AGPL v3" from django.db import models from django.utils import timezone +from django.utils.functional import cached_property from ..managers import ( FellowshipNominationQuerySet, @@ -11,6 +12,8 @@ from ..managers import ( FellowshipNominationVoteQuerySet, ) +from colleges.models import Fellowship + from scipost.models import get_sentinel_user @@ -105,6 +108,20 @@ class FellowshipNomination(models.Model): return "Latest voting round is ongoing, and not everybody has voted." return "No voting round found." + # FIX: This is wrong semantically... + @property + def get_eligible_voters(self): + specialties_slug_list = [s.slug for s in self.profile.specialties.all()] + + eligible_voters = ( + Fellowship.objects.active() + .senior() + .specialties_overlap(specialties_slug_list) + .distinct() + ) + + return eligible_voters + class FellowshipNominationEvent(models.Model): nomination = models.ForeignKey( diff --git a/scipost_django/colleges/templates/colleges/_hx_nomination_eligible_voters.html b/scipost_django/colleges/templates/colleges/_hx_nomination_eligible_voters.html new file mode 100644 index 0000000000000000000000000000000000000000..256c3de0e75d0020b73ac346cc524cc99df33298 --- /dev/null +++ b/scipost_django/colleges/templates/colleges/_hx_nomination_eligible_voters.html @@ -0,0 +1,12 @@ +<h4>Eligible Voters</h4> + +{% include 'colleges/_hx_nomination_eligible_voters_table.html' with eligible_voters=eligible_voters %} + +{% if eligible_voters|length > 0 %} + <button id="nomination-{{ nomination.id }}-start-round-btn" + hx-get="{% url 'colleges:_hx_nomination_round_start' nomination_id=nomination.id %}" + hx-target="#nomination-{{ nomination.id }}-eligible-voters" + class="nomination-start-round-btn btn btn-{% if eligible_voters|length < 5 %}warning{% else %}primary{% endif %}" + {% if eligible_voters|length < 5 %}hx-confirm="Are you sure you want to start a round with fewer than 5 voters?"{% endif %} + >Start round</button> +{% endif %} diff --git a/scipost_django/colleges/templates/colleges/_hx_nomination_eligible_voters_table.html b/scipost_django/colleges/templates/colleges/_hx_nomination_eligible_voters_table.html new file mode 100644 index 0000000000000000000000000000000000000000..009a96b264c945a49c4151bc47c823eb7d1cdf72 --- /dev/null +++ b/scipost_django/colleges/templates/colleges/_hx_nomination_eligible_voters_table.html @@ -0,0 +1,39 @@ +{% if eligible_voters %} + <table class="table"> + <thead class="table-light"> + <tr> + <th>Fellow</th> + <th>College</th> + <th>Specialties</th> + <th>Type</th> + </tr> + </thead> + + <tbody> + + {% for voter in eligible_voters %} + <tr> + <td>{{ voter.contributor }}</td> + <td>{{ voter.college.name }}</td> + <td> + + {% for specialty in voter.contributor.profile.specialties.all %} + <div class="single d-inline + {% if specialty in nominee_specialties %}text-success{% endif %} + " data-specialty="{{ specialty.slug }}" data-bs-placement="bottom" title="{{ specialty }}"> + {{ specialty.code }} + </div> + {% endfor %} + + + </td> + <td>{{ voter.get_status_display }}</td> + </tr> + {% endfor %} + + + </tbody> + </table> +{% else %} + <p class="text-danger">No eligible voters found.</p> +{% endif %} 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 ecc14a497cb643f684da0fde831c2b8023206a6f..27f9622f36cbd151df34ca917e1e9ba99569b697 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"> + <table class="table mb-0"> <thead class="table-light"> <tr> <th>Fellow</th> diff --git a/scipost_django/colleges/templates/colleges/_hx_nominations_no_round_started.html b/scipost_django/colleges/templates/colleges/_hx_nominations_no_round_started.html new file mode 100644 index 0000000000000000000000000000000000000000..cf3332cdf7d87af421e538b1ac5094b168d8e159 --- /dev/null +++ b/scipost_django/colleges/templates/colleges/_hx_nominations_no_round_started.html @@ -0,0 +1,16 @@ +{% for nomination in nominations_no_round_started %} + <details id="nomination-{{ nomination.id }}-start-round" + class="border border-2 my-2"> + <summary class="p-2 bg-light">{{ nomination }}</summary> + + <div class="p-2 mt-2"> + <div id="nomination-{{ nomination.id }}-eligible-voters" + hx-get="{% url 'colleges:_hx_nomination_eligible_voters' nomination_id=nomination.id %}" + hx-trigger="toggle from:#nomination-{{ nomination.id }}-start-round"></div> + + </div> + + </details> +{% empty %} + <p>All nominations have had their voting round(s) started.</p> +{% endfor %} diff --git a/scipost_django/colleges/templates/colleges/_hx_voting_round_results.html b/scipost_django/colleges/templates/colleges/_hx_voting_round_results.html index 646a4a9f4f5fbcd08b38ba82a2072f4f746b6d1c..a6d5f3d3e2ab558281a220250aad72f30d7b5043 100644 --- a/scipost_django/colleges/templates/colleges/_hx_voting_round_results.html +++ b/scipost_django/colleges/templates/colleges/_hx_voting_round_results.html @@ -10,7 +10,7 @@ {% endif %} <div class="row mb-0"> - <div class="col"> + <div class="col-12 col-md mt-3"> <h3>Decision</h3> {% if voting_round.decision.outcome == 'elected' %} @@ -37,6 +37,6 @@ {% endif %} </div> - <div class="col">{% include "colleges/_voting_results_box.html" with voting_round=voting_round %}</div> + <div class="col-12 col-md mt-3">{% include "colleges/_voting_results_box.html" with voting_round=voting_round %}</div> </div> </div> diff --git a/scipost_django/colleges/templates/colleges/nominations.html b/scipost_django/colleges/templates/colleges/nominations.html index bfabf3fb51cb342b6ad682ee246bbf01645681fd..d8eb941542c924e4238aa4712f33b7c14312b49e 100644 --- a/scipost_django/colleges/templates/colleges/nominations.html +++ b/scipost_django/colleges/templates/colleges/nominations.html @@ -106,13 +106,20 @@ <details id="ensure-specialties-details" class="border border-danger border-2 mt-4"> <summary class="bg-danger bg-opacity-10 p-2 d-flex list-triangle"> - <div class="fs-5">Ensure specialties</div> + <div class="fs-5">Start voting</div> </summary> <div class="p-2"> + <h3>Add specialties</h3> <div id="nominations_needing_specialties" hx-get="{% url 'colleges:_hx_nominations_needing_specialties' %}" hx-trigger="toggle from:#ensure-specialties-details"></div> + + <h3 class="mt-4 ">Start round</h3> + <div id="nominations_no_round_started" + hx-get="{% url 'colleges:_hx_nominations_no_round_started' %}" + hx-trigger="toggle from:#ensure-specialties-details, click from:body target:.nomination-start-round-btn"> + </div> </div> </details> {% endif %} @@ -133,7 +140,8 @@ <div class="p-2"> <div id="voting_tablist" hx-get="{% url 'colleges:_hx_voting_rounds' %}?tab= {% if 'edadmin' in user_roles %}ongoing{% else %}ongoing-vote_required{% endif %} - " hx-trigger="toggle from:#voting-details" hx-target="this" hx-swap="innerHTML"></div> + " hx-trigger="toggle from:#voting-details, click from:body target:.nomination-start-round-btn" hx-target="this" hx-swap="innerHTML"> + </div> </div> </details> diff --git a/scipost_django/colleges/urls.py b/scipost_django/colleges/urls.py index 11e17f1a86e27ff8634415fecb6bbb48bc5579a0..01027c691445297340487b6834ee380a6fc3b682 100644 --- a/scipost_django/colleges/urls.py +++ b/scipost_django/colleges/urls.py @@ -189,6 +189,21 @@ urlpatterns = [ views._hx_nominations_needing_specialties, name="_hx_nominations_needing_specialties", ), + path( + "_hx_nominations_no_round_started", + views._hx_nominations_no_round_started, + name="_hx_nominations_no_round_started", + ), + path( + "_hx_nomination_eligible_voters/<int:nomination_id>", + views._hx_nomination_eligible_voters, + name="_hx_nomination_eligible_voters", + ), + path( + "_hx_nomination_round_start/<int:nomination_id>", + views._hx_nomination_round_start, + name="_hx_nomination_round_start", + ), path( "_hx_voting_rounds", views._hx_voting_rounds, diff --git a/scipost_django/colleges/views.py b/scipost_django/colleges/views.py index 7650a34b3f8d5390b809eb4a34dc2e052385b818..ec3c3c6276b1455f20d5f60d4b55dd8662380ab0 100644 --- a/scipost_django/colleges/views.py +++ b/scipost_django/colleges/views.py @@ -28,6 +28,7 @@ from colleges.permissions import ( is_edadmin_or_advisory_or_active_regular_or_senior_fellow, ) from colleges.utils import check_profile_eligibility_for_fellowship +from scipost.permissions import HTMXResponse from submissions.models import Submission from .constants import ( @@ -762,6 +763,63 @@ def _hx_nominations_needing_specialties(request): ) +@login_required +@user_passes_test(is_edadmin_or_senior_fellow) +def _hx_nominations_no_round_started(request): + nominations_no_round_started = FellowshipNomination.objects.exclude( + profile__specialties__isnull=True + ).filter(voting_rounds__isnull=True) + context = { + "nominations_no_round_started": nominations_no_round_started, + } + return render( + request, + "colleges/_hx_nominations_no_round_started.html", + context, + ) + + +@login_required +@user_passes_test(is_edadmin_or_senior_fellow) +def _hx_nomination_eligible_voters(request, nomination_id): + nomination = FellowshipNomination.objects.get(pk=nomination_id) + eligible_voters = nomination.get_eligible_voters + context = { + "nomination": nomination, + "eligible_voters": eligible_voters, + "nominee_specialties": nomination.profile.specialties.all(), + } + return render( + request, + "colleges/_hx_nomination_eligible_voters.html", + context, + ) + + +def _hx_nomination_round_start(request, nomination_id): + """Create a voting round for the specified nomination using the default eligible voters.""" + nomination = get_object_or_404(FellowshipNomination, pk=nomination_id) + # Find ongoing round for this nomination, if any + ongoing_round = nomination.voting_rounds.ongoing().first() + if ongoing_round: + return HTMXResponse( + f"Round for {nomination.profile} already ongoing until {ongoing_round.voting_deadline}", + tag="danger", + ) + voting_round = FellowshipNominationVotingRound( + nomination=nomination, + voting_opens=timezone.now(), + voting_deadline=timezone.now() + datetime.timedelta(days=14), + ) + voting_round.save() + voting_round.eligible_to_vote.set(nomination.get_eligible_voters) + voting_round.save() + return HTMXResponse( + f"Started round for {nomination.profile} from now until {voting_round.voting_deadline}", + tag="success", + ) + + @login_required @user_passes_test(is_edadmin_or_advisory_or_active_regular_or_senior_fellow) def _hx_nominations(request):