Skip to content

Commit

Permalink
Merge pull request #890 from fecgov/release/sprint-42
Browse files Browse the repository at this point in the history
Release/sprint 42
  • Loading branch information
lbeaufort authored May 28, 2024
2 parents fdd5c9d + a0f7d8f commit 3a42a8b
Show file tree
Hide file tree
Showing 22 changed files with 312 additions and 163 deletions.
6 changes: 3 additions & 3 deletions django-backend/fecfiler/authentication/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def login_dot_gov_logout(request):
}
query = urlencode(params)
op_logout_url = OIDC_OP_LOGOUT_ENDPOINT
redirect_url = "{url}?{query}".format(url=op_logout_url, query=query)
redirect_url = f"{op_logout_url}?{query}"

return redirect_url

Expand All @@ -54,13 +54,13 @@ def generate_username(uuid):


def handle_valid_login(user):
logger.debug("Successful login: {}".format(user))
logger.debug(f"Successful login: {user}")
response = HttpResponse()
return response


def handle_invalid_login(username):
logger.debug("Unauthorized login attempt: {}".format(username))
logger.debug(f"Unauthorized login attempt: {username}")
return HttpResponse('Unauthorized', status=401)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# 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,
),
]
2 changes: 1 addition & 1 deletion django-backend/fecfiler/committee_accounts/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_remove_member(self):
membership_uuid = UUID("136a21f2-66fe-4d56-89e9-0d1d4612741c")
view = CommitteeMembershipViewSet()
request = self.factory.get(
"/api/v1/committee-members/{membership_uuid}/remove-member"
f"/api/v1/committee-members/{membership_uuid}/remove-member"
)
request.user = self.user
request.session = {
Expand Down
55 changes: 31 additions & 24 deletions django-backend/fecfiler/committee_accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from uuid import UUID
from fecfiler.user.models import User
from rest_framework import filters, viewsets, mixins
from rest_framework import filters, viewsets, mixins, pagination
from django.contrib.sessions.exceptions import SuspiciousSession
from fecfiler.transactions.models import (
Transaction,
Expand All @@ -27,6 +27,11 @@
logger = structlog.get_logger(__name__)


class CommitteeMemberListPagination(pagination.PageNumberPagination):
page_size = 10
page_size_query_param = "page_size"


class CommitteeViewSet(viewsets.GenericViewSet, mixins.ListModelMixin):
serializer_class = CommitteeAccountSerializer

Expand Down Expand Up @@ -86,9 +91,21 @@ def get_committee_id(self):
raise SuspiciousSession("session has invalid committee_id")
return committee_id

def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
if "page" in request.query_params:
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)

serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)


class CommitteeMembershipViewSet(CommitteeOwnedViewMixin, viewsets.ModelViewSet):
serializer_class = CommitteeMembershipSerializer
pagination_class = CommitteeMemberListPagination
filter_backends = [filters.OrderingFilter]
ordering_fields = ["name", "email", "role", "is_active", "created"]
ordering = ["-created"]
Expand All @@ -110,28 +127,12 @@ def get_queryset(self):
Value(""),
output_field=TextField(),
),
email=Coalesce(
"user__email", "pending_email", output_field=TextField()
),
email=Coalesce("user__email", "pending_email", output_field=TextField()),
is_active=~Q(user=None),
)
)

def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())

if "page" in request.query_params:
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)

serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)

@action(
detail=False, methods=["post"], url_path="add-member", url_name="add_member"
)
@action(detail=False, methods=["post"], url_path="add-member", url_name="add_member")
def add_member(self, request):
committee_uuid = self.request.session["committee_uuid"]
committee = CommitteeAccount.objects.filter(id=committee_uuid).first()
Expand Down Expand Up @@ -161,7 +162,7 @@ def add_member(self, request):

