diff --git a/.safety.dependency.ignore b/.safety.dependency.ignore index 106c584cd5..940fad95da 100644 --- a/.safety.dependency.ignore +++ b/.safety.dependency.ignore @@ -8,9 +8,9 @@ # Example: # 40104 2022-01-15 # -48040 2022-12-01 # django -48041 2022-12-01 # django -48542 2022-12-01 # pyjwt -49733 2022-12-01 # django -50454 2022-12-01 # django -51340 2022-12-01 # django +48040 2022-12-31 # django +48041 2022-12-31 # django +48542 2022-12-31 # pyjwt +49733 2022-12-31 # django +50454 2022-12-31 # django +51340 2022-12-31 # django diff --git a/Dockerfile-e2e b/Dockerfile-e2e new file mode 100644 index 0000000000..9430dfee9a --- /dev/null +++ b/Dockerfile-e2e @@ -0,0 +1,16 @@ +FROM python:3.8 +ENV PYTHONUNBUFFERED=1 + +RUN mkdir /opt/nxg_fec_e2e +WORKDIR /opt/nxg_fec_e2e +ADD requirements.txt /opt +ADD django-backend /opt/nxg_fec_e2e/ +RUN pip3 install -r /opt/requirements.txt + +RUN mv /etc/localtime /etc/localtime.backup && ln -s /usr/share/zoneinfo/EST5EDT /etc/localtime + +RUN useradd nxgu --no-create-home --home /opt/nxg_fec_e2e && chown -R nxgu:nxgu /opt/nxg_fec_e2e +USER nxgu + +EXPOSE 8080 +ENTRYPOINT ["/bin/sh", "-c", "python wait_for_db.py && python manage.py migrate && python manage.py loaddata fixtures/e2e-test-data.json && gunicorn --bind 0.0.0.0:8080 fecfiler.wsgi -w 10 -t 200 --reload"] diff --git a/Worker_Dockerfile-e2e b/Worker_Dockerfile-e2e new file mode 100644 index 0000000000..e593c50b77 --- /dev/null +++ b/Worker_Dockerfile-e2e @@ -0,0 +1,12 @@ +FROM python:3.8 +ENV PYTHONUNBUFFERED=1 + +RUN mkdir /opt/nxg_fec_e2e +WORKDIR /opt/nxg_fec_e2e +ADD requirements.txt /opt +ADD django-backend /opt/nxg_fec_e2e/ +RUN pip3 install -r /opt/requirements.txt + +RUN mv /etc/localtime /etc/localtime.backup && ln -s /usr/share/zoneinfo/EST5EDT /etc/localtime + +ENTRYPOINT ["/bin/sh", "-c", "celery -A fecfiler worker --loglevel=info"] diff --git a/django-backend/fecfiler/authentication/authenticate_login.py b/django-backend/fecfiler/authentication/authenticate_login.py deleted file mode 100644 index dc83a17b0e..0000000000 --- a/django-backend/fecfiler/authentication/authenticate_login.py +++ /dev/null @@ -1,112 +0,0 @@ -from django.contrib.auth import authenticate, logout -from rest_framework.decorators import ( - authentication_classes, - permission_classes, - api_view, -) -from rest_framework import permissions, status, views, viewsets -from rest_framework.response import Response -from rest_framework_jwt.settings import api_settings -from fecfiler.authentication.token import jwt_payload_handler -from .models import Account -from datetime import datetime -from django.http import JsonResponse -from .serializers import AccountSerializer -from .permissions import IsAccountOwner -import logging -from fecfiler.settings import E2E_TESTING_LOGIN - -logger = logging.getLogger(__name__) - - -def update_last_login_time(account): - account.last_login = datetime.now() - account.save() - - -def handle_invalid_login(username): - logger.debug("Unauthorized login attempt: {}".format(username)) - return JsonResponse({ - "is_allowed": False, - "status": "Unauthorized", - "message": "ID/Password combination invalid.", - }, status=401) - - -def handle_valid_login(account): - update_last_login_time(account) - payload = jwt_payload_handler(account) - jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER - token = jwt_encode_handler(payload) - - logger.debug("Successful login: {}".format(account)) - return JsonResponse({ - "is_allowed": True, - "committee_id": account.cmtee_id, - "email": account.email, - "token": token, - }, status=200, safe=False) - - -@api_view(["POST", "GET"]) -@authentication_classes([]) -@permission_classes([]) -def authenticate_login(request): - if request.method == "GET": - return JsonResponse({"endpoint_available": E2E_TESTING_LOGIN}) - - if not E2E_TESTING_LOGIN: - return JsonResponse(status=405, safe=False) - - username = request.data.get("username", None) - password = request.data.get("password", None) - account = authenticate( - request=request, username=username, password=password - ) # Returns an account if the username is found and the password is valid - - if account: - return handle_valid_login(account) - else: - return handle_invalid_login(username) - - -class AccountViewSet(viewsets.ModelViewSet): - lookup_field = "username" - serializer_class = AccountSerializer - - def get_queryset(self): - queryset = Account.objects.all() - queryset = queryset.filter(self.request.user) - serializer_class = AccountSerializer(Account, many=True) - return JsonResponse(serializer_class.data, safe=False) - - def get_permissions(self): - if self.request.method in permissions.SAFE_METHODS: - return (permissions.AllowAny(),) - if self.request.method == "POST": - return (permissions.AllowAny(),) - return permissions.IsAuthenticated(), IsAccountOwner() - - def create(self, request): - serializer = self.serializer_class(data=request.data) - - if serializer.is_valid(): - Account.objects.create_user(**serializer.validated_data) - return Response(serializer.validated_data, status=status.HTTP_201_CREATED) - - return Response( - { - "status": "Bad request", - "message": "Account could not be created with received data.", - "details": str(serializer.errors), - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - -class LogoutView(views.APIView): - permission_classes = (permissions.IsAuthenticated,) - - def post(self, request, format=None): - logout(request) - return Response({}, status=status.HTTP_204_NO_CONTENT) diff --git a/django-backend/fecfiler/authentication/models.py b/django-backend/fecfiler/authentication/models.py index e4bd7070dd..7ebd561361 100644 --- a/django-backend/fecfiler/authentication/models.py +++ b/django-backend/fecfiler/authentication/models.py @@ -4,7 +4,7 @@ BaseUserManager, ) from django.db import models -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.utils import timezone from django.core.mail import send_mail diff --git a/django-backend/fecfiler/authentication/permissions.py b/django-backend/fecfiler/authentication/permissions.py deleted file mode 100644 index c14e4ad312..0000000000 --- a/django-backend/fecfiler/authentication/permissions.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework import permissions - - -class IsAccountOwner(permissions.BasePermission): - def has_object_permission(self, request, view, account): - if request.user: - return account == request.user - return False diff --git a/django-backend/fecfiler/authentication/serializers.py b/django-backend/fecfiler/authentication/serializers.py index ca0030d677..2bb0bdb610 100644 --- a/django-backend/fecfiler/authentication/serializers.py +++ b/django-backend/fecfiler/authentication/serializers.py @@ -4,9 +4,6 @@ class AccountSerializer(serializers.ModelSerializer): - # password = serializers.CharField(write_only=True, required=False) - # confirm_password = serializers.CharField(write_only=True, required=False) - class Meta: model = Account fields = ( diff --git a/django-backend/fecfiler/authentication/test_authentication.py b/django-backend/fecfiler/authentication/test_authentication.py deleted file mode 100644 index 0dd56474ba..0000000000 --- a/django-backend/fecfiler/authentication/test_authentication.py +++ /dev/null @@ -1,29 +0,0 @@ -from django.test import RequestFactory, TestCase -from fecfiler.authentication.models import Account -from fecfiler.authentication.authenticate_login import ( - update_last_login_time, - handle_invalid_login, - handle_valid_login, -) - - -class TestAuthentication(TestCase): - fixtures = ["test_accounts"] - acc = None - - def setUp(self): - self.factory = RequestFactory() - self.acc = Account.objects.get(email="unit_tester@test.com") - - def test_update_login_time(self): - prev_time = self.acc.last_login - update_last_login_time(self.acc) - self.assertNotEqual(self.acc.last_login, prev_time) - - def test_invalid_login(self): - resp = handle_invalid_login("random_username") - self.assertEqual(resp.status_code, 401) - - def test_valid_login(self): - resp = handle_valid_login(self.acc) - self.assertEqual(resp.status_code, 200) diff --git a/django-backend/fecfiler/authentication/test_token.py b/django-backend/fecfiler/authentication/test_token.py deleted file mode 100644 index d513fcadec..0000000000 --- a/django-backend/fecfiler/authentication/test_token.py +++ /dev/null @@ -1,34 +0,0 @@ -import unittest -from unittest.mock import Mock - - -from django.test import RequestFactory - -from fecfiler.authentication.token import (login_dot_gov_logout, - generate_username) - - -class TestToken(unittest.TestCase): - - def setUp(self): - self.factory = RequestFactory() - - def test_login_dot_gov_logout_happy_path(self): - test_state = 'test_state' - - mock_request = Mock() - mock_request.session = Mock() - mock_request.get_signed_cookie.return_value = test_state - - retval = login_dot_gov_logout(mock_request) - self.maxDiff = None - self.assertEqual(retval, ('https://idp.int.identitysandbox.gov' - '/openid_connect/logout?' - 'client_id=None' - '&post_logout_redirect_uri=None' - '&state=test_state')) - - def test_generate_username(self): - test_uuid = 'test_uuid' - retval = generate_username(test_uuid) - self.assertEqual(test_uuid, retval) diff --git a/django-backend/fecfiler/authentication/test_views.py b/django-backend/fecfiler/authentication/test_views.py new file mode 100644 index 0000000000..431f1452fd --- /dev/null +++ b/django-backend/fecfiler/authentication/test_views.py @@ -0,0 +1,65 @@ +from unittest.mock import Mock +from django.test import RequestFactory, TestCase +from fecfiler.authentication.models import Account +from fecfiler.authentication.views import (handle_invalid_login, + handle_valid_login, + update_last_login_time) + +from .views import (generate_username, + login_dot_gov_logout, + login_redirect, + logout_redirect) + + +class AuthenticationTest(TestCase): + fixtures = ["test_accounts"] + acc = None + + def setUp(self): + self.user = Account.objects.get(cmtee_id="C12345678") + self.factory = RequestFactory() + self.acc = Account.objects.get(email="unit_tester@test.com") + + def test_login_dot_gov_logout_happy_path(self): + test_state = 'test_state' + + mock_request = Mock() + mock_request.session = Mock() + mock_request.get_signed_cookie.return_value = test_state + + retval = login_dot_gov_logout(mock_request) + self.maxDiff = None + self.assertEqual(retval, ('https://idp.int.identitysandbox.gov' + '/openid_connect/logout?' + 'client_id=None' + '&post_logout_redirect_uri=None' + '&state=test_state')) + + def test_login_dot_gov_login_redirect(self): + request = self.factory.get("/") + request.user = self.user + request.session = {} + retval = login_redirect(request) + self.assertEqual(retval.status_code, 302) + + def test_login_dot_gov_logout_redirect(self): + retval = logout_redirect(self.factory.get('/')) + self.assertEqual(retval.status_code, 302) + + def test_generate_username(self): + test_uuid = 'test_uuid' + retval = generate_username(test_uuid) + self.assertEqual(test_uuid, retval) + + def test_update_login_time(self): + prev_time = self.acc.last_login + update_last_login_time(self.acc) + self.assertNotEqual(self.acc.last_login, prev_time) + + def test_invalid_login(self): + resp = handle_invalid_login("random_username") + self.assertEqual(resp.status_code, 401) + + def test_valid_login(self): + resp = handle_valid_login(self.acc) + self.assertEqual(resp.status_code, 200) diff --git a/django-backend/fecfiler/authentication/token.py b/django-backend/fecfiler/authentication/token.py deleted file mode 100644 index cd89074ff9..0000000000 --- a/django-backend/fecfiler/authentication/token.py +++ /dev/null @@ -1,89 +0,0 @@ -import warnings -from calendar import timegm -from datetime import datetime -from fecfiler.settings import SECRET_KEY -import jwt -from rest_framework_jwt.compat import get_username_field, get_username -from rest_framework_jwt.settings import api_settings, settings -import logging -from urllib.parse import urlencode - -logger = logging.getLogger(__name__) - - -def login_dot_gov_logout(request): - client_id = settings.OIDC_RP_CLIENT_ID - post_logout_redirect_uri = settings.LOGOUT_REDIRECT_URL - state = request.get_signed_cookie('oidc_state') - - params = { - 'client_id': client_id, - 'post_logout_redirect_uri': post_logout_redirect_uri, - 'state': state, - } - query = urlencode(params) - op_logout_url = settings.OIDC_OP_LOGOUT_ENDPOINT - redirect_url = '{url}?{query}'.format(url=op_logout_url, query=query) - - return redirect_url - - -def generate_username(uuid): - return uuid - - -def jwt_payload_handler(user): - username_field = get_username_field() - username = get_username(user) - - warnings.warn( - "The following fields will be removed in the future: " - "`email` and `user_id`. ", - DeprecationWarning, - ) - - payload = { - "user_id": user.pk, - "email": user.email, - "username": username, - "role": user.role, - "exp": datetime.utcnow() + api_settings.JWT_EXPIRATION_DELTA, - } - - payload[username_field] = username - - # Include original issued at time for a brand new token, - # to allow token refresh - if api_settings.JWT_ALLOW_REFRESH: - payload["orig_iat"] = timegm(datetime.utcnow().utctimetuple()) - - if api_settings.JWT_AUDIENCE is not None: - payload["aud"] = api_settings.JWT_AUDIENCE - - if api_settings.JWT_ISSUER is not None: - payload["iss"] = api_settings.JWT_ISSUER - - return payload - - -def verify_token(token_received): - options = { - "verify_exp": True, # Skipping expiration date check - "verify_aud": False, - } # Skipping audience check - payload = jwt.decode( - token_received, key=SECRET_KEY, algorithms="HS256", options=options - ) - return payload - - -def token_verification(request): - try: - token_received = request.headers["token"] - payload = verify_token(token_received) - return payload - except Exception as e: - logger.debug( - "exception occurred while generating token for email option.", str(e) - ) - raise e diff --git a/django-backend/fecfiler/authentication/urls.py b/django-backend/fecfiler/authentication/urls.py index 318c9470d0..655c73cdc9 100644 --- a/django-backend/fecfiler/authentication/urls.py +++ b/django-backend/fecfiler/authentication/urls.py @@ -1,23 +1,20 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import AccountViewSet -from .authenticate_login import authenticate_login, LogoutView from .views import ( - LoginDotGovSuccessSpaRedirect, - LoginDotGovSuccessLogoutSpaRedirect + authenticate_login, + authenticate_logout, + login_redirect, + logout_redirect, ) # Create a router and register our viewsets with it. router = DefaultRouter() -router.register(r"committee/users", AccountViewSet, basename="committee/users") # The API URLs are now determined automatically by the router. urlpatterns = [ - path("auth/logout/", LogoutView.as_view(), name="logout"), - path("auth/login-redirect", LoginDotGovSuccessSpaRedirect.as_view(), - name="login-redirect"), - path("auth/logout-redirect", LoginDotGovSuccessLogoutSpaRedirect.as_view(), - name="logout-redirect"), + path("user/login/authenticate", authenticate_login), + path("auth/logout", authenticate_logout), + path("auth/login-redirect", login_redirect), + path("auth/logout-redirect", logout_redirect), path("", include(router.urls)), - path("user/login/authenticate", authenticate_login, name="login_authenticate") ] diff --git a/django-backend/fecfiler/authentication/views.py b/django-backend/fecfiler/authentication/views.py index 69f2eefd2b..1ef3c82936 100644 --- a/django-backend/fecfiler/authentication/views.py +++ b/django-backend/fecfiler/authentication/views.py @@ -1,20 +1,33 @@ -from django.views.generic import View from django.http import HttpResponseRedirect - +from django.contrib.auth import authenticate, logout, login +from django.views.decorators.http import require_http_methods +from rest_framework.decorators import ( + authentication_classes, + permission_classes, + api_view, +) from fecfiler.settings import ( LOGIN_REDIRECT_CLIENT_URL, FFAPI_COMMITTEE_ID_COOKIE_NAME, FFAPI_EMAIL_COOKIE_NAME, FFAPI_COOKIE_DOMAIN, + OIDC_RP_CLIENT_ID, + LOGOUT_REDIRECT_URL, + OIDC_OP_LOGOUT_ENDPOINT, + E2E_TESTING_LOGIN, ) -from rest_framework import filters +from rest_framework.response import Response +from rest_framework import filters, status from rest_framework.viewsets import GenericViewSet from rest_framework.mixins import ListModelMixin from django.db.models import Value, CharField from django.db.models.functions import Concat from .models import Account from .serializers import AccountSerializer +from urllib.parse import urlencode +from datetime import datetime +from django.http import JsonResponse import logging logger = logging.getLogger(__name__) @@ -28,6 +41,7 @@ class AccountViewSet(GenericViewSet, ListModelMixin): of a user object versus other objects. (IE - having a "cmtee_id" field instead of "committee_id") """ + serializer_class = AccountSerializer filter_backends = [filters.OrderingFilter] ordering_fields = [ @@ -37,39 +51,137 @@ class AccountViewSet(GenericViewSet, ListModelMixin): "email", "role", "is_active", - "name" + "name", ] ordering = ["name"] def get_queryset(self): - queryset = Account.objects.annotate( - name=Concat('last_name', Value(', '), 'first_name', output_field=CharField()) - ).filter(cmtee_id=self.request.user.cmtee_id).all() + queryset = ( + Account.objects.annotate( + name=Concat( + "last_name", Value(", "), "first_name", output_field=CharField() + ) + ) + .filter(cmtee_id=self.request.user.cmtee_id) + .all() + ) return queryset -class LoginDotGovSuccessSpaRedirect(View): - def get(self, request, *args, **kwargs): - redirect = HttpResponseRedirect(LOGIN_REDIRECT_CLIENT_URL) - redirect.set_cookie(FFAPI_COMMITTEE_ID_COOKIE_NAME, - request.user.cmtee_id, - domain=FFAPI_COOKIE_DOMAIN, - secure=True) - redirect.set_cookie(FFAPI_EMAIL_COOKIE_NAME, - request.user.email, - domain=FFAPI_COOKIE_DOMAIN, - secure=True) - return redirect - - -class LoginDotGovSuccessLogoutSpaRedirect(View): - def get(self, request, *args, **kwargs): - response = HttpResponseRedirect(LOGIN_REDIRECT_CLIENT_URL) - response.delete_cookie(FFAPI_COMMITTEE_ID_COOKIE_NAME, - domain=FFAPI_COOKIE_DOMAIN) - response.delete_cookie(FFAPI_EMAIL_COOKIE_NAME, - domain=FFAPI_COOKIE_DOMAIN) - response.delete_cookie('csrftoken', - domain=FFAPI_COOKIE_DOMAIN) - return response +def login_dot_gov_logout(request): + client_id = OIDC_RP_CLIENT_ID + post_logout_redirect_uri = LOGOUT_REDIRECT_URL + state = request.get_signed_cookie("oidc_state") + + params = { + "client_id": client_id, + "post_logout_redirect_uri": post_logout_redirect_uri, + "state": state, + } + query = urlencode(params) + op_logout_url = OIDC_OP_LOGOUT_ENDPOINT + redirect_url = "{url}?{query}".format(url=op_logout_url, query=query) + + return redirect_url + + +def generate_username(uuid): + return uuid + + +def update_last_login_time(account): + account.last_login = datetime.now() + account.save() + + +def handle_valid_login(account): + update_last_login_time(account) + + logger.debug("Successful login: {}".format(account)) + return JsonResponse( + {"is_allowed": True, "committee_id": account.cmtee_id, "email": account.email}, + status=200, + safe=False, + ) + + +def handle_invalid_login(username): + logger.debug("Unauthorized login attempt: {}".format(username)) + return JsonResponse( + { + "is_allowed": False, + "status": "Unauthorized", + "message": "ID/Password combination invalid.", + }, + status=401, + ) + + +def delete_user_logged_in_cookies(response): + response.delete_cookie(FFAPI_COMMITTEE_ID_COOKIE_NAME, domain=FFAPI_COOKIE_DOMAIN) + response.delete_cookie(FFAPI_EMAIL_COOKIE_NAME, domain=FFAPI_COOKIE_DOMAIN) + response.delete_cookie("oidc_state", domain=FFAPI_COOKIE_DOMAIN) + response.delete_cookie("csrftoken", domain=FFAPI_COOKIE_DOMAIN) + + +@api_view(["GET"]) +@require_http_methods(["GET"]) +def login_redirect(request): + request.session["user_id"] = request.user.pk + redirect = HttpResponseRedirect(LOGIN_REDIRECT_CLIENT_URL) + redirect.set_cookie( + FFAPI_COMMITTEE_ID_COOKIE_NAME, + request.user.cmtee_id, + domain=FFAPI_COOKIE_DOMAIN, + secure=True, + ) + redirect.set_cookie( + FFAPI_EMAIL_COOKIE_NAME, + request.user.email, + domain=FFAPI_COOKIE_DOMAIN, + secure=True, + ) + return redirect + + +@api_view(["GET"]) +@require_http_methods(["GET"]) +@permission_classes([]) +def logout_redirect(request): + response = HttpResponseRedirect(LOGIN_REDIRECT_CLIENT_URL) + delete_user_logged_in_cookies(response) + return response + + +@api_view(["GET", "POST"]) +@authentication_classes([]) +@permission_classes([]) +@require_http_methods(["GET", "POST"]) +def authenticate_login(request): + if request.method == "GET": + return JsonResponse({"endpoint_available": E2E_TESTING_LOGIN}) + + if not E2E_TESTING_LOGIN: + return JsonResponse(status=405, safe=False) + + username = request.data.get("username", None) + password = request.data.get("password", None) + account = authenticate( + request=request, username=username, password=password + ) # Returns an account if the username is found and the password is valid + + if account: + login(request, account) + return handle_valid_login(account) + else: + return handle_invalid_login(username) + + +@api_view(["GET"]) +@authentication_classes([]) +@permission_classes([]) +@require_http_methods(["GET"]) +def authenticate_logout(request): + logout(request) + return Response({}, status=status.HTTP_204_NO_CONTENT) diff --git a/django-backend/fecfiler/contacts/serializers.py b/django-backend/fecfiler/contacts/serializers.py index b73b9ba17c..d7758bf9bd 100644 --- a/django-backend/fecfiler/contacts/serializers.py +++ b/django-backend/fecfiler/contacts/serializers.py @@ -2,6 +2,7 @@ from fecfiler.committee_accounts.serializers import CommitteeOwnedSerializer from fecfiler.validation import serializers +from rest_framework.serializers import IntegerField from .models import Contact @@ -12,12 +13,12 @@ class ContactSerializer( serializers.FecSchemaValidatorSerializerMixin, CommitteeOwnedSerializer ): contact_value = dict( - COM="Committee", - IND="Individual", - ORG="Organization", - CAN="Candidate", + COM="Committee", IND="Individual", ORG="Organization", CAN="Candidate", ) + # Contains the number of transactions linked to the contact + transaction_count = IntegerField(required=False) + def get_schema_name(self, data): return f"Contact_{self.contact_value[data.get('type', None)]}" @@ -26,15 +27,20 @@ class Meta: fields = [ f.name for f in Contact._meta.get_fields() - if f.name - not in [ - "deleted", - "schatransaction", - ] + if f.name not in ["deleted", "schatransaction"] ] + fields.append("transaction_count") read_only_fields = [ "uuid", "deleted", "created", "updated", + "transaction_count", ] + + def to_internal_value(self, data): + # Remove the transactin_count because it is an annotated field + # delivered to the front end. + if "transaction_count" in data: + del data["transaction_count"] + return super().to_internal_value(data) diff --git a/django-backend/fecfiler/contacts/test_views.py b/django-backend/fecfiler/contacts/test_views.py index a48c18f589..c78b73611b 100644 --- a/django-backend/fecfiler/contacts/test_views.py +++ b/django-backend/fecfiler/contacts/test_views.py @@ -33,6 +33,14 @@ def setUp(self): self.user = Account.objects.get(cmtee_id="C12345678") self.factory = RequestFactory() + def test_committee_lookup_no_auth(self): + self.assertEqual(True, True) + request = self.factory.get("/api/v1/contacts/committee_lookup") + + response = ContactViewSet.as_view({"get": "committee_lookup"})(request) + + self.assertEqual(response.status_code, 403) + @mock.patch("requests.get", side_effect=mocked_requests_get) def test_committee_lookup_no_q(self, mock_get): self.assertEqual(True, True) diff --git a/django-backend/fecfiler/contacts/views.py b/django-backend/fecfiler/contacts/views.py index a44305c35f..aad783632f 100644 --- a/django-backend/fecfiler/contacts/views.py +++ b/django-backend/fecfiler/contacts/views.py @@ -3,7 +3,7 @@ from urllib.parse import urlencode import requests -from django.db.models import CharField, Q, Value +from django.db.models import CharField, Q, Value, Count from django.db.models.functions import Concat, Lower from django.http import HttpResponseBadRequest, JsonResponse from fecfiler.committee_accounts.views import CommitteeOwnedViewSet @@ -27,13 +27,16 @@ class ContactViewSet(CommitteeOwnedViewSet): """ serializer_class = ContactSerializer - permission_classes = [] """Note that this ViewSet inherits from CommitteeOwnedViewSet The queryset will be further limmited by the user's committee in CommitteeOwnedViewSet's implementation of get_queryset() """ - queryset = Contact.objects.all().order_by("-created") + queryset = ( + Contact.objects.annotate(transaction_count=Count("schatransaction")) + .all() + .order_by("-created") + ) @action(detail=False) def committee_lookup(self, request): @@ -52,12 +55,7 @@ def committee_lookup(self, request): max_allowed_results, ) - query_params = urlencode( - { - "q": q, - "api_key": FEC_API_KEY, - } - ) + query_params = urlencode({"q": q, "api_key": FEC_API_KEY}) url = "{url}?{query_params}".format( url=FEC_API_COMMITTEE_LOOKUP_ENDPOINT, query_params=query_params ) @@ -72,8 +70,11 @@ def committee_lookup(self, request): .order_by("-committee_id") ) fec_api_committees = json_results.get("results", []) - fec_api_committees = [fac for fac in fec_api_committees if not any( - fac["id"] == ffc["committee_id"] for ffc in fecfile_committees)] + fec_api_committees = [ + fac + for fac in fec_api_committees + if not any(fac["id"] == ffc["committee_id"] for ffc in fecfile_committees) + ] fec_api_committees = fec_api_committees[:max_fec_results] fecfile_committees = fecfile_committees[:max_fecfile_results] return_value = { diff --git a/django-backend/fecfiler/f3x_summaries/fixtures/report_code_labels.json b/django-backend/fecfiler/f3x_summaries/fixtures/report_code_labels.json index 53e3943d84..4e0b30d3e6 100644 --- a/django-backend/fecfiler/f3x_summaries/fixtures/report_code_labels.json +++ b/django-backend/fecfiler/f3x_summaries/fixtures/report_code_labels.json @@ -1,177 +1,177 @@ -[ -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "APRIL 15 (Q1)", - "report_code": "Q1" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "JULY 15 (Q2)", - "report_code": "Q2" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "OCTOBER 15 (Q3)", - "report_code": "Q3" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "JANUARY 31 (YE)", - "report_code": "YE" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "TERMINATION (TER)", - "report_code": "TER" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "JULY 31 (MY)", - "report_code": "MY" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "GENERAL (12G)", - "report_code": "12G" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "PRIMARY (12P)", - "report_code": "12P" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "RUNOFF (12R)", - "report_code": "12R" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "SPECIAL (12S)", - "report_code": "12S" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "CONVENTION (12C)", - "report_code": "12C" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "GENERAL (30G)", - "report_code": "30G" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "RUNOFF (30R)", - "report_code": "30R" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "SPECIAL (30S)", - "report_code": "30S" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "FEBRUARY 20 (M2)", - "report_code": "M2" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "MARCH 30 (M3)", - "report_code": "M3" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "APRIL 20 (M4)", - "report_code": "M4" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "MAY 20 (M5)", - "report_code": "M5" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "JUNE 20 (M6)", - "report_code": "M6" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "JULY 20 (M7)", - "report_code": "M7" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "AUGUST 20 (M8)", - "report_code": "M8" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "SEPTEMBER 20 (M9)", - "report_code": "M9" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "OCTOBER 20 (M10)", - "report_code": "M10" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "NOVEMBER 20 (M11)", - "report_code": "M11" - } -}, -{ - "model": "f3x_summaries.reportcodelabel", - "fields": { - "label": "DECEMBER 20 (M12)", - "report_code": "M12" - } -} -] +[ + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "APRIL 15 (Q1)", + "report_code": "Q1" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "JULY 15 (Q2)", + "report_code": "Q2" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "OCTOBER 15 (Q3)", + "report_code": "Q3" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "JANUARY 31 (YE)", + "report_code": "YE" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "TERMINATION (TER)", + "report_code": "TER" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "JULY 31 (MY)", + "report_code": "MY" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "GENERAL (12G)", + "report_code": "12G" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "PRIMARY (12P)", + "report_code": "12P" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "RUNOFF (12R)", + "report_code": "12R" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "SPECIAL (12S)", + "report_code": "12S" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "CONVENTION (12C)", + "report_code": "12C" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "GENERAL (30G)", + "report_code": "30G" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "RUNOFF (30R)", + "report_code": "30R" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "SPECIAL (30S)", + "report_code": "30S" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "FEBRUARY 20 (M2)", + "report_code": "M2" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "MARCH 30 (M3)", + "report_code": "M3" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "APRIL 20 (M4)", + "report_code": "M4" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "MAY 20 (M5)", + "report_code": "M5" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "JUNE 20 (M6)", + "report_code": "M6" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "JULY 20 (M7)", + "report_code": "M7" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "AUGUST 20 (M8)", + "report_code": "M8" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "SEPTEMBER 20 (M9)", + "report_code": "M9" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "OCTOBER 20 (M10)", + "report_code": "M10" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "NOVEMBER 20 (M11)", + "report_code": "M11" + } + }, + { + "model": "f3x_summaries.reportcodelabel", + "fields": { + "label": "DECEMBER 20 (M12)", + "report_code": "M12" + } + } +] \ No newline at end of file diff --git a/django-backend/fecfiler/f3x_summaries/migrations/0010_auto_20220525_1157.py b/django-backend/fecfiler/f3x_summaries/migrations/0010_auto_20220525_1157.py index e2cb4e0475..deab5f6eda 100644 --- a/django-backend/fecfiler/f3x_summaries/migrations/0010_auto_20220525_1157.py +++ b/django-backend/fecfiler/f3x_summaries/migrations/0010_auto_20220525_1157.py @@ -8,9 +8,9 @@ def forwards_func(apps, schema_editor): original_apps = serializers.python.apps serializers.python.apps = apps - fixture_file = 'fecfiler/f3x_summaries/fixtures/report_code_labels.json' + fixture_file = "fecfiler/f3x_summaries/fixtures/report_code_labels.json" fixture = open(fixture_file) - objects = serializers.deserialize('json', fixture) + objects = serializers.deserialize("json", fixture) for obj in objects: obj.save() fixture.close() diff --git a/django-backend/fecfiler/f3x_summaries/migrations/0014_patch_report_code_label_MY_20220808_1122.py b/django-backend/fecfiler/f3x_summaries/migrations/0014_patch_report_code_label_MY_20220808_1122.py index 22840bd4fe..8c7eaa70b9 100644 --- a/django-backend/fecfiler/f3x_summaries/migrations/0014_patch_report_code_label_MY_20220808_1122.py +++ b/django-backend/fecfiler/f3x_summaries/migrations/0014_patch_report_code_label_MY_20220808_1122.py @@ -13,12 +13,11 @@ def patch_report_code_label(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('f3x_summaries', '0013_auto_20220807_0743'), + ("f3x_summaries", "0013_auto_20220807_0743"), ] operations = [ migrations.RunPython( - code=patch_report_code_label, - reverse_code=migrations.RunPython.noop + code=patch_report_code_label, reverse_code=migrations.RunPython.noop ) ] diff --git a/django-backend/fecfiler/f3x_summaries/migrations/0026_auto_20221130_1459.py b/django-backend/fecfiler/f3x_summaries/migrations/0026_auto_20221130_1459.py new file mode 100644 index 0000000000..03b1d73701 --- /dev/null +++ b/django-backend/fecfiler/f3x_summaries/migrations/0026_auto_20221130_1459.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.12 on 2022-11-30 19:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('f3x_summaries', '0025_auto_20220915_1309'), + ] + + operations = [ + migrations.AlterField( + model_name='f3xsummary', + name='report_code', + field=models.TextField(blank=True, null=True), + ), + migrations.DeleteModel( + name='ReportCodeLabel', + ), + ] diff --git a/django-backend/fecfiler/f3x_summaries/models.py b/django-backend/fecfiler/f3x_summaries/models.py index 8052ab77ce..9ea7ee47c2 100644 --- a/django-backend/fecfiler/f3x_summaries/models.py +++ b/django-backend/fecfiler/f3x_summaries/models.py @@ -8,18 +8,6 @@ logger = logging.getLogger(__name__) -class ReportCodeLabel(models.Model): - label = models.TextField(null=True, blank=True) - report_code = models.TextField(null=True, blank=True, unique=True) - - def __str__(self): - return self.label - - class Meta: - db_table = "report_code_labels" - indexes = [models.Index(fields=["report_code"])] - - class F3XSummary(SoftDeleteModel, CommitteeOwnedModel): """Generated model from json schema""" @@ -48,14 +36,7 @@ class F3XSummary(SoftDeleteModel, CommitteeOwnedModel): ) state = models.TextField(null=True, blank=True) zip = models.TextField(null=True, blank=True) - report_code = models.ForeignKey( - ReportCodeLabel, - models.SET_NULL, - null=True, - blank=True, - to_field="report_code", - db_column="report_code", - ) + report_code = models.TextField(null=True, blank=True) election_code = models.TextField(null=True, blank=True) date_of_election = models.DateField(null=True, blank=True) state_of_election = models.TextField(null=True, blank=True) diff --git a/django-backend/fecfiler/f3x_summaries/report_codes/__init__.py b/django-backend/fecfiler/f3x_summaries/report_codes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django-backend/fecfiler/f3x_summaries/report_codes/views.py b/django-backend/fecfiler/f3x_summaries/report_codes/views.py new file mode 100644 index 0000000000..a6a5272be1 --- /dev/null +++ b/django-backend/fecfiler/f3x_summaries/report_codes/views.py @@ -0,0 +1,29 @@ +from django.db.models import Case, Value, When + +report_code_label_mapping = Case( + When(report_code="Q1", then=Value("APRIL 15 (Q1)")), + When(report_code="Q2", then=Value("JULY 15 (Q2)")), + When(report_code="Q3", then=Value("OCTOBER 15 (Q3)")), + When(report_code="YE", then=Value("JANUARY 31 (YE)")), + When(report_code="TER", then=Value("TERMINATION (TER)")), + When(report_code="MY", then=Value("JULY 31 (MY)")), + When(report_code="12G", then=Value("GENERAL (12G)")), + When(report_code="12P", then=Value("PRIMARY (12P)")), + When(report_code="12R", then=Value("RUNOFF (12R)")), + When(report_code="12S", then=Value("SPECIAL (12S)")), + When(report_code="12C", then=Value("CONVENTION (12C)")), + When(report_code="30G", then=Value("GENERAL (30G)")), + When(report_code="30R", then=Value("RUNOFF (30R)")), + When(report_code="30S", then=Value("SPECIAL (30S)")), + When(report_code="M2", then=Value("FEBRUARY 20 (M2)")), + When(report_code="M3", then=Value("MARCH 30 (M3)")), + When(report_code="M4", then=Value("APRIL 20 (M4)")), + When(report_code="M5", then=Value("MAY 20 (M5)")), + When(report_code="M6", then=Value("JUNE 20 (M6)")), + When(report_code="M7", then=Value("JULY 20 (M7)")), + When(report_code="M8", then=Value("AUGUST 20 (M8)")), + When(report_code="M9", then=Value("SEPTEMBER 20 (M9)")), + When(report_code="M10", then=Value("OCTOBER 20 (M10)")), + When(report_code="M11", then=Value("NOVEMBER 20 (M11)")), + When(report_code="M12", then=Value("DECEMBER 20 (M12)")), +) diff --git a/django-backend/fecfiler/f3x_summaries/serializers.py b/django-backend/fecfiler/f3x_summaries/serializers.py index ebb62fda5d..398ad2b6b4 100644 --- a/django-backend/fecfiler/f3x_summaries/serializers.py +++ b/django-backend/fecfiler/f3x_summaries/serializers.py @@ -1,9 +1,7 @@ -from .models import F3XSummary, ReportCodeLabel +from .models import F3XSummary from rest_framework.serializers import ( - ModelSerializer, - SlugRelatedField, EmailField, - CharField + CharField, ) from fecfiler.committee_accounts.serializers import CommitteeOwnedSerializer from fecfiler.web_services.serializers import ( @@ -18,13 +16,6 @@ class F3XSummarySerializer(CommitteeOwnedSerializer, FecSchemaValidatorSerializerMixin): schema_name = "F3X" - report_code = SlugRelatedField( - many=False, - required=False, - read_only=False, - slug_field="report_code", - queryset=ReportCodeLabel.objects.all(), - ) confirmation_email_1 = EmailField( max_length=44, min_length=None, @@ -48,6 +39,9 @@ class F3XSummarySerializer(CommitteeOwnedSerializer, FecSchemaValidatorSerialize report_status = CharField( read_only=True, ) + report_code_label = CharField( + read_only=True, + ) class Meta: model = F3XSummary @@ -63,17 +57,5 @@ class Meta: "uploadsubmission", "webprintsubmission", ] - ] + ["report_status"] - read_only_fields = [ - "id", - "deleted", - "created", - "updated", - ] - foreign_key_fields = {"report_code": "report_code"} - - -class ReportCodeLabelSerializer(ModelSerializer): - class Meta: - model = ReportCodeLabel - fields = ("label", "report_code") + ] + ["report_status", "report_code_label"] + read_only_fields = ["id", "deleted", "created", "updated"] diff --git a/django-backend/fecfiler/f3x_summaries/urls.py b/django-backend/fecfiler/f3x_summaries/urls.py index 9d4cf69267..26f721684d 100644 --- a/django-backend/fecfiler/f3x_summaries/urls.py +++ b/django-backend/fecfiler/f3x_summaries/urls.py @@ -1,14 +1,10 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import F3XSummaryViewSet, ReportCodeLabelViewSet +from .views import F3XSummaryViewSet # Create a router and register our viewsets with it. router = DefaultRouter() router.register(r"f3x-summaries", F3XSummaryViewSet, basename="f3x-summaries") -router.register( - r"report-code-labels", - ReportCodeLabelViewSet, - basename="report-code-labels") # The API URLs are now determined automatically by the router. urlpatterns = [ diff --git a/django-backend/fecfiler/f3x_summaries/views.py b/django-backend/fecfiler/f3x_summaries/views.py index 40e4f0451e..78d569b869 100644 --- a/django-backend/fecfiler/f3x_summaries/views.py +++ b/django-backend/fecfiler/f3x_summaries/views.py @@ -3,20 +3,41 @@ from rest_framework.response import Response from rest_framework.decorators import action from rest_framework.viewsets import GenericViewSet -from rest_framework.mixins import ListModelMixin from fecfiler.committee_accounts.views import CommitteeOwnedViewSet -from .models import F3XSummary, ReportCodeLabel +from .models import F3XSummary +from .report_codes.views import report_code_label_mapping from fecfiler.scha_transactions.models import SchATransaction from fecfiler.web_services.models import FECSubmissionState, FECStatus from fecfiler.memo_text.models import MemoText from fecfiler.web_services.models import DotFEC, UploadSubmission, WebPrintSubmission -from .serializers import F3XSummarySerializer, ReportCodeLabelSerializer -from django.db.models import Case, Value, When +from .serializers import F3XSummarySerializer +from django.db.models import Case, Value, When, Q import logging logger = logging.getLogger(__name__) +def get_status_mapping(): + """returns Django Case that determines report status based on upload submission""" + in_progress = Q(upload_submission__fec_status=None) | Q(upload_submission=None) + submitted = Q( + upload_submission__fecfile_task_state__in=[ + FECSubmissionState.INITIALIZING, + FECSubmissionState.CREATING_FILE, + FECSubmissionState.SUBMITTING, + ] + ) | Q(upload_submission__fec_status__in=[FECStatus.ACCEPTED, FECStatus.PROCESSING]) + failed = Q(upload_submission__fecfile_task_state=FECSubmissionState.FAILED) + rejected = Q(upload_submission__fec_status=FECStatus.REJECTED) + + return Case( + When(in_progress, then=Value("In-Progress")), + When(submitted, then=Value("Submitted")), + When(failed, then=Value("Failed")), + When(rejected, then=Value("Rejected")), + ) + + class F3XSummaryViewSet(CommitteeOwnedViewSet): """ This viewset automatically provides `list`, `create`, `retrieve`, @@ -27,49 +48,20 @@ class F3XSummaryViewSet(CommitteeOwnedViewSet): in CommitteeOwnedViewSet's implementation of get_queryset() """ - queryset = F3XSummary.objects.select_related("report_code").annotate( - report_status=Case( - When(upload_submission=None, then=Value('In-Progress')), - When( - upload_submission__fecfile_task_state=FECSubmissionState.INITIALIZING, - then=Value('Submitted') - ), - When( - upload_submission__fecfile_task_state=FECSubmissionState.CREATING_FILE, - then=Value('Submitted') - ), - When( - upload_submission__fecfile_task_state=FECSubmissionState.SUBMITTING, - then=Value('Submitted') - ), - When( - upload_submission__fecfile_task_state=FECSubmissionState.FAILED, - then=Value('Failed') - ), - When( - upload_submission__fec_status=FECStatus.ACCEPTED, - then=Value('Submitted') - ), - When( - upload_submission__fec_status=FECStatus.PROCESSING, - then=Value('Submitted') - ), - When( - upload_submission__fec_status=FECStatus.REJECTED, - then=Value('Rejected') - ), - When(upload_submission__fec_status=None, then=Value('In-Progress')), - When(upload_submission__fec_status='', then=Value('In-Progress')), - ) - ).all() - """Join on report code labels""" + queryset = ( + F3XSummary.objects.annotate(report_code_label=report_code_label_mapping) + .annotate(report_status=get_status_mapping()) + .all() + ) serializer_class = F3XSummarySerializer - permission_classes = [] filter_backends = [filters.OrderingFilter] ordering_fields = [ - "form_type", "report_code__label", "coverage_through_date", - "upload_submission__fec_status", "submission_status" + "form_type", + "report_code_label", + "coverage_through_date", + "upload_submission__fec_status", + "submission_status", ] ordering = ["form_type"] @@ -124,12 +116,6 @@ def hard_delete_reports(self, request): return Response(f"Deleted {report_count} Reports") -class ReportCodeLabelViewSet(GenericViewSet, ListModelMixin): - queryset = ReportCodeLabel.objects.all() - serializer_class = ReportCodeLabelSerializer - pagination_class = None - - class ReportViewMixin(GenericViewSet): def get_queryset(self): report_id = ( diff --git a/django-backend/fecfiler/scha_transactions/managers.py b/django-backend/fecfiler/scha_transactions/managers.py index bc8b4b1479..73c326fb29 100644 --- a/django-backend/fecfiler/scha_transactions/managers.py +++ b/django-backend/fecfiler/scha_transactions/managers.py @@ -38,6 +38,7 @@ def get_itemization_clause(self): "INDIVIDUAL_NATIONAL_PARTY_CONVENTION_JF_TRANSFER_MEMO", "INDIVIDUAL_RECOUNT_RECEIPT", "JF_TRANSFER_NATIONAL_PARTY_CONVENTION_ACCOUNT", + "JF_TRANSFER_NATIONAL_PARTY_HEADQUARTERS_ACCOUNT", "OFFSET_TO_OPERATING_EXPENDITURES", "OTHER_COMMITTEE_NON_CONTRIBUTION_ACCOUNT", "OTHER_RECEIPT", @@ -45,6 +46,7 @@ def get_itemization_clause(self): "TRIBAL_NATIONAL_PARTY_CONVENTION_JF_TRANSFER_MEMO", "TRIBAL_RECOUNT_RECEIPT", "PAC_NATIONAL_PARTY_CONVENTION_JF_TRANSFER_MEMO", + "PAC_NATIONAL_PARTY_RECOUNT_ACCOUNT", "PAC_RECOUNT_RECEIPT", "PARTY_RECOUNT_RECEIPT", "BUSINESS_LABOR_NON_CONTRIBUTION_ACCOUNT", @@ -53,8 +55,21 @@ def get_itemization_clause(self): "INDIVIDUAL_NATIONAL_PARTY_RECOUNT_JF_TRANSFER_MEMO", "TRIBAL_NATIONAL_PARTY_RECOUNT_JF_TRANSFER_MEMO", "INDIVIDUAL_RECEIPT_NON_CONTRIBUTION_ACCOUNT", + "INDIVIDUAL_NATIONAL_PARTY_HEADQUARTERS_ACCOUNT", + "PAC_NATIONAL_PARTY_CONVENTION_ACCOUNT", + "PAC_NATIONAL_PARTY_HEADQUARTERS_ACCOUNT", + "PARTY_NATIONAL_PARTY_HEADQUARTERS_ACCOUNT", + "TRIBAL_NATIONAL_PARTY_HEADQUARTERS_ACCOUNT", + "TRIBAL_NATIONAL_PARTY_CONVENTION_ACCOUNT", + "INDIVIDUAL_NATIONAL_PARTY_HEADQUARTERS_JF_TRANSFER_MEMO", + "PAC_NATIONAL_PARTY_HEADQUARTERS_JF_TRANSFER_MEMO", + "INDIVIDUAL_NATIONAL_PARTY_RECOUNT_ACCOUNT", + "INDIVIDUAL_NATIONAL_PARTY_CONVENTION_ACCOUNT", + "PARTY_NATIONAL_PARTY_CONVENTION_ACCOUNT", + "TRIBAL_NATIONAL_PARTY_RECOUNT_ACCOUNT", ] return Case( + When(contribution_aggregate__lt=Value(Decimal(0)), then=Value(True)), When( transaction_type_identifier__in=over_two_hundred_types, then=Q(contribution_aggregate__gt=Value(Decimal(200))), diff --git a/django-backend/fecfiler/scha_transactions/test_managers.py b/django-backend/fecfiler/scha_transactions/test_managers.py index 9e61a221af..4abab65f6a 100644 --- a/django-backend/fecfiler/scha_transactions/test_managers.py +++ b/django-backend/fecfiler/scha_transactions/test_managers.py @@ -30,7 +30,7 @@ def test_aggregate_three(self): def test_aggregate_negative_offset(self): scha_transaction = SchATransaction.objects.get(transaction_id="5") self.assertEquals(scha_transaction.contribution_aggregate, Decimal("-111.11")) - self.assertFalse(scha_transaction.itemized) + self.assertTrue(scha_transaction.itemized) def test_aggregate_itemize_jf_transfer(self): scha_transaction = SchATransaction.objects.get(transaction_id="6") diff --git a/django-backend/fecfiler/scha_transactions/views.py b/django-backend/fecfiler/scha_transactions/views.py index 76261ee3ad..3df4dcffde 100644 --- a/django-backend/fecfiler/scha_transactions/views.py +++ b/django-backend/fecfiler/scha_transactions/views.py @@ -43,7 +43,6 @@ class SchATransactionViewSet(CommitteeOwnedViewSet, ReportViewMixin): ) serializer_class = SchATransactionParentSerializer - permission_classes = [] filter_backends = [filters.OrderingFilter] ordering_fields = [ "id", diff --git a/django-backend/fecfiler/settings/base.py b/django-backend/fecfiler/settings/base.py index f6a043f439..dc03f16f87 100644 --- a/django-backend/fecfiler/settings/base.py +++ b/django-backend/fecfiler/settings/base.py @@ -3,7 +3,6 @@ """ import os -import datetime import dj_database_url import requests @@ -21,7 +20,6 @@ TEMPLATE_DEBUG = DEBUG CSRF_COOKIE_DOMAIN = env.get_credential("FFAPI_COOKIE_DOMAIN") - CSRF_TRUSTED_ORIGINS = [ env.get_credential("CSRF_TRUSTED_ORIGINS", "http://localhost:4200") ] @@ -34,13 +32,6 @@ LOGIN_TIMEOUT_TIME = 15 LOGIN_MAX_RETRY = 3 -OTP_MAX_RETRY = 20 -OTP_DIGIT = 6 -OTP_TIME_EXPIRY = 300 -OTP_TIMEOUT_TIME = 30 -OTP_DISABLE = True -OTP_DEFAULT_PASSCODE = "111111" -JWT_PASSWORD_EXPIRY = 1800 # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = env.get_credential("DJANGO_SECRET_KEY", get_random_string(50)) @@ -54,7 +45,7 @@ # Application definition -SESSION_COOKIE_AGE = 15 * 60 # Inactivity timeout +SESSION_COOKIE_AGE = 30 * 60 # Inactivity timeout SESSION_SAVE_EVERY_REQUEST = True INSTALLED_APPS = [ @@ -78,8 +69,6 @@ "fecfiler.soft_delete", "fecfiler.validation", "fecfiler.web_services", - "django_otp", - "django_otp.plugins.otp_totp", "fecfiler.triage", ] @@ -134,29 +123,11 @@ DEFAULT_AUTO_FIELD = "django.db.models.AutoField" -# Password validation -# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] - -# OIDC settings start -AUTHENTICATION_BACKENDS = ( +# OpenID Connect settings start +AUTHENTICATION_BACKENDS = [ "django.contrib.auth.backends.ModelBackend", "mozilla_django_oidc.auth.OIDCAuthenticationBackend", -) +] OIDC_CREATE_USER = True OIDC_STORE_ID_TOKEN = True @@ -201,10 +172,10 @@ "acr_values": "http://idmanagement.gov/ns/assurance/ial/1" } -OIDC_OP_LOGOUT_URL_METHOD = "fecfiler.authentication.token.login_dot_gov_logout" +OIDC_OP_LOGOUT_URL_METHOD = "fecfiler.authentication.views.login_dot_gov_logout" -OIDC_USERNAME_ALGO = "fecfiler.authentication.token.generate_username" -# OIDC settings end +OIDC_USERNAME_ALGO = "fecfiler.authentication.views.generate_username" +# OpenID Connect settings end LANGUAGE_CODE = "en-us" TIME_ZONE = "America/New_York" @@ -226,23 +197,15 @@ STATICFILES_LOCATION = "static" REST_FRAMEWORK = { - "DEFAULT_PERMISSION_CLASSES": ( - "rest_framework.permissions.IsAuthenticatedOrReadOnly", - ), + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",), "DEFAULT_AUTHENTICATION_CLASSES": ( - "rest_framework_jwt.authentication.JSONWebTokenAuthentication", "rest_framework.authentication.SessionAuthentication", "rest_framework.authentication.BasicAuthentication", ), "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", "PAGE_SIZE": 10, -} - -JWT_AUTH = { - "JWT_ALLOW_REFRESH": True, - "JWT_EXPIRATION_DELTA": datetime.timedelta(seconds=3600), - "JWT_PAYLOAD_HANDLER": "fecfiler.authentication.token.jwt_payload_handler", + 'EXCEPTION_HANDLER': 'fecfiler.utils.custom_exception_handler', } LOGGING = { @@ -252,14 +215,9 @@ "standard": {"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"}, }, "handlers": { - "default": { - "class": "logging.StreamHandler", - "formatter": "standard", - }, - }, - "loggers": { - "": {"handlers": ["default"], "level": "INFO", "propagate": True}, + "default": {"class": "logging.StreamHandler", "formatter": "standard"}, }, + "loggers": {"": {"handlers": ["default"], "level": "INFO", "propagate": True}}, } """Celery configurations diff --git a/django-backend/fecfiler/settings/e2e.py b/django-backend/fecfiler/settings/e2e.py index 3af58f98ec..cb30fcaa2e 100644 --- a/django-backend/fecfiler/settings/e2e.py +++ b/django-backend/fecfiler/settings/e2e.py @@ -21,7 +21,7 @@ } # E2E Testing Login API -os.environ["DOCKERFILE"] = "Dockerfile-e2e" +os.environ["DB_DOCKERFILE"] = "Dockerfile-e2e" E2E_TESTING_LOGIN = True try: diff --git a/django-backend/fecfiler/urls.py b/django-backend/fecfiler/urls.py index c88847c155..25779b64f9 100644 --- a/django-backend/fecfiler/urls.py +++ b/django-backend/fecfiler/urls.py @@ -2,11 +2,8 @@ from django.urls import re_path from rest_framework.decorators import api_view from rest_framework.response import Response -from rest_framework_jwt.views import obtain_jwt_token, refresh_jwt_token from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView -from .authentication.authenticate_login import LogoutView - BASE_V1_URL = r"^api/v1/" @@ -20,7 +17,9 @@ def test_celery(request): urlpatterns = [ re_path(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework")), - re_path(r"^api/schema/", SpectacularAPIView.as_view(api_version="v1"), name="schema"), + re_path( + r"^api/schema/", SpectacularAPIView.as_view(api_version="v1"), name="schema" + ), re_path( r"^api/docs/", SpectacularSwaggerView.as_view( @@ -31,12 +30,9 @@ def test_celery(request): re_path(BASE_V1_URL, include("fecfiler.f3x_summaries.urls")), re_path(BASE_V1_URL, include("fecfiler.scha_transactions.urls")), re_path(BASE_V1_URL, include("fecfiler.memo_text.urls")), - re_path(r"^api/v1/auth/logout/$", LogoutView.as_view(), name="logout"), - re_path(r"^api/v1/token/obtain$", obtain_jwt_token), - re_path(r"^api/v1/token/refresh$", refresh_jwt_token), re_path(BASE_V1_URL, include("fecfiler.triage.urls")), re_path(BASE_V1_URL, include("fecfiler.authentication.urls")), re_path(BASE_V1_URL, include("fecfiler.web_services.urls")), - re_path(r"^oidc/", include('mozilla_django_oidc.urls')), + re_path(r"^oidc/", include("mozilla_django_oidc.urls")), re_path(r"^celery-test/", test_celery), ] diff --git a/django-backend/fecfiler/utils.py b/django-backend/fecfiler/utils.py new file mode 100644 index 0000000000..79cf36f4f7 --- /dev/null +++ b/django-backend/fecfiler/utils.py @@ -0,0 +1,18 @@ +from fecfiler.authentication.views import delete_user_logged_in_cookies +from rest_framework.views import exception_handler + + +def custom_exception_handler(exc, context): + # Call REST framework's default exception handler first, + # to get the standard error response. + response = exception_handler(exc, context) + + # Delete user cookies on forbidden http response. + # this will ensure that when the user is redirected + # to the login page due to the 403, any cookies + # (such as indicating committee id) are removed to + # allow for a clean new login. + if response is not None and response.status_code == 403: + delete_user_logged_in_cookies(response) + + return response diff --git a/docker-compose.yml b/docker-compose.yml index 8a7e370b1b..0f27a33e1f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: db: build: context: './db' - dockerfile: ${DOCKERFILE:-Dockerfile} + dockerfile: ${DB_DOCKERFILE:-Dockerfile} args: ENCRYPTION_PASSWORD: ${ENCRYPTION_PASSWORD} image: fecfile-db @@ -28,7 +28,7 @@ services: api-worker: build: context: './' - dockerfile: './Worker_Dockerfile' + dockerfile: '${WORKER_DOCKERFILE:-Worker_Dockerfile}' image: fecfile-celery-worker container_name: fecfile-celery-worker volumes: @@ -63,7 +63,7 @@ services: api: build: context: './' - dockerfile: './Dockerfile' + dockerfile: '${API_DOCKERFILE:-Dockerfile}' image: fecfile-api container_name: fecfile-api volumes: diff --git a/manifest-dev.yml b/manifest-dev.yml index 3d1e1d9805..3ba4a3177b 100644 --- a/manifest-dev.yml +++ b/manifest-dev.yml @@ -18,7 +18,7 @@ applications: DISABLE_COLLECTSTATIC: 1 DJANGO_SETTINGS_MODULE: fecfiler.settings.production CORS_ALLOWED_ORIGINS: https://fecfile-web-app-dev.app.cloud.gov - CSRF_TRUSTED_ORIGINS: fecfile-web-app-dev.app.cloud.gov + CSRF_TRUSTED_ORIGINS: https://fecfile-web-app-dev.app.cloud.gov FFAPI_COOKIE_DOMAIN: app.cloud.gov FRONTEND_URL: fecfile-web-app-dev.app.cloud.gov LOGIN_REDIRECT_CLIENT_URL: https://fecfile-web-app-dev.app.cloud.gov diff --git a/manifest-prod.yml b/manifest-prod.yml index e3829a6843..f24873bd04 100644 --- a/manifest-prod.yml +++ b/manifest-prod.yml @@ -16,7 +16,7 @@ applications: - fecfile-api-creds-prod env: CORS_ALLOWED_ORIGINS: https://fecfile-web-app-prod.app.cloud.gov - CSRF_TRUSTED_ORIGINS: fecfile-web-app-prod.app.cloud.gov + CSRF_TRUSTED_ORIGINS: https://fecfile-web-app-prod.app.cloud.gov DISABLE_COLLECTSTATIC: 1 DJANGO_SETTINGS_MODULE: fecfiler.settings.production FFAPI_COOKIE_DOMAIN: app.cloud.gov diff --git a/manifest-stage.yml b/manifest-stage.yml index 5f92eeadfb..6f546c6345 100644 --- a/manifest-stage.yml +++ b/manifest-stage.yml @@ -16,7 +16,7 @@ applications: - fecfile-api-creds-stage env: CORS_ALLOWED_ORIGINS: https://fecfile-web-app-stage.app.cloud.gov - CSRF_TRUSTED_ORIGINS: fecfile-web-app-stage.app.cloud.gov + CSRF_TRUSTED_ORIGINS: https://fecfile-web-app-stage.app.cloud.gov DISABLE_COLLECTSTATIC: 1 DJANGO_SETTINGS_MODULE: fecfiler.settings.production FFAPI_COOKIE_DOMAIN: app.cloud.gov diff --git a/requirements.txt b/requirements.txt index c00d75a79a..0695f62b9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,30 +1,28 @@ -boto3==1.24.34 +boto3==1.26.20 celery[redis]==5.2.7 -cfenv==0.5.2 +cfenv==0.5.3 coreapi==2.3.3 coreschema==0.0.4 -decorator==4.3.0 -dj-database-url==0.3.0 +decorator==5.1.1 +dj-database-url==1.0.0 dj-static==0.0.6 -Django==3.2.12 -django-cors-headers==3.11.0 -django-storages==1.12.3 -djangorestframework==3.13.1 -djangorestframework-jwt==1.11.0 -drf-spectacular==0.21.2 -git+https://github.com/fecgov/fecfile-validate@9f6f85442b2e4ae130c040fc2eb4259a925c8589#egg=fecfile_validate&subdirectory=fecfile_validate_python -GitPython==3.1.27 -gunicorn==19.10.0 +Django==4.1.3 +django-cors-headers==3.13.0 +django-storages==1.13.1 +djangorestframework==3.14.0 +drf-spectacular==0.24.2 +git+https://github.com/fecgov/fecfile-validate@f1fc910feab28ffe6b5fa9f2f9ee028fbb587e22#egg=fecfile_validate&subdirectory=fecfile_validate_python +GitPython==3.1.30 +gunicorn==20.1.0 Jinja2==3.1.2 -invoke==1.7.0 -itypes==1.1.0 +invoke==1.7.3 +itypes==1.2.0 MarkupSafe==2.1.1 openapi-codec==1.3.2 -psycopg2-binary==2.9.3 -PyJWT==1.6.4 -pytz==2021.3 +psycopg2-binary==2.9.5 +pytz==2022.6 retry==0.9.2 -static3==0.6.1 -django-otp==0.9.3 +static3==0.7.0 +django-otp==1.1.4 git+https://github.com/fecgov/mozilla-django-oidc.git@main#egg=mozilla_django_oidc -zeep==4.1.0 +zeep==4.2.1