From 63f33ec13eccd3c51744afb1c4e0617c7ffb97b4 Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Thu, 3 Sep 2026 15:10:59 -0400 Subject: [PATCH 1/2] Setup unassigning transactions --- .../committee_accounts/utils/accounts.py | 2 + .../fecfiler/reports/form_3/serializers.py | 74 +++++++++---------- .../reports/form_3/tests/test_serializers.py | 25 ++++--- .../fecfiler/transactions/tests/test_views.py | 26 +++---- django-backend/fecfiler/transactions/views.py | 33 ++++++++- 5 files changed, 96 insertions(+), 64 deletions(-) diff --git a/django-backend/fecfiler/committee_accounts/utils/accounts.py b/django-backend/fecfiler/committee_accounts/utils/accounts.py index 9e77fc296f..b8f5ec51c8 100644 --- a/django-backend/fecfiler/committee_accounts/utils/accounts.py +++ b/django-backend/fecfiler/committee_accounts/utils/accounts.py @@ -194,6 +194,8 @@ def get_production_committee_data(committee_id): def get_eligible_report_types_processed(committee_data: dict): + + return ["F3", "F99", 'F3X', 'F24', 'F1M'] fallback_reports = ["F99"] if committee_data is None: logger.error( diff --git a/django-backend/fecfiler/reports/form_3/serializers.py b/django-backend/fecfiler/reports/form_3/serializers.py index 29f48b8d4f..39727637b1 100644 --- a/django-backend/fecfiler/reports/form_3/serializers.py +++ b/django-backend/fecfiler/reports/form_3/serializers.py @@ -1,4 +1,4 @@ -from django.db import transaction +from django.db.transaction import atomic from fecfiler.reports.models import Report, ReportTransaction from fecfiler.reports.form_3.models import Form3 from fecfiler.reports.serializers import ( @@ -19,13 +19,7 @@ logger = structlog.get_logger(__name__) -COVERAGE_DATES_EXCLUDE_EXISTING_TRANSACTIONS = ValidationError( - { - "coverage_from_date_and_coverage_to_date": [ - "Coverage date(s) exclude existing transaction(s) for report" - ] - } -) + class BaseForm3Serializer(ReportSerializer): @@ -62,45 +56,47 @@ def save(self, **kwargs): return super().save(**kwargs) def update(self, instance, validated_data): - prior_coverage_through_date = instance.coverage_through_date - prior_coverage_from_date = instance.coverage_from_date + with atomic(): + prior_coverage_through_date = instance.coverage_through_date + prior_coverage_from_date = instance.coverage_from_date - transactions_outside_coverage_dates = ReportTransaction.objects.filter( - ~Q(transaction__memo_code=True), - self.get_transaction_date_outside_coverage_dates_clause(), - transaction__deleted=None, - report_id=instance.id, - ).count() + transactions_outside_coverage_dates = ReportTransaction.objects.filter( + ~Q(transaction__memo_code=True), + self.get_transaction_date_outside_coverage_dates_clause(), + transaction__deleted=None, + report_id=instance.id, + ) + if transactions_outside_coverage_dates.exists(): + transactions_outside_coverage_dates.delete() + if instance.can_unamend: + instance.can_unamend = False - if transactions_outside_coverage_dates > 0: - raise COVERAGE_DATES_EXCLUDE_EXISTING_TRANSACTIONS + if self.overlaps_other_f3_report( + instance.id, + instance.committee_account.id, + validated_data, + ): + raise COVERAGE_DATE_REPORT_CODE_COLLISION - if self.overlaps_other_f3_report( - instance.id, - instance.committee_account.id, - validated_data, - ): - raise COVERAGE_DATE_REPORT_CODE_COLLISION - - form = getattr(instance, self.related_form_attr) + form = getattr(instance, self.related_form_attr) - for attr, value in validated_data.items(): - if attr != "id": - setattr(form, attr, value) + for attr, value in validated_data.items(): + if attr != "id": + setattr(form, attr, value) - form.save() + form.save() - updated = super().update(instance, validated_data) + updated = super().update(instance, validated_data) - coverage_from_changed = prior_coverage_from_date != updated.coverage_from_date - coverage_through_changed = ( - prior_coverage_through_date != updated.coverage_through_date - ) + coverage_from_changed = prior_coverage_from_date != updated.coverage_from_date + coverage_through_changed = ( + prior_coverage_through_date != updated.coverage_through_date + ) - if coverage_from_changed or coverage_through_changed: - Report.mark_calculations_dirty(Report.objects.filter(id=updated.id)) + if coverage_from_changed or coverage_through_changed: + Report.mark_calculations_dirty(Report.objects.filter(id=updated.id)) - return updated + return updated def get_transaction_date_outside_coverage_dates_clause(self): """Returns a clause that checks if the transaction date is outside @@ -407,7 +403,7 @@ class Form3Serializer(BaseForm3Serializer): ) def create(self, validated_data: dict): - with transaction.atomic(): + with atomic(): form_3_data = get_model_data(validated_data, Form3) report_data = get_model_data(validated_data, Report) form_3 = Form3.objects.create(**form_3_data) diff --git a/django-backend/fecfiler/reports/form_3/tests/test_serializers.py b/django-backend/fecfiler/reports/form_3/tests/test_serializers.py index 53961839fe..6704064b5a 100644 --- a/django-backend/fecfiler/reports/form_3/tests/test_serializers.py +++ b/django-backend/fecfiler/reports/form_3/tests/test_serializers.py @@ -2,11 +2,10 @@ from ..serializers import ( Form3Serializer, COVERAGE_DATE_REPORT_CODE_COLLISION, - COVERAGE_DATES_EXCLUDE_EXISTING_TRANSACTIONS, ) from fecfiler.user.models import User -from fecfiler.reports.models import Report +from fecfiler.reports.models import Report, ReportTransaction from rest_framework.request import Request, HttpRequest from fecfiler.reports.tests.utils import create_form3 from fecfiler.transactions.tests.utils import create_schedule_a @@ -170,27 +169,33 @@ def test_update_coverage_to_overlapping_dates(self): def test_update_coverage_to_exclude_transaction(self): report_a = create_form3(self.committee, "2024-01-01", "2024-03-31") - create_schedule_a( + transaction = create_schedule_a( "INDIVIDUAL_RECEIPT", self.committee, None, "2024-03-31", 250, report=report_a ) + self.assertEqual(ReportTransaction.objects.filter(report=report_a).count(), 1) + serializer = Form3Serializer( data=self.valid_f3_report, context={"request": self.mock_request}, ) - serializer.is_valid() - self.assertRaises( - type(COVERAGE_DATES_EXCLUDE_EXISTING_TRANSACTIONS), - serializer.update, + self.assertTrue(serializer.is_valid()) + + serializer.update( report_a, { "coverage_from_date": datetime.strptime("2024-01-01", "%Y-%m-%d").date(), - "coverage_through_date": datetime.strptime( - "2024-02-28", "%Y-%m-%d" - ).date(), + "coverage_through_date": datetime.strptime("2024-02-28", "%Y-%m-%d").date(), }, ) + self.assertFalse( + ReportTransaction.objects.filter( + report=report_a, transaction=transaction + ).exists() + ) + self.assertEqual(ReportTransaction.objects.filter(report=report_a).count(), 0) + def test_update_coverage_to_exclude_memo_transaction(self): report_a = create_form3(self.committee, "2024-01-01", "2024-03-31") create_schedule_a( diff --git a/django-backend/fecfiler/transactions/tests/test_views.py b/django-backend/fecfiler/transactions/tests/test_views.py index 2492f44745..f24ec64bde 100644 --- a/django-backend/fecfiler/transactions/tests/test_views.py +++ b/django-backend/fecfiler/transactions/tests/test_views.py @@ -1085,7 +1085,7 @@ def test_multi_sorting(self): for i in range(ordered_queryset.count()): self.assertEqual(ordered_queryset[i].id, memos_sorted[i].id) - def test_list_unassociated(self): + def test_list_unassigned(self): Transaction.objects.filter(committee_account=self.committee).delete() indiviual_receipt_data = [ @@ -1127,7 +1127,7 @@ def test_list_unassociated(self): ) request = self.get_request( - "api/v1/transactions/list/unassociated", + "api/v1/transactions/list/unassigned", { "page": 1, "ordering": "amount", @@ -1139,16 +1139,16 @@ def test_list_unassociated(self): self.view.action = "list" self.view.format_kwarg = None - response = self.view.list_unassociated_transactions(request) + response = self.view.list_unassigned_transactions(request) transactions = response.data["results"] self.assertEqual(response.data["count"], 5) self.assertEqual(len(transactions), 2) self.assertEqual(transactions[0]["amount"], "100.00") - def test_list_unassociated_non_paginated(self): + def test_list_unassigned_non_paginated(self): request = self.get_request( - "api/v1/transactions/list/unassociated", + "api/v1/transactions/list/unassigned", { "ordering": "date", }, @@ -1158,11 +1158,11 @@ def test_list_unassociated_non_paginated(self): self.view.action = "list" self.view.format_kwarg = None - response = self.view.list_unassociated_transactions(request) + response = self.view.list_unassigned_transactions(request) self.assertEqual(response.status_code, 400) request = self.get_request( - "api/v1/transactions/list/unassociated", + "api/v1/transactions/list/unassigned", { "ordering": "date", "page": None, @@ -1170,11 +1170,11 @@ def test_list_unassociated_non_paginated(self): ) self.view.request = request - response = self.view.list_unassociated_transactions(request) + response = self.view.list_unassigned_transactions(request) self.assertEqual(response.status_code, 400) request = self.get_request( - "api/v1/transactions/list/unassociated", + "api/v1/transactions/list/unassigned", { "ordering": "date", "page": 1, @@ -1182,10 +1182,10 @@ def test_list_unassociated_non_paginated(self): ) self.view.request = request - response = self.view.list_unassociated_transactions(request) + response = self.view.list_unassigned_transactions(request) self.assertEqual(response.status_code, 200) - def test_list_unassociated_by_schedule(self): + def test_list_unassigned_by_schedule(self): Transaction.objects.filter(committee_account=self.committee).delete() indiviual_receipt_data = [ @@ -1222,7 +1222,7 @@ def test_list_unassociated_by_schedule(self): ) request = self.get_request( - "api/v1/transactions/list/unassociated", + "api/v1/transactions/list/unassigned", {"page": 1, "ordering": "-amount", "page_size": 5, "schedules": "B"}, ) @@ -1230,7 +1230,7 @@ def test_list_unassociated_by_schedule(self): self.view.action = "list" self.view.format_kwarg = None - response = self.view.list_unassociated_transactions(request) + response = self.view.list_unassigned_transactions(request) transactions = response.data["results"] self.assertEqual(response.data["count"], 2) diff --git a/django-backend/fecfiler/transactions/views.py b/django-backend/fecfiler/transactions/views.py index 11996bb284..281287ed8b 100644 --- a/django-backend/fecfiler/transactions/views.py +++ b/django-backend/fecfiler/transactions/views.py @@ -154,8 +154,8 @@ def get_queryset(self): return queryset - # @action(detail=False, methods=["get"], url_path=r"list/unassociated") - def list_unassociated_transactions(self, request, *args, **kwargs): + @action(detail=False, methods=["get"], url_path=r"list/unassigned") + def list_unassigned_transactions(self, request, *args, **kwargs): if "page" not in request.query_params or request.query_params["page"] is None: return Response("page is required", status=400) @@ -295,6 +295,35 @@ def remove_transaction_from_report(self, request): return Response("Transaction removed from report") + @action(detail=False, methods=["get"], url_path=r"outside") + def transactions_outside_report(self, request): + from_date_str = request.query_params.get("from") + through_date_str = request.query_params.get("through") + + if not from_date_str or not through_date_str: + return Response( + {"error": "Both 'from' and 'through' query parameters are required."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + try: + from_date = datetime.strptime(from_date_str, "%m/%d/%Y").date() + through_date = datetime.strptime(through_date_str, "%m/%d/%Y").date() + except ValueError: + return Response( + {"error": "Invalid date format. Use MM/DD/YYYY."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + queryset = self.filter_queryset(self.get_queryset()) + outside_queryset = queryset.filter(date__isnull=False).exclude( + date__range=(from_date, through_date) + ) + + count = outside_queryset.count() + return Response(count, status=status.HTTP_200_OK) + + @action(detail=False, methods=["get"], url_path=r"previous/entity") def previous_transaction_by_entity(self, request): contact_1_id = request.query_params["contact_1_id"] From 64771b2093a67da6ac571bb3cc582d17dd0ee345 Mon Sep 17 00:00:00 2001 From: Sasha Dresden Date: Fri, 4 Sep 2026 16:27:07 -0400 Subject: [PATCH 2/2] Limit unassigning transactions by flag --- .../committee_accounts/utils/accounts.py | 40 +++---- .../fecfiler/reports/form_3/serializers.py | 18 ++- .../reports/form_3/tests/test_serializers.py | 4 +- django-backend/fecfiler/settings/base.py | 4 + .../fecfiler/transactions/tests/test_views.py | 112 +++++++++++++++--- django-backend/fecfiler/transactions/views.py | 41 ++++--- docker-compose.yml | 1 + manifests/manifest-dev.yml | 1 + manifests/manifest-prod.yml | 1 + manifests/manifest-stage.yml | 1 + manifests/manifest-test.yml | 1 + 11 files changed, 167 insertions(+), 57 deletions(-) diff --git a/django-backend/fecfiler/committee_accounts/utils/accounts.py b/django-backend/fecfiler/committee_accounts/utils/accounts.py index b8f5ec51c8..b4be10a83f 100644 --- a/django-backend/fecfiler/committee_accounts/utils/accounts.py +++ b/django-backend/fecfiler/committee_accounts/utils/accounts.py @@ -112,11 +112,9 @@ def logout_committee_sessions(committee_id): sessions_to_delete.append(session.pk) Session.objects.filter(pk__in=sessions_to_delete).delete() - logger.info( - f""" + logger.info(f""" Successfully logged out {len(sessions_to_delete)} users from {committee_id} - """ - ) + """) def enable_committee_account(committee_id): @@ -194,8 +192,6 @@ def get_production_committee_data(committee_id): def get_eligible_report_types_processed(committee_data: dict): - - return ["F3", "F99", 'F3X', 'F24', 'F1M'] fallback_reports = ["F99"] if committee_data is None: logger.error( @@ -283,9 +279,9 @@ def augment_processed_committee_data(committee_data: dict): committee_data.get("committee_type") in PRODUCTION_QUALIFIED_COMMITTEES ) - committee_data[ - "eligible_report_types" - ] = get_eligible_report_types_processed(committee_data) + committee_data["eligible_report_types"] = get_eligible_report_types_processed( + committee_data + ) add_candidate_office_state_if_needed(committee_data) @@ -301,7 +297,7 @@ def add_candidate_office_state_if_needed(committee_data: dict): } candidate = query_fec_api_single( f"{settings.PRODUCTION_OPEN_FEC_API}committee/{committee_id}/candidates/", - params + params, ) if candidate: committee_data["candidate_office"] = candidate.get("office", None) @@ -365,9 +361,9 @@ def get_raw_committee_data(committee_id): committee_data["filing_frequency"] = "Q" committee_data = convert_raw_to_processed(committee_data) - committee_data[ - "eligible_report_types" - ] = get_eligible_report_types_raw(committee_data) + committee_data["eligible_report_types"] = get_eligible_report_types_raw( + committee_data + ) return committee_data @@ -433,9 +429,9 @@ def get_test_committee_data(committee_id): committee_data["filing_frequency"] = "Q" committee_data = convert_raw_to_processed(committee_data) - committee_data[ - "eligible_report_types" - ] = get_eligible_report_types_raw(committee_data) + committee_data["eligible_report_types"] = get_eligible_report_types_raw( + committee_data + ) return committee_data @@ -469,13 +465,13 @@ def get_mocked_committee_data(committee_id): if committee is not None: if committee.get("counts_as_processed"): - committee[ - "eligible_report_types" - ] = get_eligible_report_types_processed(committee) + committee["eligible_report_types"] = get_eligible_report_types_processed( + committee + ) else: - committee[ - "eligible_report_types" - ] = get_eligible_report_types_raw(committee) + committee["eligible_report_types"] = get_eligible_report_types_raw( + committee + ) return committee diff --git a/django-backend/fecfiler/reports/form_3/serializers.py b/django-backend/fecfiler/reports/form_3/serializers.py index 39727637b1..089804e298 100644 --- a/django-backend/fecfiler/reports/form_3/serializers.py +++ b/django-backend/fecfiler/reports/form_3/serializers.py @@ -1,4 +1,5 @@ from django.db.transaction import atomic +from fecfiler.settings import FLAG__ENABLE_UNASSIGNED_TRANSACTIONS from fecfiler.reports.models import Report, ReportTransaction from fecfiler.reports.form_3.models import Form3 from fecfiler.reports.serializers import ( @@ -19,7 +20,13 @@ logger = structlog.get_logger(__name__) - +COVERAGE_DATES_EXCLUDE_EXISTING_TRANSACTIONS = ValidationError( + { + "coverage_from_date_and_coverage_to_date": [ + "Coverage date(s) exclude existing transaction(s) for report" + ] + } +) class BaseForm3Serializer(ReportSerializer): @@ -67,9 +74,12 @@ def update(self, instance, validated_data): report_id=instance.id, ) if transactions_outside_coverage_dates.exists(): - transactions_outside_coverage_dates.delete() - if instance.can_unamend: - instance.can_unamend = False + if FLAG__ENABLE_UNASSIGNED_TRANSACTIONS: + transactions_outside_coverage_dates.delete() + if instance.can_unamend: + instance.can_unamend = False + else: + raise COVERAGE_DATES_EXCLUDE_EXISTING_TRANSACTIONS if self.overlaps_other_f3_report( instance.id, diff --git a/django-backend/fecfiler/reports/form_3/tests/test_serializers.py b/django-backend/fecfiler/reports/form_3/tests/test_serializers.py index 6704064b5a..694720b343 100644 --- a/django-backend/fecfiler/reports/form_3/tests/test_serializers.py +++ b/django-backend/fecfiler/reports/form_3/tests/test_serializers.py @@ -185,7 +185,9 @@ def test_update_coverage_to_exclude_transaction(self): report_a, { "coverage_from_date": datetime.strptime("2024-01-01", "%Y-%m-%d").date(), - "coverage_through_date": datetime.strptime("2024-02-28", "%Y-%m-%d").date(), + "coverage_through_date": datetime.strptime( + "2024-02-28", "%Y-%m-%d" + ).date(), }, ) diff --git a/django-backend/fecfiler/settings/base.py b/django-backend/fecfiler/settings/base.py index 4b276eb701..fc3d3aeb35 100644 --- a/django-backend/fecfiler/settings/base.py +++ b/django-backend/fecfiler/settings/base.py @@ -605,6 +605,10 @@ def get_logging_processors(): env.get_credential("FLAG__ENABLE_EMAIL", "False") ) +FLAG__ENABLE_UNASSIGNED_TRANSACTIONS = get_boolean_from_string( + env.get_credential("FLAG__ENABLE_UNASSIGNED_TRANSACTIONS", "False") +) + PRODUCTION_OPEN_FEC_API = env.get_credential("PRODUCTION_OPEN_FEC_API") PRODUCTION_OPEN_FEC_API_KEY = env.get_credential("PRODUCTION_OPEN_FEC_API_KEY") diff --git a/django-backend/fecfiler/transactions/tests/test_views.py b/django-backend/fecfiler/transactions/tests/test_views.py index f24ec64bde..0addf49af7 100644 --- a/django-backend/fecfiler/transactions/tests/test_views.py +++ b/django-backend/fecfiler/transactions/tests/test_views.py @@ -32,6 +32,7 @@ process_aggregation_by_payee_candidate, ) from fecfiler.transactions.schedule_d.views import create_in_future_reports +from unittest.mock import patch import structlog logger = structlog.get_logger(__name__) @@ -1085,6 +1086,7 @@ def test_multi_sorting(self): for i in range(ordered_queryset.count()): self.assertEqual(ordered_queryset[i].id, memos_sorted[i].id) + @patch("fecfiler.transactions.views.FLAG__ENABLE_UNASSIGNED_TRANSACTIONS", True) def test_list_unassigned(self): Transaction.objects.filter(committee_account=self.committee).delete() @@ -1146,6 +1148,7 @@ def test_list_unassigned(self): self.assertEqual(len(transactions), 2) self.assertEqual(transactions[0]["amount"], "100.00") + @patch("fecfiler.transactions.views.FLAG__ENABLE_UNASSIGNED_TRANSACTIONS", True) def test_list_unassigned_non_paginated(self): request = self.get_request( "api/v1/transactions/list/unassigned", @@ -1185,6 +1188,7 @@ def test_list_unassigned_non_paginated(self): response = self.view.list_unassigned_transactions(request) self.assertEqual(response.status_code, 200) + @patch("fecfiler.transactions.views.FLAG__ENABLE_UNASSIGNED_TRANSACTIONS", True) def test_list_unassigned_by_schedule(self): Transaction.objects.filter(committee_account=self.committee).delete() @@ -2801,9 +2805,7 @@ def test_adjust_running_totals_schedule_f_year_behavior(self): ) def test_committee_locked_parent_transaction(self): - other_committee = CommitteeAccount( - committee_id="C12344321" - ) + other_committee = CommitteeAccount(committee_id="C12344321") other_committee.save() @@ -2811,19 +2813,11 @@ def test_committee_locked_parent_transaction(self): "date": "2023-01-01", "amount": "200.00", "group": "GENERAL", - "memo": True + "memo": True, } - good_contact = create_test_individual_contact( - "Good", - "Guy", - other_committee.id - ) - evil_contact = create_test_individual_contact( - "Evil", - "Guy", - self.committee.id - ) + good_contact = create_test_individual_contact("Good", "Guy", other_committee.id) + evil_contact = create_test_individual_contact("Evil", "Guy", self.committee.id) original_transaction = create_schedule_a( "INDIVIDUAL_RECEIPT", @@ -2862,7 +2856,7 @@ def test_create_with_contact_of_other_committee_fails(self): payload, TransactionViewSet, "create", - committee=self.committee + committee=self.committee, ) self.assertEqual(response.status_code, 400) @@ -2999,3 +2993,91 @@ def test_update_transaction_with_debt_from_other_committee_fails(self): ) self.assertEqual(response.status_code, 400) + + @patch("fecfiler.transactions.views.FLAG__ENABLE_UNASSIGNED_TRANSACTIONS", True) + def test_transactions_outside_report_missing_params(self): + request = self.get_request("/api/v1/transactions/outside") + self.view.request = request + response = self.view.transactions_outside_report(request) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.data["error"], + "Both 'from' and 'through' query parameters are required.", + ) + + request = self.get_request( + "/api/v1/transactions/outside", params={"from": "01/01/2024"} + ) + self.view.request = request + response = self.view.transactions_outside_report(request) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + @patch("fecfiler.transactions.views.FLAG__ENABLE_UNASSIGNED_TRANSACTIONS", True) + def test_transactions_outside_report_invalid_date_format(self): + request = self.get_request( + "/api/v1/transactions/outside", + params={"from": "2024-01-01", "through": "03/31/2024"}, + ) + self.view.request = request + response = self.view.transactions_outside_report(request) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["error"], "Invalid date format. Use MM/DD/YYYY.") + + @patch("fecfiler.transactions.views.FLAG__ENABLE_UNASSIGNED_TRANSACTIONS", True) + def test_transactions_outside_report_success_and_memo_exclusion(self): + Transaction.objects.filter(committee_account=self.committee).delete() + + create_schedule_a( + "INDIVIDUAL_RECEIPT", + self.committee, + self.contact_1, + "2024-01-05", + "100.00", + memo_code=False, + ) + + create_schedule_a( + "INDIVIDUAL_RECEIPT", + self.committee, + self.contact_1, + "2024-01-10", + "150.00", + memo_code=None, + ) + + create_schedule_a( + "INDIVIDUAL_RECEIPT", + self.committee, + self.contact_1, + "2024-01-15", + "200.00", + memo_code=True, + ) + + create_schedule_a( + "INDIVIDUAL_RECEIPT", + self.committee, + self.contact_1, + "2024-02-15", + "300.00", + memo_code=False, + ) + + create_schedule_a( + "INDIVIDUAL_RECEIPT", + self.committee, + self.contact_1, + "2024-04-05", + "400.00", + memo_code=False, + ) + + request = self.get_request( + "/api/v1/transactions/outside", + params={"from": "02/01/2024", "through": "03/31/2024"}, + ) + self.view.request = request + response = self.view.transactions_outside_report(request) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, 3) diff --git a/django-backend/fecfiler/transactions/views.py b/django-backend/fecfiler/transactions/views.py index 281287ed8b..7ad6708619 100644 --- a/django-backend/fecfiler/transactions/views.py +++ b/django-backend/fecfiler/transactions/views.py @@ -1,7 +1,7 @@ from django.db import transaction as db_transaction, models from rest_framework import pagination from rest_framework.filters import OrderingFilter - +from fecfiler.settings import FLAG__ENABLE_UNASSIGNED_TRANSACTIONS from rest_framework.decorators import action from rest_framework.response import Response from rest_framework import status @@ -156,6 +156,11 @@ def get_queryset(self): @action(detail=False, methods=["get"], url_path=r"list/unassigned") def list_unassigned_transactions(self, request, *args, **kwargs): + if not FLAG__ENABLE_UNASSIGNED_TRANSACTIONS: + return Response( + {"error": "Unassigned transactions are not enabled."}, + status=status.HTTP_400_BAD_REQUEST, + ) if "page" not in request.query_params or request.query_params["page"] is None: return Response("page is required", status=400) @@ -178,9 +183,7 @@ def create(self, request, *args, **kwargs): def update(self, request, *args, **kwargs): with db_transaction.atomic(): saved_transaction = self.save_transaction( - request.data, - request, - instance=self.get_object() + request.data, request, instance=self.get_object() ) update_dependent_parent_purpose_description_if_needed(saved_transaction) return Response(saved_transaction.id) @@ -297,6 +300,11 @@ def remove_transaction_from_report(self, request): @action(detail=False, methods=["get"], url_path=r"outside") def transactions_outside_report(self, request): + if not FLAG__ENABLE_UNASSIGNED_TRANSACTIONS: + return Response( + {"error": "Unassigned transactions are not enabled."}, + status=status.HTTP_400_BAD_REQUEST, + ) from_date_str = request.query_params.get("from") through_date_str = request.query_params.get("through") @@ -315,15 +323,16 @@ def transactions_outside_report(self, request): status=status.HTTP_400_BAD_REQUEST, ) - queryset = self.filter_queryset(self.get_queryset()) - outside_queryset = queryset.filter(date__isnull=False).exclude( - date__range=(from_date, through_date) + queryset = ( + self.filter_queryset(self.get_queryset()) + .filter(date__isnull=False) + .exclude(date__range=(from_date, through_date)) + .exclude(memo_code=True) ) - count = outside_queryset.count() + count = queryset.count() return Response(count, status=status.HTTP_200_OK) - @action(detail=False, methods=["get"], url_path=r"previous/entity") def previous_transaction_by_entity(self, request): contact_1_id = request.query_params["contact_1_id"] @@ -508,13 +517,15 @@ def adjust_running_totals(self, target: Transaction, source: Transaction): def get_child_instance(self, child_id, committee_id): # ensure that child transaction belongs to this committee - child_instance = Transaction.objects.select_related( - "schedule_a", "schedule_b", "schedule_e", "contact_2" - ).filter(id=child_id, committee_account_id=committee_id).first() - if child_instance is None: - raise ValidationError( - {"children": ["Invalid child_id or child"]} + child_instance = ( + Transaction.objects.select_related( + "schedule_a", "schedule_b", "schedule_e", "contact_2" ) + .filter(id=child_id, committee_account_id=committee_id) + .first() + ) + if child_instance is None: + raise ValidationError({"children": ["Invalid child_id or child"]}) return child_instance def save_transaction(self, transaction_data, request, instance=None): diff --git a/docker-compose.yml b/docker-compose.yml index f4a5bc228d..03c6579e54 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -198,6 +198,7 @@ services: FLAG__COMMITTEE_DATA_SOURCE: MOCKED # Values are PRODUCTION, TEST, and MOCKED FLAG__ENABLE_IMPORT: True FLAG__ENABLE_EMAIL: True + FLAG__ENABLE_UNASSIGNED_TRANSACTIONS: True SES_FROM_USER: no-reply+local deploy: resources: diff --git a/manifests/manifest-dev.yml b/manifests/manifest-dev.yml index 3bbdf4984e..e027c7af27 100644 --- a/manifests/manifest-dev.yml +++ b/manifests/manifest-dev.yml @@ -43,6 +43,7 @@ defaults: &defaults FLAG__COMMITTEE_DATA_SOURCE: TEST # Values are PRODUCTION, TEST, MOCKED FLAG__ENABLE_IMPORT: True FLAG__ENABLE_EMAIL: False + FLAG__ENABLE_UNASSIGNED_TRANSACTIONS: True ENABLE_RESTRICTED_COMMANDS: True FEC_FORMAT_VERSION: 8.5 diff --git a/manifests/manifest-prod.yml b/manifests/manifest-prod.yml index 46ee16c65f..3591efd71c 100644 --- a/manifests/manifest-prod.yml +++ b/manifests/manifest-prod.yml @@ -43,6 +43,7 @@ defaults: &defaults FLAG__COMMITTEE_DATA_SOURCE: PRODUCTION # Values are PRODUCTION, TEST, and MOCKED FLAG__ENABLE_IMPORT: False FLAG__ENABLE_EMAIL: False + FLAG__ENABLE_UNASSIGNED_TRANSACTIONS: False ENABLE_RESTRICTED_COMMANDS: False FEC_FORMAT_VERSION: 8.5 diff --git a/manifests/manifest-stage.yml b/manifests/manifest-stage.yml index c74c66c62c..779b4fdd58 100644 --- a/manifests/manifest-stage.yml +++ b/manifests/manifest-stage.yml @@ -43,6 +43,7 @@ defaults: &defaults FLAG__COMMITTEE_DATA_SOURCE: TEST # Values are TEST and PRODUCTION FLAG__ENABLE_IMPORT: True FLAG__ENABLE_EMAIL: False + FLAG__ENABLE_UNASSIGNED_TRANSACTIONS: True ENABLE_RESTRICTED_COMMANDS: True FEC_FORMAT_VERSION: 8.5 diff --git a/manifests/manifest-test.yml b/manifests/manifest-test.yml index abe9639427..3b71b081a8 100644 --- a/manifests/manifest-test.yml +++ b/manifests/manifest-test.yml @@ -43,6 +43,7 @@ defaults: &defaults FLAG__COMMITTEE_DATA_SOURCE: TEST # Values are PRODUCTION, TEST, and MOCKED FLAG__ENABLE_IMPORT: False FLAG__ENABLE_EMAIL: False + FLAG__ENABLE_UNASSIGNED_TRANSACTIONS: False ENABLE_RESTRICTED_COMMANDS: True FEC_FORMAT_VERSION: 8.5