Skip to content
Commits on Source (24)
......@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](<http://keepachangelog.com/en/1.0.0/>)
and this project adheres to [Semantic Versioning](<http://semver.org/spec/v2.0.0.html>).
## [1.3.5]
### Added
- election results models, management tasks, api viewsets and serializers, admin section and tests (#456)
## [1.3.4]
### Fixed
......@@ -589,7 +595,8 @@ in the test stage.
[atoka]: https://atoka.io
[unchanged]: https://gitlab.depp.it/openpolis/opdm/opdm-service/compare/v1.3.4...master
[unchanged]: https://gitlab.depp.it/openpolis/opdm/opdm-service/compare/v1.3.5...master
[1.3.5]: https://gitlab.depp.it/openpolis/opdm/opdm-service/compare/v1.3.4...v1.3.5
[1.3.4]: https://gitlab.depp.it/openpolis/opdm/opdm-service/compare/v1.3.3...v1.3.4
[1.3.3]: https://gitlab.depp.it/openpolis/opdm/opdm-service/compare/v1.3.2...v1.3.3
[1.3.2]: https://gitlab.depp.it/openpolis/opdm/opdm-service/compare/v1.3.1...v1.3.2
......
"""Settings for local development."""
from .base import * # noqa
# CACHES
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#caches
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "",
}
}
# PASSWORDS
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#password-hashers
PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"]
# EMAIL
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend
EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
# Audit log settings
# -----------------------------------------------------------------------------
# Turn off audit log during local development
DJANGO_EASY_AUDIT_WATCH_MODEL_EVENTS = False
DJANGO_EASY_AUDIT_WATCH_AUTH_EVENTS = False
DJANGO_EASY_AUDIT_WATCH_REQUEST_EVENTS = True
# Do not connect labels signals during local development
LABELS_CONNECT_SIGNALS = False
TOPICS_CONNECT_SIGNALS = False
# Turn off normal logging during local development
LOGGING['loggers']['project']['handlers'] = [] # noqa
# Django Extensions
# https://django-extensions.readthedocs.io/en/stable/graph_models.html
SHELL_PLUS_PRINT_SQL = True
GRAPH_MODELS = {
"all_applications": False,
"arrow_shape": "diamond",
"disable_abstract_fields": False,
"disable_fields": False,
"exclude_columns": [
"created_at",
"updated_at",
],
"exclude_models": ",".join(
(
"Timestampable",
)
),
"group_models": True,
"hide_edge_labels": False,
"inheritance": False,
"language": "it",
"layout": "dot",
"relations_as_fields": True,
"sort_fields": False,
"theme": "django2018",
"verbose_names": True,
}
This diff is collapsed.
......@@ -3,7 +3,7 @@ Openpolis Data Manager service package (backend)
"""
from typing import Optional
__version__ = (1, 3, 4)
__version__ = (1, 3, 5)
def get_version_str() -> str:
......
......@@ -8,7 +8,7 @@ from django_filters.rest_framework import (
CharFilter,
NumberFilter,
LookupChoiceFilter,
BooleanFilter,
BooleanFilter, BaseInFilter,
)
from popolo.models import (
......@@ -31,6 +31,10 @@ from project.api_v1.filters.filtersets.generic import (
)
class CharInFilter(BaseInFilter, CharFilter):
pass
class PersonFilterSet(
FilterSet, DateframeableFilterSet, TimestampableFilterset, IdentifiersFilterSet
):
......@@ -264,6 +268,13 @@ class AreaFilterSet(
help_text=_('Filter areas by exact name. Case insensitive. Example: "Roma"'),
)
name__icontains = CharFilter(
field_name="name",
lookup_expr="icontains",
label=_("Case insensitive containment"),
help_text=_('Filter areas by string in name. Case insensitive. Example: "Piemonte 1"'),
)
min_inhabitants = NumberFilter(
field_name="inhabitants",
lookup_expr="gte",
......@@ -278,9 +289,9 @@ class AreaFilterSet(
help_text=_("Maximum number of inhabitants"),
)
classification = CharFilter(
classification = CharInFilter(
field_name="classification",
lookup_expr="exact",
lookup_expr="in",
label=_("Classification"),
help_text=_("Geonames classification: ADM1, ADM2, ADM3, ..."),
)
......@@ -310,6 +321,15 @@ class AreaFilterSet(
),
)
identifier__istartswith = CharFilter(
field_name="identifier",
lookup_expr="istartswith",
label=_("Identifier starts with"),
help_text=_(
"Get areas by main identifier's prefix."
),
)
parent__identifier = CharFilter(
field_name="parent__identifier",
label=_("Parent identifier"),
......@@ -326,6 +346,8 @@ class AreaFilterSet(
model = Area
fields = [
"name__iexact",
"name__icontains",
"identifier__istartswith",
"parent__identifier",
"min_inhabitants",
"max_inhabitants",
......
......@@ -29,7 +29,7 @@ from project.api_v1.views.viewsets import (
ClassificationViewSet,
OrganizationRelationshipViewSet)
from elections.views import (
from project.elections.views import (
ElectoralCandidateResultViewSet,
ElectoralListResultViewSet,
ElectoralResultViewSet,
......
......@@ -733,7 +733,6 @@ class OwnershipSerializer(serializers.HyperlinkedModelSerializer):
"owned_organization",
"owner_organization",
"owner_person",
"percentage",
"start_date",
"end_date",
"sources",
......
......@@ -76,7 +76,7 @@ class BrowsableAPIRendererWithoutDescription(BrowsableAPIRenderer):
class LargeResultsSetPagination(PageNumberPagination):
page_size = 50
page_size_query_param = "page_size"
max_page_size = 200
max_page_size = 250
class StandardResultsSetPagination(PageNumberPagination):
......
"""Define elections admins."""
from copy import deepcopy
from django.contrib import admin
from elections.models import PoliticalColour
from django.utils.translation import gettext_lazy as _
from project.elections.models import (
ElectoralCandidateResult,
ElectoralListResult,
ElectoralResult,
PoliticalColour,
)
@admin.register(PoliticalColour)
......@@ -10,3 +19,241 @@ class PoliticalColourAdmin(admin.ModelAdmin):
list_display = ("name",)
ordering = ("name", "pk")
@admin.register(ElectoralListResult)
class ElectoralListResultAdmin(admin.ModelAdmin):
"""Define electoral list result admin."""
autocomplete_fields = (
"candidate_result",
"result",
"party",
)
fieldsets = (
(None, {"fields": ("name", "candidate_result", "image")}),
(_("Results"), {"fields": (("votes", "votes_perc", "seats"),)}),
(_("Second turn"), {"fields": ("ballot_candidate_result",)}),
)
list_display = (
"name",
"result",
"image",
"votes",
"votes_perc",
"seats",
"political_colour",
)
list_filter = (
("result", admin.RelatedOnlyFieldListFilter),
("political_colour", admin.RelatedOnlyFieldListFilter),
)
ordering = ("result", "name", "pk")
search_fields = ("name",)
def has_add_permission(self, request):
"""Inhibit object add."""
return False
def has_change_permission(self, request, obj=None):
"""Inhibit object change."""
return False
def has_delete_permission(self, request, obj=None):
"""Inhibit object delete."""
return False
def get_fieldsets(self, request, obj=None):
"""Return fieldset."""
fieldsets = super().get_fieldsets(request, obj=None)
if not getattr(obj.result, "has_second_round", False):
fieldsdict = dict(deepcopy(fieldsets))
del fieldsdict[_("Second turn")]
return fieldsdict.items()
return fieldsets
class ElectoralListResultInline(admin.TabularInline):
"""Define ElectoralListResult inline."""
autocomplete_fields = (
"candidate_result",
"result",
"party",
)
can_delete = False
extra = 0
fields = (
"name",
"result",
"image",
"votes",
"votes_perc",
"seats",
"political_colour",
)
fk_name = "candidate_result"
model = ElectoralListResult
show_change_link = True
@admin.register(ElectoralCandidateResult)
class ElectoralCandidateResultAdmin(admin.ModelAdmin):
"""Define electoral candidate result admin."""
autocomplete_fields = (
"candidate",
"result",
)
fieldsets = (
(None, {"fields": ("name", "result", "is_counselor", "is_top")}),
(_("Results (First Turn)"), {"fields": ("votes", "votes_perc")}),
(_("Results (Second Turn)"), {"fields": ("ballot_votes", "ballot_votes_perc")}),
)
inlines = (ElectoralListResultInline,)
list_display = (
"name",
"result",
"votes",
"votes_perc",
"is_counselor",
"is_top",
"political_colour",
)
list_filter = (
"is_counselor",
"is_top",
("result", admin.RelatedOnlyFieldListFilter),
("political_colour", admin.RelatedOnlyFieldListFilter),
)
ordering = ("result", "name", "pk")
search_fields = ("name",)
def has_add_permission(self, request):
"""Inhibit object add."""
return False
def has_change_permission(self, request, obj=None):
"""Inhibit object change."""
return False
def has_delete_permission(self, request, obj=None):
"""Inhibit object delete."""
return False
def get_fieldsets(self, request, obj=None):
"""Return fieldset."""
fieldsets = super().get_fieldsets(request, obj=None)
if not getattr(obj.result, "has_second_round", False):
fieldsdict = dict(deepcopy(fieldsets))
del fieldsdict[_("Results (Second Turn)")]
fieldsdict[_("Results")] = fieldsdict.pop(_("Results (First Turn)"))
return fieldsdict.items()
return fieldsets
class ElectoralCandidateResultInline(admin.TabularInline):
"""Define ElectoralCandidateResult inline."""
autocomplete_fields = (
"candidate",
"result",
)
can_delete = False
extra = 0
fields = (
"name",
"result",
"votes",
"votes_perc",
"is_counselor",
"is_top",
"political_colour",
)
model = ElectoralCandidateResult
show_change_link = True
@admin.register(ElectoralResult)
class ElectoralResultAdmin(admin.ModelAdmin):
"""Define electoral result admin."""
autocomplete_fields = (
"institution",
"constituency",
)
fieldsets = (
(None, {"fields": ("__str__", "is_total", "is_valid", "registered_voters")}),
(
_("Results (First Turn)"),
{
"fields": (
("votes_cast", "votes_cast_perc"),
("blank_votes", "invalid_votes", "valid_votes"),
)
},
),
(
_("Results (Second Turn)"),
{
"fields": (
("ballot_votes_cast", "ballot_votes_cast_perc"),
(
"ballot_blank_votes",
"ballot_invalid_votes",
"ballot_valid_votes",
),
)
},
),
)
inlines = (ElectoralCandidateResultInline,)
list_display = (
"electoral_event",
"institution",
"constituency",
"registered_voters",
"votes_cast",
"votes_cast_perc",
"invalid_votes",
"blank_votes",
"valid_votes",
"is_valid",
"is_total",
)
list_filter = (
"is_valid",
"is_total",
("electoral_event", admin.RelatedOnlyFieldListFilter),
("institution", admin.RelatedOnlyFieldListFilter),
("constituency", admin.RelatedOnlyFieldListFilter),
)
ordering = ("electoral_event", "pk")
readonly_fields = ("__str__",)
search_fields = (
"electoral_event__name",
"institution__name",
"constituency__name",
)
def has_add_permission(self, request):
"""Inhibit object add."""
return False
def has_change_permission(self, request, obj=None):
"""Inhibit object change."""
return False
def has_delete_permission(self, request, obj=None):
"""Inhibit object delete."""
return False
def get_fieldsets(self, request, obj=None):
"""Return fieldset."""
fieldsets = super().get_fieldsets(request, obj=None)
if not getattr(obj, "has_second_round", False):
fieldsdict = dict(deepcopy(fieldsets))
del fieldsdict[_("Results (Second Turn)")]
fieldsdict[_("Results")] = fieldsdict.pop(_("Results (First Turn)"))
return fieldsdict.items()
return fieldsets
......@@ -7,5 +7,5 @@ from django.utils.translation import gettext_lazy as _
class ElectionsConfig(AppConfig):
"""The elections app configuration."""
name = "elections"
name = "project.elections"
verbose_name = _("Elections")
"""Define elections filtersets."""
from django.utils.translation import gettext_lazy as _
from django_filters import FilterSet
from django_filters.rest_framework import BooleanFilter
from elections.models import ElectoralResult
from django_filters.rest_framework import FilterSet, BooleanFilter
from project.elections.models import ElectoralResult, ElectoralListResult
class ElectoralResultFilterSet(FilterSet):
"""A filterset for ElectoralResultViewSet."""
is_second_round = BooleanFilter(
field_name="first_round_result",
label=_("Is second round"),
help_text=_("Select Yes for second round results."),
has_second_round = BooleanFilter(
field_name="ballot_votes_cast",
label=_("Has second round"),
help_text=_("Select Yes for results with second round."),
lookup_expr="isnull",
exclude=True,
)
class Meta:
"""Define filterset Meta."""
model = ElectoralResult
fields = ["constituency", "electoral_event", "institution"]
fields = [
"electoral_event__id",
"electoral_event__event_type",
"has_second_round",
"institution__id",
"constituency__id"
]
class ElectoralListResultFilterSet(FilterSet):
"""A filterset for ElectoralListResultViewSet."""
class Meta:
"""Define filterset Meta."""
model = ElectoralListResult
fields = []
"""Load elections module."""
from datetime import date
from decimal import Decimal
from enum import Enum
from typing import List, Optional, Union, Tuple
from pydantic import BaseModel, HttpUrl
class Lista(BaseModel):
"""Risultato lista elettorale."""
candidato: Optional[str]
candidato_ballot: Optional[str]
denominazione: str
seggi: int
simbolo: HttpUrl
perc_voti: Optional[Decimal]
voti: Optional[int]
class Candidato(BaseModel):
"""Risultato candidato."""
nome: str
voti: Optional[int]
voti_ballot: Optional[int]
perc_voti: Optional[Decimal]
perc_voti_ballot: Optional[Decimal]
eletto_chief: bool
eletto_consigliere: bool
liste: List[Lista]
class Risultato(BaseModel):
"""Risultato elettorale."""
area: int
bianche: Optional[int]
bianche_ballot: Optional[int]
elettori: int
elezione_valida: bool
fonte: HttpUrl
istituzione: int
nulle: Optional[int]
nulle_ballot: Optional[int]
perc_votanti: Optional[Decimal]
perc_votanti_ballot: Optional[Decimal]
totale: bool
votanti: Optional[int]
votanti_ballot: Optional[int]
candidati: List[Candidato]
class TipoEvento(str, Enum):
"""Tipo evento elettorale."""
politiche = "ELE-POL"
regionali = "ELE-REG"
comunali = "ELE-COM"
class Evento(BaseModel):
"""Evento elettorale."""
data: date
tipo: TipoEvento
risultati: List[Risultato]
class Importazione(BaseModel):
"""Importazione eventi elettorali."""
eventi: List[Evento]
def load_json_items(path: str) -> Tuple[Union[Importazione, None], str]:
"""Load elections from a file."""
try:
importazione = Importazione.parse_file(path)
except Exception as e:
return None, str(e)
else:
return importazione, ""
"""Elections management commands."""
"""Elections command to import electoral results JSON file."""
from pathlib import Path
from taskmanager.management.base import LoggingBaseCommand
from project.elections.models import ElectoralResult
class Command(LoggingBaseCommand):
"""Command to import electoral results JSON file."""
help = "Import electoral results, from a JSON file."
requires_migrations_checks = True
requires_system_checks = True
def add_arguments(self, parser):
"""Add arguments to the command."""
parser.add_argument(
"--filepath",
dest="filepath",
type=Path,
help="Path to the JSON file.",
)
def handle(self, *args, **options):
"""
Execute the import function.
Returned value will be printed to stdout when command finishes.
"""
super().handle(__name__, *args, formatter_key="simple", **options)
try:
filepath = options["filepath"]
self.logger.info("Starting procedure")
stats = ElectoralResult.load_results(filepath)
imports = ", ".join(f"{v} {k}" for k, v in stats.items() if k != "errors")
self.logger.info(f"Imported: {imports}")
for error in stats["errors"]:
self.logger.error(error)
self.logger.info("End of procedure")
except (KeyboardInterrupt, SystemExit):
return "\nInterrupted by the user."
return "Done!"
import django.contrib.postgres.fields.jsonb
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("elections", "0003_auto_20201026_1116"),
]
operations = [
migrations.RemoveField(
model_name="electoralresult",
name="abstentions",
),
migrations.RemoveField(
model_name="electoralresult",
name="abstentions_perc",
),
migrations.RemoveField(
model_name="electoralresult",
name="blank_votes_perc",
),
migrations.RemoveField(
model_name="electoralresult",
name="invalid_votes_perc",
),
migrations.RemoveField(
model_name="electoralresult",
name="list_votes",
),
migrations.RemoveField(
model_name="electoralresult",
name="list_votes_perc",
),
migrations.RemoveField(
model_name="electoralresult",
name="registered_voters_perc",
),
migrations.RemoveField(
model_name="electoralresult",
name="turnout",
),
migrations.RemoveField(
model_name="electoralresult",
name="turnout_perc",
),
migrations.RemoveField(
model_name="electoralresult",
name="valid_votes_perc",
),
migrations.AddField(
model_name="electoralcandidateresult",
name="name",
field=models.CharField(default="", max_length=512, verbose_name="name"),
preserve_default=False,
),
migrations.AddField(
model_name="electorallistresult",
name="image",
field=models.URLField(default="", verbose_name="image"),
preserve_default=False,
),
migrations.AddField(
model_name="electorallistresult",
name="tmp_list",
field=django.contrib.postgres.fields.jsonb.JSONField(
blank=True, default=dict, verbose_name="temp list"
),
),
migrations.AlterField(
model_name="electoralcandidateresult",
name="is_elected",
field=models.BooleanField(blank=True, null=True, verbose_name="elected"),
),
migrations.AlterField(
model_name="electoralcandidateresult",
name="is_top",
field=models.BooleanField(blank=True, null=True, verbose_name="top"),
),
migrations.AlterField(
model_name="electoralcandidateresult",
name="votes",
field=models.PositiveIntegerField(
blank=True, null=True, verbose_name="votes"
),
),
migrations.AlterField(
model_name="electoralcandidateresult",
name="votes_perc",
field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=5,
null=True,
verbose_name="votes percent",
),
),
migrations.AlterField(
model_name="electorallistresult",
name="name",
field=models.CharField(max_length=512, verbose_name="name"),
),
migrations.AlterField(
model_name="electoralresult",
name="constituency",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="popolo.Area",
verbose_name="constituency",
),
),
migrations.AlterField(
model_name="electoralresult",
name="is_total",
field=models.BooleanField(default=True, verbose_name="total"),
),
]
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("elections", "0004_auto_20201028_1131"),
]
operations = [
migrations.RemoveField(
model_name="electoralcandidateresult",
name="is_elected",
),
migrations.RemoveField(
model_name="electorallistresult",
name="seats_perc",
),
migrations.AddField(
model_name="electoralcandidateresult",
name="is_counselor",
field=models.BooleanField(blank=True, null=True, verbose_name="counselor"),
),
]
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("elections", "0005_auto_20201109_1036"),
]
operations = [
migrations.RemoveField(
model_name="electoralresult",
name="first_round_result",
),
migrations.AddField(
model_name="electoralcandidateresult",
name="ballot_votes",
field=models.PositiveIntegerField(
blank=True, null=True, verbose_name="votes (second round)"
),
),
migrations.AddField(
model_name="electoralcandidateresult",
name="ballot_votes_perc",
field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=5,
null=True,
verbose_name="votes percent (second round)",
),
),
migrations.AddField(
model_name="electorallistresult",
name="ballot_candidate_result",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="ballot_list_results",
to="elections.ElectoralCandidateResult",
verbose_name="candidate result (second round)",
),
),
migrations.AddField(
model_name="electoralresult",
name="ballot_blank_votes",
field=models.PositiveIntegerField(
blank=True, null=True, verbose_name="blank votes (second round)"
),
),
migrations.AddField(
model_name="electoralresult",
name="ballot_invalid_votes",
field=models.PositiveIntegerField(
blank=True, null=True, verbose_name="invalid votes (second round)"
),
),
migrations.AddField(
model_name="electoralresult",
name="ballot_valid_votes",
field=models.PositiveIntegerField(
blank=True, null=True, verbose_name="valid votes (second round)"
),
),
migrations.AddField(
model_name="electoralresult",
name="ballot_votes_cast",
field=models.PositiveIntegerField(
blank=True, null=True, verbose_name="votes cast (second round)"
),
),
migrations.AddField(
model_name="electoralresult",
name="ballot_votes_cast_perc",
field=models.DecimalField(
blank=True,
decimal_places=2,
max_digits=5,
null=True,
verbose_name="votes cast percent (second round)",
),
),
]
# Generated by Django 2.2.18 on 2021-03-25 11:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('elections', '0006_auto_20201118_1422'),
]
operations = [
migrations.AlterField(
model_name='electoralresult',
name='blank_votes',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='blank votes'),
),
migrations.AlterField(
model_name='electoralresult',
name='invalid_votes',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='invalid votes'),
),
migrations.AlterField(
model_name='electoralresult',
name='valid_votes',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='valid votes'),
),
migrations.AlterField(
model_name='electoralresult',
name='votes_cast',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='votes cast'),
),
migrations.AlterField(
model_name='electoralresult',
name='votes_cast_perc',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='votes cast percent'),
),
]
"""Define elections models."""
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.utils.translation import gettext_lazy as _
from popolo.behaviors.models import Timestampable
from popolo.mixins import SourceShortcutsMixin
from popolo.models import Area, KeyEvent, Organization
from project.elections.load import load_json_items
class PoliticalColour(Timestampable, models.Model):
......@@ -27,49 +32,41 @@ class PoliticalColour(Timestampable, models.Model):
return self.name
class ElectoralResult(Timestampable, models.Model):
class ElectoralResult(SourceShortcutsMixin, Timestampable, models.Model):
"""Electoral result."""
registered_voters = models.PositiveIntegerField(_("registered voters"))
registered_voters_perc = models.DecimalField(
_("registered voters percent"), max_digits=5, decimal_places=2
)
votes_cast = models.PositiveIntegerField(_("votes cast"))
# first round
votes_cast = models.PositiveIntegerField(_("votes cast"), null=True, blank=True)
votes_cast_perc = models.DecimalField(
_("votes cast percent"), max_digits=5, decimal_places=2
_("votes cast percent"), max_digits=5, decimal_places=2, null=True, blank=True
)
invalid_votes = models.PositiveIntegerField(_("invalid votes"))
invalid_votes_perc = models.DecimalField(
_("invalid votes percent"),
max_digits=5,
decimal_places=2,
invalid_votes = models.PositiveIntegerField(_("invalid votes"), null=True, blank=True)
blank_votes = models.PositiveIntegerField(_("blank votes"), null=True, blank=True)
valid_votes = models.PositiveIntegerField(_("valid votes"), null=True, blank=True)
# second round
ballot_votes_cast = models.PositiveIntegerField(
_("votes cast (second round)"), null=True, blank=True
)
blank_votes = models.PositiveIntegerField(_("blank votes"))
blank_votes_perc = models.DecimalField(
_("blank votes percent"),
ballot_votes_cast_perc = models.DecimalField(
_("votes cast percent (second round)"),
max_digits=5,
decimal_places=2,
null=True,
blank=True,
)
list_votes = models.PositiveIntegerField(_("list votes"))
list_votes_perc = models.DecimalField(
_("list percent"), max_digits=5, decimal_places=2
)
valid_votes = models.PositiveIntegerField(_("valid votes"))
valid_votes_perc = models.DecimalField(
_("valid votes percent"), max_digits=5, decimal_places=2
)
turnout = models.PositiveIntegerField(
_("turnout"),
ballot_invalid_votes = models.PositiveIntegerField(
_("invalid votes (second round)"), null=True, blank=True
)
turnout_perc = models.DecimalField(
_("turnout votes percent"), max_digits=5, decimal_places=2
ballot_blank_votes = models.PositiveIntegerField(
_("blank votes (second round)"), null=True, blank=True
)
abstentions = models.PositiveIntegerField(_("abstentions"))
abstentions_perc = models.DecimalField(
_("abstentions percent"), max_digits=5, decimal_places=2
ballot_valid_votes = models.PositiveIntegerField(
_("valid votes (second round)"), null=True, blank=True
)
# election
is_valid = models.BooleanField(_("valid"))
is_total = models.BooleanField(_("total"))
is_total = models.BooleanField(_("total"), default=True)
electoral_event = models.ForeignKey(
"popolo.KeyEvent",
on_delete=models.PROTECT,
......@@ -88,8 +85,12 @@ class ElectoralResult(Timestampable, models.Model):
verbose_name=_("constituency"),
null=True,
)
first_round_result = models.OneToOneField(
"self", _("first round"), null=True, blank=True
# array of items referencing "http://popoloproject.com/schemas/source.json#"
sources = GenericRelation(
to="popolo.SourceRel",
blank=True,
null=True,
help_text=_("URLs to source documents about the Electoral Result"),
)
class Meta:
......@@ -100,45 +101,235 @@ class ElectoralResult(Timestampable, models.Model):
def __str__(self):
"""Return string representation."""
return f"{self.electoral_event.name} - {self.institution.name} - {self.constituency.name}"
return (
f"{self.electoral_event.name} - "
f"{self.institution.name} - "
f"{self.constituency.name}"
)
@property
def is_second_round(self):
"""Return true if it's the second round."""
return self.first_round_result_id is not None
def has_second_round(self):
"""Return true if it has the second round."""
return self.ballot_votes_cast is not None
@classmethod
def load_results(cls, path: str):
"""Load electoral results from a path."""
stats = {"events": 0, "results": 0, "candidates": 0, "lists": 0, "errors": []}
importazione, error_str = load_json_items(path)
if error_str:
stats["errors"].append(error_str)
for event in getattr(importazione, "eventi", []):
event_type = f"{event.tipo}"
event_type_text = dict(KeyEvent.EVENT_TYPES).get(event_type, event_type)
key_event, _ = KeyEvent.objects.get_or_create(
start_date=f"{event.data}",
event_type=f"{event.tipo}",
defaults={
"name": f"{event_type_text} {event.data:%d/%m/%Y}",
"identifier": f"{event_type}_{event.data}",
},
)
stats["events"] += 1
for result in event.risultati:
area_id = result.area
try:
area = Area.objects.get(id=area_id)
except Area.DoesNotExist:
stats["errors"].append(f"Area {area_id} does not exists.")
continue
organization_id = result.istituzione
try:
organization = Organization.objects.get(id=organization_id)
except Organization.DoesNotExist:
stats["errors"].append(
f"Organization {organization_id} does not exists."
)
continue
electoral_res, _ = ElectoralResult.objects.update_or_create(
electoral_event=key_event,
institution=organization,
constituency=area,
defaults={
"blank_votes": result.bianche,
"ballot_blank_votes": result.bianche_ballot,
"registered_voters": result.elettori,
"is_valid": result.elezione_valida,
"invalid_votes": result.nulle,
"ballot_invalid_votes": result.nulle_ballot,
"votes_cast_perc": result.perc_votanti,
"ballot_votes_cast_perc": result.perc_votanti_ballot,
"is_total": result.totale,
"votes_cast": result.votanti,
"ballot_votes_cast": result.votanti_ballot,
"valid_votes": (
None
if (
result.nulle is None
or result.votanti is None
)
else result.votanti - result.nulle
),
"ballot_valid_votes": (
None
if (
result.nulle_ballot is None
or result.votanti_ballot is None
)
else result.votanti_ballot - result.nulle_ballot
),
},
)
stats["results"] += 1
for candidate in result.candidati:
candidate_res, _ = ElectoralCandidateResult.objects.update_or_create(
result=electoral_res,
name=candidate.nome,
defaults={
"votes": candidate.voti,
"ballot_votes": candidate.voti_ballot,
"votes_perc": candidate.perc_voti,
"ballot_votes_perc": candidate.perc_voti_ballot,
"is_top": candidate.eletto_chief,
"is_counselor": candidate.eletto_consigliere,
},
)
stats["candidates"] += 1
for party in candidate.liste:
ballot_cand_res = None
candidate_result = candidate_res
tmp_list = {}
if party.candidato:
try:
candidate_result = ElectoralCandidateResult.objects.get(
name=party.candidato,
result=electoral_res,
)
except ElectoralCandidateResult.DoesNotExist:
candidate_result = None
else:
ballot_cand_res = candidate_res
if candidate_result is not None:
if party.candidato_ballot:
try:
ballot_cand_res = (
ElectoralCandidateResult.objects.get(
name=party.candidato_ballot,
result=electoral_res,
)
)
except ElectoralCandidateResult.DoesNotExist:
ballot_cand_res = None
tmp_list[
"candidato_ballot"
] = party.candidato_ballot
ElectoralListResult.objects.update_or_create(
result=electoral_res,
candidate_result=candidate_result,
name=party.denominazione,
defaults={
"votes": party.voti,
"votes_perc": party.perc_voti,
"seats": party.seggi,
"ballot_candidate_result": ballot_cand_res,
"tmp_list": tmp_list,
},
)
stats["lists"] += 1
return stats
class ElectoralCandidateResult(Timestampable, models.Model):
"""Electoral candidate result."""
name = models.CharField(_("name"), max_length=512)
# first round
votes = models.PositiveIntegerField(_("votes"), blank=True, null=True)
votes_perc = models.DecimalField(
_("votes percent"), max_digits=5, decimal_places=2, blank=True, null=True
)
# second round
ballot_votes = models.PositiveIntegerField(
_("votes (second round)"), blank=True, null=True
)
ballot_votes_perc = models.DecimalField(
_("votes percent (second round)"),
max_digits=5,
decimal_places=2,
blank=True,
null=True,
)
# election
is_counselor = models.BooleanField(_("counselor"), blank=True, null=True)
is_top = models.BooleanField(_("top"), blank=True, null=True)
candidate = models.ForeignKey(
"popolo.Person",
on_delete=models.SET_NULL,
verbose_name=_("candidate"),
related_name="candidate_results",
blank=True,
null=True,
)
result = models.ForeignKey(
ElectoralResult,
on_delete=models.PROTECT,
verbose_name=_("result"),
related_name="candidate_results",
)
political_colour = models.ForeignKey(
PoliticalColour,
on_delete=models.SET_NULL,
verbose_name=_("political colour"),
related_name="candidate_results",
blank=True,
null=True,
)
tmp_candidate = JSONField(_("temp candidate"), default=dict, blank=True)
class Meta:
"""Define model Meta."""
verbose_name = _("Electoral candidate result")
verbose_name_plural = _("Electoral candidate results")
def __str__(self):
"""Return string representation."""
return f"{self.name} - {self.result}"
class ElectoralListResult(Timestampable, models.Model):
"""Electoral list result."""
name = models.CharField(
_("name"),
max_length=255,
unique=True,
)
name = models.CharField(_("name"), max_length=512)
image = models.URLField(_("image"))
votes = models.PositiveIntegerField(_("votes"), blank=True, null=True)
votes_perc = models.DecimalField(
_("votes percent"), max_digits=5, decimal_places=2, blank=True, null=True
)
seats = models.PositiveIntegerField(_("seats"), blank=True, null=True)
seats_perc = models.DecimalField(
_("seats percent"), max_digits=5, decimal_places=2, blank=True, null=True
)
result = models.ForeignKey(
"ElectoralResult",
ElectoralResult,
on_delete=models.PROTECT,
verbose_name=_("result"),
related_name="list_results",
)
candidate_result = models.ForeignKey(
"ElectoralCandidateResult",
ElectoralCandidateResult,
on_delete=models.PROTECT,
verbose_name=_("candidate result"),
related_name="list_results",
null=True,
)
ballot_candidate_result = models.ForeignKey(
ElectoralCandidateResult,
on_delete=models.PROTECT,
verbose_name=_("candidate result (second round)"),
related_name="ballot_list_results",
null=True,
blank=True,
)
political_colour = models.ForeignKey(
"PoliticalColour",
PoliticalColour,
on_delete=models.SET_NULL,
verbose_name=_("political colour"),
related_name="list_results",
......@@ -153,6 +344,7 @@ class ElectoralListResult(Timestampable, models.Model):
blank=True,
null=True,
)
tmp_list = JSONField(_("temp list"), default=dict, blank=True)
class Meta:
"""Define model Meta."""
......@@ -162,42 +354,4 @@ class ElectoralListResult(Timestampable, models.Model):
def __str__(self):
"""Return string representation."""
return self.name
class ElectoralCandidateResult(Timestampable, models.Model):
"""Electoral candidate result."""
votes = models.PositiveIntegerField(_("votes"))
votes_perc = models.DecimalField(_("votes percent"), max_digits=5, decimal_places=2)
is_elected = models.BooleanField(_("elected"))
is_top = models.BooleanField(_("top"))
candidate = models.ForeignKey(
"popolo.Person",
on_delete=models.SET_NULL,
verbose_name=_("candidate"),
related_name="candidate_results",
blank=True,
null=True,
)
result = models.ForeignKey(
"ElectoralResult",
on_delete=models.PROTECT,
verbose_name=_("result"),
related_name="candidate_results",
)
political_colour = models.ForeignKey(
"PoliticalColour",
on_delete=models.SET_NULL,
verbose_name=_("political colour"),
related_name="candidate_results",
blank=True,
null=True,
)
tmp_candidate = JSONField(_("temp candidate"), default=dict, blank=True)
class Meta:
"""Define model Meta."""
verbose_name = _("Electoral candidate result")
verbose_name_plural = _("Electoral candidate results")
return f"{self.name} - {self.result}"