diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index 8175c9a..2786169 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -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 diff --git a/cmsmanage/settings.py b/cmsmanage/settings.py index 4924b5b..569a94c 100644 --- a/cmsmanage/settings.py +++ b/cmsmanage/settings.py @@ -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", - }, } } diff --git a/doorcontrol/migrations/0001_initial.py b/doorcontrol/migrations/0001_initial.py index 7ddef4d..1c9b1d6 100644 --- a/doorcontrol/migrations/0001_initial.py +++ b/doorcontrol/migrations/0001_initial.py @@ -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, + }, ), ] diff --git a/doorcontrol/migrations/0002_door_remove_hidevent_door_name_and_more.py b/doorcontrol/migrations/0002_door_remove_hidevent_door_name_and_more.py deleted file mode 100644 index 3d8116a..0000000 --- a/doorcontrol/migrations/0002_door_remove_hidevent_door_name_and_more.py +++ /dev/null @@ -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" - ), - ), - ] diff --git a/doorcontrol/migrations/0003_door_ip.py b/doorcontrol/migrations/0003_door_ip.py deleted file mode 100644 index 5b6be4e..0000000 --- a/doorcontrol/migrations/0003_door_ip.py +++ /dev/null @@ -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, - ), - ] diff --git a/doorcontrol/migrations/0004_hidevent_is_red.py b/doorcontrol/migrations/0004_hidevent_is_red.py deleted file mode 100644 index 8fbcdb4..0000000 --- a/doorcontrol/migrations/0004_hidevent_is_red.py +++ /dev/null @@ -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(), - ), - ), - ] diff --git a/doorcontrol/migrations/0005_doorcardholdermember_and_more.py b/doorcontrol/migrations/0005_doorcardholdermember_and_more.py deleted file mode 100644 index 26eb69e..0000000 --- a/doorcontrol/migrations/0005_doorcardholdermember_and_more.py +++ /dev/null @@ -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" - ), - ), - ] diff --git a/doorcontrol/migrations/0006_schedule_door_access_field_flagschedulerule_and_more.py b/doorcontrol/migrations/0006_schedule_door_access_field_flagschedulerule_and_more.py deleted file mode 100644 index ef874b2..0000000 --- a/doorcontrol/migrations/0006_schedule_door_access_field_flagschedulerule_and_more.py +++ /dev/null @@ -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, - }, - ), - ] diff --git a/doorcontrol/models.py b/doorcontrol/models.py index 3309991..6a1e7ca 100644 --- a/doorcontrol/models.py +++ b/doorcontrol/models.py @@ -183,7 +183,7 @@ class HIDEvent(models.Model): ] ), output_field=models.BooleanField(), - db_persist=False, + db_persist=True, ) objects = HIDEventQuerySet.as_manager() diff --git a/doorcontrol/tasks/scrapehidevents.py b/doorcontrol/tasks/scrapehidevents.py index 287133d..56bfa64 100644 --- a/doorcontrol/tasks/scrapehidevents.py +++ b/doorcontrol/tasks/scrapehidevents.py @@ -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",), ) diff --git a/doorcontrol/views.py b/doorcontrol/views.py index a363388..80121f8 100644 --- a/doorcontrol/views.py +++ b/doorcontrol/views.py @@ -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") diff --git a/membershipworks/migrations/0001_initial.py b/membershipworks/migrations/0001_initial.py index 0ccc207..8bba051 100644 --- a/membershipworks/migrations/0001_initial.py +++ b/membershipworks/migrations/0001_initial.py @@ -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, ), ] diff --git a/membershipworks/migrations/0002_remove_member_accepted_covid19_policy_and_more.py b/membershipworks/migrations/0002_remove_member_accepted_covid19_policy_and_more.py deleted file mode 100644 index 08d9597..0000000 --- a/membershipworks/migrations/0002_remove_member_accepted_covid19_policy_and_more.py +++ /dev/null @@ -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", - ), - ] diff --git a/membershipworks/migrations/0003_alter_transaction_sid.py b/membershipworks/migrations/0003_alter_transaction_sid.py deleted file mode 100644 index 6a13f96..0000000 --- a/membershipworks/migrations/0003_alter_transaction_sid.py +++ /dev/null @@ -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), - ), - ] diff --git a/membershipworks/migrations/0004_alter_memberflag_member_alter_transaction_member.py b/membershipworks/migrations/0004_alter_memberflag_member_alter_transaction_member.py deleted file mode 100644 index 2d2af89..0000000 --- a/membershipworks/migrations/0004_alter_memberflag_member_alter_transaction_member.py +++ /dev/null @@ -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", - ), - ), - ] diff --git a/membershipworks/migrations/0005_event_eventcategory_eventext_event_category_and_more.py b/membershipworks/migrations/0005_event_eventcategory_eventext_event_category_and_more.py deleted file mode 100644 index b71edf9..0000000 --- a/membershipworks/migrations/0005_event_eventcategory_eventext_event_category_and_more.py +++ /dev/null @@ -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" - ), - ), - ] diff --git a/membershipworks/migrations/0006_eventext_instructor_flat_rate_and_more.py b/membershipworks/migrations/0006_eventext_instructor_flat_rate_and_more.py deleted file mode 100644 index 9a80c3f..0000000 --- a/membershipworks/migrations/0006_eventext_instructor_flat_rate_and_more.py +++ /dev/null @@ -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), - ), - ] diff --git a/membershipworks/migrations/0007_eventmeetingtime_duration.py b/membershipworks/migrations/0007_eventmeetingtime_duration.py deleted file mode 100644 index 59289af..0000000 --- a/membershipworks/migrations/0007_eventmeetingtime_duration.py +++ /dev/null @@ -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(), - ), - ), - ] diff --git a/membershipworks/migrations/0008_event_occurred.py b/membershipworks/migrations/0008_event_occurred.py deleted file mode 100644 index 30b1fc2..0000000 --- a/membershipworks/migrations/0008_event_occurred.py +++ /dev/null @@ -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(), - ), - ), - ] diff --git a/membershipworks/migrations/0009_eventext_materials_fee_included_in_price.py b/membershipworks/migrations/0009_eventext_materials_fee_included_in_price.py deleted file mode 100644 index f90d780..0000000 --- a/membershipworks/migrations/0009_eventext_materials_fee_included_in_price.py +++ /dev/null @@ -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), - ), - ] diff --git a/membershipworks/migrations/0010_alter_eventext_options.py b/membershipworks/migrations/0010_alter_eventext_options.py deleted file mode 100644 index 57a28be..0000000 --- a/membershipworks/migrations/0010_alter_eventext_options.py +++ /dev/null @@ -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"}, - ), - ] diff --git a/membershipworks/migrations/0011_eventext_details.py b/membershipworks/migrations/0011_eventext_details.py deleted file mode 100644 index 3e2f57d..0000000 --- a/membershipworks/migrations/0011_eventext_details.py +++ /dev/null @@ -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(), - ), - ), - ] diff --git a/membershipworks/migrations/0012_eventattendeestats_eventtickettype.py b/membershipworks/migrations/0012_eventattendeestats_eventtickettype.py deleted file mode 100644 index 00a2cff..0000000 --- a/membershipworks/migrations/0012_eventattendeestats_eventtickettype.py +++ /dev/null @@ -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, - ), - ] diff --git a/membershipworks/migrations/0013_eventattendee.py b/membershipworks/migrations/0013_eventattendee.py deleted file mode 100644 index 75fbe11..0000000 --- a/membershipworks/migrations/0013_eventattendee.py +++ /dev/null @@ -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, - ), - ] diff --git a/membershipworks/migrations/0014_remove_eventext_details_timestamp.py b/membershipworks/migrations/0014_remove_eventext_details_timestamp.py deleted file mode 100644 index 33bf8f0..0000000 --- a/membershipworks/migrations/0014_remove_eventext_details_timestamp.py +++ /dev/null @@ -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", - ), - ] diff --git a/membershipworks/migrations/0015_eventmeetingtime_end_after_start.py b/membershipworks/migrations/0015_eventmeetingtime_end_after_start.py deleted file mode 100644 index bb1da94..0000000 --- a/membershipworks/migrations/0015_eventmeetingtime_end_after_start.py +++ /dev/null @@ -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" - ), - ), - ] diff --git a/membershipworks/migrations/0016_eventinvoice.py b/membershipworks/migrations/0016_eventinvoice.py deleted file mode 100644 index d38891e..0000000 --- a/membershipworks/migrations/0016_eventinvoice.py +++ /dev/null @@ -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", - ), - ), - ], - ), - ] diff --git a/membershipworks/migrations/0017_eventext_registrations_alter_eventinvoice_pdf.py b/membershipworks/migrations/0017_eventext_registrations_alter_eventinvoice_pdf.py deleted file mode 100644 index b06790d..0000000 --- a/membershipworks/migrations/0017_eventext_registrations_alter_eventinvoice_pdf.py +++ /dev/null @@ -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/"), - ), - ] diff --git a/membershipworks/migrations/0018_eventext_details_timestamp.py b/membershipworks/migrations/0018_eventext_details_timestamp.py deleted file mode 100644 index 7c3da20..0000000 --- a/membershipworks/migrations/0018_eventext_details_timestamp.py +++ /dev/null @@ -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", - ), - ), - ] diff --git a/membershipworks/migrations/0019_eventext_should_survey_eventext_survey_email_sent.py b/membershipworks/migrations/0019_eventext_should_survey_eventext_survey_email_sent.py deleted file mode 100644 index 0dd7153..0000000 --- a/membershipworks/migrations/0019_eventext_should_survey_eventext_survey_email_sent.py +++ /dev/null @@ -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), - ), - ] diff --git a/membershipworks/migrations/0020_remove_eventmeetingtime_unique_event_start_end_and_more.py b/membershipworks/migrations/0020_remove_eventmeetingtime_unique_event_start_end_and_more.py deleted file mode 100644 index 934c3e5..0000000 --- a/membershipworks/migrations/0020_remove_eventmeetingtime_unique_event_start_end_and_more.py +++ /dev/null @@ -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", - ), - ] diff --git a/membershipworks/models.py b/membershipworks/models.py index 4847113..91a0526 100644 --- a/membershipworks/models.py +++ b/membershipworks/models.py @@ -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: diff --git a/membershipworks/tasks/scrape.py b/membershipworks/tasks/scrape.py index 1d196a3..6a004f7 100644 --- a/membershipworks/tasks/scrape.py +++ b/membershipworks/tasks/scrape.py @@ -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() diff --git a/membershipworks/views.py b/membershipworks/views.py index 2f835d4..255f12b 100644 --- a/membershipworks/views.py +++ b/membershipworks/views.py @@ -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", ", ")) ), ) ) diff --git a/paperwork/views.py b/paperwork/views.py index 922bb16..c81be0a 100644 --- a/paperwork/views.py +++ b/paperwork/views.py @@ -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), ) ) diff --git a/pdm.lock b/pdm.lock index bc63ad6..3d6611c 100644 --- a/pdm.lock +++ b/pdm.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index f014300..52b8918 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/reservations/migrations/0001_initial.py b/reservations/migrations/0001_initial.py index df3d347..458884b 100644 --- a/reservations/migrations/0001_initial.py +++ b/reservations/migrations/0001_initial.py @@ -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") ), diff --git a/reservations/models.py b/reservations/models.py index a2bf070..98afd3c 100644 --- a/reservations/models.py +++ b/reservations/models.py @@ -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()