For The Chance Encounter and The Chance Encounter Operations I need a system to take event reservations. I went to have a list of event timeslots, let's say hourly or half-hourly, that are listed in an org mode document
I think in line with the design of the Concept Operating System it should flow from the design of the documents themselves, how do I want a reservation system to look when I need to work with it, versus when the friend or customer are trying to register for it?
The data-flow I had come up with last night is:
an org-mode document with time spans that the event can be reserved, and times that are already reserved
an org-mode document parser that extracts the calendar data and generates a web page which people can click-through to register, fill in information in to a form and submit it
an django-admin function which calls the org-protocol stuff to pull in reservations to the org doc
then i can email the person right there to confirm the reservation when i do it using org-mime+gnus
close the reservation time off
So take Summer 2024 Private Japanese Tea Experiences, it's a list of headings with some property drawers, the events have AVAIL_DATES and AVAIL_TIMES, a matrix of these should be generated and shoved in to a form after being extracted from the Arcology roam.models.HeadingProperty extracted from the headings.
Data Models
These parser will only run if an ARCOLOGY_RESERVATIONS keyword on the Page.
from __future__ import annotations
from typing import List
from django.db import models
from django.conf import settings
from django_prometheus.models import ExportModelOperationsMixin as EMOM
import arroyo.arroyo_rs as native
import roam.models
import arrow
import logging
logger = logging.getLogger(__name__)reservations.models.Event
each heading on an ARROYO_RESERVATIONS page an event, this is basically just to make it possible to generate a cross-page index of events and to keep every heading from needing to be parsed for the appropriate HeadingProperty. The base event object is pretty simple, it only stores the heading properties in a partially normalized state and then further create_from_arroyo functions will generate all of the EventSlots which can be used to reserve times for Events.
class Event(EMOM('reservations'), models.Model):
file_path = models.CharField(max_length=512)
heading_id = models.CharField(max_length=512)
name = models.CharField(max_length=512)
avail_dates = models.TextField()
avail_times = models.TextField()Most Arcology models work from the document but it's a bit easier here for us to ask the ORM for some of this data; the headings' properties will have already been extracted from the document at this point, Arcology can just narrow a query for the properties that contain the event metadata, and do some very basic normalization to prepare the EventSlot generator.
@classmethod
def create_from_arroyo(cls, doc: native.Document) -> List[Event]:
is_reservation_doc = len(doc.collect_keywords("ARCOLOGY_RESERVATIONS")) > 0
if not is_reservation_doc:
logger.debug(f"no reservations md for doc {doc.path}")
return list()
f = roam.models.File.objects.get(path=doc.path)
def proc_one_heading(heading):
dates_qs = heading.headingproperty_set.filter(keyword="AVAIL_DATES")
times_qs = heading.headingproperty_set.filter(keyword="AVAIL_TIMES")
if dates_qs.count() == 0 or times_qs.count() == 0:
logger.debug(f"no dates or times for header {heading.node_id}")
return None
dates_hpv = map(lambda v: v.get("value"), dates_qs.values("value"))
times_hpv = map(lambda v: v.get("value"), times_qs.values("value"))
return cls.objects.get_or_create(
file_path=f.path,
heading_id=heading.node_id,
name=heading.title,
avail_dates=', '.join(dates_hpv),
avail_times=', '.join(times_hpv)
)[0]
return [
e for e in
[
proc_one_heading(heading)
for heading in f.heading_set.all()
]
if e is not None
]reservations.models.EventSlot
EventSlots are created for each hour interval in AVAIL_TIMES, for each of AVAIL_DAYS, except for intervals which a PendingReservation is created.
from django.utils import timezone
class EventSlot(EMOM('reservations'), models.Model):
class Meta:
ordering = ["start_time"]
event = models.ForeignKey(
Event,
on_delete=models.CASCADE
)
start_time = models.DateTimeField()
end_time = models.DateTimeField()
def __str__(self):
the_time = timezone.localtime(self.start_time)
return f"{self.event.name} @ {the_time.strftime('%a %B %-d %-I:%M%p')}"I'll refactor this expand_to_start_end code at some point soon, but this is the code that calculates the possible time slots of an event. It involves maybe too much string munging, I'd like to make the date.shift code a little bit more reasonable.
@classmethod
def create_from_arroyo(cls, doc: native.Document) -> List[EventSlot]:
es = Event.objects.filter(file_path=doc.path)
def slots_for_event(e: Event):
dates = e.avail_dates
times = e.avail_times
def expand_to_start_end(sdate, time_range):
date = arrow.get(sdate, "YYYY-MM-DD ddd", tzinfo=settings.TIME_ZONE)
(start, end) = times.split('-')
(start_h, start_m) = start.split(":")
start_d = date.shift(hours=int(start_h), minutes=int(start_m))
(end_h, end_m) = end.split(":")
end_d = date.shift(hours=int(end_h), minutes=int(end_m))
return arrow.Arrow.interval("hour", start_d, end_d,
interval=1, exact=True)
return [
cls.objects.get_or_create(
event=e,
start_time=time_tup[0].datetime,
end_time=time_tup[1].datetime,
)[0]
for date in dates.split(', ')
for time_tup in expand_to_start_end(date, times)
]
# flatten each event's slots in to a flat list
return [
es
for e in es
for es in slots_for_event(e)
] @classmethod
def available_slots(cls, page=None):
qs = cls.objects \
.annotate(num_res=models.Count("pendingreservation")) \
.filter(num_res=0).order_by("start_time")
if page is not None:
qs = qs.filter(event__file_path=page.file.path)
return qsreservations.models.PendingReservation
When someone fills out the reservation form, a PendingReservation object is created. This prevents double-registration, and captures the information in to a model that is visible in the Django Admin panel. A button will redirect to an org-protocol URI that will capture the form details back in to the Org document for the event as a noexport heading.
PAYMENT_METHOD_CHOICES = [
("venmo", "Venmo @ryan_rix"),
("cashapp", "CashApp $rrix"),
("cash", "Day-of Cash"),
("check", "Day-of Check made payable to Ryan Rix"),
]
class PendingReservation(EMOM('reservations'), models.Model):
event_slot = models.ForeignKey(
EventSlot,
on_delete=models.CASCADE,
verbose_name="Session Time",
)
event = models.ForeignKey(
Event,
on_delete=models.CASCADE
)
name = models.CharField(max_length=512)
email = models.EmailField(max_length=512)
payment_method = models.CharField(max_length=64, choices=PAYMENT_METHOD_CHOICES)
notes = models.TextField() @classmethod
def create_from_arroyo(cls, doc: native.Document) -> List[EventSlot]:
passNEXT Need more robust data handling, maybe time to set up routing...
org-protocol capture template
capture the form contents in to the org doc, further add a template email to the page so that I can email them a confirmation
reservations.models.ParsedReservation
Those captured reservations should still be reflected in the available event slots so that people don't double-book. I don't want to have to rely on the sqlite3 DB being long-lasting so these will need to be pulled out of the document and the hidden headings' metadata.
Embeddable Form
Add this HTML to the page you want the form to reside in:
<div id="embed-reservation"
hx-get="/reservations/form"
hx-trigger="load"
hx-swap="outerHTML"
> </div>It'll cause HTMX to load the form partial in on page-load, here is the form:
from django import forms
import reservations.models
class ReservationForm(forms.ModelForm):
class Meta:
model = reservations.models.PendingReservation
fields = ["name", "email",
"event_slot", "notes",
# "payment_method"
]
event_slot = forms.ModelChoiceField(
queryset=reservations.models.EventSlot.available_slots(),
label="Session Time"
)
# also need to make sure this can filter to show events on the page, rather than all of them as it doesfrom django.urls import path
from reservations import views
urlpatterns = [
path("form", views.reservation_form),
]{% load django_htmx %}
{% load tz %}
<style type="text/css">
form > label {
width: 25%;
display: inline-block;
text-align: right;
vertical-align: top;
}
form > input,select,textarea {
width: 70%;
display: inline-block;
background-color: var(--light-gray);
font-family: "Vulf Sans";
font-size: medium;
}
form > input[type="submit"] {
width: 50%;
display: block;
margin: auto;
}
ul.errorlist {
color: var(--alert);
font-weight: 900;
}
@media all and (max-width: 991px) {
div.content { padding: 0 };
main { width: 100%; };
form > label { width: 20%; };
}
</style>
{% timezone "America/Los_Angeles" %}
<form id="res-form"
action="/reservations/form" method="post"
>
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit"
hx-post="/reservations/form" hx-trigger="click"
hx-target="#res-form">
</form>
{% endtimezone %}{% load django_htmx %}
{% load tz %}
{% timezone "America/Los_Angeles" %}
<b>Hey thanks! I'll email you before the event with more details!</b>
{% endtimezone %}from django.forms import modelformset_factory
from django.shortcuts import render
import arcology.models
import reservations.models
import reservations.forms
def reservation_form(request):
if request.method == "POST":
form = reservations.forms.ReservationForm(request.POST)
if form.is_valid():
data = form.cleaned_data
data["event_id"] = data["event_slot"].event_id
the_pending_reservation = reservations.models.PendingReservation(**data)
the_pending_reservation.save()
return render(request, "reservations/success.html", {"res": the_pending_reservation})
else:
form = reservations.forms.ReservationForm()
if request.htmx:
# 'http://localhost:29543/teasite/reservations'
page = arcology.models.Page.find_by_url(request.htmx.current_url)
form.fields["event_slot"].queryset = reservations.models.EventSlot.available_slots(page)
return render(request, "reservations/_form.html", {"form": form})Admin
from django.contrib import admin
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
import reservations.models
class SlotInline(admin.TabularInline):
model = reservations.models.EventSlot
# fk_name = "dependent"
@admin.register(reservations.models.Event)
class EventAdmin(admin.ModelAdmin):
list_display = ["name", "avail_dates"]
inlines = [ SlotInline ]
@admin.register(reservations.models.EventSlot)
class SlotAdmin(admin.ModelAdmin):
list_display = ["event_name", "start_time"]
@admin.display(description="Name")
def event_name(self, obj):
return obj.event.name
@admin.register(reservations.models.PendingReservation)
class PendingResAdmin(admin.ModelAdmin):
list_display = ["event_slot", "name", "email"]File Configuration
# Local Variables: # org-src-preserve-indentation: t # End: