diff --git a/django-backend/fecfiler/f3x_summaries/fixtures/report_code_labels.json b/django-backend/fecfiler/f3x_summaries/fixtures/report_code_labels.json new file mode 100644 index 000000000..c5cb2735e --- /dev/null +++ b/django-backend/fecfiler/f3x_summaries/fixtures/report_code_labels.json @@ -0,0 +1,202 @@ +[ +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 1, + "fields": { + "label": "APRIL 15 (Q1)", + "report_code": "Q1" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 2, + "fields": { + "label": "JULY 15 (Q2)", + "report_code": "Q2" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 3, + "fields": { + "label": "OCTOBER 15 (Q3)", + "report_code": "Q3" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 4, + "fields": { + "label": "JANUARY 31 (YE)", + "report_code": "YE" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 5, + "fields": { + "label": "TERMINATION (TER)", + "report_code": "TER" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 6, + "fields": { + "label": "JANUARY 31 (31)", + "report_code": "MY" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 7, + "fields": { + "label": "GENERAL (12G)", + "report_code": "12G" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 8, + "fields": { + "label": "PRIMARY (12P)", + "report_code": "12P" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 9, + "fields": { + "label": "RUNOFF (12R)", + "report_code": "12R" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 10, + "fields": { + "label": "SPECIAL (12S)", + "report_code": "12S" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 11, + "fields": { + "label": "CONVENTION (12C)", + "report_code": "12C" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 12, + "fields": { + "label": "GENERAL (30G)", + "report_code": "30G" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 13, + "fields": { + "label": "RUNOFF (30R)", + "report_code": "30R" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 14, + "fields": { + "label": "SPECIAL (30S)", + "report_code": "30S" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 15, + "fields": { + "label": "FEBRUARY 20 (M2)", + "report_code": "M2" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 16, + "fields": { + "label": "MARCH 30 (M3)", + "report_code": "M3" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 17, + "fields": { + "label": "APRIL 20 (M4)", + "report_code": "M4" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 18, + "fields": { + "label": "MAY 20 (M5)", + "report_code": "M5" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 19, + "fields": { + "label": "JUNE 20 (M6)", + "report_code": "M6" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 20, + "fields": { + "label": "JULY 20 (M7)", + "report_code": "M7" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 21, + "fields": { + "label": "AUGUST 20 (M8)", + "report_code": "M8" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 22, + "fields": { + "label": "SEPTEMBER 20 (M9)", + "report_code": "M9" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 23, + "fields": { + "label": "OCTOBER 20 (M10)", + "report_code": "M10" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 24, + "fields": { + "label": "NOVEMBER 20 (M11)", + "report_code": "M11" + } +}, +{ + "model": "f3x_summaries.reportcodelabel", + "pk": 25, + "fields": { + "label": "DECEMBER 20 (M12)", + "report_code": "M12" + } +} +] diff --git a/django-backend/fecfiler/f3x_summaries/migrations/0010_auto_20220525_1157.py b/django-backend/fecfiler/f3x_summaries/migrations/0010_auto_20220525_1157.py new file mode 100644 index 000000000..edf037ded --- /dev/null +++ b/django-backend/fecfiler/f3x_summaries/migrations/0010_auto_20220525_1157.py @@ -0,0 +1,60 @@ +# Generated by Django 3.2.12 on 2022-05-25 15:57 + +from django.db import migrations, models +import django.db.models.deletion +from django.core.management import call_command + + +def forwards_func(apps, schema_editor): + call_command( + "loaddata", + "fecfiler/f3x_summaries/fixtures/report_code_labels.json", + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("f3x_summaries", "0009_auto_20220515_2116"), + ] + + operations = [ + migrations.CreateModel( + name="ReportCodeLabel", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("label", models.TextField(blank=True, null=True)), + ("report_code", models.TextField(blank=True, null=True, unique=True)), + ], + options={ + "db_table": "report_code_labels", + }, + ), + migrations.AddIndex( + model_name="reportcodelabel", + index=models.Index( + fields=["report_code"], name="report_code_report__9cd13b_idx" + ), + ), + migrations.RunPython(forwards_func), + migrations.AlterField( + model_name="f3xsummary", + name="report_code", + field=models.ForeignKey( + blank=True, + db_column="report_code", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="f3x_summaries.reportcodelabel", + to_field="report_code", + ), + ), + ] diff --git a/django-backend/fecfiler/f3x_summaries/models.py b/django-backend/fecfiler/f3x_summaries/models.py index 0b2f8b44c..e9012d340 100644 --- a/django-backend/fecfiler/f3x_summaries/models.py +++ b/django-backend/fecfiler/f3x_summaries/models.py @@ -3,6 +3,18 @@ from fecfiler.committee_accounts.models import CommitteeOwnedModel +class ReportCodeLabel(models.Model): + label = models.TextField(null=True, blank=True) + report_code = models.TextField(null=True, blank=True, unique=True) + + def __str__(self): + return self.label + + class Meta: + db_table = "report_code_labels" + indexes = [models.Index(fields=["report_code"])] + + class F3XSummary(SoftDeleteModel, CommitteeOwnedModel): """Generated model from json schema""" @@ -16,7 +28,14 @@ class F3XSummary(SoftDeleteModel, CommitteeOwnedModel): state = models.TextField(null=True, blank=True) zip = models.TextField(null=True, blank=True) - report_code = models.TextField(null=True, blank=True) + report_code = models.ForeignKey( + ReportCodeLabel, + models.SET_NULL, + null=True, + blank=True, + to_field="report_code", + db_column="report_code", + ) election_code = models.TextField(null=True, blank=True) date_of_election = models.TextField(null=True, blank=True) state_of_election = models.TextField(null=True, blank=True) diff --git a/django-backend/fecfiler/f3x_summaries/serializers.py b/django-backend/fecfiler/f3x_summaries/serializers.py index faf4f3c25..7fc286ec2 100644 --- a/django-backend/fecfiler/f3x_summaries/serializers.py +++ b/django-backend/fecfiler/f3x_summaries/serializers.py @@ -1,4 +1,5 @@ -from .models import F3XSummary +from .models import F3XSummary, ReportCodeLabel +from rest_framework.serializers import ModelSerializer, SlugRelatedField from fecfiler.committee_accounts.serializers import CommitteeOwnedSerializer from fecfiler.validation import serializers import logging @@ -10,6 +11,13 @@ class F3XSummarySerializer( CommitteeOwnedSerializer, serializers.FecSchemaValidatorSerializerMixin ): schema_name = "F3X" + report_code = SlugRelatedField( + many=False, + required=False, + read_only=False, + slug_field="report_code", + queryset=ReportCodeLabel.objects.all(), + ) class Meta: model = F3XSummary @@ -20,3 +28,10 @@ class Meta: "created", "updated", ] + foreign_key_fields = {"report_code": "report_code"} + + +class ReportCodeLabelSerializer(ModelSerializer): + class Meta: + model = ReportCodeLabel + fields = ("label", "report_code") diff --git a/django-backend/fecfiler/f3x_summaries/urls.py b/django-backend/fecfiler/f3x_summaries/urls.py index 26f721684..9d4cf6926 100644 --- a/django-backend/fecfiler/f3x_summaries/urls.py +++ b/django-backend/fecfiler/f3x_summaries/urls.py @@ -1,10 +1,14 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import F3XSummaryViewSet +from .views import F3XSummaryViewSet, ReportCodeLabelViewSet # Create a router and register our viewsets with it. router = DefaultRouter() router.register(r"f3x-summaries", F3XSummaryViewSet, basename="f3x-summaries") +router.register( + r"report-code-labels", + ReportCodeLabelViewSet, + basename="report-code-labels") # The API URLs are now determined automatically by the router. urlpatterns = [ diff --git a/django-backend/fecfiler/f3x_summaries/views.py b/django-backend/fecfiler/f3x_summaries/views.py index a9381d929..8ffadef51 100644 --- a/django-backend/fecfiler/f3x_summaries/views.py +++ b/django-backend/fecfiler/f3x_summaries/views.py @@ -1,8 +1,9 @@ from rest_framework import filters -from django.db.models import Case, Value, When +from rest_framework.viewsets import GenericViewSet +from rest_framework.mixins import ListModelMixin from fecfiler.committee_accounts.views import CommitteeOwnedViewSet -from .models import F3XSummary -from .serializers import F3XSummarySerializer +from .models import F3XSummary, ReportCodeLabel +from .serializers import F3XSummarySerializer, ReportCodeLabelSerializer class F3XSummaryViewSet(CommitteeOwnedViewSet): @@ -11,40 +12,21 @@ class F3XSummaryViewSet(CommitteeOwnedViewSet): `update` and `destroy` actions. Note that this ViewSet inherits from CommitteeOwnedViewSet - The queryset will be further limmited by the user's committee + The queryset will be further limited by the user's committee in CommitteeOwnedViewSet's implementation of get_queryset() """ - queryset = F3XSummary.objects.alias(report_code_label=Case( - When(report_code='Q1', then=Value('APRIL 15 (Q1)')), - When(report_code='Q2', then=Value('JULY 15 (Q2)')), - When(report_code='Q3', then=Value('OCTOBER 15 (Q3)')), - When(report_code='YE', then=Value('JANUARY 31 (YE)')), - When(report_code='TER', then=Value('TERMINATION (TER)')), - When(report_code='MY', then=Value('JANUARY 31 (MY)')), - When(report_code='12G', then=Value('GENERAL (12G)')), - When(report_code='12P', then=Value('PRIMARY (12P)')), - When(report_code='12R', then=Value('RUNOFF (12R)')), - When(report_code='12S', then=Value('SPECIAL (12S)')), - When(report_code='12C', then=Value('CONVENTION (12C)')), - When(report_code='30G', then=Value('GENERAL (30G)')), - When(report_code='30R', then=Value('RUNOFF (30R)')), - When(report_code='30S', then=Value('SPECIAL (30S)')), - When(report_code='M2', then=Value('FEBRUARY 20 (M2)')), - When(report_code='M3', then=Value('MARCH 20 (M3)')), - When(report_code='M4', then=Value('APRIL 20 (M4)')), - When(report_code='M5', then=Value('MAY 20 (M5)')), - When(report_code='M6', then=Value('JUNE 20 (M6)')), - When(report_code='M7', then=Value('JULY 20 (M7)')), - When(report_code='M8', then=Value('AUGUST 20 (M8)')), - When(report_code='M9', then=Value('SEPTEMBER 20 (M9)')), - When(report_code='M10', then=Value('OCTOBER 20 (M10)')), - When(report_code='M11', then=Value('NOVEMBER 20 (M11)')), - When(report_code='M12', then=Value('DECEMBER 20 (M12)')), - default=Value('_NOT_FOUND_'), - )).all() + queryset = F3XSummary.objects.select_related("report_code").all() + """Join on report code labels""" + serializer_class = F3XSummarySerializer permission_classes = [] filter_backends = [filters.OrderingFilter] - ordering_fields = ['form_type', 'report_code_label', 'coverage_through_date'] - ordering = ['form_type'] + ordering_fields = ["form_type", "report_code__label", "coverage_through_date"] + ordering = ["form_type"] + + +class ReportCodeLabelViewSet(GenericViewSet, ListModelMixin): + queryset = ReportCodeLabel.objects.all() + serializer_class = ReportCodeLabelSerializer + pagination_class = None diff --git a/django-backend/fecfiler/validation/serializers.py b/django-backend/fecfiler/validation/serializers.py index 0ea6eff00..6f99df485 100644 --- a/django-backend/fecfiler/validation/serializers.py +++ b/django-backend/fecfiler/validation/serializers.py @@ -48,6 +48,32 @@ def get_schema_name(self, data): ) return self.schema_name + def get_validation_candidate(self, data): + """Returns a copy of data where foreign key fields are replaced with the + underlying foreign key field. + + Example: the f3x table is related to the report code label table by the + report code field. DRF gives us the whole + :py:class:`fecfiler.f3x_summaries.models.ReportCodeLabel` object. + for validating purposes, we just want the report code. With the + foreign_key_fields mapping defined in Meta, we replace the foreign-key- + related object with the key only. + """ + validation_candidate = data.copy() + for (foreign_key_field, actual_key) in self.get_foreign_key_fields().items(): + if hasattr(validation_candidate.get(foreign_key_field, {}), actual_key): + validation_candidate[foreign_key_field] = getattr( + validation_candidate.get(foreign_key_field, {}), actual_key + ) + + return validation_candidate + + def get_foreign_key_fields(self): + """Returns a dictionary of foreign key fields""" + meta = getattr(self, "Meta", None) + foreign_key_fields = getattr(meta, "foreign_key_fields", None) + return dict(foreign_key_fields) if foreign_key_fields else {} + def validate(self, data): """Overrides Django Rest Framework's Serializer validate to validate with fecfile_validate rules. @@ -63,8 +89,11 @@ def validate(self, data): fields_to_validate = ( fields_to_validate_str.split(",") if fields_to_validate_str else [] ) + validation_result = validate.validate( - self.get_schema_name(data), data, fields_to_validate + self.get_schema_name(data), + self.get_validation_candidate(data), + fields_to_validate, ) if validation_result.errors: