From 4536d03f26dd8e61f74657d1dbe900461d1f69ae Mon Sep 17 00:00:00 2001
From: George Katsikas <giorgakis.katsikas@gmail.com>
Date: Tue, 5 Sep 2023 15:46:38 +0200
Subject: [PATCH] add voting round searchable list to nominations

---
 scipost_django/colleges/forms.py              | 171 +++++++++++++++++-
 scipost_django/colleges/managers.py           |   4 +
 scipost_django/colleges/models/nomination.py  |   8 +-
 .../_hx_nomination_decision_form.html         |   6 +-
 .../colleges/_hx_nomination_li_contents.html  |  30 +--
 .../colleges/_hx_voting_round_details.html    |  11 ++
 .../_hx_voting_round_li_contents.html         |  19 ++
 .../colleges/_hx_voting_round_list.html       |  26 +++
 .../_hx_voting_round_search_form.html         |  10 +
 .../colleges/_hx_voting_round_summary.html    |  36 ++++
 .../templates/colleges/nominations.html       |  37 ++++
 scipost_django/colleges/urls.py               |  63 ++++++-
 scipost_django/colleges/views.py              |  54 +++++-
 .../commands/add_groups_and_permissions.py    |  12 ++
 14 files changed, 442 insertions(+), 45 deletions(-)
 create mode 100644 scipost_django/colleges/templates/colleges/_hx_voting_round_details.html
 create mode 100644 scipost_django/colleges/templates/colleges/_hx_voting_round_li_contents.html
 create mode 100644 scipost_django/colleges/templates/colleges/_hx_voting_round_list.html
 create mode 100644 scipost_django/colleges/templates/colleges/_hx_voting_round_search_form.html
 create mode 100644 scipost_django/colleges/templates/colleges/_hx_voting_round_summary.html

diff --git a/scipost_django/colleges/forms.py b/scipost_django/colleges/forms.py
index 5e921e727..7b56621b2 100644
--- a/scipost_django/colleges/forms.py
+++ b/scipost_django/colleges/forms.py
@@ -3,8 +3,10 @@ __license__ = "AGPL v3"
 
 
 import datetime
+from typing import Dict
 
 from django import forms
+from django.contrib.sessions.backends.db import SessionStore
 from django.db.models import Q
 
 from crispy_forms.helper import FormHelper
@@ -28,6 +30,7 @@ from .models import (
     FellowshipNomination,
     FellowshipNominationComment,
     FellowshipNominationDecision,
+    FellowshipNominationVotingRound,
     FellowshipInvitation,
 )
 from .constants import (
@@ -560,11 +563,11 @@ class FellowshipNominationDecisionForm(forms.ModelForm):
             Field("voting_round", type="hidden"),
             Field("fixed_on", type="hidden"),
             Div(
-                Div(Field("comments"), css_class="col-8"),
+                Div(Field("comments"), css_class="col-12 col-lg-8"),
                 Div(
                     Field("outcome"),
                     ButtonHolder(Submit("submit", "Submit")),
-                    css_class="col-4",
+                    css_class="col-12 col-lg-4",
                 ),
                 css_class="row",
             ),
@@ -574,6 +577,170 @@ class FellowshipNominationDecisionForm(forms.ModelForm):
             self.fields["outcome"].initial = voting_round.vote_outcome
 
 
+#################
+# Voting Rounds #
+#################
+
+
+class FellowshipNominationVotingRoundSearchForm(forms.Form):
+    all_rounds = FellowshipNominationVotingRound.objects.all()
+
+    nominee = forms.CharField(max_length=100, required=False, label="Nominee")
+
+    college = forms.MultipleChoiceField(
+        choices=College.objects.all().order_by("name").values_list("id", "name"),
+        required=False,
+    )
+
+    decision = forms.ChoiceField(
+        choices=[("", "Any"), ("pending", "Pending")]
+        + FellowshipNominationDecision.OUTCOME_CHOICES,
+        required=False,
+    )
+
+    can_vote = forms.BooleanField(
+        label="I can vote",
+        required=False,
+        initial=True,
+    )
+    voting_open = forms.BooleanField(
+        label="Voting open",
+        required=False,
+        initial=True,
+    )
+
+    orderby = forms.ChoiceField(
+        label="Order by",
+        choices=(
+            ("voting_deadline", "Deadline"),
+            ("voting_opens", "Voting start"),
+            ("decision__outcome", "Decision"),
+            ("nomination__profile__last_name", "Nominee"),
+        ),
+        required=False,
+    )
+    ordering = forms.ChoiceField(
+        label="Ordering",
+        choices=(
+            # FIXME: Emperically, the ordering appers to be reversed for dates?
+            ("-", "Ascending"),
+            ("+", "Descending"),
+        ),
+        required=False,
+    )
+
+    def __init__(self, *args, **kwargs):
+        self.user = kwargs.pop("user")
+        self.session_key = kwargs.pop("session_key", None)
+        super().__init__(*args, **kwargs)
+
+        # Set the initial values of the form fields from the session data
+        if self.session_key:
+            session = SessionStore(session_key=self.session_key)
+
+            for field in self.fields:
+                if field in session:
+                    self.fields[field].initial = session[field]
+
+        self.helper = FormHelper()
+        self.helper.layout = Layout(
+            Div(
+                Div(
+                    Div(
+                        Div(FloatingField("nominee"), css_class="col-6 col-lg-6"),
+                        Div(FloatingField("decision"), css_class="col-3 col-lg-4"),
+                        Div(
+                            Div(
+                                Div(Field("can_vote"), css_class="col-12"),
+                                Div(Field("voting_open"), css_class="col-12"),
+                                css_class="row mb-0",
+                            ),
+                            css_class="col-3 col-lg-2",
+                        ),
+                        Div(FloatingField("orderby"), css_class="col-6"),
+                        Div(FloatingField("ordering"), css_class="col-6"),
+                        css_class="row mb-0",
+                    ),
+                    css_class="col",
+                ),
+                Div(
+                    Field("college", size=5),
+                    css_class="col-12 col-md-6 col-lg-4",
+                ),
+                css_class="row mb-0",
+            ),
+        )
+
+    def apply_filter_set(self, filters: Dict, none_on_empty: bool = False):
+        # Apply the filter set to the form
+        for key in self.fields:
+            if key in filters:
+                self.fields[key].initial = filters[key]
+            elif none_on_empty:
+                if isinstance(self.fields[key], forms.MultipleChoiceField):
+                    self.fields[key].initial = []
+                else:
+                    self.fields[key].initial = None
+
+    def search_results(self):
+        # Save the form data to the session
+        if self.session_key is not None:
+            session = SessionStore(session_key=self.session_key)
+
+            for key in self.cleaned_data:
+                session[key] = self.cleaned_data.get(key)
+
+            session.save()
+
+        rounds = FellowshipNominationVotingRound.objects.all()
+
+        if self.cleaned_data.get("can_vote") or not self.user.has_perm(
+            "scipost.can_view_all_nomination_voting_rounds"
+        ):
+            # Restrict rounds to those the user can vote on
+            rounds = rounds.where_user_can_vote(self.user)
+
+        if nominee := self.cleaned_data.get("nominee"):
+            rounds = rounds.filter(
+                Q(nomination__profile__first_name__icontains=nominee)
+                | Q(nomination__profile__last_name__icontains=nominee)
+            )
+        if college := self.cleaned_data.get("college"):
+            rounds = rounds.filter(nomination__college__id__in=college)
+        if decision := self.cleaned_data.get("decision"):
+            if decision == "pending":
+                rounds = rounds.filter(decision__isnull=True)
+            else:
+                rounds = rounds.filter(decision__outcome=decision)
+        if self.cleaned_data.get("voting_open"):
+            rounds = rounds.filter(
+                Q(voting_opens__lte=timezone.now())
+                & Q(voting_deadline__gte=timezone.now())
+            )
+
+        # Ordering of streams
+        # Only order if both fields are set
+        if (orderby_value := self.cleaned_data.get("orderby")) and (
+            ordering_value := self.cleaned_data.get("ordering")
+        ):
+            # Remove the + from the ordering value, causes a Django error
+            ordering_value = ordering_value.replace("+", "")
+
+            # Ordering string is built by the ordering (+/-), and the field name
+            # from the orderby field split by "," and joined together
+            rounds = rounds.order_by(
+                *[
+                    ordering_value + order_part
+                    for order_part in orderby_value.split(",")
+                ]
+            )
+
+        return rounds
+
+
+###############
+# Invitations #
+###############
 class FellowshipInvitationResponseForm(forms.ModelForm):
     class Meta:
         model = FellowshipInvitation
diff --git a/scipost_django/colleges/managers.py b/scipost_django/colleges/managers.py
index 1cda9e56e..1e7c47e99 100644
--- a/scipost_django/colleges/managers.py
+++ b/scipost_django/colleges/managers.py
@@ -141,6 +141,10 @@ class FellowshipNominationVotingRoundQuerySet(models.QuerySet):
         now = timezone.now()
         return self.filter(voting_deadline__lte=now)
 
+    def where_user_can_vote(self, user):
+        user_fellowships = user.contributor.fellowships.active()
+        return self.filter(eligible_to_vote__in=user_fellowships)
+
 
 class FellowshipNominationVoteQuerySet(models.QuerySet):
     def agree(self):
diff --git a/scipost_django/colleges/models/nomination.py b/scipost_django/colleges/models/nomination.py
index 1db4b616d..a3e6cf745 100644
--- a/scipost_django/colleges/models/nomination.py
+++ b/scipost_django/colleges/models/nomination.py
@@ -189,9 +189,9 @@ class FellowshipNominationVotingRound(models.Model):
         blank=True,
     )
 
-    voting_opens = models.DateTimeField()
+    voting_opens = models.DateTimeField(blank=True)
 
-    voting_deadline = models.DateTimeField()
+    voting_deadline = models.DateTimeField(blank=True)
 
     objects = FellowshipNominationVotingRoundQuerySet.as_manager()
 
@@ -295,10 +295,10 @@ class FellowshipNominationDecision(models.Model):
 
     OUTCOME_ELECTED = "elected"
     OUTCOME_NOT_ELECTED = "notelected"
-    OUTCOME_CHOICES = (
+    OUTCOME_CHOICES = [
         (OUTCOME_ELECTED, "Elected"),
         (OUTCOME_NOT_ELECTED, "Not elected"),
-    )
+    ]
     outcome = models.CharField(max_length=16, choices=OUTCOME_CHOICES)
 
     fixed_on = models.DateTimeField(default=timezone.now)
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 aa9e4d6fb..1cb537575 100644
--- a/scipost_django/colleges/templates/colleges/_hx_nomination_decision_form.html
+++ b/scipost_django/colleges/templates/colleges/_hx_nomination_decision_form.html
@@ -6,13 +6,13 @@
   {% if voting_round.decision %}
 
     {% if voting_round.decision.outcome == 'elected' %}
-      <div class="badge fs-4 mb-2 bg-success">{{ voting_round.decision.get_outcome_display }}</div>
+      <div class="badge fs-5 mb-2 bg-success">{{ voting_round.decision.get_outcome_display }}</div>
     {% elif voting_round.decision.outcome == 'notelected' %}
-      <div class="badge fs-4 mb-2 bg-danger">{{ voting_round.decision.get_outcome_display }}</div>
+      <div class="badge fs-5 mb-2 bg-danger">{{ voting_round.decision.get_outcome_display }}</div>
     {% endif %}
 
     {% if voting_round.decision.comments %}
-      <h5>Decision comments</h5>
+      <h4>Decision comments</h4>
       <p>{{ voting_round.decision.comments }}</p>
     {% endif %}
 
diff --git a/scipost_django/colleges/templates/colleges/_hx_nomination_li_contents.html b/scipost_django/colleges/templates/colleges/_hx_nomination_li_contents.html
index f8baf32b2..9cae025f1 100644
--- a/scipost_django/colleges/templates/colleges/_hx_nomination_li_contents.html
+++ b/scipost_django/colleges/templates/colleges/_hx_nomination_li_contents.html
@@ -125,35 +125,7 @@
   </div>
 
 
