diff --git a/wcivf/apps/elections/managers.py b/wcivf/apps/elections/managers.py index b62b99993..2658e88fe 100644 --- a/wcivf/apps/elections/managers.py +++ b/wcivf/apps/elections/managers.py @@ -1,4 +1,5 @@ from django.db import models +from django.db.models import Sum, Count from django.utils import timezone from .helpers import EEHelper @@ -22,8 +23,6 @@ def past(self): """ return self.filter(current=False, election_date__lt=timezone.now()) - -class ElectionManager(models.Manager.from_queryset(ElectionQuerySet)): def get_explainer(self, election): ee = EEHelper() ee_data = ee.get_data(election["id"]) @@ -62,11 +61,16 @@ def election_id_to_type(self, election_id): parts = election_id.split(".") return parts[0] - def past(self): + def parties(self): """ - Allows past method on QuerySet object to be called directly from the manager + Return a list of party models that are standing in this election """ - return self.get_queryset().past() + + return ( + self.values("postelection__personpost__party") + .annotate(count=Count("postelection__personpost__party")) + .order_by("-postelection__personpost__party") + ) class PostManager(models.Manager): @@ -104,3 +108,8 @@ def update_or_create_from_ynr(self, post_dict): ) return (post, created) + + +class PostElectionQueryset(models.QuerySet): + def seats_total(self): + return self.aggregate(sum=Sum("winner_count"))["sum"] diff --git a/wcivf/apps/elections/models.py b/wcivf/apps/elections/models.py index 62c0b9bb6..fe4ed68cb 100644 --- a/wcivf/apps/elections/models.py +++ b/wcivf/apps/elections/models.py @@ -1,5 +1,7 @@ import datetime import re + +from django.db.models import Count from django.utils import timezone import pytz @@ -14,7 +16,7 @@ from django.utils.text import slugify from .helpers import get_election_timetable -from .managers import ElectionManager +from .managers import PostElectionQueryset, ElectionQuerySet LOCAL_TZ = pytz.timezone("Europe/London") @@ -46,7 +48,7 @@ class Election(models.Model): metadata = JSONField(null=True) any_non_by_elections = models.BooleanField(default=False) - objects = ElectionManager() + objects = ElectionQuerySet.as_manager() class Meta: ordering = ["election_date"] @@ -220,6 +222,24 @@ def pluralized_division_name(self): return pluralise.get(suffix, f"{suffix}s") + def parties(self): + parties_with_counts = { + d["personpost__party__party_id"]: { + "count": d["personpost__party__count"] + } + for d in self.postelection_set.values("personpost__party__party_id") + .annotate(Count("personpost__party")) + .order_by("-personpost__party") + } + from parties.models import Party + + party_model_qs = Party.objects.filter( + party_id__in=parties_with_counts.keys() + ) + for party in party_model_qs: + parties_with_counts[party.party_id]["party"] = party + return parties_with_counts + class Post(models.Model): """ @@ -335,6 +355,8 @@ class PostElection(models.Model): wikipedia_url = models.CharField(blank=True, null=True, max_length=800) wikipedia_bio = models.TextField(null=True) + objects = PostElectionQueryset.as_manager() + @property def expected_sopn_date(self): try: diff --git a/wcivf/apps/elections/templates/elections/election_view.html b/wcivf/apps/elections/templates/elections/election_view.html index 556629f54..ac19b4e1e 100644 --- a/wcivf/apps/elections/templates/elections/election_view.html +++ b/wcivf/apps/elections/templates/elections/election_view.html @@ -1,5 +1,5 @@ {% extends "base.html" %} - +{% load markdown_deux_tags %} {% load breadcrumb_tags %} {% load humanize %} @@ -8,44 +8,68 @@ {% block og_description %}The {{ object.name }} {% if object.in_past %}was{% else %}will be{% endif %} held on {{ object.election_date }}.{% endblock og_description %} {% block content %} +
+
+

