diff --git a/.circleci/config.yml b/.circleci/config.yml index f78e535627..34679fb95c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,20 +5,22 @@ version: 2.1 # See: https://circleci.com/docs/2.0/orb-intro/ orbs: # See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/python - python: circleci/python@3.2.0 - browser-tools: circleci/browser-tools@2.2.1 - node: circleci/node@7.1.1 + python: circleci/python@4.0.0 + node: circleci/node@7.2.1 -# See: https://circleci.com/docs/2.0/configuration-reference/#jobs -jobs: - test: - # These next lines defines a Docker executors: https://circleci.com/docs/2.0/executor-types/ - # A list of available CircleCI Docker convenience images are available here: https://circleci.com/developer/images/image/cimg/python - docker: - - image: cimg/python:3.13-node - - image: cimg/postgres:14.13 - - image: cimg/redis:6.2.6 +commands: + unified-requirements-cache: + steps: + - run: + name: Create unified requirements so CircleCI can cache them + command: | + cd ~/project/ + ls -l + cat requirements.txt > requirements-all.txt + echo >> requirements-all.txt # blank in case new newline at end of requirements.txt + cat requirements-test.txt >> requirements-all.txt + check-env-vars: steps: - run: name: Check for necessary environment variables @@ -29,62 +31,37 @@ jobs: FECFILE_FEC_WEBSITE_API_KEY EOF exit 0 +# See: https://circleci.com/docs/2.0/configuration-reference/#jobs +jobs: + test: + # These next lines defines a Docker executors: https://circleci.com/docs/2.0/executor-types/ + # A list of available CircleCI Docker convenience images are available here: https://circleci.com/developer/images/image/cimg/python + docker: + - image: &python313_image cimg/python:3.13-node + - image: cimg/postgres:14.13 + - image: cimg/redis:6.2.6 + resource_class: large + steps: + - check-env-vars - checkout: method: full - - run: - name: Create unified requirements so CircleCI can cache them - command: | - cd ~/project/ - ls -l - cat requirements.txt > requirements-all.txt - echo >> requirements-all.txt # blank in case new newline at end of requirements.txt - cat requirements-test.txt >> requirements-all.txt - + - unified-requirements-cache - python/install-packages: pkg-manager: pip app-dir: ~/project/ pip-dependency-file: requirements-all.txt - run: - name: Load test database fixure - command: | - psql ${DATABASE_URL} < e2e-test-db.sql - working_directory: ~/project/db - - - run: - name: Check for missing migrations - command: | - python manage.py makemigrations --check - working_directory: ~/project/django-backend/ - - - run: - name: Check for breaking change migrations - # After December 2024 - command: | - python manage.py lintmigrations --git-commit-id d73068e23fc5b035af2b224b16d4726b7b20d67c --project-root-path '.' - working_directory: ~/project/django-backend/ - - - run: - name: Run migrations - command: | - python manage.py migrate --no-input --traceback --verbosity 3 + name: Run tests + # Use built-in Django test module + command: coverage run --rcfile=.coveragerc manage.py test --parallel 4 --timing working_directory: ~/project/django-backend/ - run: - name: Run lint - command: | - flake8 --config django-backend/.flake8 - - - run: - name: Run deptry - command: deptry ~/project/ - - - run: - name: Run tests - # Use built-in Django test module - command: coverage run --source='.' --rcfile=.coveragerc manage.py test + name: Combine coverage data + command: coverage combine --rcfile=.coveragerc working_directory: ~/project/django-backend/ - run: @@ -133,14 +110,84 @@ jobs: SONARQUBE_SCANNER_PARAMS: '{"sonar.host.url":"https://sonarcloud.io"}' - save_cache: key: v1-sonarcloud-scanner-7.1.0.4889 - paths: /tmp/cache/scanner + paths: + - /tmp/cache/scanner - store_artifacts: path: /tmp/sonar_report destination: sonar_report + schema-checks: + docker: + - image: *python313_image + - image: cimg/postgres:14.13 + - image: cimg/redis:6.2.6 + + steps: + - check-env-vars + - checkout: + method: full + + - unified-requirements-cache + - python/install-packages: + pkg-manager: pip + app-dir: ~/project/ + pip-dependency-file: requirements-all.txt + + - run: + name: Check for missing migrations + command: | + python manage.py makemigrations --check + working_directory: ~/project/django-backend/ + + - run: + name: Check for breaking change migrations + # After December 2024 + command: | + python manage.py lintmigrations --git-commit-id d73068e23fc5b035af2b224b16d4726b7b20d67c --project-root-path '.' + working_directory: ~/project/django-backend/ + + - run: + name: Run migrations + command: | + python manage.py migrate --no-input --traceback --verbosity 3 + working_directory: ~/project/django-backend/ + + lint: + docker: + - image: *python313_image + + steps: + - checkout + - unified-requirements-cache + - python/install-packages: + pkg-manager: pip + app-dir: ~/project/ + pip-dependency-file: requirements-all.txt + + - run: + name: Run lint + command: | + flake8 --config django-backend/.flake8 + + dependency-check: + docker: + - image: *python313_image + + steps: + - checkout + - unified-requirements-cache + - python/install-packages: + pkg-manager: pip + app-dir: ~/project/ + pip-dependency-file: requirements-all.txt + + - run: + name: Run deptry + command: deptry ~/project/ + deploy-job: docker: - - image: cimg/python:3.13 + - image: *python313_image steps: - checkout @@ -164,20 +211,11 @@ jobs: docs-build: docker: - - image: cimg/python:3.13 + - image: *python313_image steps: - checkout - - - run: - name: Create unified requirements so CircleCI can cache them - command: | - cd ~/project/ - ls -l - cat requirements.txt > requirements-all.txt - echo >> requirements-all.txt # blank in case new newline at end of requirements.txt - cat requirements-test.txt >> requirements-all.txt - + - unified-requirements-cache - python/install-packages: pkg-manager: pip app-dir: ~/project/ @@ -190,7 +228,8 @@ jobs: - persist_to_workspace: root: ~/project/docs/_build - paths: html + paths: + - html docs-deploy: docker: @@ -244,6 +283,9 @@ jobs: workflows: primary: # This is the name of the workflow, feel free to change it to better match your workflow. jobs: + - lint + - dependency-check + - schema-checks - test # This job is triggered whenever a commit is made to the dev/stage/test/prod branches. # It kicks off the e2e-test pipeline in the fecfile-web-app project. @@ -253,6 +295,9 @@ workflows: only: /develop|release\/sprint-[\.\d]+|release\/test|main/ - deploy-job: requires: + - lint + - dependency-check + - schema-checks - test filters: branches: diff --git a/Dockerfile b/Dockerfile index 6e58397e95..96b0a1f6ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12 +FROM python:3.13 ENV PYTHONUNBUFFERED=1 RUN mkdir /opt/nxg_fec diff --git a/Dockerfile-e2e b/Dockerfile-e2e index 2b2666c8e2..00b11422d9 100644 --- a/Dockerfile-e2e +++ b/Dockerfile-e2e @@ -1,4 +1,4 @@ -FROM python:3.12 +FROM python:3.13 ENV PYTHONUNBUFFERED=1 RUN mkdir /opt/nxg_fec_e2e diff --git a/Scheduler_Dockerfile b/Scheduler_Dockerfile index 5a281158d1..b60194fde0 100644 --- a/Scheduler_Dockerfile +++ b/Scheduler_Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12 +FROM python:3.13 ENV PYTHONUNBUFFERED=1 RUN mkdir /opt/nxg_fec diff --git a/Worker_Dockerfile b/Worker_Dockerfile index 3cb8dd4a17..a5aecbf990 100644 --- a/Worker_Dockerfile +++ b/Worker_Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.12 +FROM python:3.13 ENV PYTHONUNBUFFERED=1 RUN mkdir /opt/nxg_fec diff --git a/Worker_Dockerfile-e2e b/Worker_Dockerfile-e2e index 513a74a58d..f9aa08bebd 100644 --- a/Worker_Dockerfile-e2e +++ b/Worker_Dockerfile-e2e @@ -1,4 +1,4 @@ -FROM python:3.12 +FROM python:3.13 ENV PYTHONUNBUFFERED=1 RUN mkdir /opt/nxg_fec_e2e diff --git a/django-backend/.coveragerc b/django-backend/.coveragerc index 32d237e785..4762eff38a 100644 --- a/django-backend/.coveragerc +++ b/django-backend/.coveragerc @@ -1,6 +1,9 @@ # .coveragerc to control coverage.py # https://coverage.readthedocs.io/en/coverage-4.3.3/source.html#execution [run] +source = . +parallel = True +concurrency = multiprocessing omit = # Excluding dev scripts from code coverage ./scripts/json_schema_to_django_model.py \ No newline at end of file diff --git a/django-backend/fecfiler/cash_on_hand/migrations/0001_initial.py b/django-backend/fecfiler/cash_on_hand/migrations/0001_squashed_0002_alter_cashonhandyearly_cash_on_hand_and_more.py similarity index 74% rename from django-backend/fecfiler/cash_on_hand/migrations/0001_initial.py rename to django-backend/fecfiler/cash_on_hand/migrations/0001_squashed_0002_alter_cashonhandyearly_cash_on_hand_and_more.py index 407a0fb686..d1b9f1996e 100644 --- a/django-backend/fecfiler/cash_on_hand/migrations/0001_initial.py +++ b/django-backend/fecfiler/cash_on_hand/migrations/0001_squashed_0002_alter_cashonhandyearly_cash_on_hand_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-01-16 20:15 +# Generated by Django 5.2.11 on 2026-03-06 21:37 from django.db import migrations, models import django.db.models.deletion @@ -6,10 +6,16 @@ class Migration(migrations.Migration): + + replaces = [ + ("cash_on_hand", "0001_initial"), + ("cash_on_hand", "0002_alter_cashonhandyearly_cash_on_hand_and_more"), + ] + initial = True dependencies = [ - ("committee_accounts", "0001_initial"), + ("committee_accounts", "0001_squashed_0007_alter_committeeaccount_members"), ] operations = [ @@ -26,13 +32,8 @@ class Migration(migrations.Migration): unique=True, ), ), - ( - "cash_on_hand", - models.DecimalField( - blank=True, decimal_places=2, max_digits=11, null=True - ), - ), - ("year", models.TextField(blank=True, null=True)), + ("cash_on_hand", models.DecimalField(decimal_places=2, max_digits=11)), + ("year", models.TextField()), ("created", models.DateTimeField(auto_now_add=True)), ("updated", models.DateTimeField(auto_now=True)), ( diff --git a/django-backend/fecfiler/cash_on_hand/migrations/0002_alter_cashonhandyearly_cash_on_hand_and_more.py b/django-backend/fecfiler/cash_on_hand/migrations/0002_alter_cashonhandyearly_cash_on_hand_and_more.py deleted file mode 100644 index 0eb4f21e18..0000000000 --- a/django-backend/fecfiler/cash_on_hand/migrations/0002_alter_cashonhandyearly_cash_on_hand_and_more.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.1.1 on 2024-11-20 09:18 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('cash_on_hand', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='cashonhandyearly', - name='cash_on_hand', - field=models.DecimalField(decimal_places=2, default=0, max_digits=11), - preserve_default=False, - ), - migrations.AlterField( - model_name='cashonhandyearly', - name='year', - field=models.TextField(default=2020), - preserve_default=False, - ), - ] diff --git a/django-backend/fecfiler/committee_accounts/migrations/0001_initial.py b/django-backend/fecfiler/committee_accounts/migrations/0001_initial.py deleted file mode 100644 index d0af2c5659..0000000000 --- a/django-backend/fecfiler/committee_accounts/migrations/0001_initial.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-16 20:15 - -import django.core.validators -from django.db import migrations, models -import uuid - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="CommitteeAccount", - fields=[ - ("deleted", models.DateTimeField(blank=True, null=True)), - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True, - ), - ), - ( - "committee_id", - models.CharField( - max_length=9, - unique=True, - validators=[ - django.core.validators.RegexValidator( - "^C[0-9]{8}$", "invalid committee id format" - ) - ], - ), - ), - ("created", models.DateTimeField(auto_now_add=True)), - ("updated", models.DateTimeField(auto_now=True)), - ], - options={ - "db_table": "committee_accounts", - }, - ), - ] diff --git a/django-backend/fecfiler/committee_accounts/migrations/0001_squashed_0007_alter_committeeaccount_members.py b/django-backend/fecfiler/committee_accounts/migrations/0001_squashed_0007_alter_committeeaccount_members.py new file mode 100644 index 0000000000..ae61695d5a --- /dev/null +++ b/django-backend/fecfiler/committee_accounts/migrations/0001_squashed_0007_alter_committeeaccount_members.py @@ -0,0 +1,122 @@ +# Generated by Django 5.2.11 on 2026-03-06 22:00 + +import django.core.validators +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [ + ("committee_accounts", "0001_initial"), + ("committee_accounts", "0002_membership"), + ( + "committee_accounts", + "0003_membership_pending_email_alter_membership_id_and_more", + ), + ("committee_accounts", "0004_remove_duplicate_memberships"), + ("committee_accounts", "0005_remove_pending_emails"), + ("committee_accounts", "0006_alter_membership_committee_account_and_more"), + ("committee_accounts", "0007_alter_committeeaccount_members"), + ] + + initial = True + + dependencies = [ + ("user", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="CommitteeAccount", + fields=[ + ("deleted", models.DateTimeField(blank=True, null=True)), + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "committee_id", + models.CharField( + max_length=9, + unique=True, + validators=[ + django.core.validators.RegexValidator( + "^C[0-9]{8}$", "invalid committee id format" + ) + ], + ), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ], + options={ + "db_table": "committee_accounts", + }, + ), + migrations.CreateModel( + name="Membership", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "role", + models.CharField( + choices=[ + ("COMMITTEE_ADMINISTRATOR", "Committee Administrator"), + ("MANAGER", "Manager"), + ], + max_length=25, + ), + ), + ( + "pending_email", + models.EmailField(blank=True, max_length=254, null=True), + ), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ( + "committee_account", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="committee_accounts.committeeaccount", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddField( + model_name="committeeaccount", + name="members", + field=models.ManyToManyField( + through="committee_accounts.Membership", + through_fields=("committee_account", "user"), + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/django-backend/fecfiler/committee_accounts/migrations/0002_membership.py b/django-backend/fecfiler/committee_accounts/migrations/0002_membership.py deleted file mode 100644 index 14a1fbb490..0000000000 --- a/django-backend/fecfiler/committee_accounts/migrations/0002_membership.py +++ /dev/null @@ -1,83 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-25 14:11 - -from django.db import migrations, models -import django.db.models.deletion -import django.contrib.auth.validators -import django.utils.timezone -from fecfiler.committee_accounts.models import Membership as MembershipModel - - -def create_memberships(apps, schema_editor): - CommitteeAccount = apps.get_model("committee_accounts", "CommitteeAccount") # noqa - User = apps.get_model("user", "User") # noqa - Membership = apps.get_model("committee_accounts", "Membership") # noqa - db_alias = schema_editor.connection.alias - users = User.objects.using(db_alias).all() - for user in users: - committee = ( - CommitteeAccount.objects.using(db_alias) - .filter(committee_id=user.cmtee_id) - .first() - ) - if committee: - membership = Membership( - user=user, - committee_account=committee, - role=MembershipModel.CommitteeRole.COMMITTEE_ADMINISTRATOR, - ) - membership.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("user", "0001_initial"), - ("committee_accounts", "0001_initial"), - ] - - operations = [ - migrations.CreateModel( - name="Membership", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "role", - models.CharField( - choices=[ - ("COMMITTEE_ADMINISTRATOR", "Committee Administrator"), - ("REVIEWER", "Reviewer"), - ], - max_length=25, - ), - ), - ( - "committee_account", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="committee_accounts.committeeaccount", - ), - ), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="user.user" - ), - ), - ], - ), - migrations.AddField( - model_name="committeeaccount", - name="members", - field=models.ManyToManyField( - through="committee_accounts.Membership", to="user.User" - ), - ), - migrations.RunPython(create_memberships), - ] diff --git a/django-backend/fecfiler/committee_accounts/migrations/0003_membership_pending_email_alter_membership_id_and_more.py b/django-backend/fecfiler/committee_accounts/migrations/0003_membership_pending_email_alter_membership_id_and_more.py deleted file mode 100644 index 1e9809d267..0000000000 --- a/django-backend/fecfiler/committee_accounts/migrations/0003_membership_pending_email_alter_membership_id_and_more.py +++ /dev/null @@ -1,93 +0,0 @@ -# Generated by Django 4.2.7 on 2024-02-16 20:43 - -from django.conf import settings -from django.db import migrations, models -from django.utils import timezone -import uuid - - -def delete_pending_memberships(apps, schema_editor): - Membership = apps.get_model("committee_accounts", "Membership") # noqa - Membership.objects.filter(user=None).delete() - - -def generate_new_uuid(apps, schema_editor): - Membership = apps.get_model("committee_accounts", "Membership") # noqa - for membership in Membership.objects.all(): - membership.uuid = uuid.uuid4() - membership.save() - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('committee_accounts', '0002_membership'), - ] - - operations = [ - migrations.AddField( - model_name='membership', - name='pending_email', - field=models.EmailField(blank=True, max_length=254, null=True), - ), - migrations.AddField( - model_name='membership', - name='uuid', - field=models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=False, - serialize=False, - unique=False - ) - ), - migrations.RunPython( - generate_new_uuid, - migrations.RunPython.noop, - ), - migrations.RemoveField( - model_name='membership', - name='id', - ), - migrations.RenameField( - model_name='membership', - old_name='uuid', - new_name='id' - ), - migrations.AlterField( - model_name='membership', - name='id', - field=models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True - ) - ), - migrations.AlterField( - model_name='membership', - name='user', - field=models.ForeignKey( - null=True, - on_delete=models.deletion.CASCADE, - to=settings.AUTH_USER_MODEL - ), - ), - migrations.AddField( - model_name='membership', - name='created', - field=models.DateTimeField(auto_now_add=True, default=timezone.now), - preserve_default=False, - ), - migrations.AddField( - model_name='membership', - name='updated', - field=models.DateTimeField(auto_now=True), - ), - migrations.RunPython( - migrations.RunPython.noop, - delete_pending_memberships - ), - ] diff --git a/django-backend/fecfiler/committee_accounts/migrations/0004_remove_duplicate_memberships.py b/django-backend/fecfiler/committee_accounts/migrations/0004_remove_duplicate_memberships.py deleted file mode 100644 index 266317e055..0000000000 --- a/django-backend/fecfiler/committee_accounts/migrations/0004_remove_duplicate_memberships.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 4.2.7 on 2024-02-16 20:43 - -from django.db import migrations - - -def delete_memberships_with_overlapping_emails(apps, schema_editor): - Membership = apps.get_model("committee_accounts", "Membership") # noqa - Committee = apps.get_model("committee_accounts", "CommitteeAccount") # noqa - - for committee in Committee.objects.all(): - committee_memberships = Membership.objects.filter(committee_account=committee) - - unique_pending_emails = set() - emails_to_prune = set() - for membership in committee_memberships: - pending_email = str(membership.pending_email).lower() - if pending_email not in unique_pending_emails: - unique_pending_emails.add(pending_email) - else: - emails_to_prune.add(pending_email) - - for email in list(emails_to_prune): - # ordering by user places any memberships with a user first - overlapping_memberships = list( - committee_memberships.filter(pending_email__iexact=email).order_by('user') - ) - for membership_to_delete in overlapping_memberships[1:]: - membership_to_delete.delete() - - -class Migration(migrations.Migration): - - dependencies = [( - 'committee_accounts', - '0003_membership_pending_email_alter_membership_id_and_more' - )] - - operations = [ - migrations.RunPython( - delete_memberships_with_overlapping_emails, - migrations.RunPython.noop, - ), - ] diff --git a/django-backend/fecfiler/committee_accounts/migrations/0005_remove_pending_emails.py b/django-backend/fecfiler/committee_accounts/migrations/0005_remove_pending_emails.py deleted file mode 100644 index 2077e40d0f..0000000000 --- a/django-backend/fecfiler/committee_accounts/migrations/0005_remove_pending_emails.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.1.4 on 2025-02-05 18:11 - -from django.db import migrations - - -def remove_pending_emails(apps, schema_editor): - # remove pending emails from memberships that have been redeemed - Membership = apps.get_model("committee_accounts", "Membership") # noqa - Membership.objects.filter(pending_email__isnull=False, user_id__isnull=False).update( - pending_email=None - ) - - -class Migration(migrations.Migration): - - dependencies = [ - ("committee_accounts", "0004_remove_duplicate_memberships"), - ] - - operations = [ - migrations.RunPython( - remove_pending_emails, reverse_code=migrations.RunPython.noop - ), - ] diff --git a/django-backend/fecfiler/committee_accounts/migrations/0006_alter_membership_committee_account_and_more.py b/django-backend/fecfiler/committee_accounts/migrations/0006_alter_membership_committee_account_and_more.py deleted file mode 100644 index 8de0408b57..0000000000 --- a/django-backend/fecfiler/committee_accounts/migrations/0006_alter_membership_committee_account_and_more.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.1.5 on 2025-02-13 16:07 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("committee_accounts", "0005_remove_pending_emails"), - ] - - operations = [ - migrations.AlterField( - model_name="membership", - name="committee_account", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="committee_accounts.committeeaccount", - ), - ), - migrations.AlterField( - model_name="membership", - name="role", - field=models.CharField( - choices=[ - ("COMMITTEE_ADMINISTRATOR", "Committee Administrator"), - ("MANAGER", "Manager"), - ], - max_length=25, - ), - ), - ] diff --git a/django-backend/fecfiler/committee_accounts/migrations/0007_alter_committeeaccount_members.py b/django-backend/fecfiler/committee_accounts/migrations/0007_alter_committeeaccount_members.py deleted file mode 100644 index 532bc0a1d9..0000000000 --- a/django-backend/fecfiler/committee_accounts/migrations/0007_alter_committeeaccount_members.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2 on 2025-04-30 17:10 - -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('committee_accounts', '0006_alter_membership_committee_account_and_more'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AlterField( - model_name='committeeaccount', - name='members', - field=models.ManyToManyField( - through='committee_accounts.Membership', - through_fields=('committee_account', 'user'), - to=settings.AUTH_USER_MODEL - ), - ), - ] diff --git a/django-backend/fecfiler/committee_accounts/migrations/0008_committeeaccount_disabled.py b/django-backend/fecfiler/committee_accounts/migrations/0008_committeeaccount_disabled.py new file mode 100644 index 0000000000..9898c3e496 --- /dev/null +++ b/django-backend/fecfiler/committee_accounts/migrations/0008_committeeaccount_disabled.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.12 on 2026-03-23 20:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('committee_accounts', '0001_squashed_0007_alter_committeeaccount_members'), + ] + + operations = [ + migrations.AddField( + model_name='committeeaccount', + name='disabled', + field=models.DateTimeField(blank=True, default=None, null=True), + ), + ] diff --git a/django-backend/fecfiler/committee_accounts/models.py b/django-backend/fecfiler/committee_accounts/models.py index 7bbf50b52a..e3765812df 100644 --- a/django-backend/fecfiler/committee_accounts/models.py +++ b/django-backend/fecfiler/committee_accounts/models.py @@ -1,3 +1,4 @@ +from django.utils import timezone import uuid from django.db import models from django.core.validators import RegexValidator @@ -27,6 +28,15 @@ class CommitteeAccount(SoftDeleteModel): ) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) + disabled = models.DateTimeField(blank=True, null=True, default=None) + + def disable(self): + self.disabled = timezone.now() + self.save() + + def enable(self): + self.disabled = None + self.save() class Meta: db_table = "committee_accounts" diff --git a/django-backend/fecfiler/committee_accounts/utils/accounts.py b/django-backend/fecfiler/committee_accounts/utils/accounts.py index 0931dcaa08..efff89388d 100644 --- a/django-backend/fecfiler/committee_accounts/utils/accounts.py +++ b/django-backend/fecfiler/committee_accounts/utils/accounts.py @@ -6,6 +6,8 @@ import json import structlog from fecfiler.shared.utilities import query_fec_api_single +from django.contrib.sessions.models import Session +from django.utils import timezone logger = structlog.getLogger(__name__) @@ -93,6 +95,40 @@ def delete_committee_account(committee_id): logger.error(f"An error occurred while deleting the committee account: {e}") +def disable_committee_account(committee_id): + committee_account = CommitteeAccount.objects.get(committee_id=committee_id) + committee_account.disable() + logger.info(f"Committee account with ID {committee_id} has been disabled.") + + +def logout_committee_sessions(committee_id): + active_sessions = Session.objects.filter(expire_date__gte=timezone.now()) + sessions_to_delete = [] + + for session in active_sessions: + data = session.get_decoded() + if data.get("committee_id") == committee_id: + sessions_to_delete.append(session.pk) + + Session.objects.filter(pk__in=sessions_to_delete).delete() + logger.info( + f""" + Successfully logged out {len(sessions_to_delete)} users from {committee_id} + """ + ) + + +def enable_committee_account(committee_id): + try: + committee_account = CommitteeAccount.objects.get(committee_id=committee_id) + committee_account.enable() + logger.info(f"Committee account with ID {committee_id} has been enabled.") + except CommitteeAccount.DoesNotExist: + logger.error(f"Committee account with ID {committee_id} does not exist.") + except Exception as e: + logger.error(f"An error occurred while enabling the committee account: {e}") + + def get_committee_emails(committee_id): match settings.FLAG__COMMITTEE_DATA_SOURCE: case "PRODUCTION": diff --git a/django-backend/fecfiler/committee_accounts/views.py b/django-backend/fecfiler/committee_accounts/views.py index 89d933224f..69a2150ffe 100644 --- a/django-backend/fecfiler/committee_accounts/views.py +++ b/django-backend/fecfiler/committee_accounts/views.py @@ -53,8 +53,8 @@ def get_queryset(self): ) @action(detail=True, methods=["post"]) def activate(self, request, pk): - committee = self.get_object() - if not committee: + committee: CommitteeAccount = self.get_object() + if not committee or committee.disabled is not None: return Response("Committee could not be activated", status=403) request.session["committee_id"] = str(committee.committee_id) request.session["committee_uuid"] = str(committee.id) diff --git a/django-backend/fecfiler/contacts/migrations/0001_initial.py b/django-backend/fecfiler/contacts/migrations/0001_initial.py index f933ee380e..38ddb40d16 100644 --- a/django-backend/fecfiler/contacts/migrations/0001_initial.py +++ b/django-backend/fecfiler/contacts/migrations/0001_initial.py @@ -9,7 +9,10 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("committee_accounts", "0001_initial"), + ( + "committee_accounts", + "0001_squashed_0007_alter_committeeaccount_members", + ), ] operations = [ diff --git a/django-backend/fecfiler/devops/management/commands/disable_committee_account.py b/django-backend/fecfiler/devops/management/commands/disable_committee_account.py new file mode 100644 index 0000000000..fd09c35cee --- /dev/null +++ b/django-backend/fecfiler/devops/management/commands/disable_committee_account.py @@ -0,0 +1,30 @@ +from .fecfile_base import FECCommand +from fecfiler.committee_accounts.models import CommitteeAccount +from fecfiler.committee_accounts.utils.accounts import ( + disable_committee_account, + logout_committee_sessions, +) +import structlog + +logger = structlog.get_logger(__name__) + + +class Command(FECCommand): + help = "Disables a committee account" + command_name = "disable_committee_account" + + def add_arguments(self, parser): + parser.add_argument("committee_id", type=str) + + def command(self, *args, **options): + try: + committee_id = options["committee_id"] + logger.info(f"Disabling committee {committee_id}") + disable_committee_account(committee_id) + logout_committee_sessions(committee_id) + + except CommitteeAccount.DoesNotExist: + logger.error(f"Committee account with ID {committee_id} does not exist.") + except Exception as e: + logger.error(f"Error occurred while disabling committee {committee_id}: {e}") + raise diff --git a/django-backend/fecfiler/devops/management/commands/enable_committee_account.py b/django-backend/fecfiler/devops/management/commands/enable_committee_account.py new file mode 100644 index 0000000000..b8920bfeea --- /dev/null +++ b/django-backend/fecfiler/devops/management/commands/enable_committee_account.py @@ -0,0 +1,19 @@ +from fecfiler.devops.management.commands.fecfile_base import FECCommand +from fecfiler.committee_accounts.utils.accounts import enable_committee_account +import structlog + +logger = structlog.get_logger(__name__) + + +class Command(FECCommand): + help = "Enables a committee account" + command_name = "enable_committee_account" + + def add_arguments(self, parser): + parser.add_argument( + "committee_id", type=str, help="The ID of the committee account to enable" + ) + + def command(self, *args, **options): + committee_id = options["committee_id"] + enable_committee_account(committee_id) diff --git a/django-backend/fecfiler/devops/tests/test_disable_committee_account.py b/django-backend/fecfiler/devops/tests/test_disable_committee_account.py new file mode 100644 index 0000000000..e93ff752cc --- /dev/null +++ b/django-backend/fecfiler/devops/tests/test_disable_committee_account.py @@ -0,0 +1,51 @@ +from django.test import TestCase +from django.core.management import call_command +from django.contrib.sessions.models import Session +from django.utils import timezone +from fecfiler.committee_accounts.models import CommitteeAccount + +COMMITTEE_ID_TO_DISABLE = "C12345678" +OTHER_COMMITTEE_ID = "C87654321" + + +class DisableCommitteeAccountCommandTest(TestCase): + def setUp(self): + self.committee = CommitteeAccount.objects.create( + committee_id=COMMITTEE_ID_TO_DISABLE + ) + self.other_committee = CommitteeAccount.objects.create( + committee_id=OTHER_COMMITTEE_ID + ) + + def test_disable_committee_command(self): + s1 = Session.objects.create( + session_key="target_session", + expire_date=timezone.now() + timezone.timedelta(days=1), + ) + s1.session_data = Session.objects.encode( + {"committee_id": COMMITTEE_ID_TO_DISABLE} + ) + s1.save() + + s2 = Session.objects.create( + session_key="safe_session", + expire_date=timezone.now() + timezone.timedelta(days=1), + ) + s2.session_data = Session.objects.encode({"committee_id": OTHER_COMMITTEE_ID}) + s2.save() + + self.assertEqual(Session.objects.count(), 2) + + call_command("disable_committee_account", COMMITTEE_ID_TO_DISABLE) + self.committee.refresh_from_db() + self.other_committee.refresh_from_db() + self.assertIsNotNone(self.committee.disabled) + self.assertFalse(Session.objects.filter(session_key="target_session").exists()) + self.assertIsNone(self.other_committee.disabled) + self.assertTrue(Session.objects.filter(session_key="safe_session").exists()) + + def test_disable_non_existent_committee(self): + try: + call_command("disable_committee_account", "C00000000") + except Exception as e: + self.fail(f"Command crashed with error: {e}") diff --git a/django-backend/fecfiler/devops/tests/test_enable_committee_account.py b/django-backend/fecfiler/devops/tests/test_enable_committee_account.py new file mode 100644 index 0000000000..370eca78a7 --- /dev/null +++ b/django-backend/fecfiler/devops/tests/test_enable_committee_account.py @@ -0,0 +1,25 @@ +from django.test import TestCase +from django.core.management import call_command +from fecfiler.committee_accounts.models import CommitteeAccount + +COMMITTEE_ID = "C12345678" + + +class EnableCommitteeAccountCommandTest(TestCase): + def setUp(self): + self.committee = CommitteeAccount.objects.create(committee_id=COMMITTEE_ID) + self.committee.disable() + self.assertFalse( + CommitteeAccount.objects.get(committee_id=COMMITTEE_ID).disabled is None + ) + + def test_enable_committee_command(self): + call_command("enable_committee_account", COMMITTEE_ID) + re_enabled_committee = CommitteeAccount.objects.get(committee_id=COMMITTEE_ID) + self.assertIsNone(re_enabled_committee.disabled) + + def test_enable_non_existent_committee(self): + try: + call_command("enable_committee_account", "C00000000") + except Exception as e: + self.fail(f"Command crashed on non-existent ID: {e}") diff --git a/django-backend/fecfiler/memo_text/migrations/0001_initial.py b/django-backend/fecfiler/memo_text/migrations/0001_initial_squashed_0003_memotext_text_prefix.py similarity index 66% rename from django-backend/fecfiler/memo_text/migrations/0001_initial.py rename to django-backend/fecfiler/memo_text/migrations/0001_initial_squashed_0003_memotext_text_prefix.py index 8d99be1eaf..4f8bd422f3 100644 --- a/django-backend/fecfiler/memo_text/migrations/0001_initial.py +++ b/django-backend/fecfiler/memo_text/migrations/0001_initial_squashed_0003_memotext_text_prefix.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-01-16 20:15 +# Manually squashed by Dan-go 48.0.12 on 2026-03-12 16:15 from django.db import migrations, models import django.db.models.deletion @@ -8,8 +8,18 @@ class Migration(migrations.Migration): initial = True + replaces = [ + ("memo_text", "0001_initial"), + ("memo_text", "0002_initial"), + ("memo_text", "0003_memotext_text_prefix") + ] + dependencies = [ - ("committee_accounts", "0001_initial"), + ( + "committee_accounts", + "0001_squashed_0007_alter_committeeaccount_members", + ), + ("reports", "0001_initial"), ] operations = [ @@ -32,6 +42,7 @@ class Migration(migrations.Migration): ("transaction_id_number", models.TextField(blank=True, null=True)), ("transaction_uuid", models.TextField(blank=True, null=True)), ("text4000", models.TextField(blank=True, null=True)), + ("text_prefix", models.TextField(blank=True, null=True)), ( "committee_account", models.ForeignKey( @@ -40,6 +51,15 @@ class Migration(migrations.Migration): to="committee_accounts.committeeaccount", ), ), + ( + "report", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="reports.report", + ), + ), ], options={ "db_table": "memo_text", diff --git a/django-backend/fecfiler/memo_text/migrations/0002_initial.py b/django-backend/fecfiler/memo_text/migrations/0002_initial.py deleted file mode 100644 index 66369ef094..0000000000 --- a/django-backend/fecfiler/memo_text/migrations/0002_initial.py +++ /dev/null @@ -1,26 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-16 20:15 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ("reports", "0001_initial"), - ("memo_text", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="memotext", - name="report", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="reports.report", - ), - ), - ] diff --git a/django-backend/fecfiler/memo_text/migrations/0003_memotext_text_prefix.py b/django-backend/fecfiler/memo_text/migrations/0003_memotext_text_prefix.py deleted file mode 100644 index 68f88e0255..0000000000 --- a/django-backend/fecfiler/memo_text/migrations/0003_memotext_text_prefix.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-23 15:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('memo_text', '0002_initial'), - ] - - operations = [ - migrations.AddField( - model_name='memotext', - name='text_prefix', - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/django-backend/fecfiler/reports/migrations/00018_form24_name.py b/django-backend/fecfiler/reports/migrations/00018_form24_name.py deleted file mode 100644 index 77f529cd99..0000000000 --- a/django-backend/fecfiler/reports/migrations/00018_form24_name.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.db import migrations, models -from django_migration_linter import IgnoreMigration - - -def update_form24_names(apps, schema_editor): - form24 = apps.get_model("reports", "Form24") - form24_objects = list(form24.objects.all()) - for index, form in enumerate(form24_objects, start=1): - report_type = form.report_type_24_48 - form.name = f"{report_type}-HOUR Report: {index}" - form.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ("reports", "0017_form99_filing_frequency_form99_pdf_attachment"), - ] - - operations = [ - IgnoreMigration(), - migrations.AddField( - model_name="form24", - name="name", - field=models.TextField(null=True, blank=False), - ), - migrations.RunPython(update_form24_names), - migrations.AlterField( - model_name="form24", - name="name", - field=models.TextField(null=False, blank=False), - ), - ] diff --git a/django-backend/fecfiler/reports/migrations/00019_form24_name_fix.py b/django-backend/fecfiler/reports/migrations/00019_form24_name_fix.py deleted file mode 100644 index 347f039b5b..0000000000 --- a/django-backend/fecfiler/reports/migrations/00019_form24_name_fix.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db import migrations - - -def update_form24_names(apps, schema_editor): - form24 = apps.get_model("reports", "Form24") - form24_objects = list(form24.objects.all()) - for form in form24_objects: - report_type = form.report_type_24_48 - form.name = f"{report_type}-HOUR: Report of Independent Expenditure" - form.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ("reports", "00018_form24_name"), - ] - - operations = [ - migrations.RunPython(update_form24_names, migrations.RunPython.noop), - ] diff --git a/django-backend/fecfiler/reports/migrations/0001_initial.py b/django-backend/fecfiler/reports/migrations/0001_initial.py index 2b38af6cae..5e43031fad 100644 --- a/django-backend/fecfiler/reports/migrations/0001_initial.py +++ b/django-backend/fecfiler/reports/migrations/0001_initial.py @@ -9,7 +9,10 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("committee_accounts", "0001_initial"), + ( + "committee_accounts", + "0001_squashed_0007_alter_committeeaccount_members", + ), ] operations = [ @@ -26,12 +29,6 @@ class Migration(migrations.Migration): unique=True, ), ), - ("committee_name", models.TextField(blank=True, null=True)), - ("street_1", models.TextField(blank=True, null=True)), - ("street_2", models.TextField(blank=True, null=True)), - ("city", models.TextField(blank=True, null=True)), - ("state", models.TextField(blank=True, null=True)), - ("zip", models.TextField(blank=True, null=True)), ( "committee_type", models.CharField(blank=True, max_length=1, null=True), @@ -40,75 +37,10 @@ class Migration(migrations.Migration): "affiliated_date_form_f1_filed", models.DateField(blank=True, null=True), ), - ( - "affiliated_committee_fec_id", - models.TextField(blank=True, null=True), - ), - ("affiliated_committee_name", models.TextField(blank=True, null=True)), - ("I_candidate_id_number", models.TextField(blank=True, null=True)), - ("I_candidate_last_name", models.TextField(blank=True, null=True)), - ("I_candidate_first_name", models.TextField(blank=True, null=True)), - ("I_candidate_middle_name", models.TextField(blank=True, null=True)), - ("I_candidate_prefix", models.TextField(blank=True, null=True)), - ("I_candidate_suffix", models.TextField(blank=True, null=True)), - ( - "I_candidate_office", - models.CharField(blank=True, max_length=1, null=True), - ), - ("I_candidate_state", models.TextField(blank=True, null=True)), - ("I_candidate_district", models.TextField(blank=True, null=True)), ("I_date_of_contribution", models.DateField(blank=True, null=True)), - ("II_candidate_id_number", models.TextField(blank=True, null=True)), - ("II_candidate_last_name", models.TextField(blank=True, null=True)), - ("II_candidate_first_name", models.TextField(blank=True, null=True)), - ("II_candidate_middle_name", models.TextField(blank=True, null=True)), - ("II_candidate_prefix", models.TextField(blank=True, null=True)), - ("II_candidate_suffix", models.TextField(blank=True, null=True)), - ( - "II_candidate_office", - models.CharField(blank=True, max_length=1, null=True), - ), - ("II_candidate_state", models.TextField(blank=True, null=True)), - ("II_candidate_district", models.TextField(blank=True, null=True)), ("II_date_of_contribution", models.DateField(blank=True, null=True)), - ("III_candidate_id_number", models.TextField(blank=True, null=True)), - ("III_candidate_last_name", models.TextField(blank=True, null=True)), - ("III_candidate_first_name", models.TextField(blank=True, null=True)), - ("III_candidate_middle_name", models.TextField(blank=True, null=True)), - ("III_candidate_prefix", models.TextField(blank=True, null=True)), - ("III_candidate_suffix", models.TextField(blank=True, null=True)), - ( - "III_candidate_office", - models.CharField(blank=True, max_length=1, null=True), - ), - ("III_candidate_state", models.TextField(blank=True, null=True)), - ("III_candidate_district", models.TextField(blank=True, null=True)), ("III_date_of_contribution", models.DateField(blank=True, null=True)), - ("IV_candidate_id_number", models.TextField(blank=True, null=True)), - ("IV_candidate_last_name", models.TextField(blank=True, null=True)), - ("IV_candidate_first_name", models.TextField(blank=True, null=True)), - ("IV_candidate_middle_name", models.TextField(blank=True, null=True)), - ("IV_candidate_prefix", models.TextField(blank=True, null=True)), - ("IV_candidate_suffix", models.TextField(blank=True, null=True)), - ( - "IV_candidate_office", - models.CharField(blank=True, max_length=1, null=True), - ), - ("IV_candidate_state", models.TextField(blank=True, null=True)), - ("IV_candidate_district", models.TextField(blank=True, null=True)), ("IV_date_of_contribution", models.DateField(blank=True, null=True)), - ("V_candidate_id_number", models.TextField(blank=True, null=True)), - ("V_candidate_last_name", models.TextField(blank=True, null=True)), - ("V_candidate_first_name", models.TextField(blank=True, null=True)), - ("V_candidate_middle_name", models.TextField(blank=True, null=True)), - ("V_candidate_prefix", models.TextField(blank=True, null=True)), - ("V_candidate_suffix", models.TextField(blank=True, null=True)), - ( - "V_candidate_office", - models.CharField(blank=True, max_length=1, null=True), - ), - ("V_candidate_state", models.TextField(blank=True, null=True)), - ("V_candidate_district", models.TextField(blank=True, null=True)), ("V_date_of_contribution", models.DateField(blank=True, null=True)), ], ), @@ -127,11 +59,7 @@ class Migration(migrations.Migration): ), ("report_type_24_48", models.TextField(blank=True, null=True)), ("original_amendment_date", models.DateField(blank=True, null=True)), - ("street_1", models.TextField(blank=True, null=True)), - ("street_2", models.TextField(blank=True, null=True)), - ("city", models.TextField(blank=True, null=True)), - ("state", models.TextField(blank=True, null=True)), - ("zip", models.TextField(blank=True, null=True)), + ("name", models.TextField()), ], ), migrations.CreateModel( @@ -151,11 +79,6 @@ class Migration(migrations.Migration): "change_of_address", models.BooleanField(blank=True, default=False, null=True), ), - ("street_1", models.TextField(blank=True, null=True)), - ("street_2", models.TextField(blank=True, null=True)), - ("city", models.TextField(blank=True, null=True)), - ("state", models.TextField(blank=True, null=True)), - ("zip", models.TextField(blank=True, null=True)), ("election_code", models.TextField(blank=True, null=True)), ("date_of_election", models.DateField(blank=True, null=True)), ("state_of_election", models.TextField(blank=True, null=True)), @@ -163,7 +86,6 @@ class Migration(migrations.Migration): "qualified_committee", models.BooleanField(blank=True, default=False, null=True), ), - ("cash_on_hand_date", models.DateField(blank=True, null=True)), ( "L6b_cash_on_hand_beginning_period", models.DecimalField( @@ -780,20 +702,16 @@ class Migration(migrations.Migration): unique=True, ), ), - ("committee_name", models.TextField(blank=True, null=True)), - ("street_1", models.TextField(blank=True, null=True)), - ("street_2", models.TextField(blank=True, null=True)), - ("city", models.TextField(blank=True, null=True)), - ("state", models.TextField(blank=True, null=True)), - ("zip", models.TextField(blank=True, null=True)), - ("text_code", models.TextField(blank=True, null=True)), + ( + "text_code", + models.TextField(blank=False, db_default="", default=""), + ), ("message_text", models.TextField(blank=True, null=True)), ], ), migrations.CreateModel( name="Report", fields=[ - ("deleted", models.DateTimeField(blank=True, null=True)), ( "id", models.UUIDField( @@ -810,6 +728,12 @@ class Migration(migrations.Migration): ("report_code", models.TextField(blank=True, null=True)), ("coverage_from_date", models.DateField(blank=True, null=True)), ("coverage_through_date", models.DateField(blank=True, null=True)), + ("committee_name", models.TextField(blank=True, null=True)), + ("street_1", models.TextField(blank=True, null=True)), + ("street_2", models.TextField(blank=True, null=True)), + ("city", models.TextField(blank=True, null=True)), + ("state", models.TextField(blank=True, null=True)), + ("zip", models.TextField(blank=True, null=True)), ("treasurer_last_name", models.TextField(blank=True, null=True)), ("treasurer_first_name", models.TextField(blank=True, null=True)), ("treasurer_middle_name", models.TextField(blank=True, null=True)), @@ -836,6 +760,8 @@ class Migration(migrations.Migration): blank=True, default=None, max_length=44, null=True ), ), + ("can_delete", models.BooleanField(default=True)), + ("can_unamend", models.BooleanField(default=False)), ("created", models.DateTimeField(auto_now_add=True)), ("updated", models.DateTimeField(auto_now=True)), ( diff --git a/django-backend/fecfiler/reports/migrations/0002_initial.py b/django-backend/fecfiler/reports/migrations/0002_initial.py index 0a6885c0e4..1139e029b0 100644 --- a/django-backend/fecfiler/reports/migrations/0002_initial.py +++ b/django-backend/fecfiler/reports/migrations/0002_initial.py @@ -10,7 +10,7 @@ class Migration(migrations.Migration): dependencies = [ ("contacts", "0001_initial"), ("reports", "0001_initial"), - ("web_services", "0001_initial"), + ("web_services", "0001_initial_squashed_0003_polling_attempts"), ] operations = [ diff --git a/django-backend/fecfiler/reports/migrations/0003_update_f24_amendment_date.py b/django-backend/fecfiler/reports/migrations/0003_update_f24_amendment_date.py deleted file mode 100644 index 5817694260..0000000000 --- a/django-backend/fecfiler/reports/migrations/0003_update_f24_amendment_date.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.db import migrations - - -def update_f24_original_amendment_date(apps, schema_editor): - Report = apps.get_model("reports", "Report") # noqa - reports_to_update = Report.objects.filter( - form_type="F24A", - form_24__isnull=False, - upload_submission__isnull=False - ) - for report in reports_to_update: - report.form_24.original_amendment_date = report.upload_submission.created - report.form_24.save() - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ("reports", "0002_initial"), - ] - - operations = [ - migrations.RunPython( - update_f24_original_amendment_date, - migrations.RunPython.noop - ), - ] diff --git a/django-backend/fecfiler/reports/migrations/0004_form1m_date_committee_met_requirements_and_more.py b/django-backend/fecfiler/reports/migrations/0004_form1m_date_committee_met_requirements_and_more.py index b5bcb8d2ab..0e67708d9b 100644 --- a/django-backend/fecfiler/reports/migrations/0004_form1m_date_committee_met_requirements_and_more.py +++ b/django-backend/fecfiler/reports/migrations/0004_form1m_date_committee_met_requirements_and_more.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ('reports', '0003_update_f24_amendment_date'), + ("reports", "0002_initial"), ] operations = [ diff --git a/django-backend/fecfiler/reports/migrations/0005_remove_form1m_iii_candidate_district_and_more.py b/django-backend/fecfiler/reports/migrations/0005_remove_form1m_iii_candidate_district_and_more.py deleted file mode 100644 index 6b6e814621..0000000000 --- a/django-backend/fecfiler/reports/migrations/0005_remove_form1m_iii_candidate_district_and_more.py +++ /dev/null @@ -1,201 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-24 09:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('reports', '0004_form1m_date_committee_met_requirements_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='form1m', - name='III_candidate_district', - ), - migrations.RemoveField( - model_name='form1m', - name='III_candidate_first_name', - ), - migrations.RemoveField( - model_name='form1m', - name='III_candidate_id_number', - ), - migrations.RemoveField( - model_name='form1m', - name='III_candidate_last_name', - ), - migrations.RemoveField( - model_name='form1m', - name='III_candidate_middle_name', - ), - migrations.RemoveField( - model_name='form1m', - name='III_candidate_office', - ), - migrations.RemoveField( - model_name='form1m', - name='III_candidate_prefix', - ), - migrations.RemoveField( - model_name='form1m', - name='III_candidate_state', - ), - migrations.RemoveField( - model_name='form1m', - name='III_candidate_suffix', - ), - migrations.RemoveField( - model_name='form1m', - name='II_candidate_district', - ), - migrations.RemoveField( - model_name='form1m', - name='II_candidate_first_name', - ), - migrations.RemoveField( - model_name='form1m', - name='II_candidate_id_number', - ), - migrations.RemoveField( - model_name='form1m', - name='II_candidate_last_name', - ), - migrations.RemoveField( - model_name='form1m', - name='II_candidate_middle_name', - ), - migrations.RemoveField( - model_name='form1m', - name='II_candidate_office', - ), - migrations.RemoveField( - model_name='form1m', - name='II_candidate_prefix', - ), - migrations.RemoveField( - model_name='form1m', - name='II_candidate_state', - ), - migrations.RemoveField( - model_name='form1m', - name='II_candidate_suffix', - ), - migrations.RemoveField( - model_name='form1m', - name='IV_candidate_district', - ), - migrations.RemoveField( - model_name='form1m', - name='IV_candidate_first_name', - ), - migrations.RemoveField( - model_name='form1m', - name='IV_candidate_id_number', - ), - migrations.RemoveField( - model_name='form1m', - name='IV_candidate_last_name', - ), - migrations.RemoveField( - model_name='form1m', - name='IV_candidate_middle_name', - ), - migrations.RemoveField( - model_name='form1m', - name='IV_candidate_office', - ), - migrations.RemoveField( - model_name='form1m', - name='IV_candidate_prefix', - ), - migrations.RemoveField( - model_name='form1m', - name='IV_candidate_state', - ), - migrations.RemoveField( - model_name='form1m', - name='IV_candidate_suffix', - ), - migrations.RemoveField( - model_name='form1m', - name='I_candidate_district', - ), - migrations.RemoveField( - model_name='form1m', - name='I_candidate_first_name', - ), - migrations.RemoveField( - model_name='form1m', - name='I_candidate_id_number', - ), - migrations.RemoveField( - model_name='form1m', - name='I_candidate_last_name', - ), - migrations.RemoveField( - model_name='form1m', - name='I_candidate_middle_name', - ), - migrations.RemoveField( - model_name='form1m', - name='I_candidate_office', - ), - migrations.RemoveField( - model_name='form1m', - name='I_candidate_prefix', - ), - migrations.RemoveField( - model_name='form1m', - name='I_candidate_state', - ), - migrations.RemoveField( - model_name='form1m', - name='I_candidate_suffix', - ), - migrations.RemoveField( - model_name='form1m', - name='V_candidate_district', - ), - migrations.RemoveField( - model_name='form1m', - name='V_candidate_first_name', - ), - migrations.RemoveField( - model_name='form1m', - name='V_candidate_id_number', - ), - migrations.RemoveField( - model_name='form1m', - name='V_candidate_last_name', - ), - migrations.RemoveField( - model_name='form1m', - name='V_candidate_middle_name', - ), - migrations.RemoveField( - model_name='form1m', - name='V_candidate_office', - ), - migrations.RemoveField( - model_name='form1m', - name='V_candidate_prefix', - ), - migrations.RemoveField( - model_name='form1m', - name='V_candidate_state', - ), - migrations.RemoveField( - model_name='form1m', - name='V_candidate_suffix', - ), - migrations.RemoveField( - model_name='form1m', - name='affiliated_committee_fec_id', - ), - migrations.RemoveField( - model_name='form1m', - name='affiliated_committee_name', - ), - ] diff --git a/django-backend/fecfiler/reports/migrations/0006_reporttransaction.py b/django-backend/fecfiler/reports/migrations/0005_remove_form1m_squashed_0006_reporttransaction.py similarity index 80% rename from django-backend/fecfiler/reports/migrations/0006_reporttransaction.py rename to django-backend/fecfiler/reports/migrations/0005_remove_form1m_squashed_0006_reporttransaction.py index 146665aa27..00a442e3ed 100644 --- a/django-backend/fecfiler/reports/migrations/0006_reporttransaction.py +++ b/django-backend/fecfiler/reports/migrations/0005_remove_form1m_squashed_0006_reporttransaction.py @@ -7,11 +7,18 @@ class Migration(migrations.Migration): - dependencies = [ - ('committee_accounts', - '0003_membership_pending_email_alter_membership_id_and_more'), - ('transactions', '0002_remove_schedulea_contributor_city_and_more'), + replaces = [ ('reports', '0005_remove_form1m_iii_candidate_district_and_more'), + ('reports', '0006_reporttransaction'), + ] + + dependencies = [ + ( + "committee_accounts", + "0001_squashed_0007_alter_committeeaccount_members", + ), + ('transactions', '0001_initial'), + ('reports', '0004_form1m_date_committee_met_requirements_and_more'), ] operations = [ diff --git a/django-backend/fecfiler/reports/migrations/0007_remove_report_deleted.py b/django-backend/fecfiler/reports/migrations/0007_remove_report_deleted.py deleted file mode 100644 index e124f11a27..0000000000 --- a/django-backend/fecfiler/reports/migrations/0007_remove_report_deleted.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.10 on 2024-04-05 15:04 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('reports', '0006_reporttransaction'), - ] - - operations = [ - migrations.RemoveField( - model_name='report', - name='deleted', - ), - ] diff --git a/django-backend/fecfiler/reports/migrations/0013_form3_report_form_3.py b/django-backend/fecfiler/reports/migrations/0007_remove_report_deleted_squashed_00019_form24_name_fix.py similarity index 67% rename from django-backend/fecfiler/reports/migrations/0013_form3_report_form_3.py rename to django-backend/fecfiler/reports/migrations/0007_remove_report_deleted_squashed_00019_form24_name_fix.py index e236e08a3a..a76f2dae9f 100644 --- a/django-backend/fecfiler/reports/migrations/0013_form3_report_form_3.py +++ b/django-backend/fecfiler/reports/migrations/0007_remove_report_deleted_squashed_00019_form24_name_fix.py @@ -1,18 +1,238 @@ -# Generated by Django 5.1.5 on 2025-03-28 01:59 +# Generated by Django 5.2.11 on 2026-03-07 03:24 import django.db.models.deletion import uuid -from django.db import migrations, models +from django.db import migrations, models, connection + + +def _create_can_delete_trigger(apps, schema_editor): + with connection.cursor() as cursor: + cursor.execute( + """ + CREATE OR REPLACE FUNCTION check_can_delete() + RETURNS TRIGGER AS $$ + BEGIN + PERFORM update_report_can_delete(NEW); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION check_can_delete_previous() + RETURNS TRIGGER AS $$ + DECLARE + associated_report RECORD; + BEGIN + FOR associated_report IN ( + SELECT * FROM reports_report + WHERE committee_account_id = OLD.committee_account_id + AND can_delete = false + ) + LOOP + PERFORM update_report_can_delete(associated_report); + END LOOP; + + RETURN OLD; + END; + $$ LANGUAGE plpgsql; + + + CREATE OR REPLACE FUNCTION check_can_delete_transaction_update() + RETURNS TRIGGER AS $$ + DECLARE + associated_report RECORD; + BEGIN + SELECT * INTO associated_report FROM reports_report WHERE id IN ( + SELECT report_id FROM reports_reporttransaction + WHERE transaction_id = NEW.id LIMIT 1 + ); + PERFORM update_report_can_delete(associated_report); + + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION check_can_delete_transaction_insert() + RETURNS TRIGGER AS $$ + DECLARE + associated_report RECORD; + BEGIN + FOR associated_report IN ( + SELECT r1.* + FROM reports_report r1 + JOIN reports_report r2 + ON r1.committee_account_id = r2.committee_account_id + AND EXTRACT(YEAR FROM r1.coverage_from_date) = ( + EXTRACT(YEAR FROM r2.coverage_from_date)) + WHERE r1.can_delete = true + AND r2.id = NEW.report_id + ) + LOOP + PERFORM update_report_can_delete(associated_report); + END LOOP; + + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER check_can_delete_report + AFTER INSERT OR UPDATE ON reports_report + FOR EACH ROW + WHEN (pg_trigger_depth() = 0) -- Prevent infinite trigger loop + EXECUTE FUNCTION check_can_delete(); + + CREATE TRIGGER check_can_delete_previous + AFTER DELETE ON reports_report + FOR EACH ROW + WHEN (pg_trigger_depth() = 0) -- Prevent infinite trigger loop + EXECUTE FUNCTION check_can_delete_previous(); + + CREATE TRIGGER check_can_delete_transaction_insert + AFTER INSERT ON reports_reporttransaction + FOR EACH ROW + WHEN (pg_trigger_depth() = 0) -- Prevent infinite trigger loop + EXECUTE FUNCTION check_can_delete_transaction_insert(); + """ + ) + + +def _reverse_can_delete_trigger(apps, schema_editor): + report_model = apps.get_model("reports", "Report") + transaction_model = apps.get_model("transactions", "Transaction") + + triggers = [ + "check_can_delete_report", + "check_can_delete_previous", + "check_can_delete_transaction_update", + "check_can_delete_transaction_insert", + ] + + with schema_editor.atomic(): + for trigger in triggers: + schema_editor.execute( + "DROP TRIGGER IF EXISTS %s ON %s", + (trigger, report_model._meta.db_table), + ) + schema_editor.execute( + "DROP TRIGGER IF EXISTS %s ON %s", + (trigger, transaction_model._meta.db_table), + ) + + +def _create_can_unamend_trigger(apps, schema_editor): + schema_editor.execute( + """ + CREATE OR REPLACE FUNCTION update_can_unamend() + RETURNS TRIGGER AS $$ + BEGIN + UPDATE reports_report + SET can_unamend = FALSE + WHERE id IN ( + SELECT report_id + FROM reports_reporttransaction + WHERE transaction_id = NEW.id + ); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER transaction_updated + AFTER UPDATE ON transactions_transaction + FOR EACH ROW + WHEN (pg_trigger_depth() = 0) -- Prevent infinite trigger loop + EXECUTE FUNCTION update_can_unamend(); + + CREATE OR REPLACE FUNCTION update_can_unamend_new_transaction() + RETURNS TRIGGER AS $$ + BEGIN + UPDATE reports_report + SET can_unamend = FALSE + WHERE id IN ( + SELECT report_id + FROM reports_reporttransaction + WHERE id = NEW.id + ); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + + CREATE TRIGGER transaction_created + AFTER INSERT ON reports_reporttransaction + FOR EACH ROW + WHEN (pg_trigger_depth() = 0) -- Prevent infinite trigger loop + EXECUTE FUNCTION update_can_unamend_new_transaction(); + """ + ) class Migration(migrations.Migration): - dependencies = [ + replaces = [ + ("reports", "0007_remove_report_deleted"), + ("reports", "0008_remove_form1m_city_remove_form1m_committee_name_and_more"), + ("reports", "0009_report_can_delete"), + ("reports", "0010_report_can_unammend"), + ("reports", "0011_remove_form3x_cash_on_hand_date"), ("reports", "0012_alter_form99_text_code"), + ("reports", "0013_form3_report_form_3"), + ("reports", "0014_form99_swap_text_code"), + ("reports", "0015_form3x_filing_frequency_form3x_report_type_category"), + ("reports", "0016_determine_frequency_and_category"), + ("reports", "0017_form99_filing_frequency_form99_pdf_attachment"), + ("reports", "00018_form24_name"), + ("reports", "00019_form24_name_fix"), + ] + + dependencies = [ + ("reports", "0005_remove_form1m_squashed_0006_reporttransaction"), ] - l19a_field = "L19a_loan_repayments_of_loans_made_or_guaranteed_by_candidate_period" operations = [ + migrations.RunSQL( + sql=""" + CREATE OR REPLACE FUNCTION update_report_can_delete(report RECORD) + RETURNS VOID AS $$ + DECLARE + r_can_delete boolean; + BEGIN + r_can_delete = report.upload_submission_id IS NULL + AND (report.report_version IS NULL OR report.report_version = '0') + AND ( + report.form_3x_id IS NULL OR + ( + report.form_24_id IS NULL + AND NOT EXISTS( + SELECT DISTINCT rrt1.id + FROM "reports_reporttransaction" rrt1 + JOIN "transactions_transaction" tt ON ( + rrt1."transaction_id" = tt."id" + OR tt."reatt_redes_id" = rrt1."transaction_id" + OR tt."parent_transaction_id" = + rrt1."transaction_id" + OR tt."debt_id" = rrt1."transaction_id" + OR tt."loan_id" = rrt1."transaction_id" + ) + INNER JOIN "reports_reporttransaction" rrt2 ON ( + rrt2."transaction_id" = tt."id" + AND rrt2."report_id" <> report.id + ) + WHERE rrt1."report_id" = report.id + ) + ) + ); + UPDATE reports_report SET can_delete = r_can_delete + WHERE id = report.id + AND can_delete <> r_can_delete; + END; + $$ LANGUAGE plpgsql; + """ + ), + migrations.RunPython( + code=_create_can_delete_trigger, + reverse_code=_reverse_can_delete_trigger, + ), + migrations.RunPython( + code=_create_can_unamend_trigger, + ), migrations.CreateModel( name="Form3", fields=[ @@ -186,7 +406,8 @@ class Migration(migrations.Migration): ), ), ( - l19a_field, + "L19a_loan_repayments_of_loans_made_or_" + "guaranteed_by_candidate_period", models.DecimalField( blank=True, db_column="l19a_period", @@ -475,4 +696,24 @@ class Migration(migrations.Migration): to="reports.form3", ), ), + migrations.AddField( + model_name="form3x", + name="filing_frequency", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="form3x", + name="report_type_category", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="form99", + name="pdf_attachment", + field=models.BooleanField(blank=True, null=True), + ), + migrations.AddField( + model_name="form99", + name="filing_frequency", + field=models.TextField(blank=True, max_length=1, null=True), + ), ] diff --git a/django-backend/fecfiler/reports/migrations/0010_report_can_unammend.py b/django-backend/fecfiler/reports/migrations/0008_remove_can_unamend_trigger.py similarity index 53% rename from django-backend/fecfiler/reports/migrations/0010_report_can_unammend.py rename to django-backend/fecfiler/reports/migrations/0008_remove_can_unamend_trigger.py index 90f64ed63c..3c441f49a6 100644 --- a/django-backend/fecfiler/reports/migrations/0010_report_can_unammend.py +++ b/django-backend/fecfiler/reports/migrations/0008_remove_can_unamend_trigger.py @@ -1,9 +1,23 @@ -# Generated by Django 5.0.8 on 2024-08-20 17:53 +from django.db import connection, migrations -from django.db import migrations, models +def drop_trigger_functions(_apps, _schema_editor): + with connection.cursor() as cursor: + cursor.execute( + "DROP TRIGGER IF EXISTS transaction_updated ON transactions_transaction;" + ) + cursor.execute( + "DROP TRIGGER IF EXISTS transaction_created ON reports_reporttransaction;" + ) -def create_trigger(apps, schema_editor): + +def drop_db_functions(_apps, _schema_editor): + with connection.cursor() as cursor: + cursor.execute("DROP FUNCTION IF EXISTS update_can_unamend();") + cursor.execute("DROP FUNCTION IF EXISTS update_can_unamend_new_transaction();") + + +def _reverse_create_can_unamend_trigger(apps, schema_editor): schema_editor.execute( """ CREATE OR REPLACE FUNCTION update_can_unamend() @@ -50,16 +64,21 @@ def create_trigger(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ("reports", "0009_report_can_delete"), + ("reports", "0007_remove_report_deleted_squashed_00019_form24_name_fix"), + ( + "transactions", + "0002_remove_schedulea_squashed_0026_alter_transaction_itemized" + ), ] operations = [ - migrations.AddField( - model_name="report", - name="can_unamend", - field=models.BooleanField(default=False), + migrations.RunPython( + code=drop_trigger_functions, + reverse_code=_reverse_create_can_unamend_trigger, + ), + migrations.RunPython( + code=drop_db_functions, + reverse_code=migrations.RunPython.noop, ), - migrations.RunPython(create_trigger), ] diff --git a/django-backend/fecfiler/reports/migrations/0008_remove_form1m_city_remove_form1m_committee_name_and_more.py b/django-backend/fecfiler/reports/migrations/0008_remove_form1m_city_remove_form1m_committee_name_and_more.py deleted file mode 100644 index 94fd45d4a1..0000000000 --- a/django-backend/fecfiler/reports/migrations/0008_remove_form1m_city_remove_form1m_committee_name_and_more.py +++ /dev/null @@ -1,193 +0,0 @@ -# Generated by Django 4.2.10 on 2024-04-19 19:10 - -from django.db import migrations, models -import structlog - -logger = structlog.get_logger(__name__) - - -def migrate_committee_data(apps, schema_editor): - Report = apps.get_model("reports", "Report") # noqa - Form24 = apps.get_model("reports", "Form24") # noqa - Form3x = apps.get_model("reports", "Form3X") # noqa - Form99 = apps.get_model("reports", "Form99") # noqa - Form1m = apps.get_model("reports", "Form1M") # noqa - - for form in Form24.objects.all(): - report = Report.objects.filter(form_24=form).first() - if report is not None: - report.street_1 = form.street_1 - report.street_2 = form.street_2 - report.city = form.city - report.state = form.state - report.zip = form.zip - report.save() - else: - logger.error(f"F24 Form has no corresponding report! {form}") - - for form in Form3x.objects.all(): - report = Report.objects.filter(form_3x=form).first() - if report is not None: - report.street_1 = form.street_1 - report.street_2 = form.street_2 - report.city = form.city - report.state = form.state - report.zip = form.zip - report.save() - else: - logger.error(f"F3X Form has no corresponding report! {form}") - - for form in Form99.objects.all(): - report = Report.objects.filter(form_99=form).first() - if report is not None: - report.committee_name = form.committee_name - report.street_1 = form.street_1 - report.street_2 = form.street_2 - report.city = form.city - report.state = form.state - report.zip = form.zip - report.save() - else: - logger.error(f"F99 Form has no corresponding report! {form}") - - for form in Form1m.objects.all(): - report = Report.objects.filter(form_1m=form).first() - if report is not None: - report.committee_name = form.committee_name - report.street_1 = form.street_1 - report.street_2 = form.street_2 - report.city = form.city - report.state = form.state - report.zip = form.zip - report.save() - else: - logger.error(f"F1M Form has no corresponding report! {form}") - - -class Migration(migrations.Migration): - - dependencies = [ - ("reports", "0007_remove_report_deleted"), - ] - - operations = [ - migrations.AddField( - model_name="report", - name="city", - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name="report", - name="committee_name", - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name="report", - name="state", - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name="report", - name="street_1", - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name="report", - name="street_2", - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name="report", - name="zip", - field=models.TextField(blank=True, null=True), - ), - migrations.RunPython(migrate_committee_data), - migrations.RemoveField( - model_name="form1m", - name="city", - ), - migrations.RemoveField( - model_name="form1m", - name="committee_name", - ), - migrations.RemoveField( - model_name="form1m", - name="state", - ), - migrations.RemoveField( - model_name="form1m", - name="street_1", - ), - migrations.RemoveField( - model_name="form1m", - name="street_2", - ), - migrations.RemoveField( - model_name="form1m", - name="zip", - ), - migrations.RemoveField( - model_name="form24", - name="city", - ), - migrations.RemoveField( - model_name="form24", - name="state", - ), - migrations.RemoveField( - model_name="form24", - name="street_1", - ), - migrations.RemoveField( - model_name="form24", - name="street_2", - ), - migrations.RemoveField( - model_name="form24", - name="zip", - ), - migrations.RemoveField( - model_name="form3x", - name="city", - ), - migrations.RemoveField( - model_name="form3x", - name="state", - ), - migrations.RemoveField( - model_name="form3x", - name="street_1", - ), - migrations.RemoveField( - model_name="form3x", - name="street_2", - ), - migrations.RemoveField( - model_name="form3x", - name="zip", - ), - migrations.RemoveField( - model_name="form99", - name="city", - ), - migrations.RemoveField( - model_name="form99", - name="committee_name", - ), - migrations.RemoveField( - model_name="form99", - name="state", - ), - migrations.RemoveField( - model_name="form99", - name="street_1", - ), - migrations.RemoveField( - model_name="form99", - name="street_2", - ), - migrations.RemoveField( - model_name="form99", - name="zip", - ), - ] diff --git a/django-backend/fecfiler/reports/migrations/0009_report_can_delete.py b/django-backend/fecfiler/reports/migrations/0009_report_can_delete.py deleted file mode 100644 index 87cc662370..0000000000 --- a/django-backend/fecfiler/reports/migrations/0009_report_can_delete.py +++ /dev/null @@ -1,180 +0,0 @@ -# Generated by Django 4.2.11 on 2024-07-02 17:21 - -from django.db import migrations, models -from django.db import connection - - -def populate_existing_rows(apps, schema_editor): - report_model = apps.get_model("reports", "Report") - for row in report_model.objects.all(): - row.can_delete = True - row.save() - - -def create_trigger(apps, schema_editor): - with connection.cursor() as cursor: - cursor.execute( - """ - CREATE OR REPLACE FUNCTION check_can_delete() - RETURNS TRIGGER AS $$ - BEGIN - PERFORM update_report_can_delete(NEW); - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION check_can_delete_previous() - RETURNS TRIGGER AS $$ - DECLARE - associated_report RECORD; - BEGIN - FOR associated_report IN ( - SELECT * FROM reports_report - WHERE committee_account_id = OLD.committee_account_id - AND can_delete = false - ) - LOOP - PERFORM update_report_can_delete(associated_report); - END LOOP; - - RETURN OLD; - END; - $$ LANGUAGE plpgsql; - - - CREATE OR REPLACE FUNCTION check_can_delete_transaction_update() - RETURNS TRIGGER AS $$ - DECLARE - associated_report RECORD; - BEGIN - SELECT * INTO associated_report FROM reports_report WHERE id IN ( - SELECT report_id FROM reports_reporttransaction - WHERE transaction_id = NEW.id LIMIT 1 - ); - PERFORM update_report_can_delete(associated_report); - - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION check_can_delete_transaction_insert() - RETURNS TRIGGER AS $$ - DECLARE - associated_report RECORD; - BEGIN - FOR associated_report IN ( - SELECT r1.* - FROM reports_report r1 - JOIN reports_report r2 - ON r1.committee_account_id = r2.committee_account_id - AND EXTRACT(YEAR FROM r1.coverage_from_date) = ( - EXTRACT(YEAR FROM r2.coverage_from_date)) - WHERE r1.can_delete = true - AND r2.id = NEW.report_id - ) - LOOP - PERFORM update_report_can_delete(associated_report); - END LOOP; - - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - CREATE TRIGGER check_can_delete_report - AFTER INSERT OR UPDATE ON reports_report - FOR EACH ROW - WHEN (pg_trigger_depth() = 0) -- Prevent infinite trigger loop - EXECUTE FUNCTION check_can_delete(); - - CREATE TRIGGER check_can_delete_previous - AFTER DELETE ON reports_report - FOR EACH ROW - WHEN (pg_trigger_depth() = 0) -- Prevent infinite trigger loop - EXECUTE FUNCTION check_can_delete_previous(); - - CREATE TRIGGER check_can_delete_transaction_insert - AFTER INSERT ON reports_reporttransaction - FOR EACH ROW - WHEN (pg_trigger_depth() = 0) -- Prevent infinite trigger loop - EXECUTE FUNCTION check_can_delete_transaction_insert(); - """ - ) - - -def reverse_code(apps, schema_editor): - # Get models - report_model = apps.get_model("reports", "Report") - transaction_model = apps.get_model("transactions", "Transaction") - - # Drop triggers - triggers = [ - "check_can_delete_report", - "check_can_delete_previous", - "check_can_delete_transaction_update", - "check_can_delete_transaction_insert", - ] - - with schema_editor.atomic(): - for trigger in triggers: - schema_editor.execute( - "DROP TRIGGER IF EXISTS %s ON %s", (trigger, report_model._meta.db_table) - ) - schema_editor.execute( - "DROP TRIGGER IF EXISTS %s ON %s", - (trigger, transaction_model._meta.db_table), - ) - - -class Migration(migrations.Migration): - - dependencies = [ - ("reports", "0008_remove_form1m_city_remove_form1m_committee_name_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="report", - name="can_delete", - field=models.BooleanField(default=True), - ), - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION update_report_can_delete(report RECORD) - RETURNS VOID AS $$ - DECLARE - r_can_delete boolean; - BEGIN - r_can_delete = report.upload_submission_id IS NULL - AND (report.report_version IS NULL OR report.report_version = '0') - AND ( - report.form_3x_id IS NULL OR - ( - report.form_24_id IS NULL - AND NOT EXISTS( - SELECT DISTINCT rrt1.id - FROM "reports_reporttransaction" rrt1 - JOIN "transactions_transaction" tt ON ( - rrt1."transaction_id" = tt."id" - OR tt."reatt_redes_id" = rrt1."transaction_id" - OR tt."parent_transaction_id" = rrt1."transaction_id" - OR tt."debt_id" = rrt1."transaction_id" - OR tt."loan_id" = rrt1."transaction_id" - ) - INNER JOIN "reports_reporttransaction" rrt2 ON ( - rrt2."transaction_id" = tt."id" - AND rrt2."report_id" <> report.id - ) - WHERE rrt1."report_id" = report.id - ) - ) - ); - UPDATE reports_report SET can_delete = r_can_delete - WHERE id = report.id - AND can_delete <> r_can_delete; - END; - $$ LANGUAGE plpgsql; - """ - ), - migrations.RunPython(create_trigger, reverse_code), - migrations.RunPython(populate_existing_rows), - ] diff --git a/django-backend/fecfiler/reports/migrations/0011_remove_form3x_cash_on_hand_date.py b/django-backend/fecfiler/reports/migrations/0011_remove_form3x_cash_on_hand_date.py deleted file mode 100644 index 3373a352ce..0000000000 --- a/django-backend/fecfiler/reports/migrations/0011_remove_form3x_cash_on_hand_date.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 4.2.11 on 2024-10-23 20:33 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("reports", "0010_report_can_unammend"), - ] - - operations = [ - migrations.RemoveField( - model_name="form3x", - name="cash_on_hand_date", - ), - ] diff --git a/django-backend/fecfiler/reports/migrations/0012_alter_form99_text_code.py b/django-backend/fecfiler/reports/migrations/0012_alter_form99_text_code.py deleted file mode 100644 index 2cbeb0f171..0000000000 --- a/django-backend/fecfiler/reports/migrations/0012_alter_form99_text_code.py +++ /dev/null @@ -1,37 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("reports", "0011_remove_form3x_cash_on_hand_date"), - ] - - operations = [ - # Step 1: Add the column as NULLABLE (to prevent NOT NULL constraint errors) - migrations.AddField( - model_name="form99", - name="text_code_2", - field=models.TextField(db_default=""), - ), - migrations.RunSQL( - sql=""" - UPDATE reports_form99 SET text_code_2 = COALESCE(text_code, ''); - ALTER TABLE reports_form99 ALTER COLUMN text_code SET DEFAULT ''; - ALTER TABLE reports_form99 ALTER COLUMN text_code_2 SET NOT NULL; - """, - reverse_sql=""" - ALTER TABLE reports_form99 ALTER COLUMN text_code_2 DROP DEFAULT; - ALTER TABLE reports_form99 ALTER COLUMN text_code_2 DROP NOT NULL; - """, - state_operations=[ - migrations.AlterField( - model_name="form99", - name="text_code_2", - field=models.TextField( - null=False, blank=False, default="", db_default="" - ), - ), - ], - ), - ] diff --git a/django-backend/fecfiler/reports/migrations/0014_form99_swap_text_code.py b/django-backend/fecfiler/reports/migrations/0014_form99_swap_text_code.py deleted file mode 100644 index 87dc826ec7..0000000000 --- a/django-backend/fecfiler/reports/migrations/0014_form99_swap_text_code.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.db import migrations -from django_migration_linter import IgnoreMigration - - -class Migration(migrations.Migration): - - dependencies = [ - ("reports", "0013_form3_report_form_3"), - ] - - operations = [ - IgnoreMigration(), - migrations.RemoveField(model_name="form99", name="text_code"), - migrations.RenameField( - model_name="form99", old_name="text_code_2", new_name="text_code" - ), - ] diff --git a/django-backend/fecfiler/reports/migrations/0015_form3x_filing_frequency_form3x_report_type_category.py b/django-backend/fecfiler/reports/migrations/0015_form3x_filing_frequency_form3x_report_type_category.py deleted file mode 100644 index db9f36da17..0000000000 --- a/django-backend/fecfiler/reports/migrations/0015_form3x_filing_frequency_form3x_report_type_category.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2 on 2025-05-20 19:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('reports', '0014_form99_swap_text_code'), - ] - - operations = [ - migrations.AddField( - model_name='form3x', - name='filing_frequency', - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name='form3x', - name='report_type_category', - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/django-backend/fecfiler/reports/migrations/0016_determine_frequency_and_category.py b/django-backend/fecfiler/reports/migrations/0016_determine_frequency_and_category.py deleted file mode 100644 index 4f3b442f72..0000000000 --- a/django-backend/fecfiler/reports/migrations/0016_determine_frequency_and_category.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 5.2 on 2025-05-20 19:59 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("reports", "0015_form3x_filing_frequency_form3x_report_type_category"), - ] - - operations = [ - # for reports made before we added the filing_frequency - # and report_type_category fields - migrations.RunSQL( - """ - UPDATE reports_form3x f - SET filing_frequency = CASE - WHEN r.report_code IN ( - 'M2', 'M3', 'M4', 'M5', 'M6', 'M7', - 'M8', 'M9', 'M10', 'M11', 'M12' - ) THEN 'M' - ELSE 'Q' - END, - report_type_category = CASE - WHEN r.report_code IN ( - 'M11', 'M12', 'MY' - ) THEN 'Non-Election Year' - ELSE 'Election Year' - END - FROM reports_report as r - WHERE r.form_3x_id = f.id - AND filing_frequency IS NULL AND report_type_category IS NULL; - """, - reverse_sql="", - ) - ] diff --git a/django-backend/fecfiler/reports/migrations/0017_form99_filing_frequency_form99_pdf_attachment.py b/django-backend/fecfiler/reports/migrations/0017_form99_filing_frequency_form99_pdf_attachment.py deleted file mode 100644 index dcb23d9a78..0000000000 --- a/django-backend/fecfiler/reports/migrations/0017_form99_filing_frequency_form99_pdf_attachment.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2 on 2025-06-04 21:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("reports", "0016_determine_frequency_and_category"), - ] - - operations = [ - migrations.AddField( - model_name="form99", - name="filing_frequency", - field=models.TextField(blank=True, max_length=1, null=True), - ), - migrations.AddField( - model_name="form99", - name="pdf_attachment", - field=models.BooleanField(blank=True, null=True), - ), - ] diff --git a/django-backend/fecfiler/reports/models.py b/django-backend/fecfiler/reports/models.py index 1c993704a4..a2547137bd 100644 --- a/django-backend/fecfiler/reports/models.py +++ b/django-backend/fecfiler/reports/models.py @@ -235,3 +235,17 @@ class ReportTransaction(models.Model): report = models.ForeignKey(Report, on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True) + + def save(self, *args, **kwargs): + with db_transaction.atomic(): + super(ReportTransaction, self).save(*args, **kwargs) + if self.report.can_unamend: + self.report.can_unamend = False + self.report.save() + + def delete(self, *args, **kwargs): + with db_transaction.atomic(): + super(ReportTransaction, self).delete(*args, **kwargs) + if self.report.can_unamend: + self.report.can_unamend = False + self.report.save() diff --git a/django-backend/fecfiler/reports/tests/test_models.py b/django-backend/fecfiler/reports/tests/test_models.py index 6eaf17ff93..9cf08a298f 100644 --- a/django-backend/fecfiler/reports/tests/test_models.py +++ b/django-backend/fecfiler/reports/tests/test_models.py @@ -83,8 +83,7 @@ def test_delete(self): "H2024", candidate_a, ) - ie.reports.set([f24_report_id, f3x_report_id]) - ie.save() + ie.set_reports([f24_report_id, f3x_report_id]) ie_id = ie.id f24_report.delete() diff --git a/django-backend/fecfiler/transactions/migrations/0001_initial.py b/django-backend/fecfiler/transactions/migrations/0001_initial.py index 0c5a8fd5bc..bbdbcbc552 100644 --- a/django-backend/fecfiler/transactions/migrations/0001_initial.py +++ b/django-backend/fecfiler/transactions/migrations/0001_initial.py @@ -10,10 +10,13 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ("committee_accounts", "0001_initial"), + ( + "committee_accounts", + "0001_squashed_0007_alter_committeeaccount_members", + ), ("contacts", "0001_initial"), ("reports", "0001_initial"), - ("memo_text", "0001_initial"), + ("memo_text", "0001_initial_squashed_0003_memotext_text_prefix"), ] operations = [ @@ -30,20 +33,6 @@ class Migration(migrations.Migration): unique=True, ), ), - ( - "contributor_organization_name", - models.TextField(blank=True, null=True), - ), - ("contributor_last_name", models.TextField(blank=True, null=True)), - ("contributor_first_name", models.TextField(blank=True, null=True)), - ("contributor_middle_name", models.TextField(blank=True, null=True)), - ("contributor_prefix", models.TextField(blank=True, null=True)), - ("contributor_suffix", models.TextField(blank=True, null=True)), - ("contributor_street_1", models.TextField(blank=True, null=True)), - ("contributor_street_2", models.TextField(blank=True, null=True)), - ("contributor_city", models.TextField(blank=True, null=True)), - ("contributor_state", models.TextField(blank=True, null=True)), - ("contributor_zip", models.TextField(blank=True, null=True)), ("contribution_date", models.DateField(blank=True, null=True)), ( "contribution_amount", @@ -55,22 +44,6 @@ class Migration(migrations.Migration): "contribution_purpose_descrip", models.TextField(blank=True, null=True), ), - ("contributor_employer", models.TextField(blank=True, null=True)), - ("contributor_occupation", models.TextField(blank=True, null=True)), - ("donor_committee_fec_id", models.TextField(blank=True, null=True)), - ("donor_committee_name", models.TextField(blank=True, null=True)), - ("donor_candidate_fec_id", models.TextField(blank=True, null=True)), - ("donor_candidate_last_name", models.TextField(blank=True, null=True)), - ("donor_candidate_first_name", models.TextField(blank=True, null=True)), - ( - "donor_candidate_middle_name", - models.TextField(blank=True, null=True), - ), - ("donor_candidate_prefix", models.TextField(blank=True, null=True)), - ("donor_candidate_suffix", models.TextField(blank=True, null=True)), - ("donor_candidate_office", models.TextField(blank=True, null=True)), - ("donor_candidate_state", models.TextField(blank=True, null=True)), - ("donor_candidate_district", models.TextField(blank=True, null=True)), ("election_code", models.TextField(blank=True, null=True)), ("election_other_description", models.TextField(blank=True, null=True)), ("conduit_name", models.TextField(blank=True, null=True)), @@ -103,17 +76,6 @@ class Migration(migrations.Migration): unique=True, ), ), - ("payee_organization_name", models.TextField(blank=True, null=True)), - ("payee_last_name", models.TextField(blank=True, null=True)), - ("payee_first_name", models.TextField(blank=True, null=True)), - ("payee_middle_name", models.TextField(blank=True, null=True)), - ("payee_prefix", models.TextField(blank=True, null=True)), - ("payee_suffix", models.TextField(blank=True, null=True)), - ("payee_street_1", models.TextField(blank=True, null=True)), - ("payee_street_2", models.TextField(blank=True, null=True)), - ("payee_city", models.TextField(blank=True, null=True)), - ("payee_state", models.TextField(blank=True, null=True)), - ("payee_zip", models.TextField(blank=True, null=True)), ("expenditure_date", models.DateField(blank=True, null=True)), ( "expenditure_amount", @@ -134,47 +96,6 @@ class Migration(migrations.Migration): ("conduit_state", models.TextField(blank=True, null=True)), ("conduit_zip", models.TextField(blank=True, null=True)), ("category_code", models.TextField(blank=True, null=True)), - ( - "beneficiary_committee_fec_id", - models.TextField(blank=True, null=True), - ), - ("beneficiary_committee_name", models.TextField(blank=True, null=True)), - ( - "beneficiary_candidate_fec_id", - models.TextField(blank=True, null=True), - ), - ( - "beneficiary_candidate_last_name", - models.TextField(blank=True, null=True), - ), - ( - "beneficiary_candidate_first_name", - models.TextField(blank=True, null=True), - ), - ( - "beneficiary_candidate_middle_name", - models.TextField(blank=True, null=True), - ), - ( - "beneficiary_candidate_prefix", - models.TextField(blank=True, null=True), - ), - ( - "beneficiary_candidate_suffix", - models.TextField(blank=True, null=True), - ), - ( - "beneficiary_candidate_office", - models.TextField(blank=True, null=True), - ), - ( - "beneficiary_candidate_state", - models.TextField(blank=True, null=True), - ), - ( - "beneficiary_candidate_district", - models.TextField(blank=True, null=True), - ), ("memo_text_description", models.TextField(blank=True, null=True)), ( "reference_to_si_or_sl_system_code_that_identifies_the_account", @@ -200,17 +121,6 @@ class Migration(migrations.Migration): ), ), ("receipt_line_number", models.TextField(blank=True, null=True)), - ("lender_organization_name", models.TextField(blank=True, null=True)), - ("lender_last_name", models.TextField(blank=True, null=True)), - ("lender_first_name", models.TextField(blank=True, null=True)), - ("lender_middle_name", models.TextField(blank=True, null=True)), - ("lender_prefix", models.TextField(blank=True, null=True)), - ("lender_suffix", models.TextField(blank=True, null=True)), - ("lender_street_1", models.TextField(blank=True, null=True)), - ("lender_street_2", models.TextField(blank=True, null=True)), - ("lender_city", models.TextField(blank=True, null=True)), - ("lender_state", models.TextField(blank=True, null=True)), - ("lender_zip", models.TextField(blank=True, null=True)), ("election_code", models.TextField(blank=True, null=True)), ("election_other_description", models.TextField(blank=True, null=True)), ( @@ -227,22 +137,6 @@ class Migration(migrations.Migration): "personal_funds", models.BooleanField(blank=True, default=False, null=True), ), - ("lender_committee_id_number", models.TextField(blank=True, null=True)), - ("lender_candidate_id_number", models.TextField(blank=True, null=True)), - ("lender_candidate_last_name", models.TextField(blank=True, null=True)), - ( - "lender_candidate_first_name", - models.TextField(blank=True, null=True), - ), - ( - "lender_candidate_middle_name", - models.TextField(blank=True, null=True), - ), - ("lender_candidate_prefix", models.TextField(blank=True, null=True)), - ("lender_candidate_suffix", models.TextField(blank=True, null=True)), - ("lender_candidate_office", models.TextField(blank=True, null=True)), - ("lender_candidate_state", models.TextField(blank=True, null=True)), - ("lender_candidate_district", models.TextField(blank=True, null=True)), ("memo_text_description", models.TextField(blank=True, null=True)), ], ), @@ -259,12 +153,6 @@ class Migration(migrations.Migration): unique=True, ), ), - ("lender_organization_name", models.TextField(blank=True, null=True)), - ("lender_street_1", models.TextField(blank=True, null=True)), - ("lender_street_2", models.TextField(blank=True, null=True)), - ("lender_city", models.TextField(blank=True, null=True)), - ("lender_state", models.TextField(blank=True, null=True)), - ("lender_zip", models.TextField(blank=True, null=True)), ( "loan_amount", models.DecimalField( @@ -374,18 +262,6 @@ class Migration(migrations.Migration): unique=True, ), ), - ("guarantor_last_name", models.TextField(blank=True, null=True)), - ("guarantor_first_name", models.TextField(blank=True, null=True)), - ("guarantor_middle_name", models.TextField(blank=True, null=True)), - ("guarantor_prefix", models.TextField(blank=True, null=True)), - ("guarantor_suffix", models.TextField(blank=True, null=True)), - ("guarantor_street_1", models.TextField(blank=True, null=True)), - ("guarantor_street_2", models.TextField(blank=True, null=True)), - ("guarantor_city", models.TextField(blank=True, null=True)), - ("guarantor_state", models.TextField(blank=True, null=True)), - ("guarantor_zip", models.TextField(blank=True, null=True)), - ("guarantor_employer", models.TextField(blank=True, null=True)), - ("guarantor_occupation", models.TextField(blank=True, null=True)), ( "guaranteed_amount", models.DecimalField( @@ -408,17 +284,6 @@ class Migration(migrations.Migration): ), ), ("receipt_line_number", models.TextField(blank=True, null=True)), - ("creditor_organization_name", models.TextField(blank=True, null=True)), - ("creditor_last_name", models.TextField(blank=True, null=True)), - ("creditor_first_name", models.TextField(blank=True, null=True)), - ("creditor_middle_name", models.TextField(blank=True, null=True)), - ("creditor_prefix", models.TextField(blank=True, null=True)), - ("creditor_suffix", models.TextField(blank=True, null=True)), - ("creditor_street_1", models.TextField(blank=True, null=True)), - ("creditor_street_2", models.TextField(blank=True, null=True)), - ("creditor_city", models.TextField(blank=True, null=True)), - ("creditor_state", models.TextField(blank=True, null=True)), - ("creditor_zip", models.TextField(blank=True, null=True)), ( "purpose_of_debt_or_obligation", models.TextField(blank=True, null=True), @@ -444,17 +309,6 @@ class Migration(migrations.Migration): unique=True, ), ), - ("payee_organization_name", models.TextField(blank=True, null=True)), - ("payee_last_name", models.TextField(blank=True, null=True)), - ("payee_first_name", models.TextField(blank=True, null=True)), - ("payee_middle_name", models.TextField(blank=True, null=True)), - ("payee_prefix", models.TextField(blank=True, null=True)), - ("payee_suffix", models.TextField(blank=True, null=True)), - ("payee_street_1", models.TextField(blank=True, null=True)), - ("payee_street_2", models.TextField(blank=True, null=True)), - ("payee_city", models.TextField(blank=True, null=True)), - ("payee_state", models.TextField(blank=True, null=True)), - ("payee_zip", models.TextField(blank=True, null=True)), ("election_code", models.TextField(blank=True, null=True)), ("election_other_description", models.TextField(blank=True, null=True)), ("dissemination_date", models.DateField(blank=True, null=True)), @@ -472,15 +326,6 @@ class Migration(migrations.Migration): ("category_code", models.TextField(blank=True, null=True)), ("payee_cmtte_fec_id_number", models.TextField(blank=True, null=True)), ("support_oppose_code", models.TextField(blank=True, null=True)), - ("so_candidate_id_number", models.TextField(blank=True, null=True)), - ("so_candidate_last_name", models.TextField(blank=True, null=True)), - ("so_candidate_first_name", models.TextField(blank=True, null=True)), - ("so_candidate_middle_name", models.TextField(blank=True, null=True)), - ("so_candidate_prefix", models.TextField(blank=True, null=True)), - ("so_candidate_suffix", models.TextField(blank=True, null=True)), - ("so_candidate_office", models.TextField(blank=True, null=True)), - ("so_candidate_district", models.TextField(blank=True, null=True)), - ("so_candidate_state", models.TextField(blank=True, null=True)), ("completing_last_name", models.TextField(blank=True, null=True)), ("completing_first_name", models.TextField(blank=True, null=True)), ("completing_middle_name", models.TextField(blank=True, null=True)), @@ -607,15 +452,6 @@ class Migration(migrations.Migration): to="transactions.transaction", ), ), - ( - "report", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="reports.report", - ), - ), ( "schedule_a", models.ForeignKey( diff --git a/django-backend/fecfiler/transactions/migrations/0002_remove_schedulea_contributor_city_and_more.py b/django-backend/fecfiler/transactions/migrations/0002_remove_schedulea_contributor_city_and_more.py deleted file mode 100644 index b1f92a36d3..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0002_remove_schedulea_contributor_city_and_more.py +++ /dev/null @@ -1,477 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-24 09:51 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('transactions', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='schedulea', - name='contributor_city', - ), - migrations.RemoveField( - model_name='schedulea', - name='contributor_employer', - ), - migrations.RemoveField( - model_name='schedulea', - name='contributor_first_name', - ), - migrations.RemoveField( - model_name='schedulea', - name='contributor_last_name', - ), - migrations.RemoveField( - model_name='schedulea', - name='contributor_middle_name', - ), - migrations.RemoveField( - model_name='schedulea', - name='contributor_occupation', - ), - migrations.RemoveField( - model_name='schedulea', - name='contributor_organization_name', - ), - migrations.RemoveField( - model_name='schedulea', - name='contributor_prefix', - ), - migrations.RemoveField( - model_name='schedulea', - name='contributor_state', - ), - migrations.RemoveField( - model_name='schedulea', - name='contributor_street_1', - ), - migrations.RemoveField( - model_name='schedulea', - name='contributor_street_2', - ), - migrations.RemoveField( - model_name='schedulea', - name='contributor_suffix', - ), - migrations.RemoveField( - model_name='schedulea', - name='contributor_zip', - ), - migrations.RemoveField( - model_name='schedulea', - name='donor_candidate_district', - ), - migrations.RemoveField( - model_name='schedulea', - name='donor_candidate_fec_id', - ), - migrations.RemoveField( - model_name='schedulea', - name='donor_candidate_first_name', - ), - migrations.RemoveField( - model_name='schedulea', - name='donor_candidate_last_name', - ), - migrations.RemoveField( - model_name='schedulea', - name='donor_candidate_middle_name', - ), - migrations.RemoveField( - model_name='schedulea', - name='donor_candidate_office', - ), - migrations.RemoveField( - model_name='schedulea', - name='donor_candidate_prefix', - ), - migrations.RemoveField( - model_name='schedulea', - name='donor_candidate_state', - ), - migrations.RemoveField( - model_name='schedulea', - name='donor_candidate_suffix', - ), - migrations.RemoveField( - model_name='schedulea', - name='donor_committee_fec_id', - ), - migrations.RemoveField( - model_name='schedulea', - name='donor_committee_name', - ), - migrations.RemoveField( - model_name='scheduleb', - name='beneficiary_candidate_district', - ), - migrations.RemoveField( - model_name='scheduleb', - name='beneficiary_candidate_fec_id', - ), - migrations.RemoveField( - model_name='scheduleb', - name='beneficiary_candidate_first_name', - ), - migrations.RemoveField( - model_name='scheduleb', - name='beneficiary_candidate_last_name', - ), - migrations.RemoveField( - model_name='scheduleb', - name='beneficiary_candidate_middle_name', - ), - migrations.RemoveField( - model_name='scheduleb', - name='beneficiary_candidate_office', - ), - migrations.RemoveField( - model_name='scheduleb', - name='beneficiary_candidate_prefix', - ), - migrations.RemoveField( - model_name='scheduleb', - name='beneficiary_candidate_state', - ), - migrations.RemoveField( - model_name='scheduleb', - name='beneficiary_candidate_suffix', - ), - migrations.RemoveField( - model_name='scheduleb', - name='beneficiary_committee_fec_id', - ), - migrations.RemoveField( - model_name='scheduleb', - name='beneficiary_committee_name', - ), - migrations.RemoveField( - model_name='scheduleb', - name='payee_city', - ), - migrations.RemoveField( - model_name='scheduleb', - name='payee_first_name', - ), - migrations.RemoveField( - model_name='scheduleb', - name='payee_last_name', - ), - migrations.RemoveField( - model_name='scheduleb', - name='payee_middle_name', - ), - migrations.RemoveField( - model_name='scheduleb', - name='payee_organization_name', - ), - migrations.RemoveField( - model_name='scheduleb', - name='payee_prefix', - ), - migrations.RemoveField( - model_name='scheduleb', - name='payee_state', - ), - migrations.RemoveField( - model_name='scheduleb', - name='payee_street_1', - ), - migrations.RemoveField( - model_name='scheduleb', - name='payee_street_2', - ), - migrations.RemoveField( - model_name='scheduleb', - name='payee_suffix', - ), - migrations.RemoveField( - model_name='scheduleb', - name='payee_zip', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_candidate_district', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_candidate_first_name', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_candidate_id_number', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_candidate_last_name', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_candidate_middle_name', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_candidate_office', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_candidate_prefix', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_candidate_state', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_candidate_suffix', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_city', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_committee_id_number', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_first_name', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_last_name', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_middle_name', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_organization_name', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_prefix', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_state', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_street_1', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_street_2', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_suffix', - ), - migrations.RemoveField( - model_name='schedulec', - name='lender_zip', - ), - migrations.RemoveField( - model_name='schedulec1', - name='lender_city', - ), - migrations.RemoveField( - model_name='schedulec1', - name='lender_organization_name', - ), - migrations.RemoveField( - model_name='schedulec1', - name='lender_state', - ), - migrations.RemoveField( - model_name='schedulec1', - name='lender_street_1', - ), - migrations.RemoveField( - model_name='schedulec1', - name='lender_street_2', - ), - migrations.RemoveField( - model_name='schedulec1', - name='lender_zip', - ), - migrations.RemoveField( - model_name='schedulec2', - name='guarantor_city', - ), - migrations.RemoveField( - model_name='schedulec2', - name='guarantor_employer', - ), - migrations.RemoveField( - model_name='schedulec2', - name='guarantor_first_name', - ), - migrations.RemoveField( - model_name='schedulec2', - name='guarantor_last_name', - ), - migrations.RemoveField( - model_name='schedulec2', - name='guarantor_middle_name', - ), - migrations.RemoveField( - model_name='schedulec2', - name='guarantor_occupation', - ), - migrations.RemoveField( - model_name='schedulec2', - name='guarantor_prefix', - ), - migrations.RemoveField( - model_name='schedulec2', - name='guarantor_state', - ), - migrations.RemoveField( - model_name='schedulec2', - name='guarantor_street_1', - ), - migrations.RemoveField( - model_name='schedulec2', - name='guarantor_street_2', - ), - migrations.RemoveField( - model_name='schedulec2', - name='guarantor_suffix', - ), - migrations.RemoveField( - model_name='schedulec2', - name='guarantor_zip', - ), - migrations.RemoveField( - model_name='scheduled', - name='creditor_city', - ), - migrations.RemoveField( - model_name='scheduled', - name='creditor_first_name', - ), - migrations.RemoveField( - model_name='scheduled', - name='creditor_last_name', - ), - migrations.RemoveField( - model_name='scheduled', - name='creditor_middle_name', - ), - migrations.RemoveField( - model_name='scheduled', - name='creditor_organization_name', - ), - migrations.RemoveField( - model_name='scheduled', - name='creditor_prefix', - ), - migrations.RemoveField( - model_name='scheduled', - name='creditor_state', - ), - migrations.RemoveField( - model_name='scheduled', - name='creditor_street_1', - ), - migrations.RemoveField( - model_name='scheduled', - name='creditor_street_2', - ), - migrations.RemoveField( - model_name='scheduled', - name='creditor_suffix', - ), - migrations.RemoveField( - model_name='scheduled', - name='creditor_zip', - ), - migrations.RemoveField( - model_name='schedulee', - name='payee_city', - ), - migrations.RemoveField( - model_name='schedulee', - name='payee_first_name', - ), - migrations.RemoveField( - model_name='schedulee', - name='payee_last_name', - ), - migrations.RemoveField( - model_name='schedulee', - name='payee_middle_name', - ), - migrations.RemoveField( - model_name='schedulee', - name='payee_organization_name', - ), - migrations.RemoveField( - model_name='schedulee', - name='payee_prefix', - ), - migrations.RemoveField( - model_name='schedulee', - name='payee_state', - ), - migrations.RemoveField( - model_name='schedulee', - name='payee_street_1', - ), - migrations.RemoveField( - model_name='schedulee', - name='payee_street_2', - ), - migrations.RemoveField( - model_name='schedulee', - name='payee_suffix', - ), - migrations.RemoveField( - model_name='schedulee', - name='payee_zip', - ), - migrations.RemoveField( - model_name='schedulee', - name='so_candidate_district', - ), - migrations.RemoveField( - model_name='schedulee', - name='so_candidate_first_name', - ), - migrations.RemoveField( - model_name='schedulee', - name='so_candidate_id_number', - ), - migrations.RemoveField( - model_name='schedulee', - name='so_candidate_last_name', - ), - migrations.RemoveField( - model_name='schedulee', - name='so_candidate_middle_name', - ), - migrations.RemoveField( - model_name='schedulee', - name='so_candidate_office', - ), - migrations.RemoveField( - model_name='schedulee', - name='so_candidate_prefix', - ), - migrations.RemoveField( - model_name='schedulee', - name='so_candidate_state', - ), - migrations.RemoveField( - model_name='schedulee', - name='so_candidate_suffix', - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0002_remove_schedulea_squashed_0026_alter_transaction_itemized.py b/django-backend/fecfiler/transactions/migrations/0002_remove_schedulea_squashed_0026_alter_transaction_itemized.py new file mode 100644 index 0000000000..857cdcfc35 --- /dev/null +++ b/django-backend/fecfiler/transactions/migrations/0002_remove_schedulea_squashed_0026_alter_transaction_itemized.py @@ -0,0 +1,378 @@ +# Generated by Django 5.2.11 on 2026-03-07 03:25 + +import django.contrib.postgres.fields +import django.db.models.deletion +import uuid +from django.db import connection, migrations, models +from fecfiler.transactions.schedule_a.managers import ( + over_two_hundred_types as schedule_a_over_two_hundred_types, +) +from fecfiler.transactions.schedule_b.managers import ( + over_two_hundred_types as schedule_b_over_two_hundred_types, +) + + +# Inlined from 0011_transaction_can_delete +# Keep the final live trigger that maintains Transaction.blocking_reports. +def create_trigger_function(apps, schema_editor): + with connection.cursor() as cursor: + cursor.execute( + """ + CREATE OR REPLACE FUNCTION update_transactions_can_delete() RETURNS TRIGGER AS $$ + BEGIN + UPDATE transactions_transaction + SET blocking_reports = CASE + WHEN NEW.upload_submission_id IS NOT NULL + THEN array_append(blocking_reports, NEW.id) + ELSE array_remove(blocking_reports, NEW.id) + END + -- all transactions in the submitted report + WHERE id IN ( + SELECT transaction_id + FROM reports_reporttransaction + WHERE report_id = NEW.id + ) + -- all transactions that are reattributed in the submitted report + OR id IN ( + SELECT reatt_redes_id + FROM reports_reporttransaction + JOIN transactions_transaction tt + ON reports_reporttransaction.transaction_id = tt.id + WHERE report_id = NEW.id + ) + -- all loans that are carried forward in the submitted report + OR id IN ( + SELECT loan_id + FROM reports_reporttransaction + JOIN transactions_transaction tt + ON reports_reporttransaction.transaction_id = tt.id + WHERE report_id = NEW.id + ) + -- all repayments to loans that are carried forward in the submitted report + OR loan_id IN ( + SELECT loan_id + FROM reports_reporttransaction + JOIN transactions_transaction tt + ON reports_reporttransaction.transaction_id = tt.id + WHERE report_id = NEW.id AND tt.schedule_c_id IS NOT NULL + ) + -- all debts that are carried forward in the submitted report + OR id IN ( + SELECT debt_id + FROM reports_reporttransaction + JOIN transactions_transaction tt + ON reports_reporttransaction.transaction_id = tt.id + WHERE report_id = NEW.id + ) + -- all repayments to debts that are carried forward in the submitted report + OR debt_id IN ( + SELECT debt_id + FROM reports_reporttransaction + JOIN transactions_transaction tt + ON reports_reporttransaction.transaction_id = tt.id + WHERE report_id = NEW.id AND tt.schedule_d_id IS NOT NULL + ); + RETURN NEW; + END; + $$ LANGUAGE plpgsql; + """ + ) + + +def drop_trigger_function(apps, schema_editor): + with connection.cursor() as cursor: + cursor.execute("DROP FUNCTION IF EXISTS update_transactions_can_delete();") + + +def create_trigger(apps, schema_editor): + with connection.cursor() as cursor: + cursor.execute( + """ + CREATE TRIGGER report_status_update + AFTER UPDATE OF upload_submission_id ON reports_report + FOR EACH ROW + EXECUTE FUNCTION update_transactions_can_delete(); + """ + ) + + +def drop_trigger(apps, schema_editor): + with connection.cursor() as cursor: + cursor.execute("DROP TRIGGER IF EXISTS report_status_update ON reports_report;") + + +# Inlined from 0013_transaction_itemized_and_associated_triggers +# This data is used by itemization logic and should exist on fresh installs. +def populate_over_two_hundred_types(apps, schema_editor): + OverTwoHundredTypesScheduleA = apps.get_model( # noqa: N806 + "transactions", "OverTwoHundredTypesScheduleA" + ) + OverTwoHundredTypesScheduleB = apps.get_model( # noqa: N806 + "transactions", "OverTwoHundredTypesScheduleB" + ) + scha_types_to_create = [ + OverTwoHundredTypesScheduleA(type=type_to_create) + for type_to_create in schedule_a_over_two_hundred_types + ] + OverTwoHundredTypesScheduleA.objects.bulk_create(scha_types_to_create) + schb_types_to_create = [ + OverTwoHundredTypesScheduleB(type=type_to_create) + for type_to_create in schedule_b_over_two_hundred_types + ] + OverTwoHundredTypesScheduleB.objects.bulk_create(schb_types_to_create) + + +class Migration(migrations.Migration): + + replaces = [ + ("transactions", "0002_remove_schedulea_contributor_city_and_more"), + ("transactions", "0003_alter_transaction_parent_transaction"), + ("transactions", "0004_report_transactions_link_table"), + ("transactions", "0005_schedulec_report_coverage_from_date_and_more"), + ("transactions", "0006_independent_expenditure_memos_no_aggregation_group"), + ("transactions", "0007_schedulee_so_candidate_state"), + ("transactions", "0008_transaction__calendar_ytd_per_election_office_and_more"), + ("transactions", "0009_update_calculate_loan_payment_to_date"), + ("transactions", "0010_update_aggregate_trigger_performance"), + ("transactions", "0011_transaction_can_delete"), + ("transactions", "0012_alter_transactions_blocking_reports"), + ("transactions", "0013_transaction_itemized_and_associated_triggers"), + ("transactions", "0014_drop_transaction_view"), + ("transactions", "0015_merge_transaction_triggers"), + ("transactions", "0016_schedulef_transaction_contact_4_and_more"), + ("transactions", "0017_schedulef_coordianted_to_coordinated"), + ("transactions", "0018_schedulef_general_election_year_and_more"), + ("transactions", "0019_aggregate_committee_controls"), + ("transactions", "0020_trigger_save_on_transactions"), + ("transactions", "0021_alter_transaction_reports"), + ("transactions", "0022_schedule_f_aggregation"), + ("transactions", "0023_optimize_calculate_loan_payment_to_date"), + ("transactions", "0024_scheduled_balance_at_close_and_more"), + ("transactions", "0025_drop_aggregate_triggers"), + ("transactions", "0026_alter_transaction_itemized"), + ] + + dependencies = [ + ("contacts", "0001_initial"), + ("reports", "0005_remove_form1m_squashed_0006_reporttransaction"), + ("reports", "0007_remove_report_deleted_squashed_00019_form24_name_fix"), + ("transactions", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="transaction", + name="reports", + field=models.ManyToManyField( + related_name="transactions", + through="reports.ReportTransaction", + through_fields=["transaction", "report"], + to="reports.report", + ), + ), + migrations.AddField( + model_name="schedulec", + name="report_coverage_through_date", + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name="scheduled", + name="report_coverage_from_date", + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name="schedulee", + name="so_candidate_state", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="transaction", + name="_calendar_ytd_per_election_office", + field=models.DecimalField( + blank=True, decimal_places=2, max_digits=11, null=True + ), + ), + migrations.AddField( + model_name="transaction", + name="aggregate", + field=models.DecimalField( + blank=True, decimal_places=2, max_digits=11, null=True + ), + ), + migrations.AddField( + model_name="transaction", + name="loan_payment_to_date", + field=models.DecimalField( + blank=True, decimal_places=2, max_digits=11, null=True + ), + ), + migrations.AddField( + model_name="transaction", + name="blocking_reports", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.UUIDField(), default=list, size=None + ), + ), + migrations.RunPython( + code=create_trigger_function, + reverse_code=drop_trigger_function, + ), + migrations.RunPython( + code=create_trigger, + reverse_code=drop_trigger, + ), + migrations.AddField( + model_name="transaction", + name="itemized", + field=models.BooleanField(db_default=False), + ), + migrations.CreateModel( + name="OverTwoHundredTypesScheduleA", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("type", models.TextField()), + ], + options={ + "db_table": "over_two_hundred_types_schedulea", + "indexes": [ + models.Index(fields=["type"], name="over_two_hu_type_2c8314_idx") + ], + }, + ), + migrations.CreateModel( + name="OverTwoHundredTypesScheduleB", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("type", models.TextField()), + ], + options={ + "db_table": "over_two_hundred_types_scheduleb", + "indexes": [ + models.Index(fields=["type"], name="over_two_hu_type_411a44_idx") + ], + }, + ), + migrations.RunPython( + code=populate_over_two_hundred_types, + reverse_code=django.db.migrations.operations.special.RunPython.noop, + ), + migrations.CreateModel( + name="ScheduleF", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "filer_designated_to_make_coordinated_expenditures", + models.BooleanField(blank=True, null=True), + ), + ("expenditure_date", models.DateField(blank=True, null=True)), + ( + "expenditure_amount", + models.DecimalField( + blank=True, decimal_places=2, max_digits=11, null=True + ), + ), + ( + "aggregate_general_elec_expended", + models.DecimalField( + blank=True, decimal_places=2, max_digits=11, null=True + ), + ), + ("expenditure_purpose_descrip", models.TextField(blank=True)), + ("category_code", models.TextField(blank=True, null=True)), + ("memo_text_description", models.TextField(blank=True, null=True)), + ("general_election_year", models.TextField(blank=True)), + ], + ), + migrations.AddField( + model_name="transaction", + name="schedule_f", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="transactions.schedulef", + ), + ), + migrations.AddField( + model_name="transaction", + name="contact_4", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="contact_4_transaction_set", + to="contacts.contact", + ), + ), + migrations.AddField( + model_name="transaction", + name="contact_5", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="contact_5_transaction_set", + to="contacts.contact", + ), + ), + migrations.AddField( + model_name="scheduled", + name="balance_at_close", + field=models.DecimalField( + blank=True, decimal_places=2, max_digits=11, null=True + ), + ), + migrations.AddField( + model_name="scheduled", + name="beginning_balance", + field=models.DecimalField( + blank=True, decimal_places=2, max_digits=11, null=True + ), + ), + migrations.AddField( + model_name="scheduled", + name="incurred_prior", + field=models.DecimalField( + blank=True, decimal_places=2, max_digits=11, null=True + ), + ), + migrations.AddField( + model_name="scheduled", + name="payment_prior", + field=models.DecimalField( + blank=True, decimal_places=2, max_digits=11, null=True + ), + ), + migrations.AddField( + model_name="scheduled", + name="payment_amount", + field=models.DecimalField( + blank=True, decimal_places=2, max_digits=11, null=True + ), + ), + ] diff --git a/django-backend/fecfiler/transactions/migrations/0003_alter_transaction_parent_transaction.py b/django-backend/fecfiler/transactions/migrations/0003_alter_transaction_parent_transaction.py deleted file mode 100644 index b196f9c6a5..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0003_alter_transaction_parent_transaction.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-05 03:55 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - dependencies = [ - ("transactions", "0002_remove_schedulea_contributor_city_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="transaction", - name="parent_transaction", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="children", - to="transactions.transaction", - ), - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0003_remove_unused_functions.py b/django-backend/fecfiler/transactions/migrations/0003_remove_unused_functions.py new file mode 100644 index 0000000000..9f2c0c2e0a --- /dev/null +++ b/django-backend/fecfiler/transactions/migrations/0003_remove_unused_functions.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2.12 on 2026-03-24 16:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "transactions", + "0002_remove_schedulea_squashed_0026_alter_transaction_itemized", + ), + ] + + operations = [ + migrations.RunSQL( + sql=""" + DROP FUNCTION IF EXISTS + public.calculate_calendar_ytd_per_election_office CASCADE; + DROP FUNCTION IF EXISTS public.calculate_entity_aggregates CASCADE; + DROP FUNCTION IF EXISTS public.calculate_is_loan CASCADE; + DROP FUNCTION IF EXISTS public.calculate_itemization CASCADE; + DROP FUNCTION IF EXISTS public.calculate_loan_date CASCADE; + DROP FUNCTION IF EXISTS public.calculate_loan_payment_to_date CASCADE; + DROP FUNCTION IF EXISTS public.calculate_original_loan_id CASCADE; + DROP FUNCTION IF EXISTS + public.get_children_and_grandchildren_transaction_ids CASCADE; + DROP FUNCTION IF EXISTS + public.get_parent_grandparent_transaction_ids CASCADE; + DROP FUNCTION IF EXISTS public.get_temp_tablename CASCADE; + DROP FUNCTION IF EXISTS public.handle_parent_itemization CASCADE; + DROP FUNCTION IF EXISTS public.needs_itemized_set CASCADE; + DROP FUNCTION IF EXISTS public.process_itemization CASCADE; + DROP FUNCTION IF EXISTS public.set_itemization_for_ids CASCADE; + """, + ), + ] diff --git a/django-backend/fecfiler/transactions/migrations/0004_report_transactions_link_table.py b/django-backend/fecfiler/transactions/migrations/0004_report_transactions_link_table.py deleted file mode 100644 index 5f3e82cc4d..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0004_report_transactions_link_table.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 4.2.7 on 2024-03-08 16:37 - -from django.db import migrations, models - - -def add_link_table(apps, schema_editor): - transaction = apps.get_model("transactions", "Transaction") - - for t in transaction.objects.all(): - t.reports.add(t.report) - t.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ("reports", "0006_reporttransaction"), - ("transactions", "0003_alter_transaction_parent_transaction"), - ] - - operations = [ - migrations.AddField( - model_name="transaction", - name="reports", - field=models.ManyToManyField( - through="reports.ReportTransaction", to="reports.report" - ), - ), - migrations.RunPython(add_link_table, migrations.RunPython.noop), - migrations.RemoveField(model_name="transaction", name="report"), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0005_schedulec_report_coverage_from_date_and_more.py b/django-backend/fecfiler/transactions/migrations/0005_schedulec_report_coverage_from_date_and_more.py deleted file mode 100644 index df255482f4..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0005_schedulec_report_coverage_from_date_and_more.py +++ /dev/null @@ -1,65 +0,0 @@ -# Generated by Django 4.2.10 on 2024-03-25 16:45 - -from django.db import migrations -from django.db.models import Q, deletion, DateField, ForeignKey, ManyToManyField - - -def set_coverage_date(apps, schema_editor): - transaction_model = apps.get_model("transactions", "Transaction") - - for transaction in transaction_model.objects.filter( - Q(schedule_c__isnull=False) | Q(schedule_d__isnull=False) - ): - for report in transaction.reports.all(): - if report.coverage_from_date and transaction.schedule_d: - transaction.schedule_d.report_coverage_from_date = ( - report.coverage_from_date - ) - transaction.schedule_d.save() - if report.coverage_through_date and transaction.schedule_c: - transaction.schedule_c.report_coverage_through_date = ( - report.coverage_through_date - ) - transaction.schedule_c.save() - transaction.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ("reports", "0006_reporttransaction"), - ("transactions", "0004_report_transactions_link_table"), - ] - - operations = [ - migrations.AddField( - model_name="schedulec", - name="report_coverage_through_date", - field=DateField(blank=True, null=True), - ), - migrations.AddField( - model_name="scheduled", - name="report_coverage_from_date", - field=DateField(blank=True, null=True), - ), - migrations.AlterField( - model_name="transaction", - name="parent_transaction", - field=ForeignKey( - blank=True, - null=True, - on_delete=deletion.CASCADE, - to="transactions.transaction", - ), - ), - migrations.AlterField( - model_name="transaction", - name="reports", - field=ManyToManyField( - related_name="transactions", - through="reports.ReportTransaction", - to="reports.report", - ), - ), - migrations.RunPython(set_coverage_date, migrations.RunPython.noop), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0006_independent_expenditure_memos_no_aggregation_group.py b/django-backend/fecfiler/transactions/migrations/0006_independent_expenditure_memos_no_aggregation_group.py deleted file mode 100644 index f984bba5e8..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0006_independent_expenditure_memos_no_aggregation_group.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 4.2.10 on 2024-03-25 16:45 - -from django.db import migrations -from django.db.models import Q - - -def set_aggregation_group_to_none_for_ie_memos(apps, schema_editor): - transaction_model = apps.get_model("transactions", "Transaction") - - for transaction in transaction_model.objects.filter( - Q(transaction_type_identifier__in=[ - "INDEPENDENT_EXPENDITURE_CREDIT_CARD_PAYMENT_MEMO", - "INDEPENDENT_EXPENDITURE_STAFF_REIMBURSEMENT_MEMO", - "INDEPENDENT_EXPENDITURE_PAYMENT_TO_PAYROLL_MEMO" - ]) - ): - transaction.aggregation_group = None - transaction.save() - - -def reverse_removing_aggregation_group_for_ie_memos(apps, schema_editor): - transaction_model = apps.get_model("transactions", "Transaction") - - for transaction in transaction_model.objects.filter( - Q(transaction_type_identifier__in=[ - "INDEPENDENT_EXPENDITURE_CREDIT_CARD_PAYMENT_MEMO", - "INDEPENDENT_EXPENDITURE_STAFF_REIMBURSEMENT_MEMO", - "INDEPENDENT_EXPENDITURE_PAYMENT_TO_PAYROLL_MEMO" - ]) - ): - transaction.aggregation_group = "INDEPENDENT_EXPENDITURE" - transaction.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0005_schedulec_report_coverage_from_date_and_more"), - ] - - operations = [ - migrations.RunPython( - set_aggregation_group_to_none_for_ie_memos, - reverse_removing_aggregation_group_for_ie_memos, - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0007_schedulee_so_candidate_state.py b/django-backend/fecfiler/transactions/migrations/0007_schedulee_so_candidate_state.py deleted file mode 100644 index 3901f5c8f3..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0007_schedulee_so_candidate_state.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.11 on 2024-06-04 14:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("transactions", "0006_independent_expenditure_memos_no_aggregation_group"), - ] - - operations = [ - migrations.AddField( - model_name="schedulee", - name="so_candidate_state", - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0008_transaction__calendar_ytd_per_election_office_and_more.py b/django-backend/fecfiler/transactions/migrations/0008_transaction__calendar_ytd_per_election_office_and_more.py deleted file mode 100644 index 119c96d10f..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0008_transaction__calendar_ytd_per_election_office_and_more.py +++ /dev/null @@ -1,261 +0,0 @@ -# Generated by Django 4.2.11 on 2024-05-21 20:49 - -from django.db import migrations, models - - -def populate_existing_rows(apps, schema_editor): - transaction = apps.get_model("transactions", "Transaction") - for row in transaction.objects.all(): - row.aggregate = 0.0 - row.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0007_schedulee_so_candidate_state"), - ] - - operations = [ - migrations.AddField( - model_name="transaction", - name="_calendar_ytd_per_election_office", - field=models.DecimalField( - blank=True, decimal_places=2, max_digits=11, null=True - ), - ), - migrations.AddField( - model_name="transaction", - name="aggregate", - field=models.DecimalField( - blank=True, decimal_places=2, max_digits=11, null=True - ), - ), - migrations.AddField( - model_name="transaction", - name="loan_payment_to_date", - field=models.DecimalField( - blank=True, decimal_places=2, max_digits=11, null=True - ), - ), - migrations.RunSQL( - """ - CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - """ - ), - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION calculate_entity_aggregates( - txn RECORD, - sql_committee_id TEXT, - temp_table_name TEXT - ) - RETURNS VOID AS $$ - DECLARE - schedule_date DATE; - BEGIN - IF txn.schedule_a_id IS NOT NULL THEN - SELECT contribution_date - INTO schedule_date - FROM transactions_schedulea - WHERE id = txn.schedule_a_id; - ELSIF txn.schedule_b_id IS NOT NULL THEN - SELECT expenditure_date - INTO schedule_date - FROM transactions_scheduleb - WHERE id = txn.schedule_b_id; - END IF; - - EXECUTE ' - CREATE TEMPORARY TABLE ' || temp_table_name || ' - ON COMMIT DROP AS - SELECT - id, - SUM(effective_amount) OVER (ORDER BY date, created) - AS new_sum - FROM transaction_view__' || sql_committee_id || ' - WHERE - contact_1_id = $1 - AND EXTRACT(YEAR FROM date) = $2 - AND aggregation_group = $3 - AND force_unaggregated IS NOT TRUE; - - UPDATE transactions_transaction AS t - SET aggregate = tt.new_sum - FROM ' || temp_table_name || ' AS tt - WHERE t.id = tt.id; - ' - USING - txn.contact_1_id, - EXTRACT(YEAR FROM schedule_date), - txn.aggregation_group; - END; - $$ LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION calculate_calendar_ytd_per_election_office( - txn RECORD, - sql_committee_id TEXT, - temp_table_name TEXT - ) - RETURNS VOID AS $$ - DECLARE - schedule_date DATE; - v_election_code TEXT; - v_candidate_office TEXT; - v_candidate_state TEXT; - v_candidate_district TEXT; - BEGIN - SELECT COALESCE(disbursement_date, dissemination_date) - INTO schedule_date FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - SELECT election_code - INTO v_election_code - FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - SELECT candidate_office, candidate_state, candidate_district - INTO v_candidate_office, v_candidate_state, v_candidate_district - FROM contacts WHERE id = txn.contact_2_id; - - EXECUTE ' - CREATE TEMPORARY TABLE ' || temp_table_name || ' - ON COMMIT DROP AS - SELECT - t.id, - SUM(t.effective_amount) OVER - (ORDER BY t.date, t.created) AS new_sum - FROM transactions_schedulee e - JOIN transaction_view__' || sql_committee_id || ' t - ON e.id = t.schedule_e_id - JOIN contacts c - ON t.contact_2_id = c.id - WHERE - e.election_code = $1 - AND c.candidate_office = $2 - AND ( - c.candidate_state = $3 - OR ( - c.candidate_state IS NULL - AND $3 = '''' - ) - ) - AND ( - c.candidate_district = $4 - OR ( - c.candidate_district IS NULL - AND $4 = '''' - ) - ) - AND EXTRACT(YEAR FROM t.date) = $5 - AND aggregation_group = $6 - AND force_unaggregated IS NOT TRUE; - - UPDATE transactions_transaction AS t - SET _calendar_ytd_per_election_office = tt.new_sum - FROM ' || temp_table_name || ' AS tt - WHERE t.id = tt.id; - ' - USING - v_election_code, - v_candidate_office, - COALESCE(v_candidate_state, ''), - COALESCE(v_candidate_district, ''), - EXTRACT(YEAR FROM schedule_date), - txn.aggregation_group; - END; - $$ LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION calculate_loan_payment_to_date( - txn RECORD, - sql_committee_id TEXT, - temp_table_name TEXT - ) - RETURNS VOID AS $$ - BEGIN - EXECUTE ' - CREATE TEMPORARY TABLE ' || temp_table_name || ' - ON COMMIT DROP AS - SELECT - id, - loan_key, - SUM(effective_amount) OVER (ORDER BY loan_key) AS new_sum - FROM transaction_view__' || sql_committee_id || ' - WHERE loan_key LIKE ( - SELECT transaction_id FROM transactions_transaction - WHERE id = $1 - ) || ''%%''; -- Match the loan_key with a transaction_id prefix - - UPDATE transactions_transaction AS t - SET loan_payment_to_date = tt.new_sum - FROM ' || temp_table_name || ' AS tt - WHERE t.id = tt.id - AND tt.loan_key LIKE ''%%LOAN''; - ' - USING txn.id; - END; - $$ LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION calculate_aggregates() - RETURNS TRIGGER AS $$ - DECLARE - sql_committee_id TEXT; - temp_table_name TEXT; - BEGIN - sql_committee_id := REPLACE(NEW.committee_account_id::TEXT, '-', '_'); - temp_table_name := 'temp_' || REPLACE(uuid_generate_v4()::TEXT, '-', '_'); - RAISE NOTICE 'TESTING TRIGGER'; - - -- If schedule_c2_id or schedule_d_id is not null, stop processing - IF NEW.schedule_c2_id IS NOT NULL OR NEW.schedule_d_id IS NOT NULL - THEN - RETURN NEW; - END IF; - - IF NEW.schedule_a_id IS NOT NULL OR NEW.schedule_b_id IS NOT NULL - THEN - PERFORM calculate_entity_aggregates( - NEW, sql_committee_id, temp_table_name || 'NEW'); - IF TG_OP = 'UPDATE' - AND NEW.contact_1_id <> OLD.contact_1_id - THEN - PERFORM calculate_entity_aggregates( - OLD, sql_committee_id, temp_table_name || 'OLD'); - END IF; - - ELSIF NEW.schedule_c_id IS NOT NULL OR NEW.schedule_c1_id IS NOT NULL - THEN - PERFORM calculate_loan_payment_to_date( - NEW, sql_committee_id, temp_table_name || 'NEW'); - - ELSIF NEW.schedule_e_id IS NOT NULL - THEN - PERFORM calculate_calendar_ytd_per_election_office( - NEW, sql_committee_id, temp_table_name || 'NEW'); - IF TG_OP = 'UPDATE' - THEN - PERFORM calculate_calendar_ytd_per_election_office( - OLD, sql_committee_id, temp_table_name || 'OLD'); - END IF; - END IF; - - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - CREATE TRIGGER calculate_aggregates_trigger - AFTER INSERT OR UPDATE ON transactions_transaction - FOR EACH ROW - WHEN (pg_trigger_depth() = 0) -- Prevent infinite trigger loop - EXECUTE FUNCTION calculate_aggregates(); - """ - ), - migrations.RunPython(populate_existing_rows), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0009_update_calculate_loan_payment_to_date.py b/django-backend/fecfiler/transactions/migrations/0009_update_calculate_loan_payment_to_date.py deleted file mode 100644 index b4660cbb67..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0009_update_calculate_loan_payment_to_date.py +++ /dev/null @@ -1,260 +0,0 @@ -from django.db import migrations - - -def update_existing_rows(apps, schema_editor): - transaction = apps.get_model("transactions", "Transaction") - types = [ - "LOAN_RECEIVED_FROM_INDIVIDUAL", - "LOAN_RECEIVED_FROM BANK", - "LOAN_BY_COMMITTEE", - ] - for row in transaction.objects.filter(transaction_type_identifier__in=types): - row.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0008_transaction__calendar_ytd_per_election_office_and_more"), - ] - - operations = [ - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION get_temp_tablename() - RETURNS TEXT AS $$ - BEGIN - RETURN 'temp_' || REPLACE(uuid_generate_v4()::TEXT, '-', '_'); - END; - $$ LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION calculate_entity_aggregates( - txn RECORD, - sql_committee_id TEXT - ) - RETURNS VOID AS $$ - DECLARE - schedule_date DATE; - temp_table_name TEXT; - BEGIN - temp_table_name := get_temp_tablename(); - IF txn.schedule_a_id IS NOT NULL THEN - SELECT contribution_date - INTO schedule_date - FROM transactions_schedulea - WHERE id = txn.schedule_a_id; - ELSIF txn.schedule_b_id IS NOT NULL THEN - SELECT expenditure_date - INTO schedule_date - FROM transactions_scheduleb - WHERE id = txn.schedule_b_id; - END IF; - - EXECUTE ' - CREATE TEMPORARY TABLE ' || temp_table_name || ' - ON COMMIT DROP AS - SELECT - id, - SUM(effective_amount) OVER (ORDER BY date, created) - AS new_sum - FROM transaction_view__' || sql_committee_id || ' - WHERE - contact_1_id = $1 - AND EXTRACT(YEAR FROM date) = $2 - AND aggregation_group = $3 - AND force_unaggregated IS NOT TRUE; - - UPDATE transactions_transaction AS t - SET aggregate = tt.new_sum - FROM ' || temp_table_name || ' AS tt - WHERE t.id = tt.id; - ' - USING - txn.contact_1_id, - EXTRACT(YEAR FROM schedule_date), - txn.aggregation_group; - END; - $$ LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION calculate_calendar_ytd_per_election_office( - txn RECORD, - sql_committee_id TEXT - ) - RETURNS VOID AS $$ - DECLARE - schedule_date DATE; - v_election_code TEXT; - v_candidate_office TEXT; - v_candidate_state TEXT; - v_candidate_district TEXT; - temp_table_name TEXT; - BEGIN - temp_table_name := get_temp_tablename(); - SELECT COALESCE(disbursement_date, dissemination_date) - INTO schedule_date FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - SELECT election_code - INTO v_election_code - FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - SELECT candidate_office, candidate_state, candidate_district - INTO v_candidate_office, v_candidate_state, v_candidate_district - FROM contacts WHERE id = txn.contact_2_id; - - EXECUTE ' - CREATE TEMPORARY TABLE ' || temp_table_name || ' - ON COMMIT DROP AS - SELECT - t.id, - SUM(t.effective_amount) OVER - (ORDER BY t.date, t.created) AS new_sum - FROM transactions_schedulee e - JOIN transaction_view__' || sql_committee_id || ' t - ON e.id = t.schedule_e_id - JOIN contacts c - ON t.contact_2_id = c.id - WHERE - e.election_code = $1 - AND c.candidate_office = $2 - AND ( - c.candidate_state = $3 - OR ( - c.candidate_state IS NULL - AND $3 = '''' - ) - ) - AND ( - c.candidate_district = $4 - OR ( - c.candidate_district IS NULL - AND $4 = '''' - ) - ) - AND EXTRACT(YEAR FROM t.date) = $5 - AND aggregation_group = $6 - AND force_unaggregated IS NOT TRUE; - - UPDATE transactions_transaction AS t - SET _calendar_ytd_per_election_office = tt.new_sum - FROM ' || temp_table_name || ' AS tt - WHERE t.id = tt.id; - ' - USING - v_election_code, - v_candidate_office, - COALESCE(v_candidate_state, ''), - COALESCE(v_candidate_district, ''), - EXTRACT(YEAR FROM schedule_date), - txn.aggregation_group; - END; - $$ LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION calculate_loan_payment_to_date( - txn RECORD, - sql_committee_id TEXT - ) - RETURNS VOID AS $$ - DECLARE - temp_table_name TEXT; - BEGIN - temp_table_name := get_temp_tablename(); - EXECUTE ' - CREATE TEMPORARY TABLE ' || temp_table_name || ' - ON COMMIT DROP AS - SELECT - id, - loan_key, - SUM(effective_amount) OVER (ORDER BY loan_key) AS new_sum - FROM transaction_view__' || sql_committee_id || ' - WHERE loan_key LIKE ( - SELECT - CASE - WHEN loan_id IS NULL THEN transaction_id - ELSE ( - SELECT transaction_id - FROM transactions_transaction - WHERE id = t.loan_id - ) - END - FROM transactions_transaction t - WHERE id = $1 - ) || ''%%''; -- Match the loan_key with a transaction_id prefix - - UPDATE transactions_transaction AS t - SET loan_payment_to_date = tt.new_sum - FROM ' || temp_table_name || ' AS tt - WHERE t.id = tt.id - AND tt.loan_key LIKE ''%%LOAN''; - ' - USING txn.id; - END; - $$ LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION calculate_aggregates() - RETURNS TRIGGER AS $$ - DECLARE - sql_committee_id TEXT; - BEGIN - sql_committee_id := REPLACE(NEW.committee_account_id::TEXT, '-', '_'); - - -- If schedule_c2_id or schedule_d_id is not null, stop processing - IF NEW.schedule_c2_id IS NOT NULL OR NEW.schedule_d_id IS NOT NULL - THEN - RETURN NEW; - END IF; - - IF NEW.schedule_a_id IS NOT NULL OR NEW.schedule_b_id IS NOT NULL - THEN - PERFORM calculate_entity_aggregates(NEW, sql_committee_id); - IF TG_OP = 'UPDATE' - AND NEW.contact_1_id <> OLD.contact_1_id - THEN - PERFORM calculate_entity_aggregates(OLD, sql_committee_id); - END IF; - END IF; - - IF NEW.schedule_c_id IS NOT NULL - OR NEW.schedule_c1_id IS NOT NULL - OR NEW.transaction_type_identifier = 'LOAN_REPAYMENT_MADE' - THEN - PERFORM calculate_loan_payment_to_date(NEW, sql_committee_id); - END IF; - - IF NEW.schedule_e_id IS NOT NULL - THEN - PERFORM calculate_calendar_ytd_per_election_office( - NEW, sql_committee_id); - IF TG_OP = 'UPDATE' - THEN - PERFORM calculate_calendar_ytd_per_election_office( - OLD, sql_committee_id); - END IF; - END IF; - - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ - -- Drop prior versions of these functions - DROP FUNCTION calculate_entity_aggregates(RECORD, TEXT, TEXT); - DROP FUNCTION calculate_calendar_ytd_per_election_office(RECORD, TEXT, TEXT); - DROP FUNCTION calculate_loan_payment_to_date(RECORD, TEXT, TEXT); - """ - ), - migrations.RunPython(update_existing_rows), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0010_update_aggregate_trigger_performance.py b/django-backend/fecfiler/transactions/migrations/0010_update_aggregate_trigger_performance.py deleted file mode 100644 index dba8033a4a..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0010_update_aggregate_trigger_performance.py +++ /dev/null @@ -1,205 +0,0 @@ -# Generated by Django 4.2.11 on 2024-07-17 19:59 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0009_update_calculate_loan_payment_to_date"), - ] - - operations = [ - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION calculate_entity_aggregates( - txn RECORD, sql_committee_id text - ) - RETURNS VOID AS $$ - DECLARE - schedule_date date; - BEGIN - IF txn.schedule_a_id IS NOT NULL THEN - SELECT - contribution_date INTO schedule_date - FROM - transactions_schedulea - WHERE - id = txn.schedule_a_id; - ELSIF txn.schedule_b_id IS NOT NULL THEN - SELECT - expenditure_date INTO schedule_date - FROM - transactions_scheduleb - WHERE - id = txn.schedule_b_id; - END IF; - - EXECUTE ' - UPDATE transactions_transaction AS t - SET aggregate = tc.new_sum - FROM ( - SELECT - id, - aggregate, - date, - SUM(effective_amount) OVER (ORDER BY date, created) - AS new_sum - FROM transaction_view__' || sql_committee_id || ' - WHERE - contact_1_id = $1 - AND EXTRACT(YEAR FROM date) = $2 - AND aggregation_group = $3 - AND force_unaggregated IS NOT TRUE - ) AS tc - WHERE t.id = tc.id - AND ( - tc.date > $4 - OR ( - tc.date = $4 - AND t.created >= $5 - ) - ); - ' - USING - txn.contact_1_id, - EXTRACT(YEAR FROM schedule_date), - txn.aggregation_group, - schedule_date, - txn.created; - END; - $$ - LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION calculate_calendar_ytd_per_election_office( - txn RECORD, sql_committee_id text - ) - RETURNS VOID - AS $$ - DECLARE - schedule_date date; - v_election_code text; - v_candidate_office text; - v_candidate_state text; - v_candidate_district text; - BEGIN - SELECT - COALESCE(disbursement_date, dissemination_date) INTO schedule_date - FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - - SELECT election_code INTO v_election_code - FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - - SELECT - candidate_office, - candidate_state, - candidate_district INTO v_candidate_office, - v_candidate_state, - v_candidate_district - FROM contacts - WHERE id = txn.contact_2_id; - EXECUTE ' - UPDATE transactions_transaction AS t - SET _calendar_ytd_per_election_office = tc.new_sum - FROM ( - SELECT - t.id, - t.date, - SUM(t.effective_amount) OVER - (ORDER BY t.date, t.created) AS new_sum - FROM transactions_schedulee e - JOIN transaction_view__' || sql_committee_id || ' t - ON e.id = t.schedule_e_id - JOIN contacts c - ON t.contact_2_id = c.id - WHERE - e.election_code = $1 - AND c.candidate_office = $2 - AND ( - c.candidate_state = $3 - OR ( - c.candidate_state IS NULL - AND $3 = '''' - ) - ) - AND ( - c.candidate_district = $4 - OR ( - c.candidate_district IS NULL - AND $4 = '''' - ) - ) - AND EXTRACT(YEAR FROM t.date) = $5 - AND aggregation_group = $6 - AND force_unaggregated IS NOT TRUE - ) AS tc - WHERE t.id = tc.id - AND ( - tc.date > $7 - OR ( - tc.date = $7 - AND t.created >= $8 - ) - ); - ' - USING - v_election_code, - v_candidate_office, - COALESCE(v_candidate_state, ''), - COALESCE(v_candidate_district, ''), - EXTRACT(YEAR FROM schedule_date), - txn.aggregation_group, - schedule_date, - txn.created; - END; - $$ - LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ - CREATE OR REPLACE FUNCTION calculate_loan_payment_to_date( - txn RECORD, sql_committee_id text - ) - RETURNS VOID - AS $$ - BEGIN - EXECUTE ' - UPDATE transactions_transaction AS t - SET loan_payment_to_date = tc.new_sum - FROM ( - SELECT - id, - loan_key, - SUM(effective_amount) OVER (ORDER BY loan_key) AS new_sum - FROM transaction_view__' || sql_committee_id || ' - WHERE loan_key LIKE ( - SELECT - CASE - WHEN loan_id IS NULL THEN transaction_id - ELSE ( - SELECT transaction_id - FROM transactions_transaction - WHERE id = t.loan_id - ) - END - FROM transactions_transaction t - WHERE id = $1 - ) || ''%%'' -- Match the loan_key with a transaction_id prefix - ) AS tc - WHERE t.id = tc.id - AND tc.loan_key LIKE ''%%LOAN'' - ; - ' - USING txn.id; - END; - $$ - LANGUAGE plpgsql; - """ - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0011_transaction_can_delete.py b/django-backend/fecfiler/transactions/migrations/0011_transaction_can_delete.py deleted file mode 100644 index 502dcee6c2..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0011_transaction_can_delete.py +++ /dev/null @@ -1,107 +0,0 @@ -# Generated by Django 5.0.8 on 2024-08-28 15:19 - -from django.db import connection, migrations, models -from django.contrib.postgres.fields import ArrayField - - -def create_trigger_function(apps, schema_editor): - with connection.cursor() as cursor: - cursor.execute( - """ - CREATE OR REPLACE FUNCTION update_transactions_can_delete() RETURNS TRIGGER AS $$ - BEGIN - UPDATE transactions_transaction - SET blocking_reports = CASE - WHEN NEW.upload_submission_id IS NOT NULL - THEN array_append(blocking_reports, NEW.id) - ELSE array_remove(blocking_reports, NEW.id) - END - -- all transactions in the submitted report - WHERE id IN ( - SELECT transaction_id - FROM reports_reporttransaction - WHERE report_id = NEW.id - ) - -- all transactions that are reattributed in the submtited report - OR id IN ( - SELECT reatt_redes_id - FROM reports_reporttransaction - JOIN transactions_transaction tt - ON reports_reporttransaction.transaction_id = tt.id - WHERE report_id = NEW.id - ) - -- all loans that are carried forward in the submitted report - OR id IN ( - SELECT loan_id - FROM reports_reporttransaction - JOIN transactions_transaction tt - ON reports_reporttransaction.transaction_id = tt.id - WHERE report_id = NEW.id - ) - -- all repayments to loans that are carried forward in the submitted report - OR loan_id IN ( - SELECT loan_id - FROM reports_reporttransaction - JOIN transactions_transaction tt - ON reports_reporttransaction.transaction_id = tt.id - WHERE report_id = NEW.id AND tt.schedule_c_id IS NOT NULL - ) - -- all debts that are carried forward in the submitted report - OR id IN ( - SELECT debt_id - FROM reports_reporttransaction - JOIN transactions_transaction tt - ON reports_reporttransaction.transaction_id = tt.id - WHERE report_id = NEW.id - ) - -- all repayments to debts that are carried forward in the submitted report - OR debt_id IN ( - SELECT debt_id - FROM reports_reporttransaction - JOIN transactions_transaction tt - ON reports_reporttransaction.transaction_id = tt.id - WHERE report_id = NEW.id AND tt.schedule_d_id IS NOT NULL - ); - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - """ - ) - - -def drop_trigger_function(apps, schema_editor): - with connection.cursor() as cursor: - cursor.execute("DROP FUNCTION IF EXISTS update_transactions_can_delete();") - - -def create_trigger(apps, schema_editor): - with connection.cursor() as cursor: - cursor.execute( - """ - CREATE TRIGGER report_status_update - AFTER UPDATE OF upload_submission_id ON reports_report - FOR EACH ROW - EXECUTE FUNCTION update_transactions_can_delete(); - """ - ) - - -def drop_trigger(apps, schema_editor): - with connection.cursor() as cursor: - cursor.execute("DROP TRIGGER IF EXISTS report_status_update ON reports_report;") - - -class Migration(migrations.Migration): - dependencies = [ - ("transactions", "0010_update_aggregate_trigger_performance"), - ] - - operations = [ - migrations.AddField( - model_name="transaction", - name="blocking_reports", - field=ArrayField(models.UUIDField(), blank=False, default=list()), - ), - migrations.RunPython(create_trigger_function, reverse_code=drop_trigger_function), - migrations.RunPython(create_trigger, reverse_code=drop_trigger), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0012_alter_transactions_blocking_reports.py b/django-backend/fecfiler/transactions/migrations/0012_alter_transactions_blocking_reports.py deleted file mode 100644 index 2fc955acd5..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0012_alter_transactions_blocking_reports.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.db import migrations, models -from django.contrib.postgres.fields import ArrayField - - -def update_blocking_reports_default(apps, schema_editor): - transaction = apps.get_model("transactions", "Transaction") - transaction._meta.get_field("blocking_reports").default = list - - -class Migration(migrations.Migration): - dependencies = [ - ("transactions", "0011_transaction_can_delete"), - ] - - operations = [ - migrations.AlterField( - model_name="transaction", - name="blocking_reports", - field=ArrayField( - base_field=models.UUIDField(), - blank=False, - default=list, - size=None, - ), - ), - migrations.RunPython(update_blocking_reports_default), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0013_transaction_itemized_and_associated_triggers.py b/django-backend/fecfiler/transactions/migrations/0013_transaction_itemized_and_associated_triggers.py deleted file mode 100644 index 98824f7d99..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0013_transaction_itemized_and_associated_triggers.py +++ /dev/null @@ -1,281 +0,0 @@ -from django.db import connection, migrations, models -from fecfiler.transactions.schedule_a.managers import ( - over_two_hundred_types as schedule_a_over_two_hundred_types, -) -from fecfiler.transactions.schedule_b.managers import ( - over_two_hundred_types as schedule_b_over_two_hundred_types, -) -import uuid - - -def populate_over_two_hundred_types(apps, schema_editor): - OverTwoHundredTypesScheduleA = apps.get_model( # noqa: N806 - "transactions", "OverTwoHundredTypesScheduleA" - ) - OverTwoHundredTypesScheduleB = apps.get_model( # noqa: N806 - "transactions", "OverTwoHundredTypesScheduleB" - ) - scha_types_to_create = [ - OverTwoHundredTypesScheduleA(type=type_to_create) - for type_to_create in schedule_a_over_two_hundred_types - ] - OverTwoHundredTypesScheduleA.objects.bulk_create(scha_types_to_create) - schb_types_to_create = [ - OverTwoHundredTypesScheduleB(type=type_to_create) - for type_to_create in schedule_b_over_two_hundred_types - ] - OverTwoHundredTypesScheduleB.objects.bulk_create(schb_types_to_create) - - -def drop_over_two_hundred_types(apps, schema_editor): - print("this reverses migration automatically.") - - -def create_itemized_triggers_and_functions(apps, schema_editor): - with connection.cursor() as cursor: - cursor.execute( - """ - CREATE OR REPLACE FUNCTION before_transactions_transaction_insert_or_update() - RETURNS TRIGGER AS $$ - DECLARE - needs_itemized_set boolean; - itemization boolean; - BEGIN - needs_itemized_set := needs_itemized_set(OLD, NEW); - IF needs_itemized_set THEN - NEW.itemized := calculate_itemization(NEW); - END IF; - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION calculate_itemization( - txn RECORD - ) - RETURNS BOOLEAN AS $$ - DECLARE - itemized boolean; - BEGIN - itemized := TRUE; - IF txn.force_itemized IS NOT NULL THEN - itemized := txn.force_itemized; - ELSIF txn.aggregate < 0 THEN - itemized := TRUE; - ELSIF EXISTS ( - SELECT type from ( - SELECT type - FROM over_two_hundred_types_schedulea - UNION - SELECT type - FROM over_two_hundred_types_scheduleb - ) as scha_schb_types - WHERE type = txn.transaction_type_identifier - ) THEN - IF txn.aggregate > 200 THEN - itemized := TRUE; - ELSE - itemized := FALSE; - END IF; - END IF; - return itemized; - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION after_transactions_transaction_insert_or_update() - RETURNS TRIGGER AS $$ - DECLARE - parent_and_grandparent_ids uuid[]; - children_and_grandchildren_ids uuid[]; - BEGIN - IF OLD IS NULL OR OLD.itemized <> NEW.itemized THEN - IF NEW.itemized is TRUE THEN - parent_and_grandparent_ids := - get_parent_grandparent_transaction_ids(NEW); - PERFORM set_itemization_for_ids(TRUE, parent_and_grandparent_ids); - ELSE - children_and_grandchildren_ids := - get_children_and_grandchildren_transaction_ids(NEW); - PERFORM set_itemization_for_ids( - FALSE,children_and_grandchildren_ids - ); - END IF; - END IF; - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION needs_itemized_set( - OLD RECORD, - NEW RECORD - ) - RETURNS BOOLEAN AS $$ - BEGIN - return OLD IS NULL OR ( - OLD.force_itemized IS DISTINCT FROM NEW.force_itemized - OR OLD.aggregate IS DISTINCT FROM NEW.aggregate - ); - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION set_itemization_for_ids( - itemization boolean, - ids uuid[] - ) - RETURNS VOID AS $$ - BEGIN - IF cardinality(ids) > 0 THEN - UPDATE transactions_transaction - SET - itemized = itemization - WHERE id = ANY (ids); - END IF; - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION get_parent_grandparent_transaction_ids( - txn RECORD - ) - RETURNS uuid[] AS $$ - DECLARE - ids uuid[]; - BEGIN - SELECT array( - SELECT id - FROM transactions_transaction - WHERE id IN ( - txn.parent_transaction_id, - ( - SELECT parent_transaction_id - FROM transactions_transaction - WHERE id = txn.parent_transaction_id - ) - ) - ) into ids; - RETURN ids; - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION get_children_and_grandchildren_transaction_ids( - txn RECORD - ) - RETURNS uuid[] AS $$ - DECLARE - ids uuid[]; - BEGIN - SELECT array( - SELECT id - FROM transactions_transaction - WHERE parent_transaction_id = ANY ( - array_prepend(txn.id, - array( - SELECT id - FROM transactions_transaction - WHERE parent_transaction_id = txn.id - ) - ) - ) - ) into ids; - RETURN ids; - END; - $$ LANGUAGE plpgsql; - - CREATE TRIGGER before_transactions_transaction_insert_or_update_trigger - BEFORE INSERT OR UPDATE ON transactions_transaction - FOR EACH ROW - EXECUTE FUNCTION before_transactions_transaction_insert_or_update(); - - CREATE TRIGGER zafter_transactions_transaction_insert_or_update_trigger - AFTER INSERT OR UPDATE ON transactions_transaction - FOR EACH ROW - EXECUTE FUNCTION after_transactions_transaction_insert_or_update(); - """ - ) - - -def drop_itemized_triggers_and_functions(apps, schema_editor): - schema_editor.execute( - """ - DROP TRIGGER - IF EXISTS zafter_transactions_transaction_insert_or_update_trigger - ON transactions_transaction; - - DROP TRIGGER - IF EXISTS before_transactions_transaction_insert_or_update_trigger - ON transactions_transaction; - - DROP FUNCTION IF EXISTS before_transactions_transaction_insert_or_update; - DROP FUNCTION IF EXISTS calculate_itemization; - DROP FUNCTION IF EXISTS after_transactions_transaction_insert_or_update; - DROP FUNCTION IF EXISTS needs_itemized_set; - DROP FUNCTION IF EXISTS set_itemization_for_ids; - DROP FUNCTION IF EXISTS get_parent_grandparent_transaction_ids; - DROP FUNCTION IF EXISTS get_children_and_grandchildren_transaction_ids; - """ - ) - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0012_alter_transactions_blocking_reports"), - ] - - operations = [ - migrations.AddField( - model_name="transaction", - name="itemized", - field=models.BooleanField(db_default=True), - ), - migrations.CreateModel( - name="OverTwoHundredTypesScheduleA", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True, - ), - ), - ("type", models.TextField()), - ], - options={ - "db_table": "over_two_hundred_types_schedulea", - "indexes": [ - models.Index(fields=["type"], name="over_two_hu_type_2c8314_idx") - ], - }, - ), - migrations.CreateModel( - name="OverTwoHundredTypesScheduleB", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True, - ), - ), - ("type", models.TextField()), - ], - options={ - "db_table": "over_two_hundred_types_scheduleb", - "indexes": [ - models.Index(fields=["type"], name="over_two_hu_type_411a44_idx") - ], - }, - ), - migrations.RunPython( - populate_over_two_hundred_types, - reverse_code=drop_over_two_hundred_types, - ), - migrations.RunPython( - create_itemized_triggers_and_functions, - reverse_code=drop_itemized_triggers_and_functions, - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0014_drop_transaction_view.py b/django-backend/fecfiler/transactions/migrations/0014_drop_transaction_view.py deleted file mode 100644 index 9b1cfd2b17..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0014_drop_transaction_view.py +++ /dev/null @@ -1,596 +0,0 @@ -# Generated by Django 4.2.11 on 2024-07-17 19:59 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0013_transaction_itemized_and_associated_triggers"), - ] - - operations = [ - migrations.RunSQL( - """ -CREATE OR REPLACE FUNCTION calculate_entity_aggregates( - txn RECORD, sql_committee_id text -) -RETURNS VOID AS $$ -DECLARE - schedule_date date; -BEGIN - IF txn.schedule_a_id IS NOT NULL THEN - SELECT - contribution_date INTO schedule_date - FROM - transactions_schedulea - WHERE - id = txn.schedule_a_id; - ELSIF txn.schedule_b_id IS NOT NULL THEN - SELECT - expenditure_date INTO schedule_date - FROM - transactions_scheduleb - WHERE - id = txn.schedule_b_id; - END IF; - - EXECUTE ' - UPDATE transactions_transaction AS t - SET aggregate = tc.new_sum - FROM ( - SELECT - t.id, - COALESCE( - sa.contribution_date, - sb.expenditure_date, - sc.loan_incurred_date, - se.disbursement_date, - se.dissemination_date - ) AS date, - SUM( - calculate_effective_amount( - t.transaction_type_identifier, - calculate_amount( - sa.contribution_amount, - sb.expenditure_amount, - sc.loan_amount, - sc2.guaranteed_amount, - se.expenditure_amount, - t.debt_id, - t.schedule_d_id - ), - t.schedule_c_id) - ) OVER ( - ORDER BY - COALESCE( - sa.contribution_date, - sb.expenditure_date, - sc.loan_incurred_date, - se.disbursement_date, - se.dissemination_date - ), - t.created - ) AS new_sum - FROM transactions_transaction t - LEFT JOIN transactions_schedulea AS sa ON t.schedule_a_id = sa.id - LEFT JOIN transactions_scheduleb AS sb ON t.schedule_b_id = sb.id - LEFT JOIN transactions_schedulec AS sc ON t.schedule_c_id = sc.id - LEFT JOIN transactions_schedulec2 AS sc2 ON t.schedule_c2_id = sc2.id - LEFT JOIN transactions_schedulee AS se ON t.schedule_e_id = se.id - LEFT JOIN transactions_scheduled AS sd ON t.schedule_d_id = sd.id - WHERE - contact_1_id = $1 - AND EXTRACT(YEAR FROM COALESCE( - sa.contribution_date, - sb.expenditure_date, - sc.loan_incurred_date, - se.disbursement_date, - se.dissemination_date - )) = $2 - AND aggregation_group = $3 - AND force_unaggregated IS NOT TRUE - AND deleted IS NULL - ) AS tc - WHERE t.id = tc.id - AND ( - tc.date > $4 - OR ( - tc.date = $4 - AND t.created >= $5 - ) - ); - ' - USING - txn.contact_1_id, - EXTRACT(YEAR FROM schedule_date), - txn.aggregation_group, - schedule_date, - txn.created; -END; -$$ -LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ -CREATE OR REPLACE FUNCTION calculate_calendar_ytd_per_election_office( - txn RECORD, sql_committee_id text -) -RETURNS VOID -AS $$ -DECLARE - schedule_date date; - v_election_code text; - v_candidate_office text; - v_candidate_state text; - v_candidate_district text; -BEGIN - SELECT - COALESCE(disbursement_date, dissemination_date) INTO schedule_date - FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - - SELECT election_code INTO v_election_code - FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - - SELECT - candidate_office, - candidate_state, - candidate_district INTO v_candidate_office, - v_candidate_state, - v_candidate_district - FROM contacts - WHERE id = txn.contact_2_id; - EXECUTE ' - UPDATE transactions_transaction AS t - SET _calendar_ytd_per_election_office = tc.new_sum - FROM ( - SELECT - t.id, - Coalesce( - e.disbursement_date, - e.dissemination_date - ) as date, - SUM( - calculate_effective_amount( - t.transaction_type_identifier, - calculate_amount( - NULL, - NULL, - NULL, - NULL, - e.expenditure_amount, - t.debt_id, - t.schedule_d_id - ), - t.schedule_c_id - ) - ) OVER (ORDER BY Coalesce( - e.disbursement_date, - e.dissemination_date - ), t.created) AS new_sum - FROM transactions_schedulee e - JOIN transactions_transaction t - ON e.id = t.schedule_e_id - JOIN contacts c - ON t.contact_2_id = c.id - WHERE - e.election_code = $1 - AND c.candidate_office = $2 - AND ( - c.candidate_state = $3 - OR ( - c.candidate_state IS NULL - AND $3 = '''' - ) - ) - AND ( - c.candidate_district = $4 - OR ( - c.candidate_district IS NULL - AND $4 = '''' - ) - ) - AND EXTRACT(YEAR FROM Coalesce( - e.disbursement_date, - e.dissemination_date - )) = $5 - AND aggregation_group = $6 - AND force_unaggregated IS NOT TRUE - ) AS tc - WHERE t.id = tc.id - AND ( - tc.date > $7 - OR ( - tc.date = $7 - AND t.created >= $8 - ) - ); - ' - USING - v_election_code, - v_candidate_office, - COALESCE(v_candidate_state, ''), - COALESCE(v_candidate_district, ''), - EXTRACT(YEAR FROM schedule_date), - txn.aggregation_group, - schedule_date, - txn.created; -END; -$$ -LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ -CREATE OR REPLACE FUNCTION calculate_effective_amount( - transaction_type_identifier TEXT, - amount NUMERIC, - schedule_c_id UUID -) -RETURNS NUMERIC -AS $$ -DECLARE - effective_amount NUMERIC; -BEGIN - -- Case 1: transaction type is a refund - IF transaction_type_identifier IN ( - 'TRIBAL_REFUND_NP_HEADQUARTERS_ACCOUNT', - 'TRIBAL_REFUND_NP_CONVENTION_ACCOUNT', - 'TRIBAL_REFUND_NP_RECOUNT_ACCOUNT', - 'INDIVIDUAL_REFUND_NP_HEADQUARTERS_ACCOUNT', - 'INDIVIDUAL_REFUND_NP_CONVENTION_ACCOUNT', - 'INDIVIDUAL_REFUND_NP_RECOUNT_ACCOUNT', - 'REFUND_PARTY_CONTRIBUTION', - 'REFUND_PARTY_CONTRIBUTION_VOID', - 'REFUND_PAC_CONTRIBUTION', - 'REFUND_PAC_CONTRIBUTION_VOID', - 'INDIVIDUAL_REFUND_NON_CONTRIBUTION_ACCOUNT', - 'BUSINESS_LABOR_REFUND_NON_CONTRIBUTION_ACCOUNT', - 'OTHER_COMMITTEE_REFUND_NON_CONTRIBUTION_ACCOUNT', - 'REFUND_UNREGISTERED_CONTRIBUTION', - 'REFUND_UNREGISTERED_CONTRIBUTION_VOID', - 'REFUND_INDIVIDUAL_CONTRIBUTION', - 'REFUND_INDIVIDUAL_CONTRIBUTION_VOID', - 'OTHER_COMMITTEE_REFUND_REFUND_NP_HEADQUARTERS_ACCOUNT', - 'OTHER_COMMITTEE_REFUND_REFUND_NP_CONVENTION_ACCOUNT', - 'OTHER_COMMITTEE_REFUND_REFUND_NP_RECOUNT_ACCOUNT' - ) THEN - effective_amount := amount * -1; - - -- Case 2: schedule_c exists (return NULL) - ELSIF schedule_c_id IS NOT NULL THEN - effective_amount := NULL; - - -- Default case: return the original amount - ELSE - effective_amount := amount; - END IF; - - RETURN effective_amount; -END; -$$ -LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ -CREATE OR REPLACE FUNCTION calculate_is_loan( - loan UUID, - transaction_type_identifier TEXT, - schedule_c_id UUID -) -RETURNS TEXT -AS $$ -DECLARE - loan_key TEXT; -BEGIN - IF loan IS NOT NULL AND transaction_type_identifier - IN ('LOAN_REPAYMENT_RECEIVED', 'LOAN_REPAYMENT_MADE') - THEN - loan_key := 'F'; - - ELSIF schedule_c_id IS NOT NULL THEN - loan_key := 'T'; - - ELSE - loan_key := 'F'; - END IF; - - RETURN loan_key; -END; -$$ -LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ -CREATE OR REPLACE FUNCTION calculate_original_loan_id( - transaction_id text, - loan UUID, - transaction_type_identifier TEXT, - schedule_c_id UUID, - schedule_b_id UUID -) -RETURNS TEXT -AS $$ -DECLARE - loan_transaction_id text; -BEGIN - IF loan IS NOT NULL AND transaction_type_identifier - IN ('LOAN_REPAYMENT_RECEIVED', 'LOAN_REPAYMENT_MADE') - THEN - SELECT t.transaction_id - INTO loan_transaction_id - FROM transactions_transaction t - LEFT JOIN transactions_scheduleb sb ON t.schedule_b_id = sb.id - WHERE t.id = loan; - - ELSIF schedule_c_id IS NOT NULL THEN - loan_transaction_id := transaction_id; - - ELSE - loan_transaction_id := NULL; - END IF; - - RETURN loan_transaction_id; -END; -$$ -LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ -CREATE OR REPLACE FUNCTION calculate_loan_date( - trans_id TEXT, - loan UUID, - transaction_type_identifier TEXT, - schedule_c_id UUID, - schedule_b_id UUID -) -RETURNS DATE -AS $$ -DECLARE - date DATE; -BEGIN - IF loan IS NOT NULL AND transaction_type_identifier - IN ('LOAN_REPAYMENT_RECEIVED', 'LOAN_REPAYMENT_MADE') - THEN - SELECT sb.expenditure_date - INTO date - FROM transactions_transaction t - LEFT JOIN transactions_scheduleb sb ON t.schedule_b_id = sb.id - WHERE t.transaction_id = trans_id; - - ELSIF schedule_c_id IS NOT NULL THEN - SELECT s.report_coverage_through_date INTO date - FROM transactions_schedulec s - WHERE s.id = schedule_c_id; - - ELSE - date := NULL; - END IF; - - RETURN date; -END; -$$ -LANGUAGE plpgsql; - """ - ), - migrations.RunSQL( - """ -CREATE OR REPLACE FUNCTION calculate_amount( - schedule_a_contribution_amount NUMERIC, - schedule_b_expenditure_amount NUMERIC, - schedule_c_loan_amount NUMERIC, - schedule_c2_guaranteed_amount NUMERIC, - schedule_e_expenditure_amount NUMERIC, - debt UUID, -- Reference to another transaction - schedule_d_id UUID -) -RETURNS NUMERIC -AS $$ -DECLARE - debt_incurred_amount NUMERIC; -BEGIN - IF debt IS NOT NULL THEN - SELECT sd.incurred_amount - INTO debt_incurred_amount - FROM transactions_transaction t - LEFT JOIN transactions_scheduled sd ON t.schedule_d_id = sd.id - WHERE t.id = debt; - ELSE - debt_incurred_amount := NULL; - END IF; - - RETURN COALESCE( - schedule_a_contribution_amount, - schedule_b_expenditure_amount, - schedule_c_loan_amount, - schedule_c2_guaranteed_amount, - schedule_e_expenditure_amount, - debt_incurred_amount, - (SELECT incurred_amount FROM transactions_scheduled WHERE id = schedule_d_id) - ); -END; -$$ -LANGUAGE plpgsql; -""" - ), - migrations.RunSQL( - """ -CREATE OR REPLACE FUNCTION calculate_loan_payment_to_date( - txn RECORD, sql_committee_id TEXT -) -RETURNS VOID -AS $$ -DECLARE - pulled_forward_loans RECORD; -BEGIN - EXECUTE ' - UPDATE transactions_transaction AS t - SET loan_payment_to_date = tc.new_sum - FROM ( - SELECT - data.id, - data.original_loan_id, - data.is_loan, - SUM(data.effective_amount) OVER ( - PARTITION BY data.original_loan_id - ORDER BY data.date - ) AS new_sum - FROM ( - SELECT - t.id, - calculate_loan_date( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) AS date, - calculate_original_loan_id( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) AS original_loan_id, - calculate_is_loan( - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id - ) AS is_loan, - calculate_effective_amount( - t.transaction_type_identifier, - calculate_amount( - sa.contribution_amount, - sb.expenditure_amount, - sc.loan_amount, - sc2.guaranteed_amount, - se.expenditure_amount, - t.debt_id, - t.schedule_d_id - ), - t.schedule_c_id - ) AS effective_amount - FROM transactions_transaction t - LEFT JOIN transactions_schedulea sa ON t.schedule_a_id = sa.id - LEFT JOIN transactions_scheduleb sb ON t.schedule_b_id = sb.id - LEFT JOIN transactions_schedulec sc ON t.schedule_c_id = sc.id - LEFT JOIN transactions_schedulec2 sc2 ON t.schedule_c2_id = sc2.id - LEFT JOIN transactions_schedulee se ON t.schedule_e_id = se.id - WHERE t.deleted IS NULL - ) AS data - WHERE data.original_loan_id = ( - SELECT calculate_original_loan_id( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) - FROM transactions_transaction t - WHERE t.id = COALESCE( - (SELECT loan_id FROM transactions_transaction WHERE id = $1), - $1 - ) - ) - AND data.date <= ( - SELECT calculate_loan_date( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) - FROM transactions_transaction t - WHERE t.id = COALESCE( - (SELECT loan_id FROM transactions_transaction WHERE id = $1), - $1 - ) - ) - ) AS tc - WHERE t.id = tc.id - AND tc.is_loan = ''T''; - ' - USING txn.id; - - -- Handle pulled-forward loans - FOR pulled_forward_loans IN - SELECT t.transaction_id - FROM transactions_transaction t - WHERE t.schedule_c_id IS NOT NULL - AND t.loan_id = txn.loan_id - LOOP - -- Recalculate loan_payment_to_date for each pulled-forward loan - EXECUTE ' - UPDATE transactions_transaction AS t - SET loan_payment_to_date = tc.new_sum - FROM ( - SELECT - data.id, - data.original_loan_id, - data.is_loan, - SUM(data.effective_amount) OVER ( - PARTITION BY data.original_loan_id - ORDER BY data.date - ) AS new_sum - FROM ( - SELECT - t.id, - calculate_loan_date( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) AS date, - calculate_original_loan_id( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) AS original_loan_id, - calculate_is_loan( - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id - ) AS is_loan, - calculate_effective_amount( - t.transaction_type_identifier, - calculate_amount( - sa.contribution_amount, - sb.expenditure_amount, - sc.loan_amount, - sc2.guaranteed_amount, - se.expenditure_amount, - t.debt_id, - t.schedule_d_id - ), - t.schedule_c_id - ) AS effective_amount - FROM transactions_transaction t - LEFT JOIN transactions_schedulea sa ON t.schedule_a_id = sa.id - LEFT JOIN transactions_scheduleb sb ON t.schedule_b_id = sb.id - LEFT JOIN transactions_schedulec sc ON t.schedule_c_id = sc.id - LEFT JOIN transactions_schedulec2 sc2 ON t.schedule_c2_id = sc2.id - LEFT JOIN transactions_schedulee se ON t.schedule_e_id = se.id - WHERE t.deleted IS NULL - ) AS data - WHERE data.original_loan_id = $1 - ) AS tc - WHERE t.id = tc.id - AND tc.is_loan = ''T''; - ' - USING pulled_forward_loans.transaction_id; - END LOOP; -END; -$$ -LANGUAGE plpgsql; -""" - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0015_merge_transaction_triggers.py b/django-backend/fecfiler/transactions/migrations/0015_merge_transaction_triggers.py deleted file mode 100644 index 8b87842cbf..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0015_merge_transaction_triggers.py +++ /dev/null @@ -1,469 +0,0 @@ -from django.db import connection, migrations - - -def create_triggers(apps, schema_editor): - with connection.cursor() as cursor: - cursor.execute( - """ - CREATE TRIGGER before_transactions_transaction_trigger - BEFORE INSERT OR UPDATE ON transactions_transaction - FOR EACH ROW - EXECUTE FUNCTION before_transactions_transaction(); - - CREATE TRIGGER after_transactions_transaction_infinite_trigger - AFTER INSERT OR UPDATE ON transactions_transaction - FOR EACH ROW - EXECUTE FUNCTION after_transactions_transaction_infinite(); - - CREATE TRIGGER after_transactions_transaction_trigger - AFTER INSERT OR UPDATE ON transactions_transaction - FOR EACH ROW - WHEN (pg_trigger_depth() = 0) - EXECUTE FUNCTION after_transactions_transaction(); - """ - ) - - -def reverse_create_triggers(apps, schema): - with connection.cursor() as cursor: - cursor.execute( - """ - DROP TRIGGER - IF EXISTS before_transactions_transaction_trigger - ON transactions_transaction; - - DROP TRIGGER - IF EXISTS after_transactions_transaction_infinite_trigger - ON transactions_transaction; - - DROP TRIGGER - IF EXISTS after_transactions_transaction_trigger - ON transactions_transaction; - """ - ) - - -def before_transactions_transaction(apps, schema_editor): - with connection.cursor() as cursor: - cursor.execute( - """ - CREATE OR REPLACE FUNCTION before_transactions_transaction() - RETURNS TRIGGER AS $$ - BEGIN - NEW := process_itemization(OLD, NEW); - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - """ - ) - - -def after_transactions_transaction(apps, schema_editor): - with connection.cursor() as cursor: - cursor.execute( - """ - CREATE OR REPLACE FUNCTION after_transactions_transaction() - RETURNS TRIGGER AS $$ - BEGIN - IF TG_OP = 'UPDATE' - THEN - NEW := calculate_aggregates(OLD, NEW, TG_OP); - NEW := update_can_unamend(NEW); - ELSE - NEW := calculate_aggregates(OLD, NEW, TG_OP); - END IF; - - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION after_transactions_transaction_infinite() - RETURNS TRIGGER AS $$ - BEGIN - NEW := handle_parent_itemization(OLD, NEW); - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - """ - ) - - -def reverse_after_transactions_transaction(apps, schema): - with connection.cursor() as cursor: - cursor.execute( - """ - DROP FUNCTION IF EXISTS after_transactions_transaction - DROP FUNCTION IF EXISTS after_transactions_transaction_infinite - """ - ) - - -def drop_old_triggers(apps, schema_editor): - schema_editor.execute( - """ - DROP TRIGGER - IF EXISTS zafter_transactions_transaction_insert_or_update_trigger - ON transactions_transaction; - - DROP TRIGGER - IF EXISTS before_transactions_transaction_insert_or_update_trigger - ON transactions_transaction; - - DROP TRIGGER - IF EXISTS transaction_updated - ON transactions_transaction; - - DROP TRIGGER - IF EXISTS calculate_aggregates_trigger - ON transactions_transaction; - """ - ) - - -def reverse_drop_old_triggers(apps, schema_editor): - schema_editor.execute( - """ - CREATE TRIGGER zafter_transactions_transaction_insert_or_update_trigger - AFTER INSERT OR UPDATE ON transactions_transaction - FOR EACH ROW - EXECUTE FUNCTION after_transactions_transaction_insert_or_update(); - - CREATE TRIGGER before_transactions_transaction_insert_or_update_trigger - BEFORE INSERT OR UPDATE ON transactions_transaction - FOR EACH ROW - EXECUTE FUNCTION before_transactions_transaction_insert_or_update(); - - CREATE TRIGGER transaction_updated - AFTER UPDATE ON transactions_transaction - FOR EACH ROW - WHEN (pg_trigger_depth() = 0) -- Prevent infinite trigger loop - EXECUTE FUNCTION update_can_unamend(); - - CREATE TRIGGER calculate_aggregates_trigger - AFTER INSERT OR UPDATE ON transactions_transaction - FOR EACH ROW - WHEN (pg_trigger_depth() = 0) -- Prevent infinite trigger loop - EXECUTE FUNCTION calculate_aggregates(); - """ - ) - - -def drop_old_functions(apps, schema_editor): - schema_editor.execute( - """ - DROP FUNCTION IF EXISTS before_transactions_transaction_insert_or_update; - DROP FUNCTION IF EXISTS after_transactions_transaction_insert_or_update; - DROP FUNCTION IF EXISTS calculate_aggregates; - DROP FUNCTION IF EXISTS update_can_unamend; - """ - ) - - -def reverse_drop_old_functions(apps, schema_editor): - schema_editor.execute( - """ - CREATE OR REPLACE FUNCTION before_transactions_transaction_insert_or_update() - RETURNS TRIGGER AS $$ - DECLARE - needs_itemized_set boolean; - itemization boolean; - BEGIN - needs_itemized_set := needs_itemized_set(OLD, NEW); - IF needs_itemized_set THEN - NEW.itemized := calculate_itemization(NEW); - END IF; - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - - CREATE OR REPLACE FUNCTION after_transactions_transaction_insert_or_update() - RETURNS TRIGGER AS $$ - DECLARE - parent_and_grandparent_ids uuid[]; - children_and_grandchildren_ids uuid[]; - BEGIN - IF OLD IS NULL OR OLD.itemized <> NEW.itemized THEN - IF NEW.itemized is TRUE THEN - parent_and_grandparent_ids := - get_parent_grandparent_transaction_ids(NEW); - PERFORM set_itemization_for_ids(TRUE, parent_and_grandparent_ids); - ELSE - children_and_grandchildren_ids := - get_children_and_grandchildren_transaction_ids(NEW); - PERFORM set_itemization_for_ids( - FALSE,children_and_grandchildren_ids - ); - END IF; - END IF; - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION calculate_aggregates() - RETURNS TRIGGER AS $$ - DECLARE - sql_committee_id TEXT; - BEGIN - sql_committee_id := REPLACE(NEW.committee_account_id::TEXT, '-', '_'); - - -- If schedule_c2_id or schedule_d_id is not null, stop processing - IF NEW.schedule_c2_id IS NOT NULL OR NEW.schedule_d_id IS NOT NULL - THEN - RETURN NEW; - END IF; - - IF NEW.schedule_a_id IS NOT NULL OR NEW.schedule_b_id IS NOT NULL - THEN - PERFORM calculate_entity_aggregates(NEW, sql_committee_id); - IF TG_OP = 'UPDATE' - AND NEW.contact_1_id <> OLD.contact_1_id - THEN - PERFORM calculate_entity_aggregates(OLD, sql_committee_id); - END IF; - END IF; - - IF NEW.schedule_c_id IS NOT NULL - OR NEW.schedule_c1_id IS NOT NULL - OR NEW.transaction_type_identifier = 'LOAN_REPAYMENT_MADE' - THEN - PERFORM calculate_loan_payment_to_date(NEW, sql_committee_id); - END IF; - - IF NEW.schedule_e_id IS NOT NULL - THEN - PERFORM calculate_calendar_ytd_per_election_office( - NEW, sql_committee_id); - IF TG_OP = 'UPDATE' - THEN - PERFORM calculate_calendar_ytd_per_election_office( - OLD, sql_committee_id); - END IF; - END IF; - - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - - CREATE OR REPLACE FUNCTION update_can_unamend() - RETURNS TRIGGER AS $$ - BEGIN - UPDATE reports_report - SET can_unamend = FALSE - WHERE id IN ( - SELECT report_id - FROM reports_reporttransaction - WHERE transaction_id = NEW.id - ); - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - """ - ) - - -# Replaces old before_transactions_transaction_insert_or_update() -def process_itemization(apps, schema): - with connection.cursor() as cursor: - cursor.execute( - """ - CREATE OR REPLACE FUNCTION process_itemization( - OLD RECORD, - NEW RECORD - ) - RETURNS RECORD AS $$ - DECLARE - needs_itemized_set boolean; - itemization boolean; - BEGIN - needs_itemized_set := needs_itemized_set(OLD, NEW); - IF needs_itemized_set THEN - NEW.itemized := calculate_itemization(NEW); - END IF; - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - """ - ) - - -def reverse_process_itemization(apps, schema): - with connection.cursor() as cursor: - cursor.execute( - """ - DROP FUNCTION IF EXISTS process_itemization - """ - ) - - -# Replaces old after_transactions_transaction_insert_or_update() -def handle_parent_itemization(apps, schema): - with connection.cursor() as cursor: - cursor.execute( - """ - CREATE OR REPLACE FUNCTION handle_parent_itemization( - OLD RECORD, - NEW RECORD - ) - RETURNS RECORD AS $$ - DECLARE - parent_and_grandparent_ids uuid[]; - children_and_grandchildren_ids uuid[]; - BEGIN - IF OLD IS NULL OR OLD.itemized <> NEW.itemized THEN - IF NEW.itemized is TRUE THEN - parent_and_grandparent_ids := - get_parent_grandparent_transaction_ids(NEW); - PERFORM set_itemization_for_ids(TRUE, parent_and_grandparent_ids); - ELSE - children_and_grandchildren_ids := - get_children_and_grandchildren_transaction_ids(NEW); - PERFORM set_itemization_for_ids( - FALSE,children_and_grandchildren_ids - ); - END IF; - END IF; - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - """ - ) - - -def reverse_handle_parent_itemization(apps, schema): - with connection.cursor() as cursor: - cursor.execute( - """ - DROP FUNCTION IF EXISTS handle_parent_itemization - """ - ) - - -def calculate_aggregates(apps, schema): - with connection.cursor() as cursor: - cursor.execute( - """ - CREATE OR REPLACE FUNCTION calculate_aggregates( - OLD RECORD, - NEW RECORD, - TG_OP TEXT - ) - RETURNS RECORD AS $$ - DECLARE - sql_committee_id TEXT; - BEGIN - sql_committee_id := REPLACE(NEW.committee_account_id::TEXT, '-', '_'); - - -- If schedule_c2_id or schedule_d_id is not null, stop processing - IF NEW.schedule_c2_id IS NOT NULL OR NEW.schedule_d_id IS NOT NULL - THEN - RETURN NEW; - END IF; - - IF NEW.schedule_a_id IS NOT NULL OR NEW.schedule_b_id IS NOT NULL - THEN - PERFORM calculate_entity_aggregates(NEW, sql_committee_id); - IF TG_OP = 'UPDATE' - AND NEW.contact_1_id <> OLD.contact_1_id - THEN - PERFORM calculate_entity_aggregates(OLD, sql_committee_id); - END IF; - END IF; - - IF NEW.schedule_c_id IS NOT NULL - OR NEW.schedule_c1_id IS NOT NULL - OR NEW.transaction_type_identifier = 'LOAN_REPAYMENT_MADE' - THEN - PERFORM calculate_loan_payment_to_date(NEW, sql_committee_id); - END IF; - - IF NEW.schedule_e_id IS NOT NULL - THEN - PERFORM calculate_calendar_ytd_per_election_office( - NEW, sql_committee_id); - IF TG_OP = 'UPDATE' - THEN - PERFORM calculate_calendar_ytd_per_election_office( - OLD, sql_committee_id); - END IF; - END IF; - - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - """ - ) - - -def reverse_calculate_aggregates(apps, schema): - with connection.cursor() as cursor: - cursor.execute( - """ - DROP FUNCTION IF EXISTS calculate_aggregates - """ - ) - - -def update_can_unamend(apps, schema): - with connection.cursor() as cursor: - cursor.execute( - """ - CREATE OR REPLACE FUNCTION update_can_unamend( - NEW RECORD - ) - RETURNS RECORD AS $$ - BEGIN - UPDATE reports_report - SET can_unamend = FALSE - WHERE id IN ( - SELECT report_id - FROM reports_reporttransaction - WHERE transaction_id = NEW.id - ); - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - """ - ) - - -def reverse_update_can_unamend(apps, schema): - with connection.cursor() as cursor: - cursor.execute( - """ - DROP FUNCTION IF EXISTS update_can_unamend - """ - ) - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0014_drop_transaction_view"), - ] - - operations = [ - migrations.RunPython(drop_old_triggers, reverse_code=reverse_drop_old_triggers), - migrations.RunPython(drop_old_functions, reverse_code=reverse_drop_old_functions), - migrations.RunPython( - process_itemization, reverse_code=reverse_process_itemization - ), - migrations.RunPython( - handle_parent_itemization, reverse_code=reverse_handle_parent_itemization - ), - migrations.RunPython( - calculate_aggregates, reverse_code=reverse_calculate_aggregates - ), - migrations.RunPython(update_can_unamend, reverse_code=reverse_update_can_unamend), - migrations.RunPython( - before_transactions_transaction, - reverse_code=before_transactions_transaction, - ), - migrations.RunPython( - after_transactions_transaction, - reverse_code=reverse_after_transactions_transaction, - ), - migrations.RunPython(create_triggers, reverse_code=reverse_create_triggers), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0016_schedulef_transaction_contact_4_and_more.py b/django-backend/fecfiler/transactions/migrations/0016_schedulef_transaction_contact_4_and_more.py deleted file mode 100644 index 62a09c018d..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0016_schedulef_transaction_contact_4_and_more.py +++ /dev/null @@ -1,81 +0,0 @@ -# Generated by Django 5.1.5 on 2025-03-06 01:56 - -import django.db.models.deletion -import uuid -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("contacts", "0001_initial"), - ("transactions", "0015_merge_transaction_triggers"), - ] - - operations = [ - migrations.CreateModel( - name="ScheduleF", - fields=[ - ( - "id", - models.UUIDField( - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True, - ), - ), - ( - "filer_designated_to_make_coordianted_expenditures", - models.BooleanField(blank=True, null=True), - ), - ("expenditure_date", models.DateField(blank=True, null=True)), - ( - "expenditure_amount", - models.DecimalField( - blank=True, decimal_places=2, max_digits=11, null=True - ), - ), - ( - "aggregate_general_elec_expended", - models.DecimalField( - blank=True, decimal_places=2, max_digits=11, null=True - ), - ), - ("expenditure_purpose_descrip", models.TextField(blank=True)), - ("category_code", models.TextField(blank=True)), - ("memo_text_description", models.TextField(blank=True)), - ], - ), - migrations.AddField( - model_name="transaction", - name="contact_4", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="contact_4_transaction_set", - to="contacts.contact", - ), - ), - migrations.AddField( - model_name="transaction", - name="contact_5", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="contact_5_transaction_set", - to="contacts.contact", - ), - ), - migrations.AddField( - model_name="transaction", - name="schedule_f", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="transactions.schedulef", - ), - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0017_schedulef_coordianted_to_coordinated.py b/django-backend/fecfiler/transactions/migrations/0017_schedulef_coordianted_to_coordinated.py deleted file mode 100644 index 9bfe5905ef..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0017_schedulef_coordianted_to_coordinated.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Dan 1.0 on 2025-03-17 16:25ish - -from django.db import migrations -import django_migration_linter as linter - - -class Migration(migrations.Migration): - dependencies = [ - ("transactions", "0016_schedulef_transaction_contact_4_and_more"), - ] - - operations = [ - linter.IgnoreMigration(), - migrations.RenameField( - model_name="schedulef", - old_name="filer_designated_to_make_coordianted_expenditures", - new_name="filer_designated_to_make_coordinated_expenditures", - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0018_schedulef_general_election_year_and_more.py b/django-backend/fecfiler/transactions/migrations/0018_schedulef_general_election_year_and_more.py deleted file mode 100644 index 407ebff0a0..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0018_schedulef_general_election_year_and_more.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.1.7 on 2025-03-26 14:54 - -from django.db import migrations, models -import django_migration_linter as linter - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0017_schedulef_coordianted_to_coordinated"), - ] - - operations = [ - linter.IgnoreMigration(), - migrations.AddField( - model_name="schedulef", - name="general_election_year", - field=models.TextField(blank=True), - ), - migrations.AlterField( - model_name="schedulef", - name="category_code", - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name="schedulef", - name="memo_text_description", - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0019_aggregate_committee_controls.py b/django-backend/fecfiler/transactions/migrations/0019_aggregate_committee_controls.py deleted file mode 100644 index 9a5f1c4083..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0019_aggregate_committee_controls.py +++ /dev/null @@ -1,236 +0,0 @@ -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0018_schedulef_general_election_year_and_more"), - ] - - old_election_aggregate_function = """ -CREATE OR REPLACE FUNCTION calculate_calendar_ytd_per_election_office( - txn RECORD, sql_committee_id text -) -RETURNS VOID -AS $$ -DECLARE - schedule_date date; - v_election_code text; - v_candidate_office text; - v_candidate_state text; - v_candidate_district text; -BEGIN - SELECT - COALESCE(disbursement_date, dissemination_date) INTO schedule_date - FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - - SELECT election_code INTO v_election_code - FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - - SELECT - candidate_office, - candidate_state, - candidate_district INTO v_candidate_office, - v_candidate_state, - v_candidate_district - FROM contacts - WHERE id = txn.contact_2_id; - EXECUTE ' - UPDATE transactions_transaction AS t - SET _calendar_ytd_per_election_office = tc.new_sum - FROM ( - SELECT - t.id, - Coalesce( - e.disbursement_date, - e.dissemination_date - ) as date, - SUM( - calculate_effective_amount( - t.transaction_type_identifier, - calculate_amount( - NULL, - NULL, - NULL, - NULL, - e.expenditure_amount, - t.debt_id, - t.schedule_d_id - ), - t.schedule_c_id - ) - ) OVER (ORDER BY Coalesce( - e.disbursement_date, - e.dissemination_date - ), t.created) AS new_sum - FROM transactions_schedulee e - JOIN transactions_transaction t - ON e.id = t.schedule_e_id - JOIN contacts c - ON t.contact_2_id = c.id - WHERE - e.election_code = $1 - AND c.candidate_office = $2 - AND ( - c.candidate_state = $3 - OR ( - c.candidate_state IS NULL - AND $3 = '''' - ) - ) - AND ( - c.candidate_district = $4 - OR ( - c.candidate_district IS NULL - AND $4 = '''' - ) - ) - AND EXTRACT(YEAR FROM Coalesce( - e.disbursement_date, - e.dissemination_date - )) = $5 - AND aggregation_group = $6 - AND force_unaggregated IS NOT TRUE - ) AS tc - WHERE t.id = tc.id - AND ( - tc.date > $7 - OR ( - tc.date = $7 - AND t.created >= $8 - ) - ); - ' - USING - v_election_code, - v_candidate_office, - COALESCE(v_candidate_state, ''), - COALESCE(v_candidate_district, ''), - EXTRACT(YEAR FROM schedule_date), - txn.aggregation_group, - schedule_date, - txn.created; -END; -$$ -LANGUAGE plpgsql; - """ - - new_election_aggregate_function = """ -CREATE OR REPLACE FUNCTION calculate_calendar_ytd_per_election_office( - txn RECORD, sql_committee_id text -) -RETURNS VOID -AS $$ -DECLARE - schedule_date date; - v_election_code text; - v_candidate_office text; - v_candidate_state text; - v_candidate_district text; -BEGIN - SELECT - COALESCE(disbursement_date, dissemination_date) INTO schedule_date - FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - - SELECT election_code INTO v_election_code - FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - - SELECT - candidate_office, - candidate_state, - candidate_district INTO v_candidate_office, - v_candidate_state, - v_candidate_district - FROM contacts - WHERE id = txn.contact_2_id; - EXECUTE ' - UPDATE transactions_transaction AS t - SET _calendar_ytd_per_election_office = tc.new_sum - FROM ( - SELECT - t.id, - Coalesce( - e.disbursement_date, - e.dissemination_date - ) as date, - SUM( - calculate_effective_amount( - t.transaction_type_identifier, - calculate_amount( - NULL, - NULL, - NULL, - NULL, - e.expenditure_amount, - t.debt_id, - t.schedule_d_id - ), - t.schedule_c_id - ) - ) OVER (ORDER BY Coalesce( - e.disbursement_date, - e.dissemination_date - ), t.created) AS new_sum - FROM transactions_schedulee e - JOIN transactions_transaction t - ON e.id = t.schedule_e_id - JOIN contacts c - ON t.contact_2_id = c.id - WHERE - e.election_code = $1 - AND c.candidate_office = $2 - AND ( - c.candidate_state = $3 - OR ( - c.candidate_state IS NULL - AND $3 = '''' - ) - ) - AND ( - c.candidate_district = $4 - OR ( - c.candidate_district IS NULL - AND $4 = '''' - ) - ) - AND EXTRACT(YEAR FROM Coalesce( - e.disbursement_date, - e.dissemination_date - )) = $5 - AND aggregation_group = $6 - AND force_unaggregated IS NOT TRUE - AND t.committee_account_id = $9 - ) AS tc - WHERE t.id = tc.id - AND ( - tc.date > $7 - OR ( - tc.date = $7 - AND t.created >= $8 - ) - ); - ' - USING - v_election_code, - v_candidate_office, - COALESCE(v_candidate_state, ''), - COALESCE(v_candidate_district, ''), - EXTRACT(YEAR FROM schedule_date), - txn.aggregation_group, - schedule_date, - txn.created, - txn.committee_account_id; -END; -$$ -LANGUAGE plpgsql; - """ - - operations = [ - migrations.RunSQL( - new_election_aggregate_function, reverse_sql=old_election_aggregate_function - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0020_trigger_save_on_transactions.py b/django-backend/fecfiler/transactions/migrations/0020_trigger_save_on_transactions.py deleted file mode 100644 index f0408c911f..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0020_trigger_save_on_transactions.py +++ /dev/null @@ -1,76 +0,0 @@ -from django.db import migrations -import structlog - -logger = structlog.get_logger(__name__) - - -def trigger_save_on_transactions(apps, schema_editor): - transactions = apps.get_model("transactions", "transaction") - committees = apps.get_model("committee_accounts", "committeeaccount") - contacts = apps.get_model("contacts", "contact") - - # Update transactions for each committee - for committee in committees.objects.all(): - logger.info(f"Committee:{committee.committee_id}") - - # For each contact, update the first schedule A transaction - for contact in contacts.objects.filter(committee_account=committee): - logger.info(f"Contact: {contact.id}") - first_schedule_a = ( - transactions.objects.filter( - schedule_a__isnull=False, - contact_1=contact, - committee_account=committee, - ) - .order_by("schedule_a__contribution_date", "created") - .first() - ) - if first_schedule_a: - logger.info(f"Saving first Schedule A: {first_schedule_a.id}") - first_schedule_a.save() - - # Election Aggregates - elections = transactions.objects.filter( - schedule_e__isnull=False, - committee_account=committee, - ).values( - "contact_2__candidate_office", - "contact_2__candidate_state", - "contact_2__candidate_district", - "schedule_e__election_code", - ) - for election in elections: - logger.info("Finding first schedule E for election") - first_schedule_e = ( - transactions.objects.filter( - schedule_e__isnull=False, - contact_2__candidate_office=election[ - "contact_2__candidate_office" - ], - contact_2__candidate_state=election["contact_2__candidate_state"], - contact_2__candidate_district=election[ - "contact_2__candidate_district" - ], - schedule_e__election_code=election["schedule_e__election_code"], - committee_account=committee, - ) - .order_by( - "schedule_e__disbursement_date", - "created", - ) - .first() - ) - if first_schedule_e: - logger.info(f"Saving first Schedule E: {first_schedule_e.id}") - first_schedule_e.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0019_aggregate_committee_controls"), - ] - - operations = [ - migrations.RunPython(trigger_save_on_transactions, migrations.RunPython.noop), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0021_alter_transaction_reports.py b/django-backend/fecfiler/transactions/migrations/0021_alter_transaction_reports.py deleted file mode 100644 index 02685bde8a..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0021_alter_transaction_reports.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 5.2 on 2025-04-30 17:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('reports', '0014_form99_swap_text_code'), - ('transactions', '0020_trigger_save_on_transactions'), - ] - - operations = [ - migrations.AlterField( - model_name='transaction', - name='reports', - field=models.ManyToManyField( - related_name='transactions', - through='reports.ReportTransaction', - through_fields=['transaction', 'report'], - to='reports.report' - ), - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0022_schedule_f_aggregation.py b/django-backend/fecfiler/transactions/migrations/0022_schedule_f_aggregation.py deleted file mode 100644 index 0ba3dde8f6..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0022_schedule_f_aggregation.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 5.2 on 2025-05-07 18:18 - -from django.db import migrations -from django.db.models import F -from fecfiler.transactions.utils_aggregation_queries import filter_queryset_for_previous_transactions_in_aggregation # noqa: E501 - - -def calculate_schedule_f_aggregates(apps, schema_editor): - CommitteeAccount = apps.get_model("committee_accounts", "CommitteeAccount") # noqa - Transaction = apps.get_model("transactions", "Transaction") # noqa - - for committee in CommitteeAccount.objects.all(): - schedule_f_transactions = Transaction.objects.all().filter( - committee_account=committee, - schedule_f__isnull=False, - ).annotate( - date=F("schedule_f__expenditure_date"), - amount=F("schedule_f__expenditure_amount") - ).order_by("date") - - for trans in schedule_f_transactions: - previous_transactions = filter_queryset_for_previous_transactions_in_aggregation( # noqa: E501 - schedule_f_transactions, - trans.date, - trans.aggregation_group, - trans.id, - None, - trans.contact_2.id, - None, - trans.schedule_f.general_election_year - ) - - previous_transaction = previous_transactions.first() - previous_aggregate = 0 - if previous_transaction: - previous_aggregate = ( - previous_transaction.schedule_f.aggregate_general_elec_expended - ) - - trans.schedule_f.aggregate_general_elec_expended = ( - trans.amount + previous_aggregate - ) - trans.schedule_f.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ('transactions', '0021_alter_transaction_reports'), - ] - - operations = [ - migrations.RunPython(calculate_schedule_f_aggregates, migrations.RunPython.noop), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0023_optimize_calculate_loan_payment_to_date.py b/django-backend/fecfiler/transactions/migrations/0023_optimize_calculate_loan_payment_to_date.py deleted file mode 100644 index af27ba807d..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0023_optimize_calculate_loan_payment_to_date.py +++ /dev/null @@ -1,117 +0,0 @@ -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0022_schedule_f_aggregation"), - ] - - operations = [ - migrations.RunSQL( - """ -CREATE OR REPLACE FUNCTION public.calculate_loan_payment_to_date( - txn record, sql_committee_id text -) -RETURNS VOID AS $$ -BEGIN - EXECUTE ' - UPDATE transactions_transaction AS t - SET loan_payment_to_date = tc.new_sum - FROM ( - SELECT - data.id, - data.original_loan_id, - data.is_loan, - SUM(data.effective_amount) OVER ( - PARTITION BY data.original_loan_id - ORDER BY data.date - ) AS new_sum - FROM ( - SELECT - t.id, - calculate_loan_date( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) AS date, - calculate_original_loan_id( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) AS original_loan_id, - calculate_is_loan( - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id - ) AS is_loan, - calculate_effective_amount( - t.transaction_type_identifier, - calculate_amount( - sa.contribution_amount, - sb.expenditure_amount, - sc.loan_amount, - sc2.guaranteed_amount, - se.expenditure_amount, - t.debt_id, - t.schedule_d_id - ), - t.schedule_c_id - ) AS effective_amount - FROM transactions_transaction t - LEFT JOIN transactions_schedulea sa ON t.schedule_a_id = sa.id - LEFT JOIN transactions_scheduleb sb ON t.schedule_b_id = sb.id - LEFT JOIN transactions_schedulec sc ON t.schedule_c_id = sc.id - LEFT JOIN transactions_schedulec2 sc2 ON t.schedule_c2_id = sc2.id - LEFT JOIN transactions_schedulee se ON t.schedule_e_id = se.id - WHERE t.deleted IS NULL - ) AS data - WHERE (data.original_loan_id = ( - SELECT calculate_original_loan_id( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) - FROM transactions_transaction t - WHERE t.id = COALESCE( - (SELECT loan_id FROM transactions_transaction WHERE id = $1), - $1 - ) - ) - AND data.date <= ( - SELECT calculate_loan_date( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) - FROM transactions_transaction t - WHERE t.id = COALESCE( - (SELECT loan_id FROM transactions_transaction WHERE id = $1), - $1 - ) - )) - OR data.original_loan_id IN ( - SELECT t.transaction_id - FROM transactions_transaction t - WHERE t.schedule_c_id IS NOT NULL - AND t.loan_id = $2 - ) - ) AS tc - WHERE t.id = tc.id - AND tc.is_loan = ''T''; - ' - USING txn.id, txn.loan_id; -END; -$$ -LANGUAGE plpgsql; -""" - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0024_scheduled_balance_at_close_and_more.py b/django-backend/fecfiler/transactions/migrations/0024_scheduled_balance_at_close_and_more.py deleted file mode 100644 index ceef9321bd..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0024_scheduled_balance_at_close_and_more.py +++ /dev/null @@ -1,59 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-25 03:23 - -from django.db import migrations, models -from fecfiler.transactions.aggregation import process_aggregation_for_debts - - -def run_aggregations_for_all_debts(apps, schema_editor): - transaction = apps.get_model("transactions", "Transaction") - all_root_debts = transaction.objects.filter( - schedule_d__isnull=False, debt__isnull=True - ) - for debt in all_root_debts: - process_aggregation_for_debts(debt) - - -class Migration(migrations.Migration): - - dependencies = [ - ('transactions', '0023_optimize_calculate_loan_payment_to_date'), - ] - - operations = [ - migrations.AddField( - model_name='scheduled', - name='balance_at_close', - field=models.DecimalField( - blank=True, decimal_places=2, max_digits=11, null=True - ), - ), - migrations.AddField( - model_name='scheduled', - name='beginning_balance', - field=models.DecimalField( - blank=True, decimal_places=2, max_digits=11, null=True - ), - ), - migrations.AddField( - model_name='scheduled', - name='incurred_prior', - field=models.DecimalField( - blank=True, decimal_places=2, max_digits=11, null=True - ), - ), - migrations.AddField( - model_name='scheduled', - name='payment_prior', - field=models.DecimalField( - blank=True, decimal_places=2, max_digits=11, null=True - ), - ), - migrations.AddField( - model_name='scheduled', - name='payment_amount', - field=models.DecimalField( - blank=True, decimal_places=2, max_digits=11, null=True - ), - ), - migrations.RunPython(run_aggregations_for_all_debts, migrations.RunPython.noop) - ] diff --git a/django-backend/fecfiler/transactions/migrations/0025_drop_aggregate_triggers.py b/django-backend/fecfiler/transactions/migrations/0025_drop_aggregate_triggers.py deleted file mode 100644 index da0351adc0..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0025_drop_aggregate_triggers.py +++ /dev/null @@ -1,565 +0,0 @@ -# Generated migration to drop database trigger functions -# This migration moves aggregate calculation from database triggers to Django code - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0024_scheduled_balance_at_close_and_more"), - ] - - operations = [ - migrations.RunSQL( - """ - -- Drop all aggregate-related triggers from - -- transactions_transaction table - DROP TRIGGER IF EXISTS calculate_aggregates_trigger - ON transactions_transaction; - DROP TRIGGER IF EXISTS after_transactions_transaction_trigger - ON transactions_transaction; - DROP TRIGGER IF EXISTS - after_transactions_transaction_infinite_trigger - ON transactions_transaction; - DROP TRIGGER IF EXISTS before_transactions_transaction_trigger - ON transactions_transaction; - """, - reverse_sql=""" - -- Recreate triggers for aggregate calculation - CREATE TRIGGER before_transactions_transaction_trigger - BEFORE INSERT OR UPDATE ON transactions_transaction - FOR EACH ROW - EXECUTE FUNCTION before_transactions_transaction(); - - CREATE TRIGGER after_transactions_transaction_infinite_trigger - AFTER INSERT OR UPDATE ON transactions_transaction - FOR EACH ROW - EXECUTE FUNCTION after_transactions_transaction_infinite(); - - CREATE TRIGGER after_transactions_transaction_trigger - AFTER INSERT OR UPDATE ON transactions_transaction - FOR EACH ROW - WHEN (pg_trigger_depth() = 0) - EXECUTE FUNCTION after_transactions_transaction(); - """, - ), - migrations.RunSQL( - """ - -- Drop the calculate_entity_aggregates function - DROP FUNCTION IF EXISTS calculate_entity_aggregates( - txn RECORD, sql_committee_id TEXT, temp_table_name TEXT - ) CASCADE; - """, - reverse_sql=""" - -- Recreate calculate_entity_aggregates function - CREATE OR REPLACE FUNCTION calculate_entity_aggregates( - txn RECORD, - sql_committee_id TEXT, - temp_table_name TEXT - ) - RETURNS VOID AS $$ - DECLARE - schedule_date DATE; - BEGIN - IF txn.schedule_a_id IS NOT NULL THEN - SELECT contribution_date - INTO schedule_date - FROM transactions_schedulea - WHERE id = txn.schedule_a_id; - ELSIF txn.schedule_b_id IS NOT NULL THEN - SELECT expenditure_date - INTO schedule_date - FROM transactions_scheduleb - WHERE id = txn.schedule_b_id; - END IF; - - EXECUTE ' - CREATE TEMPORARY TABLE ' || temp_table_name || ' - ON COMMIT DROP AS - SELECT - id, - SUM(effective_amount) OVER (ORDER BY date, created) - AS new_sum - FROM transaction_view__' || sql_committee_id || ' - WHERE - contact_1_id = $1 - AND EXTRACT(YEAR FROM date) = $2 - AND aggregation_group = $3 - AND force_unaggregated IS NOT TRUE; - - UPDATE transactions_transaction AS t - SET aggregate = tt.new_sum - FROM ' || temp_table_name || ' AS tt - WHERE t.id = tt.id; - ' - USING - txn.contact_1_id, - EXTRACT(YEAR FROM schedule_date), - txn.aggregation_group; - END; - $$ LANGUAGE plpgsql; - """, - ), - migrations.RunSQL( - """ - -- Drop the calculate_calendar_ytd_per_election_office function - DROP FUNCTION IF EXISTS calculate_calendar_ytd_per_election_office( - txn RECORD, sql_committee_id TEXT, temp_table_name TEXT - ) CASCADE; - """, - reverse_sql=""" - -- Recreate calculate_calendar_ytd_per_election_office function - CREATE OR REPLACE FUNCTION calculate_calendar_ytd_per_election_office( - txn RECORD, sql_committee_id text - ) - RETURNS VOID AS $$ - DECLARE - schedule_date date; - v_election_code text; - v_candidate_office text; - v_candidate_state text; - v_candidate_district text; - BEGIN - SELECT - COALESCE(disbursement_date, dissemination_date) INTO schedule_date - FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - - SELECT election_code INTO v_election_code - FROM transactions_schedulee - WHERE id = txn.schedule_e_id; - - SELECT - candidate_office, - candidate_state, - candidate_district INTO v_candidate_office, - v_candidate_state, - v_candidate_district - FROM contacts - WHERE id = txn.contact_2_id; - EXECUTE ' - UPDATE transactions_transaction AS t - SET _calendar_ytd_per_election_office = tc.new_sum - FROM ( - SELECT - t.id, - Coalesce( - e.disbursement_date, - e.dissemination_date - ) as date, - SUM( - calculate_effective_amount( - t.transaction_type_identifier, - calculate_amount( - NULL, - NULL, - NULL, - NULL, - e.expenditure_amount, - t.debt_id, - t.schedule_d_id - ), - t.schedule_c_id - ) - ) OVER (ORDER BY Coalesce( - e.disbursement_date, - e.dissemination_date - ), t.created) AS new_sum - FROM transactions_schedulee e - JOIN transactions_transaction t - ON e.id = t.schedule_e_id - JOIN contacts c - ON t.contact_2_id = c.id - WHERE - e.election_code = $1 - AND c.candidate_office = $2 - AND ( - c.candidate_state = $3 - OR ( - c.candidate_state IS NULL - AND $3 = '''' - ) - ) - AND ( - c.candidate_district = $4 - OR ( - c.candidate_district IS NULL - AND $4 = '''' - ) - ) - AND EXTRACT(YEAR FROM Coalesce( - e.disbursement_date, - e.dissemination_date - )) = $5 - AND aggregation_group = $6 - AND force_unaggregated IS NOT TRUE - AND t.committee_account_id = $9 - ) AS tc - WHERE t.id = tc.id - AND ( - tc.date > $7 - OR ( - tc.date = $7 - AND t.created >= $8 - ) - ); - ' - USING - v_election_code, - v_candidate_office, - COALESCE(v_candidate_state, ''), - COALESCE(v_candidate_district, ''), - EXTRACT(YEAR FROM schedule_date), - txn.aggregation_group, - schedule_date, - txn.created, - txn.committee_account_id; - END; - $$ LANGUAGE plpgsql; - """, - ), - migrations.RunSQL( - """ - -- Drop the calculate_effective_amount function - DROP FUNCTION IF EXISTS calculate_effective_amount( - transaction_type_identifier TEXT, amount NUMERIC, - schedule_c_id UUID - ) CASCADE; - """, - reverse_sql=""" - -- Recreate calculate_effective_amount function - CREATE OR REPLACE FUNCTION calculate_effective_amount( - transaction_type_identifier TEXT, - amount NUMERIC, - schedule_c_id UUID - ) - RETURNS NUMERIC AS $$ - DECLARE - effective_amount NUMERIC; - BEGIN - -- Case 1: transaction type is a refund - IF transaction_type_identifier IN ( - 'TRIBAL_REFUND_NP_HEADQUARTERS_ACCOUNT', - 'TRIBAL_REFUND_NP_CONVENTION_ACCOUNT', - 'TRIBAL_REFUND_NP_RECOUNT_ACCOUNT', - 'INDIVIDUAL_REFUND_NP_HEADQUARTERS_ACCOUNT', - 'INDIVIDUAL_REFUND_NP_CONVENTION_ACCOUNT', - 'INDIVIDUAL_REFUND_NP_RECOUNT_ACCOUNT', - 'REFUND_PARTY_CONTRIBUTION', - 'REFUND_PARTY_CONTRIBUTION_VOID', - 'REFUND_PAC_CONTRIBUTION', - 'REFUND_PAC_CONTRIBUTION_VOID', - 'INDIVIDUAL_REFUND_NON_CONTRIBUTION_ACCOUNT', - 'BUSINESS_LABOR_REFUND_NON_CONTRIBUTION_ACCOUNT', - 'OTHER_COMMITTEE_REFUND_NON_CONTRIBUTION_ACCOUNT', - 'REFUND_UNREGISTERED_CONTRIBUTION', - 'REFUND_UNREGISTERED_CONTRIBUTION_VOID', - 'REFUND_INDIVIDUAL_CONTRIBUTION', - 'REFUND_INDIVIDUAL_CONTRIBUTION_VOID', - 'OTHER_COMMITTEE_REFUND_REFUND_NP_HEADQUARTERS_ACCOUNT', - 'OTHER_COMMITTEE_REFUND_REFUND_NP_CONVENTION_ACCOUNT', - 'OTHER_COMMITTEE_REFUND_REFUND_NP_RECOUNT_ACCOUNT' - ) THEN - effective_amount := amount * -1; - - -- Case 2: schedule_c exists (return NULL) - ELSIF schedule_c_id IS NOT NULL THEN - effective_amount := NULL; - - -- Default case: return the original amount - ELSE - effective_amount := amount; - END IF; - - RETURN effective_amount; - END; - $$ LANGUAGE plpgsql; - """, - ), - migrations.RunSQL( - """ - -- Drop the calculate_amount function if it exists - DROP FUNCTION IF EXISTS calculate_amount( - contribution_amount NUMERIC, - expenditure_amount NUMERIC, - loan_amount NUMERIC, - guaranteed_amount NUMERIC, - schedule_e_expenditure_amount NUMERIC, - debt_id UUID, - schedule_d_id UUID - ) CASCADE; - """, - reverse_sql=""" - -- Recreate calculate_amount function - CREATE OR REPLACE FUNCTION calculate_amount( - schedule_a_contribution_amount NUMERIC, - schedule_b_expenditure_amount NUMERIC, - schedule_c_loan_amount NUMERIC, - schedule_c2_guaranteed_amount NUMERIC, - schedule_e_expenditure_amount NUMERIC, - debt UUID, - schedule_d_id UUID - ) - RETURNS NUMERIC AS $$ - DECLARE - debt_incurred_amount NUMERIC; - BEGIN - IF debt IS NOT NULL THEN - SELECT sd.incurred_amount - INTO debt_incurred_amount - FROM transactions_transaction t - LEFT JOIN transactions_scheduled sd ON t.schedule_d_id = sd.id - WHERE t.id = debt; - ELSE - debt_incurred_amount := NULL; - END IF; - - RETURN COALESCE( - schedule_a_contribution_amount, - schedule_b_expenditure_amount, - schedule_c_loan_amount, - schedule_c2_guaranteed_amount, - schedule_e_expenditure_amount, - debt_incurred_amount, - (SELECT incurred_amount - FROM transactions_scheduled - WHERE id = schedule_d_id) - ); - END; - $$ LANGUAGE plpgsql; - """, - ), - migrations.RunSQL( - """ - -- Drop the calculate_loan_payment_to_date function if it exists - DROP FUNCTION IF EXISTS calculate_loan_payment_to_date( - txn RECORD, sql_committee_id TEXT, temp_table_name TEXT - ) CASCADE; - """, - reverse_sql=""" - -- Recreate calculate_loan_payment_to_date function - CREATE OR REPLACE FUNCTION public.calculate_loan_payment_to_date( - txn record, sql_committee_id text - ) - RETURNS VOID AS $$ - BEGIN - EXECUTE ' - UPDATE transactions_transaction AS t - SET loan_payment_to_date = tc.new_sum - FROM ( - SELECT - data.id, - data.original_loan_id, - data.is_loan, - SUM(data.effective_amount) OVER ( - PARTITION BY data.original_loan_id - ORDER BY data.date - ) AS new_sum - FROM ( - SELECT - t.id, - calculate_loan_date( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) AS date, - calculate_original_loan_id( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) AS original_loan_id, - calculate_is_loan( - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id - ) AS is_loan, - calculate_effective_amount( - t.transaction_type_identifier, - calculate_amount( - sa.contribution_amount, - sb.expenditure_amount, - sc.loan_amount, - sc2.guaranteed_amount, - se.expenditure_amount, - t.debt_id, - t.schedule_d_id - ), - t.schedule_c_id - ) AS effective_amount - FROM transactions_transaction t - LEFT JOIN transactions_schedulea sa - ON t.schedule_a_id = sa.id - LEFT JOIN transactions_scheduleb sb - ON t.schedule_b_id = sb.id - LEFT JOIN transactions_schedulec sc - ON t.schedule_c_id = sc.id - LEFT JOIN transactions_schedulec2 sc2 - ON t.schedule_c2_id = sc2.id - LEFT JOIN transactions_schedulee se - ON t.schedule_e_id = se.id - WHERE t.deleted IS NULL - ) AS data - WHERE (data.original_loan_id = ( - SELECT calculate_original_loan_id( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) - FROM transactions_transaction t - WHERE t.id = COALESCE( - (SELECT loan_id - FROM transactions_transaction - WHERE id = $1), - $1 - ) - ) - AND data.date <= ( - SELECT calculate_loan_date( - t.transaction_id, - t.loan_id, - t.transaction_type_identifier, - t.schedule_c_id, - t.schedule_b_id - ) - FROM transactions_transaction t - WHERE t.id = COALESCE( - (SELECT loan_id - FROM transactions_transaction - WHERE id = $1), - $1 - ) - )) - OR data.original_loan_id IN ( - SELECT t.transaction_id - FROM transactions_transaction t - WHERE t.schedule_c_id IS NOT NULL - AND t.loan_id = $2 - ) - ) AS tc - WHERE t.id = tc.id - AND tc.is_loan = ''T''; - ' - USING txn.id, txn.loan_id; - END; - $$ LANGUAGE plpgsql; - """, - ), - migrations.RunSQL( - """ - -- Drop the trigger handler functions - DROP FUNCTION IF EXISTS after_transactions_transaction() - CASCADE; - DROP FUNCTION IF EXISTS - after_transactions_transaction_infinite() CASCADE; - DROP FUNCTION IF EXISTS before_transactions_transaction() - CASCADE; - DROP FUNCTION IF EXISTS - before_transactions_transaction_insert_or_update() - CASCADE; - DROP FUNCTION IF EXISTS - after_transactions_transaction_insert_or_update() - CASCADE; - """, - reverse_sql=""" - -- Recreate trigger handler functions - CREATE OR REPLACE FUNCTION before_transactions_transaction() - RETURNS TRIGGER AS $$ - BEGIN - NEW := process_itemization(OLD, NEW); - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION after_transactions_transaction() - RETURNS TRIGGER AS $$ - BEGIN - IF TG_OP = 'UPDATE' - THEN - NEW := calculate_aggregates(OLD, NEW, TG_OP); - NEW := update_can_unamend(NEW); - ELSE - NEW := calculate_aggregates(OLD, NEW, TG_OP); - END IF; - - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - - CREATE OR REPLACE FUNCTION after_transactions_transaction_infinite() - RETURNS TRIGGER AS $$ - BEGIN - NEW := handle_parent_itemization(OLD, NEW); - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - """, - ), - migrations.RunSQL( - """ - -- Drop the main calculate_aggregates function that was - -- called by triggers - DROP FUNCTION IF EXISTS calculate_aggregates( - old RECORD, new RECORD, tg_op TEXT - ) CASCADE; - DROP FUNCTION IF EXISTS calculate_aggregates() CASCADE; - """, - reverse_sql=""" - -- Recreate calculate_aggregates function - CREATE OR REPLACE FUNCTION calculate_aggregates( - OLD RECORD, - NEW RECORD, - TG_OP TEXT - ) - RETURNS RECORD AS $$ - DECLARE - sql_committee_id TEXT; - BEGIN - sql_committee_id := REPLACE(NEW.committee_account_id::TEXT, '-', '_'); - - -- If schedule_c2_id or schedule_d_id is not null, stop processing - IF NEW.schedule_c2_id IS NOT NULL OR NEW.schedule_d_id IS NOT NULL - THEN - RETURN NEW; - END IF; - - IF NEW.schedule_a_id IS NOT NULL OR NEW.schedule_b_id IS NOT NULL - THEN - PERFORM calculate_entity_aggregates(NEW, sql_committee_id); - IF TG_OP = 'UPDATE' - AND NEW.contact_1_id <> OLD.contact_1_id - THEN - PERFORM calculate_entity_aggregates(OLD, sql_committee_id); - END IF; - END IF; - - IF NEW.schedule_c_id IS NOT NULL - OR NEW.schedule_c1_id IS NOT NULL - OR NEW.transaction_type_identifier = 'LOAN_REPAYMENT_MADE' - THEN - PERFORM calculate_loan_payment_to_date(NEW, sql_committee_id); - END IF; - - IF NEW.schedule_e_id IS NOT NULL - THEN - PERFORM calculate_calendar_ytd_per_election_office( - NEW, sql_committee_id); - IF TG_OP = 'UPDATE' - THEN - PERFORM calculate_calendar_ytd_per_election_office( - OLD, sql_committee_id); - END IF; - END IF; - - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - """, - ), - ] diff --git a/django-backend/fecfiler/transactions/migrations/0026_alter_transaction_itemized.py b/django-backend/fecfiler/transactions/migrations/0026_alter_transaction_itemized.py deleted file mode 100644 index 5f27857b88..0000000000 --- a/django-backend/fecfiler/transactions/migrations/0026_alter_transaction_itemized.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.9 on 2025-12-31 19:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("transactions", "0025_drop_aggregate_triggers"), - ] - - operations = [ - migrations.AlterField( - model_name="transaction", - name="itemized", - field=models.BooleanField(db_default=False), - ), - ] diff --git a/django-backend/fecfiler/transactions/models.py b/django-backend/fecfiler/transactions/models.py index 71cb685092..04a0be0e2d 100644 --- a/django-backend/fecfiler/transactions/models.py +++ b/django-backend/fecfiler/transactions/models.py @@ -1,5 +1,6 @@ from django.db import models from django.db.models import Q +from django.apps import apps from django.contrib.postgres.fields import ArrayField from fecfiler.soft_delete.models import SoftDeleteModel from fecfiler.committee_accounts.models import CommitteeOwnedModel @@ -500,6 +501,9 @@ def save(self, *args, **kwargs): error=str(e), exc_info=True, ) + + # Handle report unamending + self.reports.update(can_unamend=False) elif skip_aggregation: # Clear the flag for next save self._skip_aggregation = False @@ -655,6 +659,45 @@ def delete_coupled_transactions(self): parent.refresh_from_db(fields=["deleted"]) parent.delete() + def add_to_report(self, report_id): + ReportTransaction = apps.get_model("reports.ReportTransaction") + report_transaction = ReportTransaction.objects.filter( + transaction=self, + report_id=report_id + ).first() + + if report_transaction is None: + ReportTransaction.objects.create( + transaction=self, + report_id=report_id + ) + + def remove_from_report(self, report_id): + ReportTransaction = apps.get_model("reports.ReportTransaction") + report_transaction = ReportTransaction.objects.filter( + transaction=self, + report_id=report_id + ).first() + + if report_transaction is not None: + report_transaction.delete() + + def set_reports(self, report_ids): + current_report_ids = set() + current_report_id_dicts = list(self.reports.values("id")) + for report_id_dict in current_report_id_dicts: + current_report_ids.add(str(report_id_dict["id"])) + + updated_report_ids = set(report_ids) + report_ids_to_reset_can_unamend = current_report_ids ^ updated_report_ids + + Report = apps.get_model("reports.Report") + Report.objects.filter( + id__in=report_ids_to_reset_can_unamend + ).update(can_unamend=False) + + self.reports.set(report_ids) + class Meta: indexes = [models.Index(fields=["_form_type"])] diff --git a/django-backend/fecfiler/transactions/schedule_c/tests/test_views.py b/django-backend/fecfiler/transactions/schedule_c/tests/test_views.py index 1d50d20d66..f7ad747f04 100644 --- a/django-backend/fecfiler/transactions/schedule_c/tests/test_views.py +++ b/django-backend/fecfiler/transactions/schedule_c/tests/test_views.py @@ -58,7 +58,7 @@ def setUp(self): self.loan.save() self.loan.memo_text.transaction_uuid = self.loan.id self.loan.memo_text.save() - self.loan.reports.add(self.report_1) + self.loan.add_to_report(self.report_1.id) def test_create_loan_in_future_report(self): save_hook(self.loan, False) diff --git a/django-backend/fecfiler/transactions/schedule_c2/tests/test_views.py b/django-backend/fecfiler/transactions/schedule_c2/tests/test_views.py index bd0cc65ee5..c95c559515 100644 --- a/django-backend/fecfiler/transactions/schedule_c2/tests/test_views.py +++ b/django-backend/fecfiler/transactions/schedule_c2/tests/test_views.py @@ -40,7 +40,7 @@ def setUp(self): self.schedule_c.save() self.loan.schedule_c = self.schedule_c self.loan.save() - self.loan.reports.add(self.report_1) + self.loan.add_to_report(self.report_1.id) self.report_2 = Report( form_type="F3XN", @@ -63,7 +63,7 @@ def setUp(self): self.schedule_c2.save() self.guarantor.schedule_c2 = self.schedule_c2 self.guarantor.save() - self.guarantor.reports.add(self.report_1) + self.guarantor.add_to_report(self.report_1.id) def test_create_guarantor_in_future_report(self): c2_hook(self.guarantor, False) diff --git a/django-backend/fecfiler/transactions/schedule_d/tests/test_views.py b/django-backend/fecfiler/transactions/schedule_d/tests/test_views.py index 41cfb476a0..2d4be47c8d 100644 --- a/django-backend/fecfiler/transactions/schedule_d/tests/test_views.py +++ b/django-backend/fecfiler/transactions/schedule_d/tests/test_views.py @@ -42,7 +42,7 @@ def setUp(self): self.schedule_d.save() self.debt.schedule_d = self.schedule_d self.debt.save() - self.debt.reports.add(self.report_1) + self.debt.add_to_report(self.report_1.id) def test_create_debt_in_future_report(self): save_hook(self.debt, False) diff --git a/django-backend/fecfiler/transactions/tests/test_manager.py b/django-backend/fecfiler/transactions/tests/test_manager.py index a9442419d5..d30eab8eac 100644 --- a/django-backend/fecfiler/transactions/tests/test_manager.py +++ b/django-backend/fecfiler/transactions/tests/test_manager.py @@ -289,7 +289,7 @@ def test_debts(self): q1_report = create_form3x(self.committee, "2024-01-01", "2024-02-01", {}) original_debt = create_debt(self.committee, self.contact_1, Decimal("123.00")) original_debt.save() - original_debt.reports.add(q1_report) + original_debt.add_to_report(q1_report.id) first_repayment = create_schedule_b( "OPERATING_EXPENDITURE", self.committee, diff --git a/django-backend/fecfiler/transactions/tests/test_models.py b/django-backend/fecfiler/transactions/tests/test_models.py index 82709a41d8..2cbd92dc86 100644 --- a/django-backend/fecfiler/transactions/tests/test_models.py +++ b/django-backend/fecfiler/transactions/tests/test_models.py @@ -1,6 +1,7 @@ from decimal import Decimal from django.test import TestCase from fecfiler.reports.tests.utils import create_form3x +from fecfiler.reports.models import ReportTransaction from fecfiler.committee_accounts.models import CommitteeAccount from fecfiler.transactions.models import Transaction from fecfiler.memo_text.models import MemoText @@ -239,7 +240,7 @@ def test_delete_debt_transactions(self): carried forward copies of it (along with repayments to them)""" original_debt = create_debt(self.committee, self.contact_1, Decimal("123.00")) original_debt.save() - original_debt.reports.add(self.q1_report) + original_debt.add_to_report(self.q1_report.id) carried_forward_debt = carry_forward_debt(original_debt, self.m1_report) first_repayment = create_schedule_b( "OPERATING_EXPENDITURE", @@ -1465,6 +1466,110 @@ def test_reaggregation_after_report_deletion(self): # Transaction 4 should now have aggregate of $20 (10 + 10) self.assertEqual(transaction_4.aggregate, Decimal("20.00")) + def test_creating_a_transaction_resets_can_unamend(self): + # Create a report that can be unamended + report_1 = create_form3x(self.committee, "2024-01-01", "2024-01-31", {}) + report_1.can_unamend = True + report_1.save() + + # Create a new transaction + transaction_1 = create_schedule_a( + "INDIVIDUAL_RECEIPT", + self.committee, + self.contact_3, + "2024-01-05", + "100.00", + report=report_1, + ) + transaction_1.refresh_from_db() + + # The report should no longer be able to be unamended + report_1.refresh_from_db() + self.assertFalse(report_1.can_unamend) + + def test_updating_a_transaction_resets_can_unamend(self): + # Create a report that can be unamended + report_1 = create_form3x(self.committee, "2024-01-01", "2024-01-31", {}) + + # Create a new transaction + transaction_1 = create_schedule_a( + "INDIVIDUAL_RECEIPT", + self.committee, + self.contact_3, + "2024-01-05", + "100.00", + report=report_1, + ) + transaction_1.refresh_from_db() + + # Set can_unamend to True + report_1.can_unamend = True + report_1.save() + + # Saving the transaction should reset can_unamend + transaction_1.save() + report_1.refresh_from_db() + self.assertFalse(report_1.can_unamend) + + def test_deleting_a_transaction_resets_can_unamend(self): + # Create a report that can be unamended + report_1 = create_form3x(self.committee, "2024-01-01", "2024-01-31", {}) + + # Create a new transaction + transaction_1 = create_schedule_a( + "INDIVIDUAL_RECEIPT", + self.committee, + self.contact_3, + "2024-01-05", + "100.00", + report=report_1, + ) + transaction_1.refresh_from_db() + + # Set can_unamend to True + report_1.can_unamend = True + report_1.save() + + # Deleting the transaction should reset can_unamend + transaction_1.delete() + report_1.refresh_from_db() + self.assertFalse(report_1.can_unamend) + + def test_updating_a_transaction_resets_can_unamend_in_all_affiliated_reports(self): + # Create a report that can be unamended + report_1 = create_form3x(self.committee, "2024-01-01", "2024-01-31", {}) + + # Create a new transaction + transaction_1 = create_schedule_a( + "INDIVIDUAL_RECEIPT", + self.committee, + self.contact_3, + "2024-01-05", + "100.00", + report=report_1, + ) + transaction_1.refresh_from_db() + + # Set can_unamend to True + report_1.can_unamend = True + report_1.save() + + # Updating a transaction should reset can_unamend in all affiliated reports + report_2 = create_form3x(self.committee, "2024-02-01", "2024-02-28", {}) + report_2.can_unamend = True + report_2.save() + + ReportTransaction.objects.create( + transaction=transaction_1, + report=report_2 + ) + + transaction_1.save() + report_1.refresh_from_db() + report_2.refresh_from_db() + self.assertFalse(report_1.can_unamend) + self.assertFalse(report_2.can_unamend) + def undelete(transaction): transaction.deleted = None diff --git a/django-backend/fecfiler/transactions/tests/utils.py b/django-backend/fecfiler/transactions/tests/utils.py index 06ba64db8e..386209153a 100644 --- a/django-backend/fecfiler/transactions/tests/utils.py +++ b/django-backend/fecfiler/transactions/tests/utils.py @@ -298,7 +298,7 @@ def create_test_transaction( **(transaction_data or {}) ) if report: - create_report_transaction(report, transaction) + transaction.set_reports([report.id]) return transaction diff --git a/django-backend/fecfiler/transactions/views.py b/django-backend/fecfiler/transactions/views.py index ed63463082..8a0ac895a6 100644 --- a/django-backend/fecfiler/transactions/views.py +++ b/django-backend/fecfiler/transactions/views.py @@ -236,7 +236,7 @@ def add_transaction_to_report(self, request): transaction = Transaction.objects.get(id=request.data.get("transaction_id")) transactions = transaction.get_transaction_family() for t in transactions: - t.reports.add(report) + t.add_to_report(report.id) except Transaction.DoesNotExist: return Response("No transaction matching id provided", status=404) @@ -254,7 +254,8 @@ def remove_transaction_from_report(self, request): except Transaction.DoesNotExist: return Response("No transaction matching id provided", status=404) - transaction.reports.remove(report) + transaction.remove_from_report(report.id) + return Response("Transaction removed from report") @action(detail=False, methods=["get"], url_path=r"previous/entity") @@ -514,7 +515,9 @@ def save_transaction(self, transaction_data, request): transaction_instance = transaction_serializer.save(**save_kwargs) # Link the transaction to all the reports it references in report_ids - transaction_instance.reports.set(report_ids) + transaction_instance.set_reports(report_ids) + + # handle loans and debts if transaction_instance.schedule_c or transaction_instance.schedule_d: reports = Report.objects.filter(id__in=report_ids) coverage_through_date = None diff --git a/django-backend/fecfiler/user/migrations/0001_initial.py b/django-backend/fecfiler/user/migrations/0001_initial.py index b8db93a851..9abe4469a8 100644 --- a/django-backend/fecfiler/user/migrations/0001_initial.py +++ b/django-backend/fecfiler/user/migrations/0001_initial.py @@ -44,7 +44,6 @@ class Migration(migrations.Migration): verbose_name="superuser status", ), ), - ("cmtee_id", models.CharField(max_length=9)), ( "username", models.CharField( diff --git a/django-backend/fecfiler/user/migrations/0002_remove_user_cmtee_id.py b/django-backend/fecfiler/user/migrations/0002_remove_user_cmtee_id.py deleted file mode 100644 index bf1ee44fc1..0000000000 --- a/django-backend/fecfiler/user/migrations/0002_remove_user_cmtee_id.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 4.2.7 on 2024-01-25 14:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("user", "0001_initial"), - ("committee_accounts", "0002_membership"), - ] - - operations = [ - migrations.RemoveField( - model_name="user", - name="cmtee_id", - ), - migrations.AlterField( - model_name="user", - name="first_name", - field=models.CharField(blank=True, max_length=150, null=True), - ), - migrations.AlterField( - model_name="user", - name="last_name", - field=models.CharField(blank=True, max_length=150, null=True), - ), - ] diff --git a/django-backend/fecfiler/user/migrations/0002_remove_user_cmtee_id_squashed_0007_user_security_consent_version.py b/django-backend/fecfiler/user/migrations/0002_remove_user_cmtee_id_squashed_0007_user_security_consent_version.py new file mode 100644 index 0000000000..3a6007397f --- /dev/null +++ b/django-backend/fecfiler/user/migrations/0002_remove_user_cmtee_id_squashed_0007_user_security_consent_version.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.11 on 2026-03-06 22:06 + +import fecfiler.user.managers +from django.db import migrations, models + + +class Migration(migrations.Migration): + + replaces = [ + ("user", "0002_remove_user_cmtee_id"), + ("user", "0003_user_security_consent_date"), + ("user", "0004_alter_user_managers"), + ("user", "0005_rename_security_consent_date_user_security_consent_exp_date"), + ("user", "0006_remove_old_login_accounts"), + ("user", "0007_user_security_consent_version"), + ] + + dependencies = [ + ( + "committee_accounts", + "0001_squashed_0007_alter_committeeaccount_members", + ), + ("user", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="first_name", + field=models.CharField(blank=True, max_length=150, null=True), + ), + migrations.AlterField( + model_name="user", + name="last_name", + field=models.CharField(blank=True, max_length=150, null=True), + ), + migrations.AddField( + model_name="user", + name="security_consent_exp_date", + field=models.DateField(blank=True, null=True), + ), + migrations.AlterModelManagers( + name="user", + managers=[ + ("objects", fecfiler.user.managers.UserManager()), + ], + ), + migrations.AddField( + model_name="user", + name="security_consent_version", + field=models.CharField(blank=True, null=True), + ), + ] diff --git a/django-backend/fecfiler/user/migrations/0003_user_security_consent_date.py b/django-backend/fecfiler/user/migrations/0003_user_security_consent_date.py deleted file mode 100644 index 33d45b9a97..0000000000 --- a/django-backend/fecfiler/user/migrations/0003_user_security_consent_date.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.7 on 2024-02-01 22:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('user', '0002_remove_user_cmtee_id'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='security_consent_date', - field=models.DateField(blank=True, null=True), - ), - ] diff --git a/django-backend/fecfiler/user/migrations/0004_alter_user_managers.py b/django-backend/fecfiler/user/migrations/0004_alter_user_managers.py deleted file mode 100644 index dd7809c987..0000000000 --- a/django-backend/fecfiler/user/migrations/0004_alter_user_managers.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.2.7 on 2024-02-16 20:43 - -from django.db import migrations -import fecfiler.user.managers - - -class Migration(migrations.Migration): - - dependencies = [ - ('user', '0003_user_security_consent_date'), - ] - - operations = [ - migrations.AlterModelManagers( - name='user', - managers=[ - ('objects', fecfiler.user.managers.UserManager()), - ], - ), - ] diff --git a/django-backend/fecfiler/user/migrations/0005_rename_security_consent_date_user_security_consent_exp_date.py b/django-backend/fecfiler/user/migrations/0005_rename_security_consent_date_user_security_consent_exp_date.py deleted file mode 100644 index 671e09b092..0000000000 --- a/django-backend/fecfiler/user/migrations/0005_rename_security_consent_date_user_security_consent_exp_date.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.7 on 2024-03-11 14:58 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("user", "0004_alter_user_managers"), - ] - - operations = [ - migrations.RenameField( - model_name="user", - old_name="security_consent_date", - new_name="security_consent_exp_date", - ), - ] diff --git a/django-backend/fecfiler/user/migrations/0006_remove_old_login_accounts.py b/django-backend/fecfiler/user/migrations/0006_remove_old_login_accounts.py deleted file mode 100644 index f281f6bbd1..0000000000 --- a/django-backend/fecfiler/user/migrations/0006_remove_old_login_accounts.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.db import migrations -from django.db.models import Q - - -def remove_old_login_accounts(apps, schema_editor): - User = apps.get_model("user", "User") # noqa - - users_to_delete = User.objects.filter( - Q(username__contains="@") | Q(username="adminnxg") | Q(username="tt") - ) - for user in users_to_delete: - user.membership_set.all().delete() - users_to_delete.delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ( - "user", - "0005_rename_security_consent_date_user_security_consent_exp_date", - ) - ] - - operations = [ - migrations.RunPython( - remove_old_login_accounts, - migrations.RunPython.noop, - ), - ] diff --git a/django-backend/fecfiler/user/migrations/0007_user_security_consent_version.py b/django-backend/fecfiler/user/migrations/0007_user_security_consent_version.py deleted file mode 100644 index eb25f6d77c..0000000000 --- a/django-backend/fecfiler/user/migrations/0007_user_security_consent_version.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.2 on 2025-10-03 16:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('user', '0006_remove_old_login_accounts'), - ] - - operations = [ - migrations.AddField( - model_name='user', - name='security_consent_version', - field=models.CharField(blank=True, null=True), - ), - ] diff --git a/django-backend/fecfiler/web_services/dot_fec/dot_fec_composer.py b/django-backend/fecfiler/web_services/dot_fec/dot_fec_composer.py index 8010bb1573..cd5f1a7998 100644 --- a/django-backend/fecfiler/web_services/dot_fec/dot_fec_composer.py +++ b/django-backend/fecfiler/web_services/dot_fec/dot_fec_composer.py @@ -120,7 +120,7 @@ def compose_header(report_id): "HDR", "FEC", FEC_FORMAT_VERSION, - "FECFile Online", + "FECfile+", "0.0.1", report.report_id, report.report_version, diff --git a/django-backend/fecfiler/web_services/dot_fec/tests/test_dot_fec_composer.py b/django-backend/fecfiler/web_services/dot_fec/tests/test_dot_fec_composer.py index decb5e3b0c..a76badfb61 100644 --- a/django-backend/fecfiler/web_services/dot_fec/tests/test_dot_fec_composer.py +++ b/django-backend/fecfiler/web_services/dot_fec/tests/test_dot_fec_composer.py @@ -84,7 +84,7 @@ def setUp(self): "GENERAL", "SA11AI", ) - self.transaction.reports.add(self.f3x) + self.transaction.add_to_report(self.f3x.id) self.transaction.save() self.report_level_memo = create_report_memo( self.committee, @@ -137,7 +137,7 @@ def test_row_contains_aggregate(self): "SA11AI", ) for transaction in [earlier_transaction, later_transaction]: - transaction.reports.add(self.f3x) + transaction.add_to_report(self.f3x.id) transaction.save() later_transaction.refresh_from_db() diff --git a/django-backend/fecfiler/web_services/dot_fec/tests/test_dot_fec_serializer.py b/django-backend/fecfiler/web_services/dot_fec/tests/test_dot_fec_serializer.py index 04cae5ce00..f164840b13 100644 --- a/django-backend/fecfiler/web_services/dot_fec/tests/test_dot_fec_serializer.py +++ b/django-backend/fecfiler/web_services/dot_fec/tests/test_dot_fec_serializer.py @@ -51,7 +51,7 @@ def setUp(self): "GENERAL", "SA11AI", ) - self.transaction.reports.add(self.f3x) + self.transaction.add_to_report(self.f3x.id) self.transaction.save() self.schc_transaction1 = create_loan( @@ -75,7 +75,7 @@ def setUp(self): self.committee, self.f3x, "dahtest2" ) - self.header = Header("HDR", "FEC", "8.5", "FECFile Online", "0.0.1") + self.header = Header("HDR", "FEC", "8.5", "FECfile+", "0.0.1") def test_serialize_field(self): f3x_field_mappings = get_field_mappings("F3X") @@ -186,7 +186,7 @@ def test_serialize_header_instance(self): self.assertEqual(split_row[0], "HDR") self.assertEqual(split_row[1], "FEC") self.assertEqual(split_row[2], "8.5") - self.assertEqual(split_row[3], "FECFile Online") + self.assertEqual(split_row[3], "FECfile+") self.assertEqual(split_row[4], "0.0.1") def test_get_value_from_path(self): diff --git a/django-backend/fecfiler/web_services/migrations/0001_initial.py b/django-backend/fecfiler/web_services/migrations/0001_initial_squashed_0003_polling_attempts.py similarity index 87% rename from django-backend/fecfiler/web_services/migrations/0001_initial.py rename to django-backend/fecfiler/web_services/migrations/0001_initial_squashed_0003_polling_attempts.py index b1b05f1d0b..2678ce9ef2 100644 --- a/django-backend/fecfiler/web_services/migrations/0001_initial.py +++ b/django-backend/fecfiler/web_services/migrations/0001_initial_squashed_0003_polling_attempts.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-01-16 20:15 +# Manually squashed by Dan-go 48.0.12 on 2026-03-12 16:15 from django.db import migrations, models import django.db.models.deletion @@ -8,6 +8,12 @@ class Migration(migrations.Migration): initial = True + replaces = [ + ("web_services", "0001_initial"), + ("web_services", "0002_uploadsubmission_task_completed_and_more"), + ("web_services", "0003_uploadsubmission_fecfile_polling_attempts_and_more"), + ] + dependencies = [ ("reports", "0001_initial"), ] @@ -64,6 +70,8 @@ class Migration(migrations.Migration): ("fec_image_url", models.CharField(max_length=255, null=True)), ("fec_batch_id", models.CharField(max_length=255, null=True)), ("fec_email", models.CharField(max_length=255, null=True)), + ("task_completed", models.DateTimeField(null=True)), + ("fecfile_polling_attempts", models.IntegerField(default=0)), ( "dot_fec", models.ForeignKey( @@ -98,6 +106,8 @@ class Migration(migrations.Migration): ("created", models.DateTimeField(auto_now_add=True)), ("updated", models.DateTimeField(auto_now=True)), ("fec_report_id", models.CharField(max_length=255, null=True)), + ("task_completed", models.DateTimeField(null=True)), + ("fecfile_polling_attempts", models.IntegerField(default=0)), ( "dot_fec", models.ForeignKey( diff --git a/django-backend/fecfiler/web_services/migrations/0002_uploadsubmission_task_completed_and_more.py b/django-backend/fecfiler/web_services/migrations/0002_uploadsubmission_task_completed_and_more.py deleted file mode 100644 index 1ee32610c1..0000000000 --- a/django-backend/fecfiler/web_services/migrations/0002_uploadsubmission_task_completed_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.0.8 on 2024-08-22 19:42 - -from django.db import migrations, models -from django.db.models import F - - -def set_default_task_completed_times(apps, schema_editor): - uploads = apps.get_model("web_services", "UploadSubmission") - web_prints = apps.get_model("web_services", "WebPrintSubmission") - - uploads.objects.all().update(task_completed=F("updated")) - web_prints.objects.all().update(task_completed=F("updated")) - - -class Migration(migrations.Migration): - - dependencies = [ - ('web_services', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='uploadsubmission', - name='task_completed', - field=models.DateTimeField(null=True), - ), - migrations.AddField( - model_name='webprintsubmission', - name='task_completed', - field=models.DateTimeField(null=True), - ), - migrations.RunPython(set_default_task_completed_times, migrations.RunPython.noop) - ] diff --git a/django-backend/fecfiler/web_services/migrations/0003_uploadsubmission_fecfile_polling_attempts_and_more.py b/django-backend/fecfiler/web_services/migrations/0003_uploadsubmission_fecfile_polling_attempts_and_more.py deleted file mode 100644 index 86f41f0dd1..0000000000 --- a/django-backend/fecfiler/web_services/migrations/0003_uploadsubmission_fecfile_polling_attempts_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.0.8 on 2024-09-11 12:22 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('web_services', '0002_uploadsubmission_task_completed_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='uploadsubmission', - name='fecfile_polling_attempts', - field=models.IntegerField(default=0), - ), - migrations.AddField( - model_name='webprintsubmission', - name='fecfile_polling_attempts', - field=models.IntegerField(default=0), - ), - ] diff --git a/django-backend/fecfiler/web_services/migrations/0004_uploadsubmission_date_filed.py b/django-backend/fecfiler/web_services/migrations/0004_uploadsubmission_date_filed.py index 47959e42cc..e057f5db83 100644 --- a/django-backend/fecfiler/web_services/migrations/0004_uploadsubmission_date_filed.py +++ b/django-backend/fecfiler/web_services/migrations/0004_uploadsubmission_date_filed.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ('web_services', '0003_uploadsubmission_fecfile_polling_attempts_and_more'), + ('web_services', '0001_initial_squashed_0003_polling_attempts'), ] operations = [ diff --git a/django-backend/fecfiler/web_services/summary/test_tasks.py b/django-backend/fecfiler/web_services/summary/test_tasks.py index 4167f4ff51..ee0d6ccd74 100644 --- a/django-backend/fecfiler/web_services/summary/test_tasks.py +++ b/django-backend/fecfiler/web_services/summary/test_tasks.py @@ -193,7 +193,7 @@ def test_report_group_recalculation_year_to_year(self): data["memo"], data["itemized"], ) - scha.reports.add(data["report"]) + scha.add_to_report(data["report"].id) scha.save() calculate_summary(next_year_report.id) @@ -325,7 +325,7 @@ def test_cash_on_hand_override_between_reports(self): data["memo"], data["itemized"], ) - scha.reports.add(data["report"]) + scha.add_to_report(data["report"].id) scha.save() years_later_report = create_form3x( @@ -369,7 +369,7 @@ def test_cash_on_hand_override_for_last_year(self): False, True, ) - schedule_a.reports.add(first_report) + schedule_a.add_to_report(first_report.id) schedule_a.save() next_year_report = create_form3x( diff --git a/django-backend/fecfiler/web_services/summary/tests/utils.py b/django-backend/fecfiler/web_services/summary/tests/utils.py index 96fc1e89bc..407e7d6629 100644 --- a/django-backend/fecfiler/web_services/summary/tests/utils.py +++ b/django-backend/fecfiler/web_services/summary/tests/utils.py @@ -860,7 +860,7 @@ def gen_schedule_a(transaction_data, f3x, committee, contact): data["memo"], data["itemized"], ) - scha.reports.add(f3x) + scha.add_to_report(f3x.id) scha.save() if data["form_type"] == "SA11AII": debt = scha @@ -879,7 +879,7 @@ def gen_schedule_b(transaction_data, f3x, committee, contact): data["form_type"], ) - schb.reports.add(f3x) + schb.add_to_report(f3x.id) schb.save() @@ -895,7 +895,7 @@ def gen_schedule_c(transaction_data, f3x, committee, contact): "LOAN_RECEIVED_FROM_INDIVIDUAL", data["form_type"], ) - schc.reports.add(f3x) + schc.add_to_report(f3x.id) def gen_schedule_d(transaction_data, f3x, committee, contact): @@ -924,7 +924,7 @@ def gen_schedule_e(transaction_data, f3x, committee, contact, candidate): candidate, data["memo_code"], ) - sche.reports.add(f3x) + sche.add_to_report(f3x.id) def gen_schedule_f( @@ -958,5 +958,5 @@ def gen_schedule_f( }, ) - schf.reports.add(f3x) + schf.add_to_report(f3x.id) schf.save() diff --git a/django-backend/manage.py b/django-backend/manage.py index f2b41e9467..c78f75a5eb 100755 --- a/django-backend/manage.py +++ b/django-backend/manage.py @@ -26,6 +26,8 @@ "fail_open_submissions", "dump_committee_data", "get_overview", + "disable_committee_account", + "enable_committee_account", ] restricted_commands = [ "loaddata", diff --git a/performance-testing/locust_run.py b/performance-testing/locust_run.py index 9105cee655..e59679c8c5 100644 --- a/performance-testing/locust_run.py +++ b/performance-testing/locust_run.py @@ -39,6 +39,9 @@ # Tracks state of Payload Contacts between test runs CREATED_PAYLOAD_CONTACTS = False +# Do we want long transaction chains? Accepts "true"/"True"/"1", otherwise false +LONG_CHAINS = str(os.environ.get("LONG_CHAINS", "false")).lower() in ("true", "1") + # Lower the interval between log reports to prevent log queue overflow runners.WORKER_LOG_REPORT_INTERVAL = 2 @@ -92,9 +95,9 @@ class Tasks(TaskSet): "CAN": {}, "COM": {}, } - last_created_schedule_a = None - last_created_schedule_b = None - last_created_schedule_c = None + saved_schedule_a = None + saved_schedule_b = None + saved_schedule_c = None def on_start(self): logging.info("Logging in") @@ -235,7 +238,11 @@ def create_schedule_a_transaction(self): ) if response.status_code != 200: raise Exception("Failed to POST new Schedule A transaction") - self.last_created_schedule_a = response.json() + + # if LONG_CHAINS is true then we only want to save a "first" transaction, + # otherwise we always save the current one so we have the last transaction + if not self.saved_schedule_a or not LONG_CHAINS: + self.saved_schedule_a = response.json() @task(ceil(DATA_ENTRY_WEIGHT * SCHEDULE_B_MULTIPLIER / 2)) def create_schedule_b_transaction(self): @@ -269,7 +276,8 @@ def create_schedule_b_transaction(self): ) if response.status_code != 200: raise Exception("Failed to POST new Schedule B transaction") - self.last_created_schedule_b = response.json() + if not self.saved_schedule_b or not LONG_CHAINS: + self.saved_schedule_b = response.json() @task(ceil(DATA_ENTRY_WEIGHT * SCHEDULE_B_MULTIPLIER / 2)) def create_schedule_b_election_transaction(self): @@ -309,7 +317,8 @@ def create_schedule_b_election_transaction(self): ) if response.status_code != 200: raise Exception("Failed to POST new Schedule B transaction") - self.last_created_schedule_b = response.json() + if not self.saved_schedule_b or not LONG_CHAINS: + self.saved_schedule_b = response.json() @task(ceil(DATA_ENTRY_WEIGHT * SCHEDULE_C_MULTIPLIER)) def create_schedule_c_transaction(self): @@ -344,7 +353,8 @@ def create_schedule_c_transaction(self): ) if response.status_code != 200: raise Exception("Failed to POST new Schedule C transaction") - self.last_created_schedule_c = response.json() + if not self.saved_schedule_c or not LONG_CHAINS: + self.saved_schedule_c = response.json() @task(ceil(DATA_ENTRY_WEIGHT * SCHEDULE_D_MULTIPLIER)) def create_schedule_d_transaction(self): @@ -373,7 +383,7 @@ def create_schedule_d_transaction(self): ceil(DATA_ENTRY_WEIGHT * SCHEDULE_A_MULTIPLIER * UPDATE_TRANSACTION_MULTIPLIER) ) def update_schedule_a_transaction(self): - transaction_id = self.last_created_schedule_a + transaction_id = self.saved_schedule_a if transaction_id: response = self.client_get( f"/api/v1/transactions/{transaction_id}/", @@ -413,6 +423,9 @@ def delete_schedule_a_transaction(self): name="delete_schedule_a_transaction", ) if response.status_code == 204: + # if we happened to delete our saved pointer, clear it so it'll reset + if transaction == self.saved_schedule_a: + self.saved_schedule_a = None return raise Exception("Failed to DELETE Schedule A transaction") @@ -420,7 +433,7 @@ def delete_schedule_a_transaction(self): ceil(DATA_ENTRY_WEIGHT * SCHEDULE_B_MULTIPLIER * UPDATE_TRANSACTION_MULTIPLIER) ) def update_schedule_b_transaction(self): - transaction_id = self.last_created_schedule_b + transaction_id = self.saved_schedule_b if transaction_id: response = self.client_get( f"/api/v1/transactions/{transaction_id}/", @@ -456,7 +469,7 @@ def update_schedule_b_transaction(self): ceil(DATA_ENTRY_WEIGHT * SCHEDULE_C_MULTIPLIER * UPDATE_TRANSACTION_MULTIPLIER) ) def update_schedule_c_transaction(self): - transaction_id = self.last_created_schedule_c + transaction_id = self.saved_schedule_c if transaction_id: response = self.client_get( f"/api/v1/transactions/{transaction_id}/", diff --git a/requirements-test.txt b/requirements-test.txt index 9db851fad8..212d631873 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -7,3 +7,4 @@ sphinx==7.2.6 django-silk==5.4.3 static3==0.7.0 dj-static==0.0.6 +tblib==3.1.0 diff --git a/requirements.txt b/requirements.txt index a6857ba961..4aa7e6e6b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,18 +7,18 @@ django-cors-headers==4.7.0 django-deprecate-fields==0.2.1 django-migration-linter==5.2.0 django-structlog==9.1.1 -Django==5.2.11 +Django==5.2.12 djangorestframework==3.16.0 drf-spectacular==0.28.0 Faker==37.6.0 -git+https://github.com/fecgov/fecfile-validate@0cf017439a59a08a7bed02e2069045708887209c#egg=fecfile_validate&subdirectory=fecfile_validate_python +git+https://github.com/fecgov/fecfile-validate@0322bb973333f175d45d2b4b06de470a0d5c48d7#egg=fecfile_validate&subdirectory=fecfile_validate_python github3.py==4.0.1 GitPython==3.1.43 gunicorn==23.0.0 invoke==2.2.0 jwcrypto==1.5.6 psycopg[pool]==3.3.0 -PyJWT==2.10.1 +PyJWT==2.12.0 redis==4.5.5 requests==2.32.4 structlog==25.3.0 diff --git a/runtime.txt b/runtime.txt index 64f28603a3..c94f67640a 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.12.x +python-3.13.x