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> {{ round.nomination.college.name }}</span> + </div> + <div> + <span>Voting started:</span><span> {{ 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: </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 @@ <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" %} + 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