Compare commits
6 Commits
66ceb1aae1
...
efb15dd118
Author | SHA1 | Date | |
---|---|---|---|
efb15dd118 | |||
ccc7a595ba | |||
69defab388 | |||
59d2ff4cb7 | |||
d19b2d19fb | |||
d25f1e673a |
@ -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,
|
||||
)
|
||||
|
||||
|
25
membershipworks/js/event_meeting_time_admin_helper.entry.ts
Normal file
25
membershipworks/js/event_meeting_time_admin_helper.entry.ts
Normal 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));
|
@ -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:
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
|
24
reservations/migrations/0003_reservation_timespan.py
Normal file
24
reservations/migrations/0003_reservation_timespan.py
Normal 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(),
|
||||
),
|
||||
),
|
||||
]
|
@ -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"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user