{{ object.nice_election_name }}

-
-

{{ object.nice_election_name }}

- -

The {{ object.nice_election_name }} - {% if object.is_election_day %} - is being held today. - Polls are open from {{ object.polls_open|time:"ga" }} till {{ object.polls_close|time:"ga" }} - {% else %} - {% if object.in_past %}was{% else %}will be{% endif %} held {{ object.election_date|naturalday:"\o\n l j F Y" }}. - {% endif %} -

+

The {{ object.nice_election_name }} + {% if object.is_election_day %} + is being held today. + Polls are open from {{ object.polls_open|time:"ga" }} till {{ object.polls_close|time:"ga" }} + {% else %} + {% if object.in_past %}was{% else %}will be{% endif %} held {{ object.election_date|naturalday:"\o\n l j F Y" }}. + {% endif %} +

- {% if object.election_type != "ref" %} - {% if election.person_set.count %} -

{% if object.locked %}There are {% else %}We know about {% endif %}{{ election.person_set.count }} candidates + {% if object.election_type != "ref" %} + {% if election.personpost_set.count %} +

{% if object.locked %}There are {% else %}We know about {% endif %}{{ election.personpost_set.count }} candidates {% if object.in_past %}that stood{% else %}standing{% endif %} for this election, - in {{ object.post_set.count }} posts.

- {% if not object.in_past and not object.locked %} -

Add more at our candidate crowd-sourcing site

- {% endif %} - {% else %} - {% if not object.in_past and not election.slug == 'parl.2017-06-08' %} -

Add some candidates at our candidate crowd-sourcing site

- {% endif %} + in {{ object.postelection_set.count }} {{ object.pluralized_division_name }}.

{% endif %} -

{{ object.pluralized_division_name|title }}

