Convert from MariaDB to PostgreSQL

MariaDB has become far too annoying/buggy, and there are some neat
features only available in PostgreSQL
This commit is contained in:
Adam Goldsmith 2024-08-26 23:45:39 -04:00
parent 97b746ba3a
commit ee61451759
39 changed files with 749 additions and 1232 deletions

View File

@ -9,21 +9,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest container: catthehacker/ubuntu:act-latest
services: services:
mariadb: postgres:
# TODO: this is pinned to avoid what apears to be a bug with image: postgres:15
# MariaDB >= 10.11.9, and collation issues with 11.x.x
image: mariadb:10.11.8
env: env:
MARIADB_ROOT_PASSWORD: whatever POSTGRES_PASSWORD: whatever
healthcheck: healthcheck:
test: test: ["CMD-SHELL", "pg_isready"]
[
"CMD",
"healthcheck.sh",
"--su-mysql",
"--connect",
"--innodb_initialized",
]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup PDM - name: Setup PDM
@ -35,7 +26,7 @@ jobs:
- name: Install apt dependencies - name: Install apt dependencies
run: >- run: >-
sudo apt-get update && sudo apt-get update &&
sudo apt-get -y install build-essential python3-dev libldap2-dev libsasl2-dev mariadb-client sudo apt-get -y install build-essential python3-dev libldap2-dev libsasl2-dev
- name: Install python dependencies - name: Install python dependencies
run: pdm sync -d -G dev run: pdm sync -d -G dev

View File

@ -28,7 +28,7 @@ class Base(Configuration):
@classmethod @classmethod
def setup(cls): def setup(cls):
super().setup() super().setup()
cls.DATABASES["default"]["OPTIONS"] = {"charset": "utf8mb4"} cls.DATABASES["default"]["OPTIONS"] = {"pool": True}
INSTALLED_APPS = [ INSTALLED_APPS = [
"dal", "dal",
@ -52,7 +52,6 @@ class Base(Configuration):
"django_tables2", "django_tables2",
"django_filters", "django_filters",
"django_db_views", "django_db_views",
"django_mysql",
"django_sendfile", "django_sendfile",
"django_bootstrap5", "django_bootstrap5",
# "tasks.apps.TasksConfig", # "tasks.apps.TasksConfig",
@ -106,9 +105,6 @@ class Base(Configuration):
WSGI_APPLICATION = "cmsmanage.wsgi.application" WSGI_APPLICATION = "cmsmanage.wsgi.application"
# mysql.W003 (unique CharField length) is irrelevant on MariaDB >= 10.4.3
SILENCED_SYSTEM_CHECKS = ["mysql.W003"]
# Default URL to redirect to after authentication # Default URL to redirect to after authentication
LOGIN_REDIRECT_URL = "/" LOGIN_REDIRECT_URL = "/"
LOGIN_URL = "/auth/login/" LOGIN_URL = "/auth/login/"
@ -213,6 +209,9 @@ class Base(Configuration):
# CMSManage specific stuff # CMSManage specific stuff
WIKI_URL = values.URLValue("https://wiki.claremontmakerspace.org") WIKI_URL = values.URLValue("https://wiki.claremontmakerspace.org")
# ID of flag for Members folder in MembershipWorks
MW_MEMBERS_FOLDER_ID = "5771675edcdf126302a2f6b9"
class NonCIBase(Base): class NonCIBase(Base):
"""required for all but CI""" """required for all but CI"""
@ -367,13 +366,10 @@ class CI(Base):
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.mysql", "ENGINE": "django.db.backends.postgresql",
"HOST": "mariadb", "HOST": "postgres",
"NAME": "CMS_Database", "NAME": "cms",
"USER": "root", "USER": "postgres",
"PASSWORD": "whatever", "PASSWORD": "whatever",
"OPTIONS": {
"charset": "utf8mb4",
},
} }
} }

View File

@ -1,14 +1,40 @@
# Generated by Django 4.1.3 on 2023-01-25 02:18 # Generated by Django 5.1 on 2024-08-21 18:31
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [] dependencies = [
("membershipworks", "0001_initial"),
]
operations = [ operations = [
migrations.CreateModel(
name="Door",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=64, unique=True)),
("ip", models.GenericIPAddressField(protocol="IPv4")),
(
"access_field",
models.TextField(
help_text="Membershipworks field that grants members access to this door",
max_length=128,
),
),
],
),
migrations.CreateModel( migrations.CreateModel(
name="HIDEvent", name="HIDEvent",
fields=[ fields=[
@ -21,7 +47,6 @@ class Migration(migrations.Migration):
verbose_name="ID", verbose_name="ID",
), ),
), ),
("door_name", models.CharField(db_column="doorName", max_length=64)),
("timestamp", models.DateTimeField()), ("timestamp", models.DateTimeField()),
( (
"event_type", "event_type",
@ -88,16 +113,173 @@ class Migration(migrations.Migration):
blank=True, db_column="rawCardNumber", max_length=8, null=True blank=True, db_column="rawCardNumber", max_length=8, null=True
), ),
), ),
(
"door",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="doorcontrol.door",
),
),
(
"is_red",
models.GeneratedField(
db_persist=True,
expression=models.Q(
(
"event_type__in",
[
1022,
1023,
2024,
2029,
2036,
2042,
2043,
2046,
4041,
4042,
4043,
4044,
4045,
],
)
),
output_field=models.BooleanField(),
),
),
], ],
options={ options={
"db_table": "hidevent", "db_table": "hidevent",
"ordering": ("-timestamp",), "ordering": ("-timestamp",),
"constraints": [
models.UniqueConstraint(
fields=("door", "timestamp", "event_type"),
name="unique_hidevent",
)
],
}, },
), ),
migrations.AddConstraint( migrations.CreateModel(
model_name="hidevent", name="DoorCardholderMember",
constraint=models.UniqueConstraint( fields=[
fields=("door_name", "timestamp", "event_type"), name="unique_hidevent" (
), "id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("cardholder_id", models.IntegerField()),
(
"door",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="doorcontrol.door",
),
),
(
"member",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.CASCADE,
to="membershipworks.member",
),
),
],
options={
"constraints": [
models.UniqueConstraint(
fields=("door", "cardholder_id"),
name="unique_door_cardholder_id",
),
models.UniqueConstraint(
fields=("door", "member"), name="unique_door_member"
),
],
},
),
migrations.CreateModel(
name="Schedule",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
],
),
migrations.CreateModel(
name="FlagScheduleRule",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("doors", models.ManyToManyField(to="doorcontrol.door")),
(
"flag",
models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.flag",
),
),
(
"schedule",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="doorcontrol.schedule",
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="AttributeScheduleRule",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"access_field",
models.CharField(
help_text="Membershipworks field that grants members access to this door using this schedule.",
max_length=128,
),
),
("doors", models.ManyToManyField(to="doorcontrol.door")),
(
"schedule",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="doorcontrol.schedule",
),
),
],
options={
"abstract": False,
},
), ),
] ]

View File

@ -1,74 +0,0 @@
# Generated by Django 4.2.5 on 2023-09-19 04:20
import django.db.models.deletion
from django.db import migrations, models
def link_events_to_doors(apps, schema_editor):
HIDEvent = apps.get_model("doorcontrol", "HIDEvent")
Door = apps.get_model("doorcontrol", "Door")
for event in HIDEvent.objects.all():
door, created = Door.objects.get_or_create(name=event.door_name)
event.door = door
event.save()
class Migration(migrations.Migration):
dependencies = [
("doorcontrol", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
name="hidevent",
options={"ordering": ("-timestamp",)},
),
migrations.CreateModel(
name="Door",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=64, unique=True)),
],
),
# create nullable foreign key to door
migrations.AddField(
model_name="hidevent",
name="door",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="doorcontrol.door",
null=True,
),
),
# create new Doors and link them to HID Events
migrations.RunPython(link_events_to_doors, atomic=True),
# make door foreign key not nullable
migrations.AlterField(
model_name="hidevent",
name="door",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="doorcontrol.door"
),
),
# remove old constaint
migrations.RemoveConstraint(model_name="hidevent", name="unique_hidevent"),
# remove old name field
migrations.RemoveField(
model_name="hidevent",
name="door_name",
),
migrations.AddConstraint(
model_name="hidevent",
constraint=models.UniqueConstraint(
fields=("door", "timestamp", "event_type"), name="unique_hidevent"
),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.5 on 2023-09-19 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("doorcontrol", "0002_door_remove_hidevent_door_name_and_more"),
]
operations = [
migrations.AddField(
model_name="door",
name="ip",
field=models.GenericIPAddressField(default="", protocol="IPv4"),
preserve_default=False,
),
]

