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)