-
    + + {{ object.description|markdown }} + +

    Parties

    +

    {{ object.parties.keys|length }} parties are contesting {{ object.postelection_set.seats_total }} seats across {{ object.postelection_set.count }} {{ object.pluralized_division_name }}.

    +
    + {% for party_id, party_data in object.parties.items %} +
    +
    +
    {% if party_data.party.emblem %}{% endif %}
    +
    + {{ party_data.party.party_name }}
    + {{ party_data.count }} candidate{{ party_data.count|pluralize }} + +
    +
    +
    + {% endfor %} + + +
    + + +

    {{ object.pluralized_division_name|title }}

    + + + + + + {% for postelection in object.postelection_set.all %} -
  • - {{ postelection.post.label }} - {{ postelection.short_cancelled_message_html }} -
  • + + + + + + {% endfor %} - - {% endif %} - - - - -{% include "elections/includes/_postcode_search_form.html" %} -{#{% include "feedback/feedback_form.html" %}#} +
    + {% include "elections/includes/_postcode_search_form.html" %} +
    + + {% endblock content %} {% block breadcrumbs %} - + {% endblock breadcrumbs %} diff --git a/wcivf/apps/elections/urls.py b/wcivf/apps/elections/urls.py index 98cf9336f..9196000a9 100644 --- a/wcivf/apps/elections/urls.py +++ b/wcivf/apps/elections/urls.py @@ -9,6 +9,7 @@ PostcodeiCalView, RedirectPostView, PartyListVew, + ElectionTypeForDateView, ) from .helpers import ElectionIDSwitcher @@ -29,7 +30,11 @@ PartyListVew.as_view(), name="party_list_view", ), - # + url( + r"^(?P[a-z]+\.\d{4}-\d{2}-\d{2})/$", + ElectionTypeForDateView.as_view(), + name="election_type_for_date_view", + ), url( "^(?P[a-z\-]+\.[^/]+)(?:/(?P[^/]+))?/$", ElectionIDSwitcher(election_view=ElectionView, ballot_view=PostView), diff --git a/wcivf/apps/elections/views/election_views.py b/wcivf/apps/elections/views/election_views.py index 3dcdd7295..c5cef0c21 100644 --- a/wcivf/apps/elections/views/election_views.py +++ b/wcivf/apps/elections/views/election_views.py @@ -1,9 +1,10 @@ +from dateutil.parser import parse from django.views.generic import TemplateView, DetailView, RedirectView from django.http import Http404 from django.shortcuts import get_object_or_404 -from django.db.models import Prefetch +from django.db.models import Prefetch, Count, Sum, Subquery, IntegerField from django.apps import apps @@ -11,7 +12,7 @@ NewSlugsRedirectMixin, PostelectionsToPeopleMixin, ) -from elections.models import PostElection +from elections.models import PostElection, Election from parties.models import LocalParty, Party from people.models import PersonPost @@ -34,6 +35,80 @@ def get_context_data(self, **kwargs): return context +class ElectionTypeForDateView(TemplateView): + template_name = "elections/election_type_for_date.html" + + supported_types = { + "local": "Local", + "parl": "UK Parliament", + "nia": "Northern Ireland Assembly", + "senedd": "Senedd Cymru", + "sp": "Scottish Parliament", + "gla": "Greater London Assembly", + "pcc": "Police and Crime Commissioner", + "mayor": "Mayoral", + } + + def get(self, request, *args, **kwargs): + self.election_type, self.election_date = self.kwargs.get( + "election" + ).split(".") + if self.election_type not in self.supported_types.keys(): + raise Http404() + return super().get(request, *args, **kwargs) + + def get_template_names(self): + return [ + f"elections/{self.election_type}_election_type_for_date.html", + "elections/generic_election_type_for_date.html", + ] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["election_type"] = self.supported_types[self.election_type] + context["election_date"] = parse(self.election_date) + + base_elections_qs = Election.objects.filter( + election_date=self.election_date, election_type=self.election_type + ).order_by("name") + + aggregate_qs = base_elections_qs.order_by("postelection") + context["total_candidates"] = aggregate_qs.all().aggregate( + total_candidates=Count("postelection__personpost", distinct=True) + )["total_candidates"] + + context["total_seats"] = aggregate_qs.all().aggregate( + total_seats=Sum("postelection__winner_count") + )["total_seats"] + + context["total_parties"] = aggregate_qs.all().aggregate( + total_parties=Count( + "postelection__personpost__party", distinct=True + ), + )["total_parties"] + + context["total_ballots"] = aggregate_qs.all().aggregate( + total_ballots=Count("postelection__ballot_paper_id", distinct=True), + )["total_ballots"] + + context["elections"] = base_elections_qs.annotate( + parties_count=Count( + "postelection__personpost__party", distinct=True + ), + candidates_count=Count("postelection__personpost", distinct=True), + ballots=Count("postelection", distinct=True), + ).annotate( + seats_total=Subquery( + base_elections_qs.order_by("postelection") + .annotate(sum_value=Sum("postelection__winner_count")) + .values("sum_value"), + output_field=IntegerField(), + ) + ) + + return context + + class ElectionView(NewSlugsRedirectMixin, DetailView): template_name = "elections/election_view.html" model = apps.get_model("elections.Election") diff --git a/wcivf/settings/base.py b/wcivf/settings/base.py index 17765d7ba..4fac315cc 100644 --- a/wcivf/settings/base.py +++ b/wcivf/settings/base.py @@ -63,6 +63,7 @@ ) MIDDLEWARE = ( + "debug_toolbar.middleware.DebugToolbarMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -71,7 +72,6 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.security.SecurityMiddleware", "core.middleware.UTMTrackerMiddleware", - "debug_toolbar.middleware.DebugToolbarMiddleware", ) ROOT_URLCONF = "wcivf.urls"
    NameCandidatesSeats
    {{ postelection.post.label }} + {{ postelection.short_cancelled_message_html }}{{ postelection.personpost_set.count }}{{ postelection.winner_count }}