-  {% with round=nomination.voting_rounds.first %}
-
-    <div class="row">
-      <div class="col">
-        <div class="card">
-          <div class="card-header">Voting Round Details</div>
-          <div class="card-body">
-
-            {% if session_fellowship and session_fellowship in round.eligible_to_vote.all or "edadmin" in user_roles %}
-
-              {% if round.is_open and session_fellowship in round.eligible_to_vote.all %}
-                <div id="nomination-{{ round.nomination.id }}-vote"
-                     hx-get="{% url 'colleges:_hx_nomination_vote' voting_round_id=round.id %}"
-                     hx-trigger="intersect once"></div>
-              {% else %}
-                {% include "colleges/_hx_voting_round_results.html" with voting_round=round %}
-              {% endif %}
-
-            {% else %}
-              <p>You are not called upon to vote in this round.</p>
-            {% endif %}
-
-          </div>
-        </div>
-      </div>
-
-    </div>
-
-  {% endwith %}
+  {% with round=nomination.voting_rounds.first %}TEMP BROKEN{% endwith %}
  
 
   {% if "edadmin" in user_roles %}
diff --git a/scipost_django/colleges/templates/colleges/_hx_voting_round_details.html b/scipost_django/colleges/templates/colleges/_hx_voting_round_details.html
new file mode 100644
index 000000000..fb76f950a
--- /dev/null
+++ b/scipost_django/colleges/templates/colleges/_hx_voting_round_details.html
@@ -0,0 +1,11 @@
+<details id="round-{{ round.id }}-details"
+         class="border border-2 mx-3 p-2 bg-primary bg-opacity-10">
+  <summary class="list-none">{% include "colleges/_hx_voting_round_summary.html" with round=round %}</summary>
+
+  <div id="round-{{ round.id }}-details-contents"
+       class="p-2 mt-2 bg-white"
+       hx-get="{% url 'colleges:_hx_voting_round_li_contents' round_id=round.id %}"
+       hx-trigger="toggle once from:#round-{{ round.id }}-details"
+       hx-indicator="#indicator-round-{{ round.id }}-details-contents"></div>
+
+</details>
diff --git a/scipost_django/colleges/templates/colleges/_hx_voting_round_li_contents.html b/scipost_django/colleges/templates/colleges/_hx_voting_round_li_contents.html
new file mode 100644
index 000000000..e72444b39
--- /dev/null
+++ b/scipost_django/colleges/templates/colleges/_hx_voting_round_li_contents.html
@@ -0,0 +1,19 @@
+<div class="row">
+  <div class="col">
+
+    {% if session_fellowship and session_fellowship in round.eligible_to_vote.all or "edadmin" in user_roles %}
+
+      {% if round.is_open and session_fellowship in round.eligible_to_vote.all %}
+        <div id="nomination-{{ round.nomination.id }}-vote"
+             hx-get="{% url 'colleges:_hx_nomination_vote' voting_round_id=round.id %}"
+             hx-trigger="intersect once"></div>
+      {% else %}
+        {% include "colleges/_hx_voting_round_results.html" with voting_round=round %}
+      {% endif %}
+
+    {% else %}
+      <p>You are not called upon to vote in this round.</p>
+    {% endif %}
+
+  </div>
+</div>
diff --git a/scipost_django/colleges/templates/colleges/_hx_voting_round_list.html b/scipost_django/colleges/templates/colleges/_hx_voting_round_list.html
new file mode 100644
index 000000000..f3b212b5e
--- /dev/null
+++ b/scipost_django/colleges/templates/colleges/_hx_voting_round_list.html
@@ -0,0 +1,26 @@
+{% for round in page_obj %}
+  <div class="ms-1 mt-2">
+    {% include 'colleges/_hx_voting_round_details.html' with round=round %}
+  </div>
+{% empty %}
+  <strong>No Voting Rounds could be found</strong>
+{% endfor %}
+
+{% if page_obj.has_next %}
+  <div hx-post="{% url 'colleges:_hx_voting_round_list' %}?page={{ page_obj.next_page_number }}"
+       hx-include="#search-voting_rounds-form"
+       hx-trigger="revealed"
+       hx-swap="afterend"
+       hx-indicator="#indicator-search-page-{{ page_obj.number }}">
+    <div id="indicator-search-page-{{ page_obj.number }}"
+         class="htmx-indicator p-2">
+      <button class="btn btn-warning" type="button" disabled>
+        <strong>Loading page {{ page_obj.next_page_number }} out of {{ page_obj.paginator.num_pages }}</strong>
+	
+        <div class="spinner-grow spinner-grow-sm ms-2"
+             role="status"
+             aria-hidden="true"></div>
+      </button>
+    </div>
+  </div>
+{% endif %}
diff --git a/scipost_django/colleges/templates/colleges/_hx_voting_round_search_form.html b/scipost_django/colleges/templates/colleges/_hx_voting_round_search_form.html
new file mode 100644
index 000000000..26fdc0d40
--- /dev/null
+++ b/scipost_django/colleges/templates/colleges/_hx_voting_round_search_form.html
@@ -0,0 +1,10 @@
+{% load crispy_forms_tags %}
+
+<form hx-post="{% url 'colleges:_hx_voting_round_list' %}"
+      hx-trigger="load, keyup delay:500ms, change delay:500ms, click from:#refresh-button"
+      hx-sync="#search-voting_rounds-form:replace"
+      hx-target="#search-voting_rounds-results"
+      hx-indicator="#indicator-search-voting_rounds">
+ 
+  <div id="search-voting_rounds-form">{% crispy form %}</div>
+</form>
diff --git a/scipost_django/colleges/templates/colleges/_hx_voting_round_summary.html b/scipost_django/colleges/templates/colleges/_hx_voting_round_summary.html
new file mode 100644
index 000000000..a2220ed10
--- /dev/null
+++ b/scipost_django/colleges/templates/colleges/_hx_voting_round_summary.html
@@ -0,0 +1,36 @@
+<div class="row mb-0 w-100">
+
+  <div class="col-12 col-sm">
+    <div class="fs-6">{{ round.nomination.profile }}</div>
+    <div class="d-none d-md-block">(click for details)</div>
+  </div>
+
+
+  <div class="col-12 col-sm-auto">
+    <div>
+      <span>Editorial College:</span><span>&emsp;{{ round.nomination.college.name }}</span>
+    </div>
+    <div>
+      <span>Voting started:</span><span>&emsp;{{ round.voting_opens|date:"Y-m-d" }}</span>
+    </div>
+  </div>
+
+  <div class="col-12 col-sm-auto">
+    <div>
+      <span>Decision:</span>
+
+      {% if round.decision.outcome == "elected" %}
+        <span class="badge bg-success">{{ round.decision.get_outcome_display }}</span>
+      {% elif round.decision.outcome == "notelected" %}
+        <span class="badge bg-danger">{{ round.decision.get_outcome_display }}</span>
+      {% else %}
+        <span class="badge bg-warning">Pending</span>
+      {% endif %}
+
+    </div>
+
+    <div>
+      <span>Deadline:&nbsp;</span><span>{{ round.voting_deadline|date:"Y-m-d" }}</span>
+    </div>
+  </div>
+</div>
diff --git a/scipost_django/colleges/templates/colleges/nominations.html b/scipost_django/colleges/templates/colleges/nominations.html
index b12440853..9b876352b 100644
--- a/scipost_django/colleges/templates/colleges/nominations.html
+++ b/scipost_django/colleges/templates/colleges/nominations.html
@@ -28,6 +28,43 @@
     &nbsp;<strong>Help out by nominating candidates!</strong>
   </p>
 
