cmsmanage/membershipworks/admin.py

313 lines
8.6 KiB
Python
Raw Normal View History

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(
'<a href="https://membershipworks.com/admin/#!event/admin/{0}">Admin</a> | '
'<a href="https://claremontmakerspace.org/events/#!event/{0}">Event List</a>',
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