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
container: catthehacker/ubuntu:act-latest
services:
mariadb:
# TODO: this is pinned to avoid what apears to be a bug with
# MariaDB >= 10.11.9, and collation issues with 11.x.x
image: mariadb:10.11.8
postgres:
image: postgres:15
env:
MARIADB_ROOT_PASSWORD: whatever
POSTGRES_PASSWORD: whatever
healthcheck:
test:
[
"CMD",
"healthcheck.sh",
"--su-mysql",
"--connect",
"--innodb_initialized",
]
test: ["CMD-SHELL", "pg_isready"]
steps:
- uses: actions/checkout@v4
- name: Setup PDM
@ -35,7 +26,7 @@ jobs:
- name: Install apt dependencies
run: >-
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
run: pdm sync -d -G dev

View File

@ -28,7 +28,7 @@ class Base(Configuration):
@classmethod
def setup(cls):
super().setup()
cls.DATABASES["default"]["OPTIONS"] = {"charset": "utf8mb4"}
cls.DATABASES["default"]["OPTIONS"] = {"pool": True}
INSTALLED_APPS = [
"dal",
@ -52,7 +52,6 @@ class Base(Configuration):
"django_tables2",
"django_filters",
"django_db_views",
"django_mysql",
"django_sendfile",
"django_bootstrap5",
# "tasks.apps.TasksConfig",
@ -106,9 +105,6 @@ class Base(Configuration):
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
LOGIN_REDIRECT_URL = "/"
LOGIN_URL = "/auth/login/"
@ -213,6 +209,9 @@ class Base(Configuration):
# CMSManage specific stuff
WIKI_URL = values.URLValue("https://wiki.claremontmakerspace.org")
# ID of flag for Members folder in MembershipWorks
MW_MEMBERS_FOLDER_ID = "5771675edcdf126302a2f6b9"
class NonCIBase(Base):
"""required for all but CI"""
@ -367,13 +366,10 @@ class CI(Base):
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"HOST": "mariadb",
"NAME": "CMS_Database",
"USER": "root",
"ENGINE": "django.db.backends.postgresql",
"HOST": "postgres",
"NAME": "cms",
"USER": "postgres",
"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
class Migration(migrations.Migration):
initial = True
dependencies = []
dependencies = [
("membershipworks", "0001_initial"),
]
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(
name="HIDEvent",
fields=[
@ -21,7 +47,6 @@ class Migration(migrations.Migration):
verbose_name="ID",
),
),
("door_name", models.CharField(db_column="doorName", max_length=64)),
("timestamp", models.DateTimeField()),
(
"event_type",
@ -88,16 +113,173 @@ class Migration(migrations.Migration):
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={
"db_table": "hidevent",
"ordering": ("-timestamp",),
"constraints": [
models.UniqueConstraint(
fields=("door", "timestamp", "event_type"),
name="unique_hidevent",
)
],
},
),
migrations.AddConstraint(
model_name="hidevent",
constraint=models.UniqueConstraint(
fields=("door_name", "timestamp", "event_type"), name="unique_hidevent"
),
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",
),
),
],
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(),
db_persist=False,
db_persist=True,
)
objects = HIDEventQuerySet.as_manager()

View File

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

View File