+  <details id="voting_rounds-filter-details" class="card my-4">
+    <summary class="card-header d-flex flex-row align-items-center justify-content-between list-triangle">
+      <div class="fs-3">Search / Filter</div>
+      <div class="d-none d-md-flex align-items-center">
+ 
+        <div id="indicator-search-voting_rounds" class="htmx-indicator">
+          <button class="btn btn-warning text-white d-none d-md-block me-2"
+                  type="button"
+                  disabled>
+            <strong>Loading...</strong>
+ 
+            <div class="spinner-grow spinner-grow-sm ms-2"
+                 role="status"
+                 aria-hidden="true"></div>
+          </button>
+        </div>
+
+        <button class="btn btn-outline-secondary me-2"
+                type="button"
+                hx-get="{% url 'colleges:_hx_voting_round_search_form' filter_set="empty" %}"
+                hx-target="#voting_round-search-form-container">Clear Filters</button>
+ 
+        <a id="refresh-button" class="m-2 btn btn-primary">
+          {% include "bi/arrow-clockwise.html" %}
+        &nbsp;Refresh</a>
+      </div>
+
+    </summary>
+    <div class="card-body">
+      <div id="voting_round-search-form-container"
+           hx-get="{% url 'colleges:_hx_voting_round_search_form' filter_set='default' %}"
+           hx-trigger="intersect once"></div>
+    </div>
+  </details>
+
+  <div id="search-voting_rounds-results" class="mt-2"></div>
+
   <details class="border border-warning border-2 mt-4">
     <summary class="bg-warning bg-opacity-10 p-2 d-block list-triangle">
       <div class="fs-5">Nominate</div>
