Compare commits

...

6 Commits

7 changed files with 145 additions and 14 deletions

View File

@ -1,3 +1,5 @@
from dataclasses import dataclass
from django.contrib import admin
from django.contrib.humanize.templatetags.humanize import naturaltime
from django.http import HttpRequest
@ -10,6 +12,7 @@ from django_object_actions import (
)
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 (
@ -111,6 +114,7 @@ class TransactionAdmin(BaseMembershipWorksAdmin):
class EventMeetingTimeInline(admin.TabularInline):
model = EventMeetingTime
show_change_link = True
fields = ["start", "end", "duration", "resources"]
readonly_fields = ["duration"]
autocomplete_fields = ["resources"]
@ -168,6 +172,7 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
"count",
"cap",
"category",
"meeting_times_match_event",
]
list_filter = [
"category",
@ -194,6 +199,8 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
"instructor_percentage",
"instructor_flat_rate",
("should_survey", "survey_email_sent"),
"links",
"meeting_times_match_event",
]
},
),
@ -203,7 +210,7 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
"classes": ["collapse"],
"fields": [
"eid",
"_url",
"url",
"start",
"end",
"duration",
@ -225,6 +232,23 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
),
]
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)
@ -234,12 +258,16 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
for field in Event._meta.get_fields():
if field.auto_created or field.many_to_many or not field.concrete:
continue
elif field.name == "url":
fields.append("_url")
else:
fields.append(field.name)
fields.insert(fields.index("end") + 1, "duration")
fields += ["details_timestamp", "details", "registrations"]
fields += [
"links",
"details_timestamp",
"details",
"registrations",
"meeting_times_match_event",
]
return fields
@admin.display(ordering="title")
@ -250,10 +278,19 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
def duration(self, obj):
return obj.duration
@admin.display(description="URL")
def _url(self, obj):
@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://claremontmakerspace.org/events/#!event/{0}">{0}</a>',
'<a href="https://membershipworks.com/admin/#!event/admin/{0}">Admin</a> | '
'<a href="https://claremontmakerspace.org/events/#!event/{0}">Event List</a>',
obj.url,
)

View File

@ -0,0 +1,25 @@
function maybeUpdateEndField(start_date_field: HTMLInputElement) {
let end_date_field: HTMLInputElement | null | undefined = start_date_field
?.closest(".form-row")
?.querySelector(".field-end .vDateField");
if (end_date_field && !end_date_field.value) {
end_date_field.value = start_date_field.value;
}
}
function setupListeners(element: ParentNode) {
for (let start_date_field of element.querySelectorAll(
"#meeting_times-group .field-start .vDateField",
) as NodeListOf<HTMLInputElement>) {
start_date_field.addEventListener("change", () => {
maybeUpdateEndField(start_date_field);
});
// element is focused after selecting a date using Django's DateTimeShortcuts
start_date_field.addEventListener("focus", () => {
maybeUpdateEndField(start_date_field);
});
}
}
$(() => setupListeners(document));
$(document).on("formset:added", (event) => setupListeners(event.target));

View File

@ -503,6 +503,28 @@ class EventExtQuerySet(models.QuerySet["EventExtAnnotated"]):
net_revenue=F("gross_revenue") - F("total_due_to_instructor"),
)
def with_meeting_times_match_event(self):
return self.annotate(
meeting_times_match_event=(
Q(
start=Subquery(
EventMeetingTime.objects.filter(event=OuterRef("pk"))
.order_by("start")
.values("start")[:1],
output_field=models.DateTimeField(),
)
)
& Q(
end=Subquery(
EventMeetingTime.objects.filter(event=OuterRef("pk"))
.order_by("-end")
.values("end")[:1],
output_field=models.DateTimeField(),
)
)
),
)
class EventExtManager(models.Manager):
def get_queryset(self) -> EventExtQuerySet:

View File

@ -10,6 +10,7 @@
},
"devDependencies": {
"@types/bootstrap": "^5.2.10",
"@types/jquery": "^3.5.30",
"@types/tabulator-tables": "^6.2.3",
"globby": "^14.0.2",
"prettier": "^3.3.3",

View File

@ -24,6 +24,9 @@ importers:
'@types/bootstrap':
specifier: ^5.2.10
version: 5.2.10
'@types/jquery':
specifier: ^3.5.30
version: 3.5.30
'@types/tabulator-tables':
specifier: ^6.2.3
version: 6.2.3
@ -288,6 +291,12 @@ packages:
'@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
'@types/jquery@3.5.30':
resolution: {integrity: sha512-nbWKkkyb919DOUxjmRVk8vwtDb0/k8FKncmUKFi+NY+QXqWltooxTrswvz4LspQwxvLdvzBN1TImr6cw3aQx2A==}
'@types/sizzle@2.3.8':
resolution: {integrity: sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==}
'@types/tabulator-tables@6.2.3':
resolution: {integrity: sha512-ZeRF/WvtwFXml/4aT7kzfkHEiwbjHZdlIsjrgqcfdmpkl9GQ9XBHY6u9BblUaHX4NUiOlBeHrQKjvai6/bQH0g==}
@ -624,6 +633,12 @@ snapshots:
'@types/estree@1.0.5': {}
'@types/jquery@3.5.30':
dependencies:
'@types/sizzle': 2.3.8
'@types/sizzle@2.3.8': {}
'@types/tabulator-tables@6.2.3': {}
anymatch@3.1.3:

View File

@ -0,0 +1,24 @@
# Generated by Django 5.1.1 on 2024-09-09 21:32
import django.contrib.postgres.fields.ranges
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("reservations", "0002_externalreservation"),
]
operations = [
migrations.AddField(
model_name="reservation",
name="timespan",
field=models.GeneratedField(
db_persist=True,
expression=models.Func(
models.F("start"), models.F("end"), function="tstzrange"
),
output_field=django.contrib.postgres.fields.ranges.DateTimeRangeField(),
),
),
]

View File

@ -3,11 +3,13 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import DateTimeRangeField
from django.db import models
from django.db.models import F, Q, QuerySet
from django.utils import timezone
from model_utils.managers import InheritanceQuerySetMixin
from psycopg.types.range import Range
if TYPE_CHECKING:
from collections.abc import Iterable
@ -55,31 +57,32 @@ class ReservationQuerySet(InheritanceQuerySetMixin, QuerySet):
Selects events that are after the specified datetime, including
overlaps but excluding exactly matching endpoints.
"""
return self.filter(Q(start__gt=start) | Q(end__gt=start))
return self.filter(timespan__overlap=Range(start, None))
def filter_before(self, end: datetime) -> ReservationQuerySet:
"""
Selects events that are before the specified datetime, including
overlaps but excluding exactly matching endpoints.
"""
return self.filter(Q(start__lt=end) | Q(end__lt=end))
return self.filter(timespan__overlap=Range(None, end))
def filter_between(self, start: datetime, end: datetime) -> ReservationQuerySet:
"""
Selects events that are between the specified datetime, including
overlaps but excluding exactly matching endpoints.
"""
return self.filter(
Q(start__gt=start, start__lt=end)
| Q(end__gt=start, end__lt=end)
| (Q(start__lt=start) & Q(end__gt=end))
)
return self.filter(timespan__overlap=Range(start, end))
class Reservation(models.Model):
resources = models.ManyToManyField(Resource, blank=True)
start = models.DateTimeField()
end = models.DateTimeField()
timespan = models.GeneratedField(
expression=models.Func(F("start"), F("end"), function="tstzrange"),
output_field=DateTimeRangeField(),
db_persist=True,
)
google_calendar_event_id = models.CharField(
max_length=1024, null=True, blank=True, unique=True
)
@ -101,6 +104,10 @@ class Reservation(models.Model):
resources = ", ".join(str(resource) for resource in self.resources.all())
return f"{resources}: {self.start} - {self.end}"
def __repr__(self) -> str:
"""Redefined to avoid __str__ and therefore .resources, which was causing recursion issues"""
return f"<{self.__class__.__name__}: {self.start} - {self.end}>"
def get_title(self) -> str:
return "Unknown Reservation"