Show paid status in admin and filter CSV exports by year (#30)
CI / test (pull_request) Successful in 5s
Unit Tests / test (pull_request) Successful in 5s

## Summary
Closes #30.
- Green check on membership/person admin when a current-year payment email matches
- Year picker on membership and payment CSV downloads
- Payments admin search by email, plus year list filters
This commit is contained in:
2026-09-03 15:48:57 -05:00
parent f37ff3e87b
commit 249163c21d
3 changed files with 490 additions and 123 deletions
+185
View File
@@ -6,12 +6,17 @@ from django.urls import reverse
from scha.settings.base import build_csrf_trusted_origins
from django.contrib.auth.models import User
from django.utils import timezone
from .admin import membership_payment_exists, person_payment_exists
from .forms import AddressForm, PeopleForm
from .models import (
AddressModel1,
CalendarEvent,
Membership,
MembershipPerson,
Payments,
SCHAOfficer,
UsefulLinks,
)
@@ -469,3 +474,183 @@ class AuthViewTests(TestCase):
response = self.views.member_posts(request)
self.assertEqual(response.status_code, 302)
self.assertIn("/login", response.url)
def _membership_with_person(email="alex@example.com"):
membership = Membership.objects.create()
AddressModel1.objects.create(
membership=membership,
address_1="123 Main St",
city="Wheaton",
state="IL",
zip_code="60189",
)
person = MembershipPerson.objects.create(
membership=membership,
first_name="Alex",
last_name="Resident",
email=email,
phone_number="+16305559876",
)
return membership, person
class AdminPaidIndicatorTests(TestCase):
def setUp(self):
self.client = Client()
self.admin_user = User.objects.create_superuser(
"admin", "admin@example.com", "pass"
)
self.client.force_login(self.admin_user)
self.this_year = timezone.now().year
self.membership, self.person = _membership_with_person("paid@example.com")
def test_person_marked_paid_when_email_matches_current_year_payment(self):
Payments.objects.create(
email="PAID@example.com",
date=datetime.date(self.this_year, 3, 1),
)
paid = MembershipPerson.objects.annotate(
_has_payment=person_payment_exists()
).get(pk=self.person.pk)
self.assertTrue(paid._has_payment)
unpaid_membership, unpaid_person = _membership_with_person("unpaid@example.com")
unpaid = MembershipPerson.objects.annotate(
_has_payment=person_payment_exists()
).get(pk=unpaid_person.pk)
self.assertFalse(unpaid._has_payment)
membership = Membership.objects.annotate(
_has_payment=membership_payment_exists()
).get(pk=self.membership.pk)
self.assertTrue(membership._has_payment)
other = Membership.objects.annotate(
_has_payment=membership_payment_exists()
).get(pk=unpaid_membership.pk)
self.assertFalse(other._has_payment)
def test_prior_year_payment_does_not_count_as_paid(self):
Payments.objects.create(
email="paid@example.com",
date=datetime.date(self.this_year - 1, 6, 1),
)
person = MembershipPerson.objects.annotate(
_has_payment=person_payment_exists()
).get(pk=self.person.pk)
self.assertFalse(person._has_payment)
def test_person_changelist_shows_paid_icon(self):
Payments.objects.create(
email="paid@example.com",
date=datetime.date(self.this_year, 4, 15),
)
url = reverse("admin:schasite_membershipperson_changelist")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Paid this year")
self.assertContains(response, "icon-yes.svg")
class AdminPaymentSearchTests(TestCase):
def setUp(self):
self.client = Client()
self.admin_user = User.objects.create_superuser(
"admin", "admin@example.com", "pass"
)
self.client.force_login(self.admin_user)
Payments.objects.create(email="dues@example.com", date=datetime.date(2026, 1, 10))
Payments.objects.create(email="other@example.com", date=datetime.date(2026, 2, 10))
def test_payments_admin_search_by_email(self):
url = reverse("admin:schasite_payments_changelist")
response = self.client.get(url, {"q": "dues@example.com"})
self.assertEqual(response.status_code, 200)
self.assertContains(response, "dues@example.com")
self.assertNotContains(response, "other@example.com")
class AdminCsvYearFilterTests(TestCase):
def setUp(self):
self.client = Client()
self.admin_user = User.objects.create_superuser(
"admin", "admin@example.com", "pass"
)
self.client.force_login(self.admin_user)
self.current, self.current_person = _membership_with_person("now@example.com")
self.old, self.old_person = _membership_with_person("old@example.com")
Membership.objects.filter(pk=self.old.pk).update(
created=timezone.make_aware(datetime.datetime(2024, 5, 1, 12, 0, 0))
)
Payments.objects.create(email="now@example.com", date=datetime.date(2026, 3, 1))
Payments.objects.create(email="old@example.com", date=datetime.date(2024, 3, 1))
def test_membership_csv_prompts_for_year(self):
url = reverse("admin:schasite_membership_changelist")
response = self.client.post(
url,
{
"action": "download_csv_by_members",
"_selected_action": [str(self.current.pk), str(self.old.pk)],
"index": "0",
},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Year:")
self.assertContains(response, "Download CSV")
def test_membership_csv_filters_by_created_year(self):
url = reverse("admin:schasite_membership_changelist")
response = self.client.post(
url,
{
"action": "download_csv_by_members",
"_selected_action": [str(self.current.pk), str(self.old.pk)],
"index": "0",
"apply": "Download CSV",
"year": "2024",
},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response["Content-Type"], "text/csv")
content = response.content.decode()
self.assertIn("old@example.com", content)
self.assertNotIn("now@example.com", content)
def test_membership_csv_by_address_filters_by_created_year(self):
url = reverse("admin:schasite_membership_changelist")
response = self.client.post(
url,
{
"action": "download_csv_by_address",
"_selected_action": [str(self.current.pk), str(self.old.pk)],
"index": "0",
"apply": "Download CSV",
"year": "2024",
},
)
self.assertEqual(response.status_code, 200)
content = response.content.decode()
self.assertIn("old@example.com", content)
self.assertNotIn("now@example.com", content)
def test_payments_csv_filters_by_payment_year(self):
url = reverse("admin:schasite_payments_changelist")
payments = list(Payments.objects.values_list("pk", flat=True))
response = self.client.post(
url,
{
"action": "download_payments",
"_selected_action": [str(pk) for pk in payments],
"index": "0",
"apply": "Download CSV",
"year": "2024",
},
)
self.assertEqual(response.status_code, 200)
content = response.content.decode()
self.assertIn("old@example.com", content)
self.assertNotIn("now@example.com", content)