diff --git a/scipost_django/colleges/urls.py b/scipost_django/colleges/urls.py
index 80cb911c3..c60f2c168 100644
--- a/scipost_django/colleges/urls.py
+++ b/scipost_django/colleges/urls.py
@@ -2,7 +2,7 @@ __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
 __license__ = "AGPL v3"
 
 
-from django.urls import path
+from django.urls import include, path
 
 from . import views
 
@@ -169,6 +169,11 @@ urlpatterns = [
         name="_hx_nomination_form",
     ),
     path("_hx_nominations", views._hx_nominations, name="_hx_nominations"),
+    path(
+        "_hx_voting_round_search_form/<str:filter_set>",
+        views._hx_voting_round_search_form,
+        name="_hx_voting_round_search_form",
+    ),
     path(
         "_hx_nomination_li_contents/<int:nomination_id>",
         views._hx_nomination_li_contents,
@@ -195,16 +200,64 @@ urlpatterns = [
         name="_hx_nominations_no_round_started",
     ),
     path(
-    path(
-        "<int:round_id>/_hx_nomination_round_remove_voter/<int:voter_id>",
-        views._hx_nomination_round_remove_voter,
-        name="_hx_nomination_round_remove_voter",
+        "<int:nomination_id>",
+        include(
+            [
+                path(
+                    "_hx_nomination_eligible_voters",
+                    views._hx_nomination_eligible_voters,
+                    name="_hx_nomination_eligible_voters",
+                ),
+                path(
+                    "_hx_nomination_round_start",
+                    views._hx_nomination_round_start,
+                    name="_hx_nomination_round_start",
+                ),
+            ]
+        ),
+    ),
+    # Nomination Rounds
+    path(
+        "<int:round_id>/",
+        include(
+            [
+                # Display round
+                path(
+                    "details",
+                    views._hx_voting_round_li_contents,
+                    name="_hx_voting_round_li_contents",
+                ),
+                # Manage voters of a nomination round
+                path(
+                    "voter/<int:voter_id>/",
+                    include(
+                        [
+                            path(
+                                "remove",
+                                views._hx_nomination_round_remove_voter,
+                                name="_hx_nomination_round_remove_voter",
+                            ),
+                            # path(
+                            #     "add",
+                            #     views._hx_nomination_round_add_voter,
+                            #     name="_hx_nomination_round_add_voter",
+                            # ),
+                        ]
+                    ),
+                ),
+            ],
+        ),
     ),
     path(
         "_hx_voting_rounds",
         views._hx_voting_rounds,
         name="_hx_voting_rounds",
     ),
+    path(
+        "_hx_voting_round_list",
+        views._hx_voting_round_list,
+        name="_hx_voting_round_list",
+    ),
     path(
         "_hx_nomination_vote/<int:voting_round_id>",
         views._hx_nomination_vote,
diff --git a/scipost_django/colleges/views.py b/scipost_django/colleges/views.py
index a8b86fb7d..479b87167 100644
--- a/scipost_django/colleges/views.py
+++ b/scipost_django/colleges/views.py
@@ -42,6 +42,7 @@ from .constants import (
 )
 from .forms import (
     CollegeChoiceForm,
+    FellowshipNominationVotingRoundSearchForm,
     FellowshipSearchForm,
     FellowshipDynSelForm,
     FellowshipForm,
@@ -707,6 +708,7 @@ def nominations(request):
     context = {
         "profile_dynsel_form": profile_dynsel_form,
         "search_nominations_form": FellowshipNominationSearchForm(),
+        "rounds": FellowshipNominationVotingRound.objects.all()[:10],
     }
     return render(request, "colleges/nominations.html", context)
 
@@ -768,7 +770,7 @@ def _hx_nominations_needing_specialties(request):
 def _hx_nominations_no_round_started(request):
     nominations_no_round_started = FellowshipNomination.objects.exclude(
         profile__specialties__isnull=True
-    ).filter(voting_rounds__isnull=True)
+    ).filter(voting_rounds__isnull=False)
     context = {
         "nominations_no_round_started": nominations_no_round_started,
     }
@@ -832,7 +834,7 @@ def _hx_nomination_round_start(request, nomination_id):
     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}",
+        f"Started round for {nomination.profile} from now until {voting_round.voting_deadline}.",
         tag="success",
     )
 
@@ -863,6 +865,54 @@ def _hx_nomination_li_contents(request, nomination_id):
     return render(request, "colleges/_hx_nomination_li_contents.html", context)
 
 
+def _hx_voting_round_search_form(request, filter_set: str):
+    voting_rounds_search_form = FellowshipNominationVotingRoundSearchForm(
+        user=request.user,
+        session_key=request.session.session_key,
+    )
+
+    if filter_set == "empty":
+        voting_rounds_search_form.apply_filter_set({}, none_on_empty=True)
+    # TODO: add more filter sets saved in the session of the user
+
+    print(type(voting_rounds_search_form))
+
+    context = {
+        "form": voting_rounds_search_form,
+    }
+    return render(request, "colleges/_hx_voting_round_search_form.html", context)
+
+
+def _hx_voting_round_list(request):
+    form = FellowshipNominationVotingRoundSearchForm(
+        request.POST or None, user=request.user, session_key=request.session.session_key
+    )
+    if form.is_valid():
+        rounds = form.search_results()
+    else:
+        rounds = FellowshipNominationVotingRound.objects.all()
+    paginator = Paginator(rounds, 16)
+    page_nr = request.GET.get("page")
+    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,
+    }
+    return render(request, "colleges/_hx_voting_round_list.html", context)
+
+
+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)
+    context = {
+        "round": round,
+    }
+    return render(request, "colleges/_hx_voting_round_li_contents.html", context)
+
+
 @login_required
 @user_passes_test(is_edadmin_or_advisory_or_active_regular_or_senior_fellow)
 def _hx_nomination_comments(request, nomination_id):