# Check for pre-existing membership
matching_memberships = self.get_queryset().filter(
Q(pending_email=email) | Q(user__email=email)
Q(pending_email__iexact=email) | Q(user__email__iexact=email)
)
if matching_memberships.count() > 0:
return Response(
Expand Down Expand Up @@ -219,12 +220,18 @@ def register_committee(committee_id, user):
f1_email = f1_line.split(FS_STR)[11]

failure_reason = None
if f1_email != email:

if ";" in f1_email:
f1_email = f1_email.split(";")
elif "," in f1_email:
f1_email = f1_email.split(",")
else:
f1_email = [f1_email]

if email not in f1_email:
failure_reason = f"Email {email} does not match committee email"

existing_account = CommitteeAccount.objects.filter(
committee_id=committee_id
).first()
existing_account = CommitteeAccount.objects.filter(committee_id=committee_id).first()
if existing_account:
failure_reason = f"Committee {committee_id} already registered"

Expand Down
3 changes: 2 additions & 1 deletion django-backend/fecfiler/contacts/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ def to_representation(self, instance, depth=0):
),
)

representation["has_transaction_or_report"] = query.exists()
if self.context.get("request") and "contacts" in self.context["request"].path:
representation["has_transaction_or_report"] = query.exists()

return representation

Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.test import TestCase
from .models import Contact
from ..models import Contact


class ContactTestCase(TestCase):
Expand Down Expand Up @@ -34,6 +34,4 @@ def test_save_and_delete(self):
self.assertEquals(soft_deleted_contact.first_name, "First")
self.assertIsNotNone(soft_deleted_contact.deleted)
soft_deleted_contact.hard_delete()
self.assertRaises(
Contact.DoesNotExist, Contact.all_objects.get, last_name="Last"
)
self.assertRaises(Contact.DoesNotExist, Contact.all_objects.get, last_name="Last")
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.test import TestCase
from .models import Contact
from .serializers import ContactSerializer
from ..models import Contact
from ..serializers import ContactSerializer
from rest_framework.request import HttpRequest, Request
from fecfiler.user.models import User

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from unittest import mock

from django.test import RequestFactory, TestCase
from rest_framework.test import force_authenticate
import uuid

from fecfiler.user.models import User
from .models import Contact
from .views import ContactViewSet, DeletedContactsViewSet
from ..models import Contact
from ..views import ContactViewSet, DeletedContactsViewSet
from .utils import create_test_individual_contact

