Files
scha/schasite/admin.py
T
westfarn 249163c21d
CI / test (pull_request) Successful in 5s
Unit Tests / test (pull_request) Successful in 5s
Show paid status in admin and filter CSV exports by year (#30)
## 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
2026-09-03 15:48:57 -05:00

417 lines
12 KiB
Python

from django.contrib import admin
from django.db.models import Exists, OuterRef, Q
from django.http import HttpResponse
from django.shortcuts import render
from django.urls import reverse
from django.utils import timezone
from datetime import datetime
import csv
import io
from .models import (
UsefulLinks,
Membership,
CalendarEvent,
MembershipServices,
AddressModel1,
MembershipPerson,
MembershipCommittee,
CalendarEventAddressModel,
Payments,
SCHAOfficer,
)
from .forms import PaymentImport
def _current_year():
return timezone.now().year
def person_payment_exists(year=None):
"""True when this MembershipPerson has a payment in `year` (email or FK)."""
year = _current_year() if year is None else year
email_match = (
Q(email__iexact=OuterRef("email")) & Q(email__isnull=False) & ~Q(email="")
)
return Exists(
Payments.objects.filter(date__year=year).filter(
Q(person_id=OuterRef("pk")) | email_match
)
)
def membership_payment_exists(year=None):
"""True when any person on this membership has a payment in `year`."""
return Exists(
MembershipPerson.objects.filter(membership_id=OuterRef("pk")).filter(
person_payment_exists(year)
)
)
def _available_years(model, field_name):
years = {d.year for d in model.objects.dates(field_name, "year")}
years.add(_current_year())
return sorted(years, reverse=True)
def _apply_year_filter(queryset, field_name, year):
if not year or year == "all":
return queryset
try:
year_int = int(year)
except (TypeError, ValueError):
return queryset
return queryset.filter(**{f"{field_name}__year": year_int})
def _csv_response(filename_suffix, header, rows):
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(header)
writer.writerows(rows)
filename = datetime.now().strftime("%Y_%m_%d_%H_%M_%S") + filename_suffix
response = HttpResponse(buf.getvalue(), content_type="text/csv")
response["Content-Disposition"] = f"attachment; filename={filename}"
return response
def _year_suffix(year):
if year and year != "all":
return f"_{year}"
return ""
def _csv_year_form_response(
modeladmin, request, *, action_name, year_field, help_text
):
opts = modeladmin.model._meta
return render(
request,
"admin/csv_year_export.html",
{
"opts": opts,
"title": "Download CSV",
"action_name": action_name,
"years": _available_years(modeladmin.model, year_field),
"default_year": _current_year(),
"help_text": help_text,
"selected_ids": request.POST.getlist("_selected_action"),
"select_across": request.POST.get("select_across", "0"),
"index": request.POST.get("index", "0"),
"changelist_url": reverse(
f"admin:{opts.app_label}_{opts.model_name}_changelist"
),
},
)
class CreatedYearListFilter(admin.SimpleListFilter):
title = "year"
parameter_name = "year"
def lookups(self, request, model_admin):
return [(year, str(year)) for year in _available_years(model_admin.model, "created")]
def queryset(self, request, queryset):
if self.value():
return queryset.filter(created__year=self.value())
return queryset
class PaymentYearListFilter(admin.SimpleListFilter):
title = "year"
parameter_name = "year"
def lookups(self, request, model_admin):
return [(year, str(year)) for year in _available_years(model_admin.model, "date")]
def queryset(self, request, queryset):
if self.value():
return queryset.filter(date__year=self.value())
return queryset
class UsefulLinksAdmin(admin.ModelAdmin):
list_display = ["name", "url"]
class MembershipAddressInline(admin.TabularInline):
model = AddressModel1
extra = 1
readonly_fields = ("id",)
class MembershipPersonInline(admin.TabularInline):
model = MembershipPerson
extra = 1
readonly_fields = ("id", "has_payment")
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(_has_payment=person_payment_exists())
@admin.display(description="Paid this year", boolean=True)
def has_payment(self, obj):
if not obj.pk:
return False
return bool(getattr(obj, "_has_payment", False))
class MembershipCommiteeInline(admin.TabularInline):
model = MembershipCommittee
extra = 1
readonly_fields = ("id",)
class MembershipServicesInline(admin.TabularInline):
model = MembershipServices
extra = 1
readonly_fields = ("id",)
def download_csv_by_members(modelAdmin, request, queryset):
if "apply" not in request.POST:
return _csv_year_form_response(
modelAdmin,
request,
action_name="download_csv_by_members",
year_field="created",
help_text="Choose a year to include. Memberships are filtered by the year the record was created.",
)
year = request.POST.get("year", "all")
queryset = _apply_year_filter(queryset, "created", year)
rows = []
for q in queryset:
people = MembershipPerson.objects.filter(membership_id=q.id)
for person in people:
rows.append(
[
q.addressmodel1.address_1,
q.addressmodel1.city,
q.addressmodel1.state,
q.addressmodel1.zip_code,
person.first_name,
person.last_name,
person.email,
person.phone_number,
]
)
return _csv_response(
f"_scha_member_by_member{_year_suffix(year)}.csv",
[
"address_1",
"city",
"state",
"zip_code",
"first_name",
"last_name",
"email",
"phone_number",
],
rows,
)
download_csv_by_members.short_description = "Download CSV by member"
def download_csv_by_address(modeladmin, request, queryset):
if "apply" not in request.POST:
return _csv_year_form_response(
modeladmin,
request,
action_name="download_csv_by_address",
year_field="created",
help_text="Choose a year to include. Memberships are filtered by the year the record was created.",
)
year = request.POST.get("year", "all")
queryset = _apply_year_filter(queryset, "created", year)
rows = []
for q in queryset:
people = list(MembershipPerson.objects.filter(membership_id=q.id))
rows.append(
[
q.addressmodel1.address_1,
q.addressmodel1.city,
q.addressmodel1.state,
q.addressmodel1.zip_code,
people[0].email if len(people) > 0 else "",
people[0].phone_number if len(people) > 0 else "",
people[0].first_name if len(people) > 0 else "",
people[0].last_name if len(people) > 0 else "",
people[1].email if len(people) > 1 else "",
people[1].phone_number if len(people) > 1 else "",
people[1].first_name if len(people) > 1 else "",
people[1].last_name if len(people) > 1 else "",
]
)
return _csv_response(
f"_scha_member_by_address{_year_suffix(year)}.csv",
[
"address_1",
"city",
"state",
"zip_code",
"person_1_email",
"person_1_phone",
"person_1_first_name",
"person_1_last_name",
"person_2_email",
"person_2_phone",
"person_2_first_name",
"person_2_last_name",
],
rows,
)
download_csv_by_address.short_description = "Download CSV by address"
class MembershipAdmin(admin.ModelAdmin):
inlines = [
MembershipAddressInline,
MembershipPersonInline,
MembershipCommiteeInline,
MembershipServicesInline,
]
list_display = ["get_address_str", "get_person_1", "has_payment"]
list_filter = [CreatedYearListFilter]
actions = [download_csv_by_address, download_csv_by_members]
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(_has_payment=membership_payment_exists())
@admin.display(description="Paid this year", boolean=True, ordering="_has_payment")
def has_payment(self, obj):
return bool(getattr(obj, "_has_payment", False))
class CalendarEventAddressInline(admin.TabularInline):
model = CalendarEventAddressModel
extra = 1
readonly_fields = ("id",)
class CalendarEventAdmin(admin.ModelAdmin):
inlines = [CalendarEventAddressInline]
list_display = [
"event_name",
"start_date",
"start_time",
"end_date",
"end_time",
"coordinator_email",
"event_link_name",
]
fields = [
"event_name",
"start_date",
"start_time",
"end_date",
"end_time",
"location_name",
"coordinator_email",
"event_link_name",
"event_url",
]
class AddressModelAdmin(admin.ModelAdmin):
pass
class MembershipPersonAdmin(admin.ModelAdmin):
list_display = ["first_name", "last_name", "email", "phone_number", "has_payment"]
search_fields = ["email", "first_name", "last_name"]
list_filter = [CreatedYearListFilter]
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.annotate(_has_payment=person_payment_exists())
@admin.display(description="Paid this year", boolean=True, ordering="_has_payment")
def has_payment(self, obj):
return bool(getattr(obj, "_has_payment", False))
class MembershipCommitteeAdmin(admin.ModelAdmin):
pass
class MembershipServicesAdmin(admin.ModelAdmin):
pass
class CalendarEventAddressModelAdmin(admin.ModelAdmin):
pass
def download_payments(modelAdmin, request, queryset):
if "apply" not in request.POST:
return _csv_year_form_response(
modelAdmin,
request,
action_name="download_payments",
year_field="date",
help_text="Choose a year to include. Payments are filtered by payment date.",
)
year = request.POST.get("year", "all")
queryset = _apply_year_filter(queryset, "date", year)
rows = []
for q in queryset:
first_name = ""
last_name = ""
phone_number = ""
if q.person:
first_name = q.person.first_name if q.person.first_name else ""
last_name = q.person.last_name if q.person.last_name else ""
phone_number = q.person.phone_number if q.person.phone_number else ""
rows.append(
[
q.email,
q.date,
q.status,
first_name,
last_name,
phone_number,
]
)
return _csv_response(
f"_scha_payments_by_member{_year_suffix(year)}.csv",
["email", "date", "status", "first_name", "last_name", "phone_number"],
rows,
)
download_payments.short_description = "Download payments CSV"
class PaymentsAdmin(admin.ModelAdmin):
list_display = ["date", "status", "email"]
search_fields = ["email"]
list_filter = [PaymentYearListFilter]
actions = [download_payments]
form = PaymentImport
class SCHAOfficerAdmin(admin.ModelAdmin):
list_display = ["position", "name", "email"]
admin.site.register(UsefulLinks, UsefulLinksAdmin)
admin.site.register(Membership, MembershipAdmin)
admin.site.register(CalendarEvent, CalendarEventAdmin)
admin.site.register(AddressModel1, AddressModelAdmin)
admin.site.register(MembershipPerson, MembershipPersonAdmin)
admin.site.register(MembershipCommittee, MembershipCommitteeAdmin)
admin.site.register(MembershipServices, MembershipServicesAdmin)
admin.site.register(CalendarEventAddressModel, CalendarEventAddressModelAdmin)
admin.site.register(Payments, PaymentsAdmin)
admin.site.register(SCHAOfficer, SCHAOfficerAdmin)