from dataclasses import dataclass from django.contrib import admin from django.contrib.humanize.templatetags.humanize import naturaltime from django.http import HttpRequest from django.utils.html import format_html from django_object_actions import ( DjangoObjectActions, action, takes_instance_or_queryset, ) from django_q.models import Task from django_q.tasks import async_task from django_vite.templatetags.django_vite import vite_asset_url from simple_history.admin import SimpleHistoryAdmin from .models import ( Event, EventExt, EventInstructor, EventInvoice, EventMeetingTime, Flag, Member, Transaction, ) from .tasks.scrape import scrape_event_details, scrape_events, scrape_membershipworks from .tasks.ucsAccounts import sync_accounts class TaskLabel: def __init__(self, label: str, task) -> None: self.label = label self.task = task def __str__(self) -> str: try: last_run = naturaltime( Task.objects.filter(group=self.task.q_task_group) .values_list("started", flat=True) .latest("started") ) except Task.DoesNotExist: last_run = "Never" return f"{self.label} [Last Run {last_run}]" def run_task_action(admin: admin.ModelAdmin, label: str, task): @action(label=TaskLabel(label, task)) def action_func(request, obj): async_task(task, group=task.q_task_group) admin.message_user( request, "Queued task, please wait a few seconds/minutes then refresh the page", ) return action_func class ReadOnlyAdminMixin: def has_add_permission(self, request, obj=None): return False def has_change_permission(self, request, obj=None): return False def has_delete_permission(self, request, obj=None): return False class BaseMembershipWorksAdmin( DjangoObjectActions, ReadOnlyAdminMixin, SimpleHistoryAdmin ): changelist_actions = ("refresh_membershipworks_data", "sync_ucs_accounts") @property def refresh_membershipworks_data(self): return run_task_action(self, "Refresh Data", scrape_membershipworks) @property def sync_ucs_accounts(self): return run_task_action(self, "Sync UCS Accounts", sync_accounts) class MemberFlagInline(admin.TabularInline): model = Member.flags.through @admin.register(Member) class MemberAdmin(BaseMembershipWorksAdmin): search_fields = ["^first_name", "^last_name", "^account_name"] inlines = [MemberFlagInline] @admin.register(Flag) class FlagAdmin(BaseMembershipWorksAdmin): inlines = [MemberFlagInline] list_display = ["name", "type"] list_filter = ["type"] show_facets = admin.ShowFacets.ALWAYS search_fields = ["name"] @admin.register(Transaction) class TransactionAdmin(BaseMembershipWorksAdmin): list_display = ["timestamp", "member", "name", "type", "sum", "note"] list_select_related = ["member"] list_filter = ["type"] show_facets = admin.ShowFacets.ALWAYS search_fields = ["member", "name", "type", "note"] date_hierarchy = "timestamp" class EventMeetingTimeInline(admin.TabularInline): model = EventMeetingTime show_change_link = True fields = ["start", "end", "duration", "resources"] readonly_fields = ["duration"] autocomplete_fields = ["resources"] extra = 0 min_num = 1 @admin.register(EventInstructor) class EventInstructorAdmin(admin.ModelAdmin): autocomplete_fields = ["member"] search_fields = ["name", "member__account_name"] list_select_related = ["member"] @admin.register(EventInvoice) class EventInvoiceAdmin(admin.ModelAdmin): model = EventInvoice list_display = [ "uuid", "event", "event__start", "event__end", "event__instructor", "date_submitted", "date_paid", "amount", ] list_select_related = ["event__instructor__member"] list_filter = [ ("date_paid", admin.EmptyFieldListFilter), ] show_facets = admin.ShowFacets.ALWAYS search_fields = [ "uuid", "event__eid", "event__title", "event__url", "event__instructor__name", "event__instructor__member__account_name", ] date_hierarchy = "date_submitted" class EventInvoiceInline(admin.StackedInline): model = EventInvoice @admin.register(EventExt) class EventAdmin(DjangoObjectActions, admin.ModelAdmin): inlines = [EventInvoiceInline, EventMeetingTimeInline] list_display = [ "unescaped_title", "start", "duration", "count", "cap", "category", "meeting_times_match_event", ] list_filter = [ "category", "calendar", "venue", ("materials_fee", admin.EmptyFieldListFilter), ] show_facets = admin.ShowFacets.ALWAYS search_fields = ["eid", "title", "url"] date_hierarchy = "start" autocomplete_fields = ["instructor"] change_actions = ["fetch_details"] actions = ["fetch_details"] changelist_actions = ["refresh_membershipworks_data"] fieldsets = [ ( None, { "fields": [ "instructor", "materials_fee", "materials_fee_included_in_price", "instructor_percentage", "instructor_flat_rate", ("should_survey", "survey_email_sent"), "links", "meeting_times_match_event", ] }, ), ( "Details", { "classes": ["collapse"], "fields": [ "eid", "url", "start", "end", "duration", "count", "cap", "category", "calendar", "venue", "occurred", ], }, ), ( "Advanced details", { "classes": ["collapse"], "fields": ["details_timestamp", "details", "registrations"], }, ), ] class Media: @dataclass(frozen=True) class LazyViteAssetUrl(str): asset: str def __str__(self) -> str: return vite_asset_url(self.asset) js = [ LazyViteAssetUrl( "membershipworks/js/event_meeting_time_admin_helper.entry.ts" ) ] def get_queryset(self, request): return EventExt.objects.with_meeting_times_match_event() @property def refresh_membershipworks_data(self): return run_task_action(self, "Refresh Data", scrape_events) def get_readonly_fields(self, request: HttpRequest, obj: EventExt) -> list[str]: fields = [] for field in Event._meta.get_fields(): if field.auto_created or field.many_to_many or not field.concrete: continue else: fields.append(field.name) fields.insert(fields.index("end") + 1, "duration") fields += [ "links", "details_timestamp", "details", "registrations", "meeting_times_match_event", ] return fields @admin.display(ordering="title") def unescaped_title(self, obj): return obj.unescaped_title @admin.display(ordering="duration") def duration(self, obj): return obj.duration @admin.display( boolean=True, description="Meeting times match event start/end", ordering="meeting_times_match_event", ) def meeting_times_match_event(self, obj) -> bool: return obj.meeting_times_match_event @admin.display(description="MembershipWorks links") def links(self, obj): return format_html( 'Admin | ' 'Event List', obj.url, ) @takes_instance_or_queryset def fetch_details(self, request, queryset): scrape_event_details(queryset) def has_add_permission(self, request, obj=None): return False def has_delete_permission(self, request, obj=None): return False @admin.register(EventMeetingTime) class EventMeetingTimeAdmin(admin.ModelAdmin): def has_module_permission(self, request: HttpRequest) -> bool: return False