mock_results = {
"results": [
Expand Down Expand Up @@ -69,9 +69,7 @@ def test_candidate_no_candidate_id(self, mock_get):

@mock.patch("requests.get", side_effect=mocked_requests_get_candidates)
def test_candidate(self, mock_get):
request = self.factory.get(
"/api/v1/contacts/candidate?" "candidate_id=P60012143"
)
request = self.factory.get("/api/v1/contacts/candidate?candidate_id=P60012143")
request.user = self.user
response = ContactViewSet.as_view({"get": "candidate"})(request)

Expand Down Expand Up @@ -219,7 +217,7 @@ def test_individual_lookup_no_q(self):

def test_individual_lookup_happy_path(self):
request = self.factory.get(
"/api/v1/contacts/individual_lookup?" "q=Lastname&max_fecfile_results=5"
"/api/v1/contacts/individual_lookup?q=Lastname&max_fecfile_results=5"
)
request.user = self.user
request.session = {
Expand All @@ -229,7 +227,7 @@ def test_individual_lookup_happy_path(self):

response = ContactViewSet.as_view({"get": "individual_lookup"})(request)

expected_json_fragment = '"last_name": "Lastname", "first_name": ' '"Firstname"'
expected_json_fragment = '"last_name": "Lastname", "first_name": "Firstname"'

self.assertEqual(response.status_code, 200)
self.assertIn(expected_json_fragment, str(response.content, encoding="utf8"))
Expand All @@ -248,7 +246,7 @@ def test_organization_lookup_no_q(self):

def test_organization_lookup_happy_path(self):
request = self.factory.get(
"/api/v1/contacts/organization_lookup?" "q=test&max_fecfile_results=5"
"/api/v1/contacts/organization_lookup?q=test&max_fecfile_results=5"
)
request.user = self.user
request.session = {
Expand Down Expand Up @@ -282,9 +280,7 @@ def test_get_contact_id_finds_contact(self):
response = ContactViewSet.as_view({"get": "get_contact_id"})(request)

self.assertEqual(response.status_code, 200)
self.assertEqual(
response.data, uuid.UUID("a03a141a-d2df-402c-93c6-e705ec6007f3")
)
self.assertEqual(response.data, uuid.UUID("a03a141a-d2df-402c-93c6-e705ec6007f3"))

def test_get_contact_id_no_match(self):
request = self.factory.get(
Expand Down Expand Up @@ -363,3 +359,42 @@ def test_update(self):
response = ContactViewSet.as_view({"put": "update"})(request, pk=contact.id)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["first_name"], "Other")

def test_list_paginated(self):
view = ContactViewSet()
view.format_kwarg = "format"
request = self.factory.get("/api/v1/contacts")
request.user = self.user
request.session = {
"committee_uuid": uuid.UUID("11111111-2222-3333-4444-555555555555"),
"committee_id": "C01234567",
}

for i in range(10):
create_test_individual_contact(
f"last{i}", f"first{i}", "11111111-2222-3333-4444-555555555555"
)
request.method = "GET"
request.query_params = {"page": 1}
view.request = request
response = view.list(request)
self.assertEqual(len(response.data["results"]), 10)

def test_list_no_pagination(self):
view = ContactViewSet()
view.format_kwarg = "format"
request = self.factory.get("/api/v1/contacts")
request.user = self.user
request.session = {
"committee_uuid": uuid.UUID("11111111-2222-3333-4444-555555555555"),
"committee_id": "C01234567",
}
request.method = "GET"
request.query_params = {}
view.request = request
response = view.list(request)
try:
response.data["results"] # A non-paginated response will throw an error here
self.assertTrue(response is None)
except TypeError:
self.assertTrue(response is not None)
11 changes: 11 additions & 0 deletions django-backend/fecfiler/contacts/tests/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from ..models import Contact


def create_test_individual_contact(last_name, first_name, committee_account_id):
contact = Contact.objects.create(
type=Contact.ContactType.INDIVIDUAL,
last_name=last_name,
first_name=first_name,
committee_account_id=committee_account_id,
)
return contact
13 changes: 9 additions & 4 deletions django-backend/fecfiler/contacts/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.db.models import CharField, Q, Value
from django.db.models.functions import Concat, Lower, Coalesce
from django.http import HttpResponseBadRequest, JsonResponse
from rest_framework import viewsets
from rest_framework import viewsets, pagination
from fecfiler.committee_accounts.views import (
CommitteeOwnedViewMixin,
)
Expand Down Expand Up @@ -40,13 +40,19 @@ def validate_and_sanitize_candidate(candidate_id):
return candidate_id


class ContactListPagination(pagination.PageNumberPagination):
page_size = 10
page_size_query_param = "page_size"


class ContactViewSet(CommitteeOwnedViewMixin, viewsets.ModelViewSet):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.
"""

serializer_class = ContactSerializer
pagination_class = ContactListPagination

"""Note that this ViewSet inherits from CommitteeOwnedViewMixin
The queryset will be further limmited by the user's committee
Expand Down Expand Up @@ -182,9 +188,7 @@ def committee_lookup(self, request):

fecfile_committees = list(
self.get_queryset()
.filter(
Q(type="COM") & (Q(committee_id__icontains=q) | Q(name__icontains=q))
)
.filter(Q(type="COM") & (Q(committee_id__icontains=q) | Q(name__icontains=q)))
.exclude(id__in=exclude_ids)
.values()
.order_by("-committee_id")
Expand Down Expand Up @@ -313,6 +317,7 @@ class DeletedContactsViewSet(
GenericViewSet,
):
serializer_class = ContactSerializer
pagination_class = ContactListPagination

queryset = (
Contact.all_objects.filter(deleted__isnull=False)
Expand Down
Loading

0 comments on commit 3a42a8b

Please sign in to comment.