View File

@ -1,40 +0,0 @@
# Generated by Django 5.0 on 2023-12-04 16:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("doorcontrol", "0003_door_ip"),
]
operations = [
migrations.AddField(
model_name="hidevent",
name="is_red",
field=models.GeneratedField(
db_persist=False,
expression=models.Q(
(
"event_type__in",
[
1022,
1023,
2024,
2029,
2036,
2042,
2043,
2046,
4041,
4042,
4043,
4044,
4045,
],
)
),
output_field=models.BooleanField(),
),
),
]

View File

@ -1,56 +0,0 @@
# Generated by Django 5.0.1 on 2024-02-09 16:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("doorcontrol", "0004_hidevent_is_red"),
("membershipworks", "0014_remove_eventext_details_timestamp"),
]
operations = [
migrations.CreateModel(
name="DoorCardholderMember",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("cardholder_id", models.IntegerField()),
(
"door",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="doorcontrol.door",
),
),
(
"member",
models.ForeignKey(
db_constraint=False,
on_delete=django.db.models.deletion.CASCADE,
to="membershipworks.member",
),
),
],
),
migrations.AddConstraint(
model_name="doorcardholdermember",
constraint=models.UniqueConstraint(
fields=("door", "cardholder_id"), name="unique_door_cardholder_id"
),
),
migrations.AddConstraint(
model_name="doorcardholdermember",
constraint=models.UniqueConstraint(
fields=("door", "member"), name="unique_door_member"
),
),
]

View File

@ -1,106 +0,0 @@
# Generated by Django 5.0.2 on 2024-02-23 18:36
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("doorcontrol", "0005_doorcardholdermember_and_more"),
("membershipworks", "0015_eventmeetingtime_end_after_start"),
]
operations = [
migrations.CreateModel(
name="Schedule",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
],
),
migrations.AddField(
model_name="door",
name="access_field",
field=models.TextField(
default="CHANGE ME",
help_text="Membershipworks field that grants members access to this door",
max_length=128,
),
preserve_default=False,
),
migrations.CreateModel(
name="FlagScheduleRule",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("doors", models.ManyToManyField(to="doorcontrol.door")),
(
"flag",
models.ForeignKey(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.flag",
),
),
(
"schedule",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="doorcontrol.schedule",
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="AttributeScheduleRule",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"access_field",
models.CharField(
help_text="Membershipworks field that grants members access to this door using this schedule.",
max_length=128,
),
),
("doors", models.ManyToManyField(to="doorcontrol.door")),
(
"schedule",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="doorcontrol.schedule",
),
),
],
options={
"abstract": False,
},
),
]

View File

@ -183,7 +183,7 @@ class HIDEvent(models.Model):
] ]
), ),
output_field=models.BooleanField(), output_field=models.BooleanField(),
db_persist=False, db_persist=True,
) )
objects = HIDEventQuerySet.as_manager() objects = HIDEventQuerySet.as_manager()

View File

@ -17,13 +17,15 @@ def get_cardholders(door: Door):
member_id=cardholder.attrib.get("custom2"), member_id=cardholder.attrib.get("custom2"),
) )
cardholders = door.controller.get_cardholders()
DoorCardholderMember.objects.bulk_create( DoorCardholderMember.objects.bulk_create(
( (
make_ch_member(cardholder) make_ch_member(cardholder)
for cardholder in door.controller.get_cardholders() for cardholder in cardholders
if "custom2" in cardholder.attrib if "custom2" in cardholder.attrib
), ),
update_conflicts=True, update_conflicts=True,
unique_fields=("door", "cardholder_id"),
update_fields=("member",), update_fields=("member",),
) )

View File

@ -2,8 +2,9 @@ import datetime
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.postgres.aggregates import StringAgg
from django.core.exceptions import BadRequest from django.core.exceptions import BadRequest
from django.db.models import Count, F, FloatField, Q, Window from django.db.models import Count, F, FloatField, Func, Q, Value, Window
from django.db.models.functions import Lead, Trunc from django.db.models.functions import Lead, Trunc
from django.urls import path, reverse_lazy from django.urls import path, reverse_lazy
from django.utils.text import slugify from django.utils.text import slugify
@ -12,8 +13,6 @@ from django.views.generic.list import ListView
import django_filters import django_filters
import django_tables2 as tables import django_tables2 as tables
from django_filters.views import BaseFilterView from django_filters.views import BaseFilterView
from django_mysql.models.aggregates import GroupConcat
from django_mysql.models.functions import ConcatWS
from django_tables2 import SingleTableMixin from django_tables2 import SingleTableMixin
from django_tables2.export.views import ExportMixin from django_tables2.export.views import ExportMixin
@ -223,8 +222,10 @@ class MostActiveMembers(BaseAccessReport):
.values("member_id") .values("member_id")
.annotate( .annotate(
access_count=Count("member_id"), access_count=Count("member_id"),
name=GroupConcat( name=StringAgg(
ConcatWS("forename", "surname", separator=" "), distinct=True Func(Value(" "), "forename", "surname", function="concat_ws"),
", ",
distinct=True,
), ),
) )
.order_by("-access_count") .order_by("-access_count")
@ -249,8 +250,10 @@ class DetailByDay(BaseAccessReport):
"member_id", "member_id",
filter=Q(event_type__in=HIDEvent.EventType.any_granted_access()), filter=Q(event_type__in=HIDEvent.EventType.any_granted_access()),
), ),
name=GroupConcat( name=StringAgg(
ConcatWS("forename", "surname", separator=" "), distinct=True Func(Value(" "), "forename", "surname", function="concat_ws"),
", ",
distinct=True,
), ),
) )
.order_by("-timestamp__date") .order_by("-timestamp__date")

View File

@ -1,13 +1,21 @@
# Generated by Django 5.0 on 2023-12-20 05:40 # Generated by Django 5.1 on 2024-08-21 18:17
import uuid
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
from django.db.models.functions import Cast
import django_db_views.migration_functions
import django_db_views.operations
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [] dependencies = [
("reservations", "0001_initial"),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
@ -115,18 +123,6 @@ class Migration(migrations.Migration):
blank=True, db_column="Parent Account ID", null=True blank=True, db_column="Parent Account ID", null=True
), ),
), ),
(
"gift_membership_purchased_by",
models.TextField(
blank=True, db_column="Gift Membership purchased by", null=True
),
),
(
"purchased_gift_membership_for",
models.TextField(
blank=True, db_column="Purchased Gift Membership for", null=True
),
),
( (
"closet_storage", "closet_storage",
models.TextField( models.TextField(
@ -151,18 +147,6 @@ class Migration(migrations.Migration):
db_column="Access Permitted Shops During Extended Hours?" db_column="Access Permitted Shops During Extended Hours?"
), ),
), ),
(
"normal_access_permitted_during_covid19_limited_operations",
models.BooleanField(
db_column="Normal Access Permitted During COVID-19 Limited Operations"
),
),
(
"access_permitted_during_covid19_staffed_period_only",
models.BooleanField(
db_column="Access Permitted During COVID-19 Staffed Period Only"
),
),
( (
"access_front_door_and_studio_space_during_extended_hours", "access_front_door_and_studio_space_during_extended_hours",
models.BooleanField( models.BooleanField(
@ -349,18 +333,15 @@ class Migration(migrations.Migration):
"liability_form_filled_out", "liability_form_filled_out",
models.BooleanField(db_column="Liability Form Filled Out"), models.BooleanField(db_column="Liability Form Filled Out"),
), ),
(
"self_certify_essential_business",
models.BooleanField(db_column="selfCertifyEssentialBusiness"),
),
(
"accepted_covid19_policy",
models.BooleanField(db_column="Accepted COVID-19 Policy"),
),
], ],
options={ options={
"db_table": "members", "db_table": "members",
"ordering": ("first_name", "last_name"), "ordering": ("first_name", "last_name"),
"indexes": [
models.Index(fields=["account_name"], name="account_name_idx"),
models.Index(fields=["first_name"], name="first_name_idx"),
models.Index(fields=["last_name"], name="last_name_idx"),
],
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
@ -386,6 +367,7 @@ class Migration(migrations.Migration):
"member", "member",
models.ForeignKey( models.ForeignKey(
db_column="uid", db_column="uid",
db_constraint=False,
on_delete=django.db.models.deletion.PROTECT, on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.member", to="membershipworks.member",
), ),
@ -393,6 +375,11 @@ class Migration(migrations.Migration):
], ],
options={ options={
"db_table": "memberflag", "db_table": "memberflag",
"constraints": [
models.UniqueConstraint(
fields=("member", "flag"), name="unique_member_flag"
)
],
}, },
), ),
migrations.AddField( migrations.AddField(
@ -416,7 +403,7 @@ class Migration(migrations.Migration):
verbose_name="ID", verbose_name="ID",
), ),
), ),
("sid", models.CharField(blank=True, max_length=27, null=True)), ("sid", models.CharField(blank=True, max_length=256, null=True)),
("timestamp", models.DateTimeField()), ("timestamp", models.DateTimeField()),
("type", models.TextField(blank=True, null=True)), ("type", models.TextField(blank=True, null=True)),
( (
@ -469,6 +456,7 @@ class Migration(migrations.Migration):
models.ForeignKey( models.ForeignKey(
blank=True, blank=True,
db_column="uid", db_column="uid",
db_constraint=False,
null=True, null=True,
on_delete=django.db.models.deletion.PROTECT, on_delete=django.db.models.deletion.PROTECT,
related_name="transactions", related_name="transactions",
@ -480,22 +468,350 @@ class Migration(migrations.Migration):
"db_table": "transactions", "db_table": "transactions",
}, },
), ),
migrations.AddConstraint( migrations.CreateModel(
model_name="memberflag", name="EventCategory",
constraint=models.UniqueConstraint( fields=[
fields=("member", "flag"), name="unique_member_flag" ("id", models.IntegerField(primary_key=True, serialize=False)),
("title", models.TextField()),
],
),
migrations.CreateModel(
name="Event",
fields=[
(
"eid",
models.CharField(max_length=255, primary_key=True, serialize=False),
),
("url", models.TextField()),
("title", models.TextField()),
("start", models.DateTimeField()),
("end", models.DateTimeField(blank=True, null=True)),
("cap", models.IntegerField(blank=True, null=True)),
("count", models.IntegerField()),
(
"calendar",
models.IntegerField(
choices=[
(0, "Hidden"),
(1, "Green"),
(2, "Red"),
(3, "Yellow"),
(4, "Blue"),
(5, "Purple"),
(6, "Magenta"),
(7, "Grey"),
(8, "Teal"),
]
),
),
("venue", models.TextField(blank=True, null=True)),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.eventcategory",
),
),
(
"occurred",
models.GeneratedField(
db_persist=True,
expression=models.Q(
("cap", 0),
("count", 0),
("calendar", 0),
_connector="OR",
_negated=True,
),
output_field=models.BooleanField(),
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="EventInstructor",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.TextField(blank=True)),
(
"member",
models.OneToOneField(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.member",
),
),
],
),
migrations.CreateModel(
name="EventExt",
fields=[
(
"event_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="membershipworks.event",
),
),
(
"materials_fee",
models.DecimalField(
blank=True, decimal_places=4, max_digits=13, null=True
),
),
(
"instructor",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.eventinstructor",
),
),
(
"instructor_flat_rate",
models.DecimalField(decimal_places=4, default=0, max_digits=13),
),
(
"instructor_percentage",
models.DecimalField(decimal_places=4, default=0.5, max_digits=5),
),
("materials_fee_included_in_price", models.BooleanField(null=True)),
("details", models.JSONField(blank=True, null=True)),
("registrations", models.JSONField(blank=True, null=True)),
(
"details_timestamp",
models.GeneratedField(
db_persist=True,
expression=models.Func(
Cast(models.F("details___ts"), models.IntegerField()),
function="to_timestamp",
),
output_field=models.DateTimeField(),
verbose_name="Last details fetch",
),
),
("should_survey", models.BooleanField(default=False)),
("survey_email_sent", models.BooleanField(default=False)),
],
options={
"verbose_name": "event",
"ordering": ["-start"],
},
bases=("membershipworks.event",),
),
migrations.CreateModel(
name="EventMeetingTime",
fields=[
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="meeting_times",
to="membershipworks.eventext",
),
),
(
"reservation_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="reservations.reservation",
),
),
],
options={
"constraints": [],
},
),
migrations.CreateModel(
name="EventInvoice",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("date_submitted", models.DateField()),
("date_paid", models.DateField(blank=True, null=True)),
("pdf", models.FileField(upload_to="protected/invoices/%Y/%m/%d/")),
("amount", models.DecimalField(decimal_places=4, max_digits=13)),
(
"event",
models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="invoice",
to="membershipworks.eventext",
),
),
],
),
migrations.CreateModel(
name="EventTicketType",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("label", models.TextField()),
("restrict_to", models.TextField(blank=True, null=True)),
("list_price", models.FloatField()),
("quantity", models.IntegerField()),
],
options={
"managed": False,
"base_manager_name": "objects",
},
),
django_db_views.operations.ViewRunPython(
code=django_db_views.migration_functions.ForwardViewMigration(
"SELECT\n row_number() over () as id,\n eventext.event_ptr_id as event_id,\n tkt.*,\n jsonb_path_query_first(\n eventext.details,\n '$.tkt[*] ? (exists (@.dsp ? (@[*] == \"5771675edcdf126302a2f6b9\"))).amt'\n )::numeric as members_price\n FROM membershipworks_eventext AS eventext,\n jsonb_to_recordset(eventext.details -> 'tkt') AS tkt (\n lbl TEXT,\n amt NUMERIC,\n cnt INT,\n dsp JSONB\n )",
"membershipworks_eventtickettype",
engine="django.db.backends.postgresql",
), ),
reverse_code=django_db_views.migration_functions.BackwardViewMigration(
"",
"membershipworks_eventtickettype",
engine="django.db.backends.postgresql",
),
atomic=False,
), ),
migrations.AddIndex( migrations.CreateModel(
model_name="member", name="EventAttendeeStats",
index=models.Index(fields=["account_name"], name="account_name_idx"), fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("gross_revenue", models.FloatField()),
],
options={
"managed": False,
},
), ),
migrations.AddIndex( django_db_views.operations.ViewRunPython(
model_name="member", code=django_db_views.migration_functions.ForwardViewMigration(
index=models.Index(fields=["first_name"], name="first_name_idx"), "SELECT eventext.event_ptr_id as event_id, SUM(usr.sum) as gross_revenue\n FROM\n membershipworks_eventext as eventext,\n jsonb_to_recordset(eventext.details -> 'usr') AS usr (\n sum NUMERIC\n )\n GROUP BY event_id",
"membershipworks_eventattendeestats",
engine="django.db.backends.postgresql",
),
reverse_code=django_db_views.migration_functions.BackwardViewMigration(
"",
"membershipworks_eventattendeestats",
engine="django.db.backends.postgresql",
),
atomic=False,
), ),
migrations.AddIndex( migrations.CreateModel(
model_name="member", name="EventAttendee",
index=models.Index(fields=["last_name"], name="last_name_idx"), fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=256)),
("email", models.CharField(max_length=256)),
("sum", models.FloatField()),
],
options={
"managed": False,
},
),
django_db_views.operations.ViewRunPython(
code=django_db_views.migration_functions.ForwardViewMigration(
"SELECT eventext.event_ptr_id as event_id, usr.*\n FROM\n membershipworks_eventext AS eventext,\n jsonb_to_recordset(eventext.details -> 'usr') AS usr (\n uid TEXT,\n nam TEXT,\n eml TEXT,\n sum NUMERIC\n )",
"membershipworks_eventattendee",
engine="django.db.backends.postgresql",
),
reverse_code=django_db_views.migration_functions.BackwardViewMigration(
"",
"membershipworks_eventattendee",
engine="django.db.backends.postgresql",
),
atomic=False,
),
migrations.CreateModel(
name="EventTicketAggregate",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("quantity", models.IntegerField()),
("amount", models.DecimalField(decimal_places=4, max_digits=13)),
("materials", models.DecimalField(decimal_places=4, max_digits=13)),
(
"amount_without_materials",
models.DecimalField(decimal_places=4, max_digits=13),
),
(
"instructor_revenue",
models.DecimalField(decimal_places=4, max_digits=13),
),
(
"instructor_amount",
models.DecimalField(decimal_places=4, max_digits=13),
),
],
options={
"managed": False,
},
),
django_db_views.operations.ViewRunPython(
code=django_db_views.migration_functions.ForwardViewMigration(
'SELECT "membershipworks_eventtickettype"."event_id", SUM("membershipworks_eventtickettype"."cnt") AS "quantity", SUM((CASE WHEN ("membershipworks_eventtickettype"."dsp" ? (SELECT U0."id" FROM "flag" U0 WHERE (U0."name" = \'Members\' AND U0."type" = \'folder\') ORDER BY U0."name" ASC LIMIT 1) OR ("membershipworks_eventtickettype"."dsp" IS NULL AND ("membershipworks_event"."start" < \'2024-07-01 00:00:00-04:00\'::timestamptz OR "membershipworks_eventtickettype"."members_price" = 0))) THEN "membershipworks_eventtickettype"."amt" ELSE "membershipworks_eventtickettype"."members_price" END * "membershipworks_eventtickettype"."cnt")) AS "amount", SUM(CASE WHEN ("membershipworks_eventext"."materials_fee_included_in_price" OR ("membershipworks_eventext"."materials_fee" = 0 AND "membershipworks_eventext"."materials_fee" IS NOT NULL)) THEN ("membershipworks_eventext"."materials_fee" * "membershipworks_eventtickettype"."cnt") WHEN "membershipworks_eventext"."materials_fee_included_in_price" IS NULL THEN NULL ELSE 0 END) AS "materials", SUM(((CASE WHEN ("membershipworks_eventtickettype"."dsp" ? (SELECT U0."id" FROM "flag" U0 WHERE (U0."name" = \'Members\' AND U0."type" = \'folder\') ORDER BY U0."name" ASC LIMIT 1) OR ("membershipworks_eventtickettype"."dsp" IS NULL AND ("membershipworks_event"."start" < \'2024-07-01 00:00:00-04:00\'::timestamptz OR "membershipworks_eventtickettype"."members_price" = 0))) THEN "membershipworks_eventtickettype"."amt" ELSE "membershipworks_eventtickettype"."members_price" END * "membershipworks_eventtickettype"."cnt") - CASE WHEN ("membershipworks_eventext"."materials_fee_included_in_price" OR ("membershipworks_eventext"."materials_fee" = 0 AND "membershipworks_eventext"."materials_fee" IS NOT NULL)) THEN ("membershipworks_eventext"."materials_fee" * "membershipworks_eventtickettype"."cnt") WHEN "membershipworks_eventext"."materials_fee_included_in_price" IS NULL THEN NULL ELSE 0 END)) AS "amount_without_materials", SUM((((CASE WHEN ("membershipworks_eventtickettype"."dsp" ? (SELECT U0."id" FROM "flag" U0 WHERE (U0."name" = \'Members\' AND U0."type" = \'folder\') ORDER BY U0."name" ASC LIMIT 1) OR ("membershipworks_eventtickettype"."dsp" IS NULL AND ("membershipworks_event"."start" < \'2024-07-01 00:00:00-04:00\'::timestamptz OR "membershipworks_eventtickettype"."members_price" = 0))) THEN "membershipworks_eventtickettype"."amt" ELSE "membershipworks_eventtickettype"."members_price" END * "membershipworks_eventtickettype"."cnt") - CASE WHEN ("membershipworks_eventext"."materials_fee_included_in_price" OR ("membershipworks_eventext"."materials_fee" = 0 AND "membershipworks_eventext"."materials_fee" IS NOT NULL)) THEN ("membershipworks_eventext"."materials_fee" * "membershipworks_eventtickettype"."cnt") WHEN "membershipworks_eventext"."materials_fee_included_in_price" IS NULL THEN NULL ELSE 0 END) * "membershipworks_eventext"."instructor_percentage")) AS "instructor_revenue", SUM(((((CASE WHEN ("membershipworks_eventtickettype"."dsp" ? (SELECT U0."id" FROM "flag" U0 WHERE (U0."name" = \'Members\' AND U0."type" = \'folder\') ORDER BY U0."name" ASC LIMIT 1) OR ("membershipworks_eventtickettype"."dsp" IS NULL AND ("membershipworks_event"."start" < \'2024-07-01 00:00:00-04:00\'::timestamptz OR "membershipworks_eventtickettype"."members_price" = 0))) THEN "membershipworks_eventtickettype"."amt" ELSE "membershipworks_eventtickettype"."members_price" END * "membershipworks_eventtickettype"."cnt") - CASE WHEN ("membershipworks_eventext"."materials_fee_included_in_price" OR ("membershipworks_eventext"."materials_fee" = 0 AND "membershipworks_eventext"."materials_fee" IS NOT NULL)) THEN ("membershipworks_eventext"."materials_fee" * "membershipworks_eventtickettype"."cnt") WHEN "membershipworks_eventext"."materials_fee_included_in_price" IS NULL THEN NULL ELSE 0 END) * "membershipworks_eventext"."instructor_percentage") + CASE WHEN ("membershipworks_eventext"."materials_fee_included_in_price" OR ("membershipworks_eventext"."materials_fee" = 0 AND "membershipworks_eventext"."materials_fee" IS NOT NULL)) THEN ("membershipworks_eventext"."materials_fee" * "membershipworks_eventtickettype"."cnt") WHEN "membershipworks_eventext"."materials_fee_included_in_price" IS NULL THEN NULL ELSE 0 END)) AS "instructor_amount" FROM "membershipworks_eventtickettype" INNER JOIN "membershipworks_eventext" ON ("membershipworks_eventtickettype"."event_id" = "membershipworks_eventext"."event_ptr_id") INNER JOIN "membershipworks_event" ON ("membershipworks_eventext"."event_ptr_id" = "membershipworks_event"."eid") GROUP BY "membershipworks_eventtickettype"."event_id"',
"membershipworks_eventticketaggregate",
engine="django.db.backends.postgresql",
),
reverse_code=django_db_views.migration_functions.BackwardViewMigration(
"",
"membershipworks_eventticketaggregate",
engine="django.db.backends.postgresql",
),
atomic=False,
), ),
] ]

