Compare commits
6 Commits
efb15dd118
...
66ceb1aae1
Author | SHA1 | Date | |
---|---|---|---|
66ceb1aae1 | |||
c31fea20ff | |||
5d98e300ea | |||
2cc6f9d2e0 | |||
7d5fd08855 | |||
7101e90f8f |
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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"),
|
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:
|
||||||
|
@ -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",
|
||||||
|
@ -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:
|
||||||
|
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 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"
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user