from dataclasses import dataclass
from django.conf import settings
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).removeprefix(settings.STATIC_URL)
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