View File

@ -1,36 +0,0 @@
# Generated by Django 4.0.2 on 2022-03-01 19:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0001_initial"),
]
operations = [
migrations.RemoveField(
model_name="member",
name="accepted_covid19_policy",
),
migrations.RemoveField(
model_name="member",
name="access_permitted_during_covid19_staffed_period_only",
),
migrations.RemoveField(
model_name="member",
name="gift_membership_purchased_by",
),
migrations.RemoveField(
model_name="member",
name="purchased_gift_membership_for",
),
migrations.RemoveField(
model_name="member",
name="normal_access_permitted_during_covid19_limited_operations",
),
migrations.RemoveField(
model_name="member",
name="self_certify_essential_business",
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 5.0 on 2023-12-20 06:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0002_remove_member_accepted_covid19_policy_and_more"),
]
operations = [
migrations.AlterField(
model_name="transaction",
name="sid",
field=models.CharField(blank=True, max_length=256, null=True),
),
]

View File

@ -1,36 +0,0 @@
# Generated by Django 5.0 on 2023-12-26 17:46
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0003_alter_transaction_sid"),
]
operations = [
migrations.AlterField(
model_name="memberflag",
name="member",
field=models.ForeignKey(
db_column="uid",
db_constraint=False,
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.member",
),
),
migrations.AlterField(
model_name="transaction",
name="member",
field=models.ForeignKey(
blank=True,
db_column="uid",
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="transactions",
to="membershipworks.member",
),
),
]

View File

