Compare commits

...

6 Commits

7 changed files with 137 additions and 14 deletions

View File

@ -10,6 +10,7 @@ from django_object_actions import (
) )
from django_q.models import Task from django_q.models import Task
from django_q.tasks import async_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 simple_history.admin import SimpleHistoryAdmin
from .models import ( from .models import (
@ -111,6 +112,7 @@ class TransactionAdmin(BaseMembershipWorksAdmin):
class EventMeetingTimeInline(admin.TabularInline): class EventMeetingTimeInline(admin.TabularInline):
model = EventMeetingTime model = EventMeetingTime
show_change_link = True
fields = ["start", "end", "duration", "resources"] fields = ["start", "end", "duration", "resources"]
readonly_fields = ["duration"] readonly_fields = ["duration"]
autocomplete_fields = ["resources"] autocomplete_fields = ["resources"]
@ -168,6 +170,7 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
"count", "count",
"cap", "cap",
"category", "category",
"meeting_times_match_event",
] ]
list_filter = [ list_filter = [
"category", "category",
@ -194,6 +197,8 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
"instructor_percentage", "instructor_percentage",
"instructor_flat_rate", "instructor_flat_rate",
("should_survey", "survey_email_sent"), ("should_survey", "survey_email_sent"),
"links",
"meeting_times_match_event",
] ]
}, },
), ),
@ -203,7 +208,7 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
"classes": ["collapse"], "classes": ["collapse"],
"fields": [ "fields": [
"eid", "eid",
"_url", "url",
"start", "start",
"end", "end",
"duration", "duration",
@ -225,6 +230,16 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
), ),
] ]
class Media:
js = [
vite_asset_url(
"membershipworks/js/event_meeting_time_admin_helper.entry.ts"
)
]
def get_queryset(self, request):
return EventExt.objects.with_meeting_times_match_event()
@property @property
def refresh_membershipworks_data(self): def refresh_membershipworks_data(self):
return run_task_action(self, "Refresh Data", scrape_events) return run_task_action(self, "Refresh Data", scrape_events)
@ -234,12 +249,16 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
for field in Event._meta.get_fields(): for field in Event._meta.get_fields():
if field.auto_created or field.many_to_many or not field.concrete: if field.auto_created or field.many_to_many or not field.concrete:
continue continue
elif field.name == "url":
fields.append("_url")
else: else:
fields.append(field.name) fields.append(field.name)
fields.insert(fields.index("end") + 1, "duration") fields.insert(fields.index("end") + 1, "duration")
fields += ["details_timestamp", "details", "registrations"] fields += [
"links",
"details_timestamp",
"details",
"registrations",
"meeting_times_match_event",
]
return fields return fields
@admin.display(ordering="title") @admin.display(ordering="title")
@ -250,10 +269,20 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
def duration(self, obj): def duration(self, obj):
return obj.duration return obj.duration
@admin.display(description="URL") @admin.display(
def _url(self, obj): boolean=True,
description="Meeting times match event start/end",
ordering="meeting_times_match_event",
)
def meeting_times_match_event(self, obj) -> bool:
print(obj.meeting_times_match_event)
return obj.meeting_times_match_event
@admin.display(description="MembershipWorks links")
def links(self, obj):
return format_html( 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, 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"), 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): class EventExtManager(models.Manager):
def get_queryset(self) -> EventExtQuerySet: def get_queryset(self) -> EventExtQuerySet:

View File

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

View File

@ -24,6 +24,9 @@ importers:
'@types/bootstrap': '@types/bootstrap':
specifier: ^5.2.10 specifier: ^5.2.10
version: 5.2.10 version: 5.2.10
'@types/jquery':
specifier: ^3.5.30
version: 3.5.30
'@types/tabulator-tables': '@types/tabulator-tables':
specifier: ^6.2.3 specifier: ^6.2.3
version: 6.2.3 version: 6.2.3
@ -288,6 +291,12 @@ packages:
'@types/estree@1.0.5': '@types/estree@1.0.5':
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} 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': '@types/tabulator-tables@6.2.3':
resolution: {integrity: sha512-ZeRF/WvtwFXml/4aT7kzfkHEiwbjHZdlIsjrgqcfdmpkl9GQ9XBHY6u9BblUaHX4NUiOlBeHrQKjvai6/bQH0g==} resolution: {integrity: sha512-ZeRF/WvtwFXml/4aT7kzfkHEiwbjHZdlIsjrgqcfdmpkl9GQ9XBHY6u9BblUaHX4NUiOlBeHrQKjvai6/bQH0g==}
@ -624,6 +633,12 @@ snapshots:
'@types/estree@1.0.5': {} '@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': {} '@types/tabulator-tables@6.2.3': {}
anymatch@3.1.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 typing import TYPE_CHECKING
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.postgres.fields import DateTimeRangeField
from django.db import models from django.db import models
from django.db.models import F, Q, QuerySet from django.db.models import F, Q, QuerySet
from django.utils import timezone from django.utils import timezone
from model_utils.managers import InheritanceQuerySetMixin from model_utils.managers import InheritanceQuerySetMixin
from psycopg.types.range import Range
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Iterable from collections.abc import Iterable
@ -55,31 +57,32 @@ class ReservationQuerySet(InheritanceQuerySetMixin, QuerySet):
Selects events that are after the specified datetime, including Selects events that are after the specified datetime, including
overlaps but excluding exactly matching endpoints. 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: def filter_before(self, end: datetime) -> ReservationQuerySet:
""" """
Selects events that are before the specified datetime, including Selects events that are before the specified datetime, including
overlaps but excluding exactly matching endpoints. 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: def filter_between(self, start: datetime, end: datetime) -> ReservationQuerySet:
""" """
Selects events that are between the specified datetime, including Selects events that are between the specified datetime, including
overlaps but excluding exactly matching endpoints. overlaps but excluding exactly matching endpoints.
""" """
return self.filter( return self.filter(timespan__overlap=Range(start, end))
Q(start__gt=start, start__lt=end)
| Q(end__gt=start, end__lt=end)
| (Q(start__lt=start) & Q(end__gt=end))
)
class Reservation(models.Model): class Reservation(models.Model):
resources = models.ManyToManyField(Resource, blank=True) resources = models.ManyToManyField(Resource, blank=True)
start = models.DateTimeField() start = models.DateTimeField()
end = 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( google_calendar_event_id = models.CharField(
max_length=1024, null=True, blank=True, unique=True 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()) resources = ", ".join(str(resource) for resource in self.resources.all())
return f"{resources}: {self.start} - {self.end}" 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: def get_title(self) -> str:
return "Unknown Reservation" return "Unknown Reservation"