diff --git a/scipost_django/scipost/management/commands/add_groups_and_permissions.py b/scipost_django/scipost/management/commands/add_groups_and_permissions.py
index 4c3fdeffe..e57c7baca 100644
--- a/scipost_django/scipost/management/commands/add_groups_and_permissions.py
+++ b/scipost_django/scipost/management/commands/add_groups_and_permissions.py
@@ -405,6 +405,16 @@ class Command(BaseCommand):
             content_type=content_type,
         )
 
+        # Fellowship Nominations
+        (
+            can_view_all_nomination_voting_rounds,
+            created,
+        ) = Permission.objects.get_or_create(
+            codename="can_view_all_nomination_voting_rounds",
+            name="Can view all voting rounds for Fellowship nominations",
+            content_type=content_type,
+        )
+
         # Assign permissions to groups
         SciPostAdmin.permissions.set(
             [
@@ -438,6 +448,7 @@ class Command(BaseCommand):
                 can_view_potentialfellowship_list,
                 can_add_potentialfellowship,
                 can_preview_new_features,
+                can_view_all_nomination_voting_rounds,
             ]
         )
 
@@ -497,6 +508,7 @@ class Command(BaseCommand):
                 can_view_potentialfellowship_list,
                 can_add_potentialfellowship,
                 can_preview_new_features,
+                can_view_all_nomination_voting_rounds,
             ]
         )
 
-- 
GitLab