@ -1,154 +0,0 @@
# Generated by Django 5.0 on 2023-12-30 19:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0004_alter_memberflag_member_alter_transaction_member"),
]
operations = [
migrations.CreateModel(
name="Event",
fields=[
(
"eid",
models.CharField(max_length=255, primary_key=True, serialize=False),
),
("url", models.TextField()),
("title", models.TextField()),
("start", models.DateTimeField()),
("end", models.DateTimeField(blank=True, null=True)),
("cap", models.IntegerField(blank=True, null=True)),
("count", models.IntegerField()),
(
"calendar",
models.IntegerField(
choices=[
(0, "Hidden"),
(1, "Green"),
(2, "Red"),
(3, "Yellow"),
(4, "Blue"),
(5, "Purple"),
(6, "Magenta"),
(7, "Grey"),
(8, "Teal"),
]
),
),
("venue", models.TextField(blank=True, null=True)),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="EventCategory",
fields=[
("id", models.IntegerField(primary_key=True, serialize=False)),
("title", models.TextField()),
],
),
migrations.CreateModel(
name="EventExt",
fields=[
(
"event_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="membershipworks.event",
),
),
(
"materials_fee",
models.DecimalField(
blank=True, decimal_places=4, max_digits=13, null=True
),
),
],
options={
"verbose_name": "event",
},
bases=("membershipworks.event",),
),
migrations.AddField(
model_name="event",
name="category",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.eventcategory",
),
),
migrations.CreateModel(
name="EventInstructor",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.TextField(blank=True)),
(
"member",
models.OneToOneField(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.member",
),
),
],
),
migrations.CreateModel(
name="EventMeetingTime",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("start", models.DateTimeField()),
("end", models.DateTimeField()),
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="meeting_times",
to="membershipworks.eventext",
),
),
],
),
migrations.AddField(
model_name="eventext",
name="instructor",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.eventinstructor",
),
),
migrations.AddConstraint(
model_name="eventmeetingtime",
constraint=models.UniqueConstraint(
fields=("event", "start", "end"), name="unique_event_start_end"
),
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 5.0 on 2024-01-01 17:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"membershipworks",
"0005_event_eventcategory_eventext_event_category_and_more",
),
]
operations = [
migrations.AddField(
model_name="eventext",
name="instructor_flat_rate",
field=models.DecimalField(decimal_places=4, default=0, max_digits=13),
),
migrations.AddField(
model_name="eventext",
name="instructor_percentage",
field=models.DecimalField(decimal_places=4, default=0.5, max_digits=5),
),
]

View File

@ -1,24 +0,0 @@
# Generated by Django 5.0.1 on 2024-01-03 19:22
import django.db.models.expressions
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0006_eventext_instructor_flat_rate_and_more"),
]
operations = [
migrations.AddField(
model_name="eventmeetingtime",
name="duration",
field=models.GeneratedField(
db_persist=False,
expression=django.db.models.expressions.CombinedExpression(
models.F("end"), "-", models.F("start")
),
output_field=models.DurationField(),
),
),
]

View File

@ -1,27 +0,0 @@
# Generated by Django 5.0.1 on 2024-01-19 20:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0007_eventmeetingtime_duration"),
]
operations = [
migrations.AddField(
model_name="event",
name="occurred",
field=models.GeneratedField(
db_persist=False,
expression=models.Q(
("cap", 0),
("count", 0),
("calendar", 0),
_connector="OR",
_negated=True,
),
output_field=models.BooleanField(),
),
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 5.0.1 on 2024-01-25 02:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0008_event_occurred"),
]
operations = [
migrations.AddField(
model_name="eventext",
name="materials_fee_included_in_price",
field=models.BooleanField(null=True),
),
]

View File

@ -1,16 +0,0 @@
# Generated by Django 5.0.1 on 2024-01-29 19:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0009_eventext_materials_fee_included_in_price"),
]
operations = [
migrations.AlterModelOptions(
name="eventext",
options={"ordering": ["-start"], "verbose_name": "event"},
),
]

View File

@ -1,29 +0,0 @@
# Generated by Django 5.0.1 on 2024-01-29 19:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0010_alter_eventext_options"),
]
operations = [
migrations.AddField(
model_name="eventext",
name="details",
field=models.JSONField(blank=True, null=True),
),
migrations.AddField(
model_name="eventext",
name="details_timestamp",
field=models.GeneratedField(
db_persist=False,
expression=models.Func(
models.Func(models.F("details___ts"), function="FROM_UNIXTIME"),
template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')",
),
output_field=models.DateTimeField(),
),
),
]

View File

@ -1,79 +0,0 @@
# Generated by Django 5.0.1 on 2024-01-29 19:19
from django.db import migrations, models
import django_db_views.migration_functions
import django_db_views.operations
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0011_eventext_details"),
]
operations = [
migrations.CreateModel(
name="EventAttendeeStats",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("gross_revenue", models.FloatField()),
],
options={
"managed": False,
},
),
django_db_views.operations.ViewRunPython(
code=django_db_views.migration_functions.ForwardViewMigration(
"SELECT\n row_number() over () as id,\n eventext.event_ptr_id AS event_id,\n tkt.label,\n tkt.list_price,\n tkt.quantity,\n GROUP_CONCAT(tkt.restrict_to SEPARATOR ',') as restrict_to\n FROM\n membershipworks_eventext AS eventext,\n JSON_TABLE (eventext.details, '$.tkt[*]' COLUMNS (\n id FOR ORDINALITY,\n label VARCHAR(256) PATH '$.lbl' ERROR ON ERROR,\n list_price DOUBLE PATH '$.amt' ERROR ON ERROR,\n quantity INTEGER PATH '$.cnt' DEFAULT 0 ON EMPTY ERROR ON ERROR,\n NESTED PATH '$.dsp[*]' COLUMNS (\n restrict_to VARCHAR(100) PATH '$' ERROR ON ERROR\n )\n )) AS tkt\n GROUP BY event_id, id",
"membershipworks_eventtickettype",
engine="django.db.backends.mysql",
),
reverse_code=django_db_views.migration_functions.BackwardViewMigration(
"", "membershipworks_eventtickettype", engine="django.db.backends.mysql"
),
atomic=False,
),
migrations.CreateModel(
name="EventTicketType",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("label", models.TextField()),
("restrict_to", models.TextField(blank=True, null=True)),
("list_price", models.FloatField()),
("quantity", models.IntegerField()),
],
options={
"managed": False,
"base_manager_name": "objects",
},
),
django_db_views.operations.ViewRunPython(
code=django_db_views.migration_functions.ForwardViewMigration(
"SELECT eventext.event_ptr_id as event_id, SUM(tkt.s) as gross_revenue\n FROM\n membershipworks_eventext as eventext,\n JSON_TABLE(eventext.details, '$.usr[*]' COLUMNS (\n s DOUBLE PATH '$.sum' DEFAULT 0 ON EMPTY\n )) as tkt\n GROUP BY event_id",
"membershipworks_eventattendeestats",
engine="django.db.backends.mysql",
),
reverse_code=django_db_views.migration_functions.BackwardViewMigration(
"",
"membershipworks_eventattendeestats",
engine="django.db.backends.mysql",
),
atomic=False,
),
]

View File

@ -1,46 +0,0 @@
# Generated by Django 5.0.1 on 2024-02-02 22:07
from django.db import migrations, models
import django_db_views.migration_functions
import django_db_views.operations
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0012_eventattendeestats_eventtickettype"),
]
operations = [
migrations.CreateModel(
name="EventAttendee",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=256)),
("email", models.CharField(max_length=256)),
("sum", models.FloatField()),
],
options={
"managed": False,
},
),
django_db_views.operations.ViewRunPython(
code=django_db_views.migration_functions.ForwardViewMigration(
"SELECT eventext.event_ptr_id as event_id, tkt.uid, tkt.name, tkt.email, tkt.sum\n FROM\n membershipworks_eventext as eventext,\n JSON_TABLE(eventext.details, '$.usr[*]' COLUMNS (\n uid VARCHAR(24) PATH '$.uid',\n name VARCHAR(256) PATH '$.nam',\n email VARCHAR(256) PATH '$.eml',\n sum DOUBLE PATH '$.sum'\n )) as tkt",
"membershipworks_eventattendee",
engine="django.db.backends.mysql",
),
reverse_code=django_db_views.migration_functions.BackwardViewMigration(
"", "membershipworks_eventattendee", engine="django.db.backends.mysql"
),
atomic=False,
),
]

View File

@ -1,16 +0,0 @@
# Generated by Django 5.0.1 on 2024-02-05 03:32
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0013_eventattendee"),
]
operations = [
migrations.RemoveField(
model_name="eventext",
name="details_timestamp",
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.0.2 on 2024-02-12 21:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0014_remove_eventext_details_timestamp"),
]
operations = [
migrations.AddConstraint(
model_name="eventmeetingtime",
constraint=models.CheckConstraint(
check=models.Q(("end__gt", models.F("start"))), name="end_after_start"
),
),
]

View File

@ -1,41 +0,0 @@
# Generated by Django 5.0.2 on 2024-03-08 21:30
import uuid
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0015_eventmeetingtime_end_after_start"),
]
operations = [
migrations.CreateModel(
name="EventInvoice",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("date_submitted", models.DateField()),
("date_paid", models.DateField(blank=True, null=True)),
("pdf", models.FileField(upload_to="invoices/%Y/%m/%d/")),
("amount", models.DecimalField(decimal_places=4, max_digits=13)),
(
"event",
models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="invoice",
to="membershipworks.eventext",
),
),
],
),
]

View File

@ -1,22 +0,0 @@
# Generated by Django 5.0.4 on 2024-04-30 05:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0016_eventinvoice"),
]
operations = [
migrations.AddField(
model_name="eventext",
name="registrations",
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name="eventinvoice",
name="pdf",
field=models.FileField(upload_to="protected/invoices/%Y/%m/%d/"),
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-08 16:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0017_eventext_registrations_alter_eventinvoice_pdf"),
]
operations = [
migrations.AddField(
model_name="eventext",
name="details_timestamp",
field=models.GeneratedField(
db_persist=False,
expression=models.Func(
models.Func(models.F("details___ts"), function="FROM_UNIXTIME"),
template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')",
),
output_field=models.DateTimeField(),
verbose_name="Last details fetch",
),
),
]

View File

@ -1,22 +0,0 @@
# Generated by Django 5.0.6 on 2024-05-20 22:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0018_eventext_details_timestamp"),
]
operations = [
migrations.AddField(
model_name="eventext",
name="should_survey",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="eventext",
name="survey_email_sent",
field=models.BooleanField(default=False),
),
]

View File

@ -1,82 +0,0 @@
# Generated by Django 5.0.7 on 2024-07-30 23:10
import django.db.models.deletion
from django.db import migrations, models
def convert_meetingtimes_to_reservations(apps, schema_editor):
Reservation = apps.get_model("reservations", "Reservation")
EventMeetingTime = apps.get_model("membershipworks", "EventMeetingTime")
for meeting_time in EventMeetingTime.objects.all():
reservation = Reservation.objects.create(
id=meeting_time.id,
start=meeting_time.start,
end=meeting_time.end,
)
meeting_time.reservation_ptr = reservation
meeting_time.save()
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0019_eventext_should_survey_eventext_survey_email_sent"),
("reservations", "0001_initial"),
]
operations = [
# add reservation field
migrations.AddField(
model_name="eventmeetingtime",
name="reservation_ptr",
field=models.OneToOneField(
auto_created=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
serialize=False,
to="reservations.reservation",
),
preserve_default=False,
),
migrations.RunPython(convert_meetingtimes_to_reservations, atomic=True),
# remove primary key
migrations.RemoveField(
model_name="eventmeetingtime",
name="id",
),
# make reservation non-nullable
migrations.AlterField(
model_name="eventmeetingtime",
name="reservation_ptr",
field=models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="reservations.reservation",
),
preserve_default=False,
),
# delete old columns
migrations.RemoveConstraint(
model_name="eventmeetingtime",
name="unique_event_start_end",
),
migrations.RemoveConstraint(
model_name="eventmeetingtime",
name="end_after_start",
),
migrations.RemoveField(
model_name="eventmeetingtime",
name="duration",
),
migrations.RemoveField(
model_name="eventmeetingtime",
name="end",
),
migrations.RemoveField(
model_name="eventmeetingtime",
name="start",
),
]

View File