@ -2,8 +2,9 @@ import datetime
from typing import TYPE_CHECKING
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.postgres.aggregates import StringAgg
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.urls import path, reverse_lazy
from django.utils.text import slugify
@ -12,8 +13,6 @@ from django.views.generic.list import ListView
import django_filters
import django_tables2 as tables
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.export.views import ExportMixin
@ -223,8 +222,10 @@ class MostActiveMembers(BaseAccessReport):
.values("member_id")
.annotate(
access_count=Count("member_id"),
name=GroupConcat(
ConcatWS("forename", "surname", separator=" "), distinct=True
name=StringAgg(
Func(Value(" "), "forename", "surname", function="concat_ws"),
", ",
distinct=True,
),
)
.order_by("-access_count")
@ -249,8 +250,10 @@ class DetailByDay(BaseAccessReport):
"member_id",
filter=Q(event_type__in=HIDEvent.EventType.any_granted_access()),
),
name=GroupConcat(
ConcatWS("forename", "surname", separator=" "), distinct=True
name=StringAgg(
Func(Value(" "), "forename", "surname", function="concat_ws"),
", ",
distinct=True,
),
)
.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
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):
initial = True
dependencies = []
dependencies = [
("reservations", "0001_initial"),
]
operations = [
migrations.CreateModel(
@ -115,18 +123,6 @@ class Migration(migrations.Migration):
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",
models.TextField(
@ -151,18 +147,6 @@ class Migration(migrations.Migration):
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",
models.BooleanField(
@ -349,18 +333,15 @@ class Migration(migrations.Migration):
"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={
"db_table": "members",
"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(
@ -386,6 +367,7 @@ class Migration(migrations.Migration):
"member",
models.ForeignKey(
db_column="uid",
db_constraint=False,
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.member",
),
@ -393,6 +375,11 @@ class Migration(migrations.Migration):
],
options={
"db_table": "memberflag",
"constraints": [
models.UniqueConstraint(
fields=("member", "flag"), name="unique_member_flag"
)
],
},
),
migrations.AddField(
@ -416,7 +403,7 @@ class Migration(migrations.Migration):
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()),
("type", models.TextField(blank=True, null=True)),
(
@ -469,6 +456,7 @@ class Migration(migrations.Migration):
models.ForeignKey(
blank=True,
db_column="uid",
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="transactions",
@ -480,22 +468,350 @@ class Migration(migrations.Migration):
"db_table": "transactions",
},
),
migrations.AddConstraint(
model_name="memberflag",
constraint=models.UniqueConstraint(
fields=("member", "flag"), name="unique_member_flag"
migrations.CreateModel(
name="EventCategory",
fields=[
("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(
model_name="member",
index=models.Index(fields=["account_name"], name="account_name_idx"),
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,
},
),
migrations.AddIndex(
model_name="member",
index=models.Index(fields=["first_name"], name="first_name_idx"),
django_db_views.operations.ViewRunPython(
code=django_db_views.migration_functions.ForwardViewMigration(
"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(
model_name="member",
index=models.Index(fields=["last_name"], name="last_name_idx"),
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, 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
from datetime import datetime, timedelta
from decimal import Decimal
from typing import TYPE_CHECKING, TypedDict
import django.core.mail.message
from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser
from django.db import models
from django.db import connection, models
from django.db.models import (
Case,
Count,
Exists,
ExpressionWrapper,
F,
Func,
OuterRef,
Q,
QuerySet,
@ -21,7 +21,7 @@ from django.db.models import (
Value,
When,
)
from django.db.models.functions import Coalesce
from django.db.models.functions import Cast, Coalesce
from django.urls import reverse
from django.utils import timezone
@ -409,7 +409,7 @@ class Event(BaseModel):
occurred = models.GeneratedField(
expression=~(Q(cap=0) | Q(count=0) | Q(calendar=EventCalendar.HIDDEN)),
output_field=models.BooleanField(),
db_persist=False,
db_persist=True,
)
# TODO:
# "lgo": {
@ -473,13 +473,7 @@ class EventExtQuerySet(models.QuerySet["EventExtAnnotated"]):
def with_financials(self) -> "QuerySet[EventExtAnnotatedWithFinancials]":
return self.annotate(
**{
field: Subquery(
EventTicketType.objects.filter(event=OuterRef("pk"))
.values("event__pk")
.annotate(d=Sum(field))
.values("d"),
output_field=models.DecimalField(),
)
field: F(f"ticket_aggregates__{field}")
for field in [
"quantity",
"amount",
@ -492,11 +486,12 @@ class EventExtQuerySet(models.QuerySet["EventExtAnnotated"]):
total_due_to_instructor=(
F("instructor_amount") + F("instructor_flat_rate")
),
gross_revenue=Coalesce(F("attendee_stats__gross_revenue"), 0.0),
net_revenue=ExpressionWrapper(
F("gross_revenue") - F("total_due_to_instructor"),
gross_revenue=Coalesce(
F("attendee_stats__gross_revenue"),
0,
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_timestamp = models.GeneratedField(
expression=Func(
Func(F("details___ts"), function="FROM_UNIXTIME"),
template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')",
expression=models.Func(
Cast(models.F("details___ts"), models.IntegerField()),
function="to_timestamp",
),
output_field=models.DateTimeField(),
db_persist=False,
db_persist=True,
verbose_name="Last details fetch",
)
@ -663,7 +658,7 @@ class EventInvoice(models.Model):
class EventTicketTypeQuerySet(models.QuerySet["EventTicketType"]):
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(
When(Q(is_members_ticket=True), Value("Members")),
default=Value("Non-Members"),
@ -685,17 +680,8 @@ class EventTicketTypeQuerySet(models.QuerySet["EventTicketType"]):
class EventTicketTypeManager(models.Manager["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()
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
# restricted ticket, but list price for unrestricted
# (Non-Members) ticket. After, use Members ticket price
@ -703,7 +689,7 @@ class EventTicketTypeManager(models.Manager["EventTicketType"]):
actual_price=Case(
When(
# member ticket
Q(restrict_to=members_folder)
Q(restrict_to__has_key=settings.MW_MEMBERS_FOLDER_ID)
| (
# non-member ticket
Q(restrict_to__isnull=True)
@ -723,7 +709,6 @@ class EventTicketTypeManager(models.Manager["EventTicketType"]):
),
default="members_price",
),
is_members_ticket=(Q(restrict_to__isnull=False)),
materials=Case(
When(
(
@ -764,37 +749,28 @@ class EventTicketType(DBView):
event = models.ForeignKey(
EventExt, on_delete=models.DO_NOTHING, related_name="ticket_types"
)
label = models.TextField()
restrict_to = models.TextField(null=True, blank=True)
list_price = models.FloatField()
quantity = models.IntegerField()
label = models.TextField(db_column="lbl")
list_price = models.DecimalField(db_column="amt", max_digits=13, decimal_places=4)
members_price = models.DecimalField(max_digits=13, decimal_places=4)
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
# 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 = """
view_definition = f"""
SELECT
row_number() over () as id,
eventext.event_ptr_id AS event_id,
tkt.label,
tkt.list_price,
tkt.quantity,
GROUP_CONCAT(tkt.restrict_to SEPARATOR ',') as restrict_to
FROM
membershipworks_eventext AS eventext,
JSON_TABLE (eventext.details, '$.tkt[*]' COLUMNS (
id FOR ORDINALITY,
label VARCHAR(256) PATH '$.lbl' ERROR ON ERROR,
list_price DOUBLE PATH '$.amt' ERROR ON ERROR,
quantity INTEGER PATH '$.cnt' DEFAULT 0 ON EMPTY ERROR ON ERROR,
NESTED PATH '$.dsp[*]' COLUMNS (
restrict_to VARCHAR(100) PATH '$' ERROR ON ERROR
)
)) AS tkt
GROUP BY event_id, id
eventext.event_ptr_id as event_id,
tkt.*,
jsonb_path_query_first(
eventext.details,
'$.tkt[*] ? (exists (@.dsp ? (@[*] == "{settings.MW_MEMBERS_FOLDER_ID}"))).amt'
)::numeric as members_price
FROM membershipworks_eventext AS eventext,
jsonb_to_recordset(eventext.details -> 'tkt') AS tkt (
lbl TEXT,
amt NUMERIC,
cnt INT,
dsp JSONB
)
"""
def __str__(self) -> str:
@ -805,19 +781,59 @@ class EventTicketType(DBView):
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):
event = models.ForeignKey(
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 = """
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
membershipworks_eventext as eventext,
JSON_TABLE(eventext.details, '$.usr[*]' COLUMNS (
s DOUBLE PATH '$.sum' DEFAULT 0 ON EMPTY
)) as tkt
jsonb_to_recordset(eventext.details -> 'usr') AS usr (
sum NUMERIC
)
GROUP BY event_id
"""
@ -830,20 +846,20 @@ class EventAttendee(DBView):
EventExt, on_delete=models.DO_NOTHING, related_name="attendees"
)
uid = models.ForeignKey(Member, on_delete=models.DO_NOTHING)
name = models.CharField(max_length=256)
email = models.CharField(max_length=256)
sum = models.FloatField()
name = models.CharField(max_length=256, db_column="nam")
email = models.CharField(max_length=256, db_column="eml")
sum = models.DecimalField(max_digits=13, decimal_places=4)
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
membershipworks_eventext as eventext,
JSON_TABLE(eventext.details, '$.usr[*]' COLUMNS (
uid VARCHAR(24) PATH '$.uid',
name VARCHAR(256) PATH '$.nam',
email VARCHAR(256) PATH '$.eml',
sum DOUBLE PATH '$.sum'
)) as tkt
membershipworks_eventext AS eventext,
jsonb_to_recordset(eventext.details -> 'usr') AS usr (
uid TEXT,
nam TEXT,
eml TEXT,
sum NUMERIC
)
"""
class Meta:

View File

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

View File

@ -10,6 +10,7 @@ from django.contrib.auth.mixins import (
AccessMixin,
PermissionRequiredMixin,
)
from django.contrib.postgres.aggregates import StringAgg
from django.core import mail
from django.core.files.base import ContentFile
from django.db.models import OuterRef, Q, Subquery
@ -33,7 +34,6 @@ import django_tables2 as tables
import weasyprint
from dal import autocomplete
from django_filters.views import BaseFilterView
from django_mysql.models.aggregates import GroupConcat
from django_sendfile import sendfile
from django_tables2 import A, SingleTableMixin
from django_tables2.export.views import ExportMixin
@ -538,7 +538,7 @@ class MissingPaperworkReport(
membership=Subquery(
qs.filter(
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.contrib.auth.decorators import login_required
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.db import models
from django.db.models import (
@ -16,14 +17,13 @@ from django.db.models import (
Value,
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.shortcuts import get_object_or_404, render
from django.views.generic import ListView
import requests
import weasyprint
from django_mysql.models.aggregates import GroupConcat
from django_tables2 import SingleTableMixin
from django_tables2.export.views import ExportMixin
@ -158,14 +158,16 @@ class InstructorOrVendorReport(
.get_table_data()
.values("name")
.annotate(
instructor_agreement_date=GroupConcat(
"instructor_agreement_date", distinct=True, ordering="asc"
instructor_agreement_date=StringAgg(
Cast("instructor_agreement_date", models.TextField()),
delimiter=", ",
distinct=True,
),
w9_date=GroupConcat("w9_date", distinct=True, ordering="asc"),
phone=GroupConcat("phone", distinct=True, ordering="asc"),
email_address=GroupConcat(
"email_address", distinct=True, ordering="asc"
w9_date=StringAgg(
Cast("w9_date", models.TextField()), ", ", distinct=True
),
phone=StringAgg("phone", ", ", distinct=True),
email_address=StringAgg("email_address", ", ", distinct=True),
)
)

89
pdm.lock generated
View File

@ -5,7 +5,7 @@
groups = ["default", "debug", "dev", "lint", "server", "typing"]
strategy = ["inherit_metadata"]
lock_version = "4.5.0"
content_hash = "sha256:4a7538bb6a4aabea5a3641a9f69edf6045587a129895ddf5108bf765141fbe6f"
content_hash = "sha256:0c560e1f2a81810e95ba8993538337bfc8b39576b6923280cfbfad91ad5b58f2"
[[metadata.targets]]
requires_python = "==3.11.*"
@ -561,21 +561,6 @@ files = [
{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]]
name = "django-nh3"
version = "0.1.1"
@ -1439,17 +1424,6 @@ files = [
{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]]
name = "nh3"
version = "0.2.18"
@ -1624,6 +1598,67 @@ files = [
{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]]
name = "ptyprocess"
version = "0.7.0"

View File

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

View File

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

View File

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