@ -1,18 +1,18 @@
import uuid import uuid
from datetime import datetime, timedelta from datetime import datetime, timedelta
from decimal import Decimal
from typing import TYPE_CHECKING, TypedDict from typing import TYPE_CHECKING, TypedDict
import django.core.mail.message import django.core.mail.message
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser from django.contrib.auth.models import AbstractBaseUser
from django.db import models from django.db import connection, models
from django.db.models import ( from django.db.models import (
Case, Case,
Count, Count,
Exists, Exists,
ExpressionWrapper, ExpressionWrapper,
F, F,
Func,
OuterRef, OuterRef,
Q, Q,
QuerySet, QuerySet,
@ -21,7 +21,7 @@ from django.db.models import (
Value, Value,
When, When,
) )
from django.db.models.functions import Coalesce from django.db.models.functions import Cast, Coalesce
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@ -409,7 +409,7 @@ class Event(BaseModel):
occurred = models.GeneratedField( occurred = models.GeneratedField(
expression=~(Q(cap=0) | Q(count=0) | Q(calendar=EventCalendar.HIDDEN)), expression=~(Q(cap=0) | Q(count=0) | Q(calendar=EventCalendar.HIDDEN)),
output_field=models.BooleanField(), output_field=models.BooleanField(),
db_persist=False, db_persist=True,
) )
# TODO: # TODO:
# "lgo": { # "lgo": {
@ -473,13 +473,7 @@ class EventExtQuerySet(models.QuerySet["EventExtAnnotated"]):
def with_financials(self) -> "QuerySet[EventExtAnnotatedWithFinancials]": def with_financials(self) -> "QuerySet[EventExtAnnotatedWithFinancials]":
return self.annotate( return self.annotate(
**{ **{
field: Subquery( field: F(f"ticket_aggregates__{field}")
EventTicketType.objects.filter(event=OuterRef("pk"))
.values("event__pk")
.annotate(d=Sum(field))
.values("d"),
output_field=models.DecimalField(),
)
for field in [ for field in [
"quantity", "quantity",
"amount", "amount",
@ -492,11 +486,12 @@ class EventExtQuerySet(models.QuerySet["EventExtAnnotated"]):
total_due_to_instructor=( total_due_to_instructor=(
F("instructor_amount") + F("instructor_flat_rate") F("instructor_amount") + F("instructor_flat_rate")
), ),
gross_revenue=Coalesce(F("attendee_stats__gross_revenue"), 0.0), gross_revenue=Coalesce(
net_revenue=ExpressionWrapper( F("attendee_stats__gross_revenue"),
F("gross_revenue") - F("total_due_to_instructor"), 0,
output_field=models.DecimalField(), output_field=models.DecimalField(),
), ),
net_revenue=F("gross_revenue") - F("total_due_to_instructor"),
) )
@ -549,12 +544,12 @@ class EventExt(Event):
) )
details = models.JSONField(null=True, blank=True) details = models.JSONField(null=True, blank=True)
details_timestamp = models.GeneratedField( details_timestamp = models.GeneratedField(
expression=Func( expression=models.Func(
Func(F("details___ts"), function="FROM_UNIXTIME"), Cast(models.F("details___ts"), models.IntegerField()),
template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')", function="to_timestamp",
), ),
output_field=models.DateTimeField(), output_field=models.DateTimeField(),
db_persist=False, db_persist=True,
verbose_name="Last details fetch", verbose_name="Last details fetch",
) )
@ -663,7 +658,7 @@ class EventInvoice(models.Model):
class EventTicketTypeQuerySet(models.QuerySet["EventTicketType"]): class EventTicketTypeQuerySet(models.QuerySet["EventTicketType"]):
def group_by_ticket_type(self): def group_by_ticket_type(self):
return self.values("is_members_ticket").annotate( return self.values(is_members_ticket=Q(restrict_to__isnull=False)).annotate(
label=Case( label=Case(
When(Q(is_members_ticket=True), Value("Members")), When(Q(is_members_ticket=True), Value("Members")),
default=Value("Non-Members"), default=Value("Non-Members"),
@ -685,17 +680,8 @@ class EventTicketTypeQuerySet(models.QuerySet["EventTicketType"]):
class EventTicketTypeManager(models.Manager["EventTicketType"]): class EventTicketTypeManager(models.Manager["EventTicketType"]):
def get_queryset(self) -> models.QuerySet["EventTicketType"]: def get_queryset(self) -> models.QuerySet["EventTicketType"]:
members_folder = Subquery(
Flag.objects.filter(name="Members", type="folder").values("id")[:1]
)
qs = super().get_queryset() qs = super().get_queryset()
return qs.annotate( return qs.annotate(
members_price=Subquery(
qs.filter(event=OuterRef("event"), restrict_to=members_folder).values(
"list_price"
),
output_field=models.FloatField(),
),
# Before 2024-07-01, use Members ticket price for any # Before 2024-07-01, use Members ticket price for any
# restricted ticket, but list price for unrestricted # restricted ticket, but list price for unrestricted
# (Non-Members) ticket. After, use Members ticket price # (Non-Members) ticket. After, use Members ticket price
@ -703,7 +689,7 @@ class EventTicketTypeManager(models.Manager["EventTicketType"]):
actual_price=Case( actual_price=Case(
When( When(
# member ticket # member ticket
Q(restrict_to=members_folder) Q(restrict_to__has_key=settings.MW_MEMBERS_FOLDER_ID)
| ( | (
# non-member ticket # non-member ticket
Q(restrict_to__isnull=True) Q(restrict_to__isnull=True)
@ -723,7 +709,6 @@ class EventTicketTypeManager(models.Manager["EventTicketType"]):
), ),
default="members_price", default="members_price",
), ),
is_members_ticket=(Q(restrict_to__isnull=False)),
materials=Case( materials=Case(
When( When(
( (
@ -764,37 +749,28 @@ class EventTicketType(DBView):
event = models.ForeignKey( event = models.ForeignKey(
EventExt, on_delete=models.DO_NOTHING, related_name="ticket_types" EventExt, on_delete=models.DO_NOTHING, related_name="ticket_types"
) )
label = models.TextField() label = models.TextField(db_column="lbl")
restrict_to = models.TextField(null=True, blank=True) list_price = models.DecimalField(db_column="amt", max_digits=13, decimal_places=4)
list_price = models.FloatField() members_price = models.DecimalField(max_digits=13, decimal_places=4)
quantity = models.IntegerField() quantity = models.IntegerField(db_column="cnt")
restrict_to = models.JSONField(db_column="dsp")
# Due to the presence of JSON_TABLE, this view must (as of MariaDB view_definition = f"""
# 11.2.2) be created as the root user. See
# https://jira.mariadb.org/browse/MDEV-27898
# nested path/group_concat to workaround inability to create JSON columns using
# JSON_TABLE in views
view_definition = """
SELECT SELECT
row_number() over () as id, row_number() over () as id,
eventext.event_ptr_id AS event_id, eventext.event_ptr_id as event_id,
tkt.label, tkt.*,
tkt.list_price, jsonb_path_query_first(
tkt.quantity, eventext.details,
GROUP_CONCAT(tkt.restrict_to SEPARATOR ',') as restrict_to '$.tkt[*] ? (exists (@.dsp ? (@[*] == "{settings.MW_MEMBERS_FOLDER_ID}"))).amt'
FROM )::numeric as members_price
membershipworks_eventext AS eventext, FROM membershipworks_eventext AS eventext,
JSON_TABLE (eventext.details, '$.tkt[*]' COLUMNS ( jsonb_to_recordset(eventext.details -> 'tkt') AS tkt (
id FOR ORDINALITY, lbl TEXT,
label VARCHAR(256) PATH '$.lbl' ERROR ON ERROR, amt NUMERIC,
list_price DOUBLE PATH '$.amt' ERROR ON ERROR, cnt INT,
quantity INTEGER PATH '$.cnt' DEFAULT 0 ON EMPTY ERROR ON ERROR, dsp JSONB
NESTED PATH '$.dsp[*]' COLUMNS ( )
restrict_to VARCHAR(100) PATH '$' ERROR ON ERROR
)
)) AS tkt
GROUP BY event_id, id
""" """
def __str__(self) -> str: def __str__(self) -> str:
@ -805,19 +781,59 @@ class EventTicketType(DBView):
base_manager_name = "objects" base_manager_name = "objects"
class EventTicketAggregate(DBView):
event = models.OneToOneField(
EventExt,
on_delete=models.DO_NOTHING,
related_name="ticket_aggregates",
primary_key=True,
)
quantity = models.IntegerField()
amount = models.DecimalField(max_digits=13, decimal_places=4)
materials = models.DecimalField(max_digits=13, decimal_places=4)
amount_without_materials = models.DecimalField(max_digits=13, decimal_places=4)
instructor_revenue = models.DecimalField(max_digits=13, decimal_places=4)
instructor_amount = models.DecimalField(max_digits=13, decimal_places=4)
@staticmethod
def view_definition():
qs = EventTicketType.objects.values("event").annotate(
**{
field: Sum(field)
for field in [
"quantity",
"amount",
"materials",
"amount_without_materials",
"instructor_revenue",
"instructor_amount",
]
},
)
with connection.cursor() as cursor:
return cursor.mogrify(*qs.query.sql_with_params())
def __str__(self) -> str:
return f"{self.event}: {self.quantity}, {self.amount}"
class Meta:
managed = False
class EventAttendeeStats(DBView): class EventAttendeeStats(DBView):
event = models.ForeignKey( event = models.ForeignKey(
EventExt, on_delete=models.DO_NOTHING, related_name="attendee_stats" EventExt, on_delete=models.DO_NOTHING, related_name="attendee_stats"
) )
gross_revenue = models.FloatField() gross_revenue = models.DecimalField(max_digits=13, decimal_places=4)
view_definition = """ view_definition = """
SELECT eventext.event_ptr_id as event_id, SUM(tkt.s) as gross_revenue SELECT eventext.event_ptr_id as event_id, SUM(usr.sum) as gross_revenue
FROM FROM
membershipworks_eventext as eventext, membershipworks_eventext as eventext,
JSON_TABLE(eventext.details, '$.usr[*]' COLUMNS ( jsonb_to_recordset(eventext.details -> 'usr') AS usr (
s DOUBLE PATH '$.sum' DEFAULT 0 ON EMPTY sum NUMERIC
)) as tkt )
GROUP BY event_id GROUP BY event_id
""" """
@ -830,20 +846,20 @@ class EventAttendee(DBView):
EventExt, on_delete=models.DO_NOTHING, related_name="attendees" EventExt, on_delete=models.DO_NOTHING, related_name="attendees"
) )
uid = models.ForeignKey(Member, on_delete=models.DO_NOTHING) uid = models.ForeignKey(Member, on_delete=models.DO_NOTHING)
name = models.CharField(max_length=256) name = models.CharField(max_length=256, db_column="nam")
email = models.CharField(max_length=256) email = models.CharField(max_length=256, db_column="eml")
sum = models.FloatField() sum = models.DecimalField(max_digits=13, decimal_places=4)
view_definition = """ view_definition = """
SELECT eventext.event_ptr_id as event_id, tkt.uid, tkt.name, tkt.email, tkt.sum SELECT eventext.event_ptr_id as event_id, usr.*
FROM FROM
membershipworks_eventext as eventext, membershipworks_eventext AS eventext,
JSON_TABLE(eventext.details, '$.usr[*]' COLUMNS ( jsonb_to_recordset(eventext.details -> 'usr') AS usr (
uid VARCHAR(24) PATH '$.uid', uid TEXT,
name VARCHAR(256) PATH '$.nam', nam TEXT,
email VARCHAR(256) PATH '$.eml', eml TEXT,
sum DOUBLE PATH '$.sum' sum NUMERIC
)) as tkt )
""" """
class Meta: class Meta:

View File

@ -137,6 +137,7 @@ def scrape_events():
events = Event.objects.bulk_create( events = Event.objects.bulk_create(
[Event.from_api_dict(event_data) for event_data in data["evt"]], [Event.from_api_dict(event_data) for event_data in data["evt"]],
update_conflicts=True, update_conflicts=True,
unique_fields=["eid"],
update_fields=[ update_fields=[
field.attname field.attname
for field in Event._meta.get_fields() for field in Event._meta.get_fields()

View File

@ -10,6 +10,7 @@ from django.contrib.auth.mixins import (
AccessMixin, AccessMixin,
PermissionRequiredMixin, PermissionRequiredMixin,
) )
from django.contrib.postgres.aggregates import StringAgg
from django.core import mail from django.core import mail
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db.models import OuterRef, Q, Subquery from django.db.models import OuterRef, Q, Subquery
@ -33,7 +34,6 @@ import django_tables2 as tables
import weasyprint import weasyprint
from dal import autocomplete from dal import autocomplete
from django_filters.views import BaseFilterView from django_filters.views import BaseFilterView
from django_mysql.models.aggregates import GroupConcat
from django_sendfile import sendfile from django_sendfile import sendfile
from django_tables2 import A, SingleTableMixin from django_tables2 import A, SingleTableMixin
from django_tables2.export.views import ExportMixin from django_tables2.export.views import ExportMixin
@ -538,7 +538,7 @@ class MissingPaperworkReport(
membership=Subquery( membership=Subquery(
qs.filter( qs.filter(
pk=OuterRef("pk"), flags__type__in=("level", "addon") pk=OuterRef("pk"), flags__type__in=("level", "addon")
).values(m=GroupConcat("flags__name")) ).values(m=StringAgg("flags__name", ", "))
), ),
) )
) )

View File

@ -3,6 +3,7 @@ from typing import TYPE_CHECKING
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.postgres.aggregates import StringAgg
from django.contrib.staticfiles import finders as staticfiles_finders from django.contrib.staticfiles import finders as staticfiles_finders
from django.db import models from django.db import models
from django.db.models import ( from django.db.models import (
@ -16,14 +17,13 @@ from django.db.models import (
Value, Value,
When, When,
) )
from django.db.models.functions import Concat from django.db.models.functions import Cast, Concat
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.views.generic import ListView from django.views.generic import ListView
import requests import requests
import weasyprint import weasyprint
from django_mysql.models.aggregates import GroupConcat
from django_tables2 import SingleTableMixin from django_tables2 import SingleTableMixin
from django_tables2.export.views import ExportMixin from django_tables2.export.views import ExportMixin
@ -158,14 +158,16 @@ class InstructorOrVendorReport(
.get_table_data() .get_table_data()
.values("name") .values("name")
.annotate( .annotate(
instructor_agreement_date=GroupConcat( instructor_agreement_date=StringAgg(
"instructor_agreement_date", distinct=True, ordering="asc" Cast("instructor_agreement_date", models.TextField()),
delimiter=", ",
distinct=True,
), ),
w9_date=GroupConcat("w9_date", distinct=True, ordering="asc"), w9_date=StringAgg(
phone=GroupConcat("phone", distinct=True, ordering="asc"), Cast("w9_date", models.TextField()), ", ", distinct=True
email_address=GroupConcat(
"email_address", distinct=True, ordering="asc"
), ),
phone=StringAgg("phone", ", ", distinct=True),
email_address=StringAgg("email_address", ", ", distinct=True),
) )
) )

View File

@ -5,7 +5,7 @@
groups = ["default", "debug", "dev", "lint", "server", "typing"] groups = ["default", "debug", "dev", "lint", "server", "typing"]
strategy = ["inherit_metadata"] strategy = ["inherit_metadata"]
lock_version = "4.5.0" lock_version = "4.5.0"
content_hash = "sha256:4a7538bb6a4aabea5a3641a9f69edf6045587a129895ddf5108bf765141fbe6f" content_hash = "sha256:0c560e1f2a81810e95ba8993538337bfc8b39576b6923280cfbfad91ad5b58f2"
[[metadata.targets]] [[metadata.targets]]
requires_python = "==3.11.*" requires_python = "==3.11.*"
@ -561,21 +561,6 @@ files = [
{file = "django_model_utils-4.5.1.tar.gz", hash = "sha256:1220f22d9a467d53a1e0f4cda4857df0b2f757edf9a29955c42461988caa648a"}, {file = "django_model_utils-4.5.1.tar.gz", hash = "sha256:1220f22d9a467d53a1e0f4cda4857df0b2f757edf9a29955c42461988caa648a"},
] ]
[[package]]
name = "django-mysql"
version = "4.14.0"
requires_python = ">=3.8"
summary = "Django-MySQL extends Django's built-in MySQL and MariaDB support their specific features not available on other databases."
groups = ["default"]
marker = "python_version == \"3.11\""
dependencies = [
"django>=3.2",
]
files = [
{file = "django_mysql-4.14.0-py3-none-any.whl", hash = "sha256:c8ae4b8004bd2e1b74999f0254d255771043913273216a8514cf09aa4bd937bb"},
{file = "django_mysql-4.14.0.tar.gz", hash = "sha256:77cb615afb8f2a92636617d46dbe11b97b28e2b97d8373cf7752c3e1f2c619f1"},
]
[[package]] [[package]]
name = "django-nh3" name = "django-nh3"
version = "0.1.1" version = "0.1.1"
@ -1439,17 +1424,6 @@ files = [
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
] ]
[[package]]
name = "mysqlclient"
version = "2.2.4"
requires_python = ">=3.8"
summary = "Python interface to MySQL"
groups = ["default"]
marker = "python_version == \"3.11\""
files = [
{file = "mysqlclient-2.2.4.tar.gz", hash = "sha256:33bc9fb3464e7d7c10b1eaf7336c5ff8f2a3d3b88bab432116ad2490beb3bf41"},
]
[[package]] [[package]]
name = "nh3" name = "nh3"
version = "0.2.18" version = "0.2.18"
@ -1624,6 +1598,67 @@ files = [
{file = "protobuf-5.27.3.tar.gz", hash = "sha256:82460903e640f2b7e34ee81a947fdaad89de796d324bcbc38ff5430bcdead82c"}, {file = "protobuf-5.27.3.tar.gz", hash = "sha256:82460903e640f2b7e34ee81a947fdaad89de796d324bcbc38ff5430bcdead82c"},
] ]
[[package]]
name = "psycopg"
version = "3.2.1"
requires_python = ">=3.8"
summary = "PostgreSQL database adapter for Python"
groups = ["default"]
marker = "python_version == \"3.11\""
dependencies = [
"backports-zoneinfo>=0.2.0; python_version < \"3.9\"",
"typing-extensions>=4.4",
"tzdata; sys_platform == \"win32\"",
]
files = [
{file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"},
{file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"},
]
[[package]]
name = "psycopg-binary"
version = "3.2.1"
requires_python = ">=3.8"
summary = "PostgreSQL database adapter for Python -- C optimisation distribution"
groups = ["default"]
marker = "implementation_name != \"pypy\" and python_version == \"3.11\""
files = [
{file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa3931f308ab4a479d0ee22dc04bea867a6365cac0172e5ddcba359da043854b"},
]
[[package]]
name = "psycopg-pool"
version = "3.2.2"
requires_python = ">=3.8"
summary = "Connection Pool for Psycopg"
groups = ["default"]
marker = "python_version == \"3.11\""
dependencies = [
"typing-extensions>=4.4",
]
files = [
{file = "psycopg_pool-3.2.2-py3-none-any.whl", hash = "sha256:273081d0fbfaced4f35e69200c89cb8fbddfe277c38cc86c235b90a2ec2c8153"},
{file = "psycopg_pool-3.2.2.tar.gz", hash = "sha256:9e22c370045f6d7f2666a5ad1b0caf345f9f1912195b0b25d0d3bcc4f3a7389c"},
]
[[package]]
name = "psycopg"
version = "3.2.1"
extras = ["binary", "pool"]
requires_python = ">=3.8"
summary = "PostgreSQL database adapter for Python"
groups = ["default"]
marker = "python_version == \"3.11\""
dependencies = [
"psycopg-binary==3.2.1; implementation_name != \"pypy\"",
"psycopg-pool",
"psycopg==3.2.1",
]
files = [
{file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"},
{file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"},
]
[[package]] [[package]]
name = "ptyprocess" name = "ptyprocess"
version = "0.7.0" version = "0.7.0"

View File

@ -16,7 +16,6 @@ dependencies = [
"markdownify~=0.13", "markdownify~=0.13",
"mdformat~=0.7", "mdformat~=0.7",
"mdformat-tables~=0.4", "mdformat-tables~=0.4",
"mysqlclient~=2.2",
"django-autocomplete-light~=3.11", "django-autocomplete-light~=3.11",
"weasyprint~=62.3", "weasyprint~=62.3",
"requests~=2.32", "requests~=2.32",
@ -34,7 +33,6 @@ dependencies = [
"tablib[ods,xlsx]~=3.6", "tablib[ods,xlsx]~=3.6",
"django-filter~=24.3", "django-filter~=24.3",
"django-db-views~=0.1", "django-db-views~=0.1",
"django-mysql~=4.14",
"django-weasyprint~=2.3", "django-weasyprint~=2.3",
"django-sendfile2~=0.7", "django-sendfile2~=0.7",
"django-bootstrap5~=24.2", "django-bootstrap5~=24.2",
@ -44,6 +42,7 @@ dependencies = [
"google-api-python-client~=2.142", "google-api-python-client~=2.142",
"google-auth-oauthlib~=1.2", "google-auth-oauthlib~=1.2",
"django-model-utils~=4.5", "django-model-utils~=4.5",
"psycopg[binary,pool]~=3.2",
] ]
requires-python = ">=3.11" requires-python = ">=3.11"

View File

@ -65,7 +65,7 @@ class Migration(migrations.Migration):
( (
"duration", "duration",
models.GeneratedField( models.GeneratedField(
db_persist=False, db_persist=True,
expression=django.db.models.expressions.CombinedExpression( expression=django.db.models.expressions.CombinedExpression(
models.F("end"), "-", models.F("start") models.F("end"), "-", models.F("start")
), ),

View File

@ -87,7 +87,7 @@ class Reservation(models.Model):
duration = models.GeneratedField( duration = models.GeneratedField(
expression=F("end") - F("start"), expression=F("end") - F("start"),
output_field=models.DurationField(), output_field=models.DurationField(),
db_persist=False, db_persist=True,
) )
objects = ReservationQuerySet.as_manager() objects = ReservationQuerySet.as_manager()