Compare commits
No commits in common. "8d3f548e8bd45d2340e22b9f8b0492b356ac9f93" and "0e486babb7bfeef90ff2a3a5f9cc5f1edaa840b6" have entirely different histories.
8d3f548e8b
...
0e486babb7
@ -9,12 +9,21 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: catthehacker/ubuntu:act-latest
|
container: catthehacker/ubuntu:act-latest
|
||||||
services:
|
services:
|
||||||
postgres:
|
mariadb:
|
||||||
image: postgres:15
|
# 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
|
||||||
env:
|
env:
|
||||||
POSTGRES_PASSWORD: whatever
|
MARIADB_ROOT_PASSWORD: whatever
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready"]
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"healthcheck.sh",
|
||||||
|
"--su-mysql",
|
||||||
|
"--connect",
|
||||||
|
"--innodb_initialized",
|
||||||
|
]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Setup PDM
|
- name: Setup PDM
|
||||||
@ -26,7 +35,7 @@ jobs:
|
|||||||
- name: Install apt dependencies
|
- name: Install apt dependencies
|
||||||
run: >-
|
run: >-
|
||||||
sudo apt-get update &&
|
sudo apt-get update &&
|
||||||
sudo apt-get -y install build-essential python3-dev libldap2-dev libsasl2-dev
|
sudo apt-get -y install build-essential python3-dev libldap2-dev libsasl2-dev mariadb-client
|
||||||
- name: Install python dependencies
|
- name: Install python dependencies
|
||||||
run: pdm sync -d -G dev
|
run: pdm sync -d -G dev
|
||||||
|
|
||||||
|
@ -28,16 +28,11 @@ class Base(Configuration):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setup(cls):
|
def setup(cls):
|
||||||
super().setup()
|
super().setup()
|
||||||
|
cls.DATABASES["default"]["OPTIONS"] = {"charset": "utf8mb4"}
|
||||||
# TODO: this is a nasty hack, since the connection
|
|
||||||
# pool doesn't seem to work well with django-q2
|
|
||||||
if "qcluster" not in sys.argv:
|
|
||||||
cls.DATABASES["default"]["OPTIONS"] = {"pool": True}
|
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"dal",
|
"dal",
|
||||||
"dal_select2",
|
"dal_select2",
|
||||||
"postgres_metrics.apps.PostgresMetrics",
|
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
"django.contrib.contenttypes",
|
"django.contrib.contenttypes",
|
||||||
@ -57,9 +52,9 @@ class Base(Configuration):
|
|||||||
"django_tables2",
|
"django_tables2",
|
||||||
"django_filters",
|
"django_filters",
|
||||||
"django_db_views",
|
"django_db_views",
|
||||||
|
"django_mysql",
|
||||||
"django_sendfile",
|
"django_sendfile",
|
||||||
"django_bootstrap5",
|
"django_bootstrap5",
|
||||||
"simple_history",
|
|
||||||
# "tasks.apps.TasksConfig",
|
# "tasks.apps.TasksConfig",
|
||||||
"rentals.apps.RentalsConfig",
|
"rentals.apps.RentalsConfig",
|
||||||
"membershipworks.apps.MembershipworksConfig",
|
"membershipworks.apps.MembershipworksConfig",
|
||||||
@ -111,6 +106,9 @@ class Base(Configuration):
|
|||||||
|
|
||||||
WSGI_APPLICATION = "cmsmanage.wsgi.application"
|
WSGI_APPLICATION = "cmsmanage.wsgi.application"
|
||||||
|
|
||||||
|
# mysql.W003 (unique CharField length) is irrelevant on MariaDB >= 10.4.3
|
||||||
|
SILENCED_SYSTEM_CHECKS = ["mysql.W003"]
|
||||||
|
|
||||||
# Default URL to redirect to after authentication
|
# Default URL to redirect to after authentication
|
||||||
LOGIN_REDIRECT_URL = "/"
|
LOGIN_REDIRECT_URL = "/"
|
||||||
LOGIN_URL = "/auth/login/"
|
LOGIN_URL = "/auth/login/"
|
||||||
@ -215,9 +213,6 @@ class Base(Configuration):
|
|||||||
# CMSManage specific stuff
|
# CMSManage specific stuff
|
||||||
WIKI_URL = values.URLValue("https://wiki.claremontmakerspace.org")
|
WIKI_URL = values.URLValue("https://wiki.claremontmakerspace.org")
|
||||||
|
|
||||||
# ID of flag for Members folder in MembershipWorks
|
|
||||||
MW_MEMBERS_FOLDER_ID = "5771675edcdf126302a2f6b9"
|
|
||||||
|
|
||||||
|
|
||||||
class NonCIBase(Base):
|
class NonCIBase(Base):
|
||||||
"""required for all but CI"""
|
"""required for all but CI"""
|
||||||
@ -372,10 +367,13 @@ class CI(Base):
|
|||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
"default": {
|
"default": {
|
||||||
"ENGINE": "django.db.backends.postgresql",
|
"ENGINE": "django.db.backends.mysql",
|
||||||
"HOST": "postgres",
|
"HOST": "mariadb",
|
||||||
"NAME": "cms",
|
"NAME": "CMS_Database",
|
||||||
"USER": "postgres",
|
"USER": "root",
|
||||||
"PASSWORD": "whatever",
|
"PASSWORD": "whatever",
|
||||||
|
"OPTIONS": {
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,6 @@ urlpatterns = [
|
|||||||
path("paperwork/", include("paperwork.urls")),
|
path("paperwork/", include("paperwork.urls")),
|
||||||
path("doorcontrol/", include("doorcontrol.urls")),
|
path("doorcontrol/", include("doorcontrol.urls")),
|
||||||
path("api/v1/", include((router.urls, "api"), namespace="v1")),
|
path("api/v1/", include((router.urls, "api"), namespace="v1")),
|
||||||
path("admin/postgres-metrics/", include("postgres_metrics.urls")),
|
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path(
|
path(
|
||||||
"auth/",
|
"auth/",
|
||||||
|
@ -44,6 +44,9 @@ class HIDEventAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|||||||
readonly_fields = ["decoded_card_number"]
|
readonly_fields = ["decoded_card_number"]
|
||||||
changelist_actions = ("refresh_all_doors",)
|
changelist_actions = ("refresh_all_doors",)
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
return super().get_queryset(request).with_decoded_card_number()
|
||||||
|
|
||||||
@admin.display(boolean=True)
|
@admin.display(boolean=True)
|
||||||
def _is_red(self, obj):
|
def _is_red(self, obj):
|
||||||
return obj.is_red
|
return obj.is_red
|
||||||
|
@ -1,62 +1,41 @@
|
|||||||
import dataclasses
|
|
||||||
from typing import Literal
|
|
||||||
|
|
||||||
import bitstring
|
import bitstring
|
||||||
|
|
||||||
# Reference for H10301 card format:
|
# Reference for H10301 card format:
|
||||||
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf
|
# https://www.hidglobal.com/sites/default/files/hid-understanding_card_data_formats-wp-en.pdf
|
||||||
|
|
||||||
|
|
||||||
class InvalidHexCode(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Not26Bit(InvalidHexCode):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__("Card number > 26 bits")
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidParity(InvalidHexCode):
|
|
||||||
def __init__(self, even_odd: Literal["even", "odd"]) -> None:
|
|
||||||
super().__init__(f"Bad {even_odd} parity")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class Credential:
|
class Credential:
|
||||||
bits: bitstring.Bits
|
def __init__(self, code=None, hex_code=None):
|
||||||
|
if code is None and hex_code is None:
|
||||||
@classmethod
|
raise TypeError("Must set either code or hex for a Credential")
|
||||||
def from_code(cls, facility=int, card_number=int) -> "Credential":
|
elif code is not None and hex_code is not None:
|
||||||
bits = bitstring.pack(
|
raise TypeError("Cannot set both code and hex for a Credential")
|
||||||
"0b000000, 0b0, uint:8=facility, uint:16=card_number, 0b0",
|
elif code is not None:
|
||||||
facility=facility,
|
self.bits = bitstring.pack(
|
||||||
card_number=card_number,
|
"0b000000, 0b0, uint:8=facility, uint:16=number, 0b0",
|
||||||
|
facility=code[0],
|
||||||
|
number=code[1],
|
||||||
)
|
)
|
||||||
bits[6] = bits[7:19].count(1) % 2 # even parity
|
self.bits[6] = self.bits[7:19].count(1) % 2 # even parity
|
||||||
bits[31] = bits[19:31].count(0) % 2 # odd parity
|
self.bits[31] = not (self.bits[19:31].count(1) % 2) # odd parity
|
||||||
return cls(bits)
|
elif hex_code is not None:
|
||||||
|
self.bits = bitstring.Bits(hex=hex_code)
|
||||||
|
|
||||||
@classmethod
|
def __repr__(self):
|
||||||
def from_hex(cls, hex_code: str) -> "Credential":
|
return f"Credential({self.code})"
|
||||||
bits = bitstring.Bits(hex=hex_code)
|
|
||||||
|
|
||||||
if bits[:6].any(1):
|
def __eq__(self, other):
|
||||||
raise Not26Bit
|
return self.bits == other.bits
|
||||||
if bits[6] != bits[7:19].count(1) % 2:
|
|
||||||
raise InvalidParity("even")
|
|
||||||
if bits[31] != (bits[19:31].count(0) % 2):
|
|
||||||
raise InvalidParity("odd")
|
|
||||||
|
|
||||||
return cls(bits)
|
def __hash__(self):
|
||||||
|
return self.bits.int
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def facility_code(self) -> int:
|
def code(self):
|
||||||
return self.bits[7:15].uint
|
facility = self.bits[7:15].uint
|
||||||
|
code = self.bits[15:31].uint
|
||||||
|
return (facility, code)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def card_number(self) -> int:
|
def hex(self):
|
||||||
return self.bits[15:31].uint
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hex(self) -> str:
|
|
||||||
return self.bits.hex.upper()
|
return self.bits.hex.upper()
|
||||||
|
@ -2,7 +2,6 @@ import contextlib
|
|||||||
import csv
|
import csv
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from itertools import takewhile
|
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import urllib3
|
import urllib3
|
||||||
@ -34,14 +33,6 @@ class RemoteError(Exception):
|
|||||||
super().__init__(f"Door Updating Error: {r.status_code} {r.reason}\n{r.text}")
|
super().__init__(f"Door Updating Error: {r.status_code} {r.reason}\n{r.text}")
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedPageSize(Exception):
|
|
||||||
def __init__(self, page_size: int) -> None:
|
|
||||||
super().__init__(
|
|
||||||
f"Page size {page_size} greater than supported by controller. "
|
|
||||||
"(controller returned moreRecords=true)"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DoorController:
|
class DoorController:
|
||||||
def __init__(self, ip, username, password):
|
def __init__(self, ip, username, password):
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
@ -161,44 +152,48 @@ class DoorController:
|
|||||||
)
|
)
|
||||||
return self.doXMLRequest(el)
|
return self.doXMLRequest(el)
|
||||||
|
|
||||||
def get_records(
|
def get_records(self, req, count, params=None, stopFunction=None):
|
||||||
self,
|
recordCount = 0
|
||||||
req,
|
moreRecords = True
|
||||||
count_attr: str,
|
|
||||||
params: dict[str, str] | None = None,
|
|
||||||
page_size: int = 100,
|
|
||||||
):
|
|
||||||
dr = self.doXMLRequest(ROOT(req({"action": "DR"})))
|
|
||||||
|
|
||||||
for offset in range(0, int(dr[0].attrib[count_attr]), page_size):
|
# note: all the "+/-1" bits are to work around a bug where the
|
||||||
|
# last returned entry is incomplete. There is probably a
|
||||||
|
# better way to do this, but for now I just get the last entry
|
||||||
|
# again in the next request. I suspect this probably ends
|
||||||
|
# poorly if the numbers line up poorly (ie an exact multiple
|
||||||
|
# of the returned record limit)
|
||||||
|
while True:
|
||||||
res = self.doXMLRequest(
|
res = self.doXMLRequest(
|
||||||
ROOT(
|
ROOT(
|
||||||
req(
|
req(
|
||||||
{
|
{
|
||||||
"action": "LR",
|
"action": "LR",
|
||||||
"recordCount": str(page_size),
|
"recordCount": str(count - recordCount + 1),
|
||||||
"recordOffset": str(offset),
|
"recordOffset": str(
|
||||||
|
recordCount - 1 if recordCount > 0 else 0
|
||||||
|
),
|
||||||
**(params or {}),
|
**(params or {}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
recordCount += int(res[0].get("recordCount")) - 1
|
||||||
|
moreRecords = res[0].get("moreRecords") == "true"
|
||||||
|
|
||||||
# The web interface does sub-pagination when needed, but that is very messy.
|
if moreRecords and (stopFunction is None or stopFunction(list(res[0]))):
|
||||||
# See previous versions of this function for an example :)
|
yield list(res[0])[:-1]
|
||||||
if res[0].attrib["moreRecords"] != "false":
|
else:
|
||||||
raise UnsupportedPageSize(page_size)
|
|
||||||
|
|
||||||
yield list(res[0])
|
yield list(res[0])
|
||||||
|
break
|
||||||
|
|
||||||
def get_cardholders(self):
|
def get_cardholders(self):
|
||||||
for page in self.get_records(
|
for page in self.get_records(
|
||||||
E.Cardholders, "cardholdersInUse", params={"responseFormat": "expanded"}
|
E.Cardholders, 1000, {"responseFormat": "expanded"}
|
||||||
):
|
):
|
||||||
yield from page
|
yield from page
|
||||||
|
|
||||||
def get_credentials(self):
|
def get_credentials(self):
|
||||||
for page in self.get_records(E.Credentials, "credentialsInUse"):
|
for page in self.get_records(E.Credentials, 1000):
|
||||||
yield from page
|
yield from page
|
||||||
|
|
||||||
def update_credential(self, rawCardNumber: str, cardholderID: str):
|
def update_credential(self, rawCardNumber: str, cardholderID: str):
|
||||||
@ -215,17 +210,19 @@ class DoorController:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_events(self, threshold: datetime):
|
def get_events(self, threshold):
|
||||||
def event_newer_than_threshold(event):
|
def event_newer_than_threshold(event):
|
||||||
return datetime.fromisoformat(event.attrib["timestamp"]) > threshold
|
return datetime.fromisoformat(event.attrib["timestamp"]) > threshold
|
||||||
|
|
||||||
# smaller page size empirically determined
|
# These door controllers only store 5000 events max
|
||||||
for page in self.get_records(E.EventMessages, "eventsInUse", page_size=25):
|
for page in self.get_records(
|
||||||
events = list(takewhile(event_newer_than_threshold, page))
|
E.EventMessages,
|
||||||
|
5000,
|
||||||
|
stopFunction=lambda events: event_newer_than_threshold(events[-1]),
|
||||||
|
):
|
||||||
|
events = [event for event in page if event_newer_than_threshold(event)]
|
||||||
if events:
|
if events:
|
||||||
yield events
|
yield events
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
def get_lock(self):
|
def get_lock(self):
|
||||||
el = ROOT(E.Doors({"action": "LR", "responseFormat": "status"}))
|
el = ROOT(E.Doors({"action": "LR", "responseFormat": "status"}))
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
from unittest import TestCase
|
|
||||||
|
|
||||||
from hypothesis import given
|
|
||||||
from hypothesis import strategies as st
|
|
||||||
|
|
||||||
from doorcontrol.hid.Credential import Credential
|
|
||||||
|
|
||||||
|
|
||||||
class CredentialTestCase(TestCase):
|
|
||||||
@given(facility_code=st.integers(0, 0xFF), card_number=st.integers(0, 0xFFFF))
|
|
||||||
def test_code_round_trip(self, facility_code: int, card_number: int):
|
|
||||||
cred = Credential.from_code(facility_code, card_number)
|
|
||||||
self.assertEqual(cred.facility_code, facility_code)
|
|
||||||
self.assertEqual(cred.card_number, card_number)
|
|
||||||
|
|
||||||
@given(facility_code=st.integers(0, 0xFF), card_number=st.integers(0, 0xFFFF))
|
|
||||||
def test_to_hex_round_trip(self, facility_code: int, card_number: int):
|
|
||||||
cred = Credential.from_code(facility_code, card_number)
|
|
||||||
hex_cred = Credential.from_hex(cred.hex)
|
|
||||||
self.assertEqual(cred, hex_cred)
|
|
@ -1,40 +1,14 @@
|
|||||||
# Generated by Django 5.1 on 2024-08-21 18:31
|
# Generated by Django 4.1.3 on 2023-01-25 02:18
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = []
|
||||||
("membershipworks", "0001_initial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
|
||||||
name="Door",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.CharField(max_length=64, unique=True)),
|
|
||||||
("ip", models.GenericIPAddressField(protocol="IPv4")),
|
|
||||||
(
|
|
||||||
"access_field",
|
|
||||||
models.TextField(
|
|
||||||
help_text="Membershipworks field that grants members access to this door",
|
|
||||||
max_length=128,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="HIDEvent",
|
name="HIDEvent",
|
||||||
fields=[
|
fields=[
|
||||||
@ -47,6 +21,7 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="ID",
|
verbose_name="ID",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
("door_name", models.CharField(db_column="doorName", max_length=64)),
|
||||||
("timestamp", models.DateTimeField()),
|
("timestamp", models.DateTimeField()),
|
||||||
(
|
(
|
||||||
"event_type",
|
"event_type",
|
||||||
@ -113,173 +88,16 @@ class Migration(migrations.Migration):
|
|||||||
blank=True, db_column="rawCardNumber", max_length=8, null=True
|
blank=True, db_column="rawCardNumber", max_length=8, null=True
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
|
||||||
"door",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="doorcontrol.door",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"is_red",
|
|
||||||
models.GeneratedField(
|
|
||||||
db_persist=True,
|
|
||||||
expression=models.Q(
|
|
||||||
(
|
|
||||||
"event_type__in",
|
|
||||||
[
|
|
||||||
1022,
|
|
||||||
1023,
|
|
||||||
2024,
|
|
||||||
2029,
|
|
||||||
2036,
|
|
||||||
2042,
|
|
||||||
2043,
|
|
||||||
2046,
|
|
||||||
4041,
|
|
||||||
4042,
|
|
||||||
4043,
|
|
||||||
4044,
|
|
||||||
4045,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
),
|
|
||||||
output_field=models.BooleanField(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"db_table": "hidevent",
|
"db_table": "hidevent",
|
||||||
"ordering": ("-timestamp",),
|
"ordering": ("-timestamp",),
|
||||||
"constraints": [
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=("door", "timestamp", "event_type"),
|
|
||||||
name="unique_hidevent",
|
|
||||||
)
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.AddConstraint(
|
||||||
name="DoorCardholderMember",
|
model_name="hidevent",
|
||||||
fields=[
|
constraint=models.UniqueConstraint(
|
||||||
(
|
fields=("door_name", "timestamp", "event_type"), name="unique_hidevent"
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("cardholder_id", models.IntegerField()),
|
|
||||||
(
|
|
||||||
"door",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="doorcontrol.door",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"member",
|
|
||||||
models.ForeignKey(
|
|
||||||
db_constraint=False,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="membershipworks.member",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"constraints": [
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=("door", "cardholder_id"),
|
|
||||||
name="unique_door_cardholder_id",
|
|
||||||
),
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=("door", "member"), name="unique_door_member"
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="Schedule",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("name", models.CharField(max_length=255, unique=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="FlagScheduleRule",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("doors", models.ManyToManyField(to="doorcontrol.door")),
|
|
||||||
(
|
|
||||||
"flag",
|
|
||||||
models.ForeignKey(
|
|
||||||
blank=True,
|
|
||||||
db_constraint=False,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
|
||||||
to="membershipworks.flag",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"schedule",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="doorcontrol.schedule",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="AttributeScheduleRule",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigAutoField(
|
|
||||||
auto_created=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
verbose_name="ID",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"access_field",
|
|
||||||
models.CharField(
|
|
||||||
help_text="Membershipworks field that grants members access to this door using this schedule.",
|
|
||||||
max_length=128,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("doors", models.ManyToManyField(to="doorcontrol.door")),
|
|
||||||
(
|
|
||||||
"schedule",
|
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
to="doorcontrol.schedule",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"abstract": False,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
# 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
18
doorcontrol/migrations/0003_door_ip.py
Normal file
18
doorcontrol/migrations/0003_door_ip.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# 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,
|
||||||
|
),
|
||||||
|
]
|
40
doorcontrol/migrations/0004_hidevent_is_red.py
Normal file
40
doorcontrol/migrations/0004_hidevent_is_red.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# 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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
56
doorcontrol/migrations/0005_doorcardholdermember_and_more.py
Normal file
56
doorcontrol/migrations/0005_doorcardholdermember_and_more.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,106 @@
|
|||||||
|
# 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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -3,14 +3,14 @@ from typing import Self
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import OuterRef, Q, Subquery
|
from django.db.models import F, Func, OuterRef, Q, Subquery
|
||||||
|
from django.db.models.functions import Mod
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
from membershipworks.models import Flag as MembershipWorksFlag
|
from membershipworks.models import Flag as MembershipWorksFlag
|
||||||
from membershipworks.models import Member
|
from membershipworks.models import Member
|
||||||
|
|
||||||
from .hid.Credential import Credential, InvalidHexCode
|
|
||||||
from .hid.DoorController import DoorController
|
from .hid.DoorController import DoorController
|
||||||
|
|
||||||
|
|
||||||
@ -102,6 +102,42 @@ class AttributeScheduleRule(AbstractScheduleRule):
|
|||||||
|
|
||||||
|
|
||||||
class HIDEventQuerySet(models.QuerySet):
|
class HIDEventQuerySet(models.QuerySet):
|
||||||
|
def with_decoded_card_number(self):
|
||||||
|
# TODO: CONV and BIT_COUNT are MySQL/MariaDB specific
|
||||||
|
class Conv(Func):
|
||||||
|
function = "CONV"
|
||||||
|
arity = 3
|
||||||
|
# This is technically not true, but fine for my purposes
|
||||||
|
output_field = models.IntegerField()
|
||||||
|
|
||||||
|
class BitCount(Func):
|
||||||
|
function = "BIT_COUNT"
|
||||||
|
arity = 1
|
||||||
|
|
||||||
|
return (
|
||||||
|
self.alias(card_number=Conv(F("raw_card_number"), 16, 10))
|
||||||
|
.alias(more_than_26_bits=F("card_number").bitrightshift(26))
|
||||||
|
.annotate(card_is_26_bit=Q(more_than_26_bits=0))
|
||||||
|
.alias(
|
||||||
|
parity_a=Mod(
|
||||||
|
BitCount(F("card_number").bitrightshift(1).bitand(0xFFF)), 2
|
||||||
|
),
|
||||||
|
parity_b=Mod(
|
||||||
|
BitCount(F("card_number").bitrightshift(13).bitand(0xFFF)), 2
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
card_is_valid_26_bit=~Q(parity_a=F("card_number").bitand(1))
|
||||||
|
& Q(parity_b=F("card_number").bitrightshift(25).bitand(1))
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
card_number_26_bit=F("card_number").bitrightshift(1).bitand(0xFFFF),
|
||||||
|
card_facility_code_26_bit=F("card_number")
|
||||||
|
.bitrightshift(17)
|
||||||
|
.bitand(0xFF),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def with_member_id(self):
|
def with_member_id(self):
|
||||||
return self.annotate(
|
return self.annotate(
|
||||||
member_id=Subquery(
|
member_id=Subquery(
|
||||||
@ -183,7 +219,7 @@ class HIDEvent(models.Model):
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
output_field=models.BooleanField(),
|
output_field=models.BooleanField(),
|
||||||
db_persist=True,
|
db_persist=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = HIDEventQuerySet.as_manager()
|
objects = HIDEventQuerySet.as_manager()
|
||||||
@ -266,10 +302,13 @@ class HIDEvent(models.Model):
|
|||||||
return event_types.get(self.event_type, f"Unknown Event Type {self.event_type}")
|
return event_types.get(self.event_type, f"Unknown Event Type {self.event_type}")
|
||||||
|
|
||||||
def decoded_card_number(self) -> str | None:
|
def decoded_card_number(self) -> str | None:
|
||||||
|
"""Requires annotations from `with_decoded_card_number`"""
|
||||||
if self.raw_card_number is None:
|
if self.raw_card_number is None:
|
||||||
return None
|
return None
|
||||||
try:
|
elif self.card_is_26_bit:
|
||||||
cred = Credential.from_hex(self.raw_card_number)
|
if self.card_is_valid_26_bit:
|
||||||
return f"{cred.facility_code} - {cred.card_number}"
|
return f"{self.card_facility_code_26_bit} - {self.card_number_26_bit}"
|
||||||
except InvalidHexCode as e:
|
else:
|
||||||
return f"Invalid: {e}"
|
return "Invalid"
|
||||||
|
else:
|
||||||
|
return "Not 26 bit card"
|
||||||
|
@ -20,7 +20,6 @@ class UnitTimeTable(tables.Table):
|
|||||||
|
|
||||||
|
|
||||||
class DeniedAccessTable(tables.Table):
|
class DeniedAccessTable(tables.Table):
|
||||||
decoded_card_number = tables.Column(orderable=False)
|
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
"{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
|
"{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
|
||||||
)
|
)
|
||||||
|
@ -17,15 +17,13 @@ def get_cardholders(door: Door):
|
|||||||
member_id=cardholder.attrib.get("custom2"),
|
member_id=cardholder.attrib.get("custom2"),
|
||||||
)
|
)
|
||||||
|
|
||||||
cardholders = door.controller.get_cardholders()
|
|
||||||
DoorCardholderMember.objects.bulk_create(
|
DoorCardholderMember.objects.bulk_create(
|
||||||
(
|
(
|
||||||
make_ch_member(cardholder)
|
make_ch_member(cardholder)
|
||||||
for cardholder in cardholders
|
for cardholder in door.controller.get_cardholders()
|
||||||
if "custom2" in cardholder.attrib
|
if "custom2" in cardholder.attrib
|
||||||
),
|
),
|
||||||
update_conflicts=True,
|
update_conflicts=True,
|
||||||
unique_fields=("door", "cardholder_id"),
|
|
||||||
update_fields=("member",),
|
update_fields=("member",),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -38,10 +38,12 @@ class DoorMember:
|
|||||||
def from_membershipworks_member(cls, member: Member, door: Door):
|
def from_membershipworks_member(cls, member: Member, door: Door):
|
||||||
if member.access_card_facility_code and member.access_card_number:
|
if member.access_card_facility_code and member.access_card_number:
|
||||||
credentials = {
|
credentials = {
|
||||||
Credential.from_code(
|
Credential(
|
||||||
|
code=(
|
||||||
member.access_card_facility_code,
|
member.access_card_facility_code,
|
||||||
member.access_card_number,
|
member.access_card_number,
|
||||||
)
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
credentials = set()
|
credentials = set()
|
||||||
@ -106,7 +108,7 @@ class DoorMember:
|
|||||||
},
|
},
|
||||||
cardholderID=data.attrib["cardholderID"],
|
cardholderID=data.attrib["cardholderID"],
|
||||||
credentials={
|
credentials={
|
||||||
Credential.from_hex(c.attrib["rawCardNumber"])
|
Credential(hex_code=(c.attrib["rawCardNumber"]))
|
||||||
for c in data.findall("{*}Credential")
|
for c in data.findall("{*}Credential")
|
||||||
},
|
},
|
||||||
schedules={r.attrib["scheduleName"] for r in data.findall("{*}Role")},
|
schedules={r.attrib["scheduleName"] for r in data.findall("{*}Role")},
|
||||||
@ -170,11 +172,9 @@ class DoorMember:
|
|||||||
xml_credentials = [
|
xml_credentials = [
|
||||||
E.Credential(
|
E.Credential(
|
||||||
{
|
{
|
||||||
"formatName": str(credential.facility_code),
|
"formatName": str(credential.code[0]),
|
||||||
"cardNumber": str(credential.card_number),
|
"cardNumber": str(credential.code[1]),
|
||||||
"formatID": self.door.card_formats[
|
"formatID": self.door.card_formats[str(credential.code[0])],
|
||||||
str(credential.facility_code)
|
|
||||||
],
|
|
||||||
"isCard": "true",
|
"isCard": "true",
|
||||||
"cardholderID": self.cardholderID,
|
"cardholderID": self.cardholderID,
|
||||||
}
|
}
|
||||||
@ -222,7 +222,7 @@ def update_door(door: Door, dry_run: bool = False):
|
|||||||
}
|
}
|
||||||
|
|
||||||
existing_door_credentials = {
|
existing_door_credentials = {
|
||||||
Credential.from_hex(c.attrib["rawCardNumber"])
|
Credential(hex_code=c.attrib["rawCardNumber"])
|
||||||
for c in door.controller.get_credentials()
|
for c in door.controller.get_credentials()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,9 @@ import datetime
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.contrib.postgres.aggregates import StringAgg
|
|
||||||
from django.core.exceptions import BadRequest
|
from django.core.exceptions import BadRequest
|
||||||
from django.db.models import Count, F, FloatField, Func, Q, Value, Window
|
from django.db.models import Count, F, FloatField, Q, Window
|
||||||
from django.db.models.functions import Lead, NullIf, Trunc
|
from django.db.models.functions import Lead, Trunc
|
||||||
from django.urls import path, reverse_lazy
|
from django.urls import path, reverse_lazy
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
@ -13,6 +12,8 @@ from django.views.generic.list import ListView
|
|||||||
import django_filters
|
import django_filters
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_filters.views import BaseFilterView
|
from django_filters.views import BaseFilterView
|
||||||
|
from django_mysql.models.aggregates import GroupConcat
|
||||||
|
from django_mysql.models.functions import ConcatWS
|
||||||
from django_tables2 import SingleTableMixin
|
from django_tables2 import SingleTableMixin
|
||||||
from django_tables2.export.views import ExportMixin
|
from django_tables2.export.views import ExportMixin
|
||||||
|
|
||||||
@ -173,7 +174,7 @@ class AccessPerUnitTime(BaseAccessReport):
|
|||||||
members_delta=(
|
members_delta=(
|
||||||
F("members")
|
F("members")
|
||||||
/ Window(
|
/ Window(
|
||||||
Lead(NullIf("members", 0.0)),
|
Lead("members"),
|
||||||
order_by="-unit_time",
|
order_by="-unit_time",
|
||||||
output_field=FloatField(),
|
output_field=FloatField(),
|
||||||
)
|
)
|
||||||
@ -184,7 +185,7 @@ class AccessPerUnitTime(BaseAccessReport):
|
|||||||
access_count_delta=(
|
access_count_delta=(
|
||||||
F("access_count")
|
F("access_count")
|
||||||
/ Window(
|
/ Window(
|
||||||
Lead(NullIf("access_count", 0.0)),
|
Lead("access_count"),
|
||||||
order_by="-unit_time",
|
order_by="-unit_time",
|
||||||
output_field=FloatField(),
|
output_field=FloatField(),
|
||||||
)
|
)
|
||||||
@ -205,7 +206,12 @@ class DeniedAccess(BaseAccessReport):
|
|||||||
denied_event_types = [
|
denied_event_types = [
|
||||||
t for t in HIDEvent.EventType if t.name.startswith("DENIED_ACCESS")
|
t for t in HIDEvent.EventType if t.name.startswith("DENIED_ACCESS")
|
||||||
]
|
]
|
||||||
return super().get_table_data().filter(event_type__in=denied_event_types)
|
return (
|
||||||
|
super()
|
||||||
|
.get_table_data()
|
||||||
|
.filter(event_type__in=denied_event_types)
|
||||||
|
.with_decoded_card_number()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_report
|
@register_report
|
||||||
@ -222,10 +228,8 @@ class MostActiveMembers(BaseAccessReport):
|
|||||||
.values("member_id")
|
.values("member_id")
|
||||||
.annotate(
|
.annotate(
|
||||||
access_count=Count("member_id"),
|
access_count=Count("member_id"),
|
||||||
name=StringAgg(
|
name=GroupConcat(
|
||||||
Func(Value(" "), "forename", "surname", function="concat_ws"),
|
ConcatWS("forename", "surname", separator=" "), distinct=True
|
||||||
", ",
|
|
||||||
distinct=True,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.order_by("-access_count")
|
.order_by("-access_count")
|
||||||
@ -250,10 +254,8 @@ class DetailByDay(BaseAccessReport):
|
|||||||
"member_id",
|
"member_id",
|
||||||
filter=Q(event_type__in=HIDEvent.EventType.any_granted_access()),
|
filter=Q(event_type__in=HIDEvent.EventType.any_granted_access()),
|
||||||
),
|
),
|
||||||
name=StringAgg(
|
name=GroupConcat(
|
||||||
Func(Value(" "), "forename", "surname", function="concat_ws"),
|
ConcatWS("forename", "surname", separator=" "), distinct=True
|
||||||
", ",
|
|
||||||
distinct=True,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.order_by("-timestamp__date")
|
.order_by("-timestamp__date")
|
||||||
|
@ -10,7 +10,6 @@ from django_object_actions import (
|
|||||||
)
|
)
|
||||||
from django_q.models import Task
|
from django_q.models import Task
|
||||||
from django_q.tasks import async_task
|
from django_q.tasks import async_task
|
||||||
from simple_history.admin import SimpleHistoryAdmin
|
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Event,
|
Event,
|
||||||
@ -22,40 +21,14 @@ from .models import (
|
|||||||
Member,
|
Member,
|
||||||
Transaction,
|
Transaction,
|
||||||
)
|
)
|
||||||
from .tasks.scrape import scrape_event_details, scrape_events, scrape_membershipworks
|
from .tasks.scrape import (
|
||||||
|
scrape_event_details,
|
||||||
|
scrape_membershipworks,
|
||||||
|
)
|
||||||
from .tasks.ucsAccounts import sync_accounts
|
from .tasks.ucsAccounts import sync_accounts
|
||||||
|
|
||||||
|
|
||||||
class TaskLabel:
|
class ReadOnlyAdmin(admin.ModelAdmin):
|
||||||
def __init__(self, label: str, task) -> None:
|
|
||||||
self.label = label
|
|
||||||
self.task = task
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
try:
|
|
||||||
last_run = naturaltime(
|
|
||||||
Task.objects.filter(group=self.task.q_task_group)
|
|
||||||
.values_list("started", flat=True)
|
|
||||||
.latest("started")
|
|
||||||
)
|
|
||||||
except Task.DoesNotExist:
|
|
||||||
last_run = "Never"
|
|
||||||
return f"{self.label} [Last Run {last_run}]"
|
|
||||||
|
|
||||||
|
|
||||||
def run_task_action(admin: admin.ModelAdmin, label: str, task):
|
|
||||||
@action(label=TaskLabel(label, task))
|
|
||||||
def action_func(request, obj):
|
|
||||||
async_task(task, group=task.q_task_group)
|
|
||||||
admin.message_user(
|
|
||||||
request,
|
|
||||||
"Queued task, please wait a few seconds/minutes then refresh the page",
|
|
||||||
)
|
|
||||||
|
|
||||||
return action_func
|
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyAdminMixin:
|
|
||||||
def has_add_permission(self, request, obj=None):
|
def has_add_permission(self, request, obj=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -66,18 +39,39 @@ class ReadOnlyAdminMixin:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class BaseMembershipWorksAdmin(
|
class BaseMembershipWorksAdmin(DjangoObjectActions, ReadOnlyAdmin):
|
||||||
DjangoObjectActions, ReadOnlyAdminMixin, SimpleHistoryAdmin
|
|
||||||
):
|
|
||||||
changelist_actions = ("refresh_membershipworks_data", "sync_ucs_accounts")
|
changelist_actions = ("refresh_membershipworks_data", "sync_ucs_accounts")
|
||||||
|
|
||||||
@property
|
# internal method from DjangoObjectActions
|
||||||
def refresh_membershipworks_data(self):
|
def _get_tool_dict(self, tool_name):
|
||||||
return run_task_action(self, "Refresh Data", scrape_membershipworks)
|
tool = super(DjangoObjectActions, self)._get_tool_dict(tool_name)
|
||||||
|
if tool_name == "refresh_membershipworks_data":
|
||||||
|
try:
|
||||||
|
last_run_time = naturaltime(
|
||||||
|
Task.objects.filter(group=scrape_membershipworks.q_task_group)
|
||||||
|
.values_list("started", flat=True)
|
||||||
|
.latest("started")
|
||||||
|
)
|
||||||
|
except Task.DoesNotExist:
|
||||||
|
last_run_time = "Never"
|
||||||
|
tool["label"] = f"Refresh Data [Last Run {last_run_time}]"
|
||||||
|
return tool
|
||||||
|
|
||||||
@property
|
@action
|
||||||
def sync_ucs_accounts(self):
|
def refresh_membershipworks_data(self, request, obj):
|
||||||
return run_task_action(self, "Sync UCS Accounts", sync_accounts)
|
async_task(scrape_membershipworks, group=scrape_membershipworks.q_task_group)
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"Queued refresh, please wait a few seconds/minutes then refresh the page",
|
||||||
|
)
|
||||||
|
|
||||||
|
@action
|
||||||
|
def sync_ucs_accounts(self, request, obj):
|
||||||
|
async_task(sync_accounts, group=sync_accounts.q_task_group)
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
"Queued refresh, please wait a few seconds/minutes then refresh the page",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MemberFlagInline(admin.TabularInline):
|
class MemberFlagInline(admin.TabularInline):
|
||||||
@ -178,56 +172,10 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|||||||
show_facets = admin.ShowFacets.ALWAYS
|
show_facets = admin.ShowFacets.ALWAYS
|
||||||
search_fields = ["eid", "title", "url"]
|
search_fields = ["eid", "title", "url"]
|
||||||
date_hierarchy = "start"
|
date_hierarchy = "start"
|
||||||
|
exclude = ["url", "details", "registrations"]
|
||||||
autocomplete_fields = ["instructor"]
|
autocomplete_fields = ["instructor"]
|
||||||
change_actions = ["fetch_details"]
|
change_actions = ["fetch_details"]
|
||||||
actions = ["fetch_details"]
|
actions = ["fetch_details"]
|
||||||
changelist_actions = ["refresh_membershipworks_data"]
|
|
||||||
|
|
||||||
fieldsets = [
|
|
||||||
(
|
|
||||||
None,
|
|
||||||
{
|
|
||||||
"fields": [
|
|
||||||
"instructor",
|
|
||||||
"materials_fee",
|
|
||||||
"materials_fee_included_in_price",
|
|
||||||
"instructor_percentage",
|
|
||||||
"instructor_flat_rate",
|
|
||||||
("should_survey", "survey_email_sent"),
|
|
||||||
]
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Details",
|
|
||||||
{
|
|
||||||
"classes": ["collapse"],
|
|
||||||
"fields": [
|
|
||||||
"eid",
|
|
||||||
"_url",
|
|
||||||
"start",
|
|
||||||
"end",
|
|
||||||
"duration",
|
|
||||||
"count",
|
|
||||||
"cap",
|
|
||||||
"category",
|
|
||||||
"calendar",
|
|
||||||
"venue",
|
|
||||||
"occurred",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"Advanced details",
|
|
||||||
{
|
|
||||||
"classes": ["collapse"],
|
|
||||||
"fields": ["details_timestamp", "details", "registrations"],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def refresh_membershipworks_data(self):
|
|
||||||
return run_task_action(self, "Refresh Data", scrape_events)
|
|
||||||
|
|
||||||
def get_readonly_fields(self, request: HttpRequest, obj: EventExt) -> list[str]:
|
def get_readonly_fields(self, request: HttpRequest, obj: EventExt) -> list[str]:
|
||||||
fields = []
|
fields = []
|
||||||
@ -239,7 +187,7 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|||||||
else:
|
else:
|
||||||
fields.append(field.name)
|
fields.append(field.name)
|
||||||
fields.insert(fields.index("end") + 1, "duration")
|
fields.insert(fields.index("end") + 1, "duration")
|
||||||
fields += ["details_timestamp", "details", "registrations"]
|
fields.append("details_timestamp")
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
@admin.display(ordering="title")
|
@admin.display(ordering="title")
|
||||||
|
@ -9,7 +9,6 @@ def post_migrate_callback(sender, **kwargs):
|
|||||||
|
|
||||||
from .tasks.event_survey_emails import send_survey_emails
|
from .tasks.event_survey_emails import send_survey_emails
|
||||||
from .tasks.scrape import scrape_events, scrape_membershipworks
|
from .tasks.scrape import scrape_events, scrape_membershipworks
|
||||||
from .tasks.simple_history import q_clean_duplicate_history
|
|
||||||
from .tasks.ucsAccounts import sync_accounts
|
from .tasks.ucsAccounts import sync_accounts
|
||||||
|
|
||||||
ensure_scheduled(
|
ensure_scheduled(
|
||||||
@ -33,11 +32,6 @@ def post_migrate_callback(sender, **kwargs):
|
|||||||
schedule_type=Schedule.HOURLY,
|
schedule_type=Schedule.HOURLY,
|
||||||
)
|
)
|
||||||
|
|
||||||
ensure_scheduled(
|
|
||||||
q_clean_duplicate_history,
|
|
||||||
schedule_type=Schedule.HOURLY,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MembershipworksConfig(AppConfig):
|
class MembershipworksConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
@ -1,21 +1,13 @@
|
|||||||
# Generated by Django 5.1 on 2024-08-21 18:17
|
# Generated by Django 5.0 on 2023-12-20 05:40
|
||||||
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
from django.db.models.functions import Cast
|
|
||||||
|
|
||||||
import django_db_views.migration_functions
|
|
||||||
import django_db_views.operations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = []
|
||||||
("reservations", "0001_initial"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
@ -123,6 +115,18 @@ class Migration(migrations.Migration):
|
|||||||
blank=True, db_column="Parent Account ID", null=True
|
blank=True, db_column="Parent Account ID", null=True
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"gift_membership_purchased_by",
|
||||||
|
models.TextField(
|
||||||
|
blank=True, db_column="Gift Membership purchased by", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"purchased_gift_membership_for",
|
||||||
|
models.TextField(
|
||||||
|
blank=True, db_column="Purchased Gift Membership for", null=True
|
||||||
|
),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"closet_storage",
|
"closet_storage",
|
||||||
models.TextField(
|
models.TextField(
|
||||||
@ -147,6 +151,18 @@ class Migration(migrations.Migration):
|
|||||||
db_column="Access Permitted Shops During Extended Hours?"
|
db_column="Access Permitted Shops During Extended Hours?"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"normal_access_permitted_during_covid19_limited_operations",
|
||||||
|
models.BooleanField(
|
||||||
|
db_column="Normal Access Permitted During COVID-19 Limited Operations"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"access_permitted_during_covid19_staffed_period_only",
|
||||||
|
models.BooleanField(
|
||||||
|
db_column="Access Permitted During COVID-19 Staffed Period Only"
|
||||||
|
),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"access_front_door_and_studio_space_during_extended_hours",
|
"access_front_door_and_studio_space_during_extended_hours",
|
||||||
models.BooleanField(
|
models.BooleanField(
|
||||||
@ -333,15 +349,18 @@ class Migration(migrations.Migration):
|
|||||||
"liability_form_filled_out",
|
"liability_form_filled_out",
|
||||||
models.BooleanField(db_column="Liability Form Filled Out"),
|
models.BooleanField(db_column="Liability Form Filled Out"),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"self_certify_essential_business",
|
||||||
|
models.BooleanField(db_column="selfCertifyEssentialBusiness"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"accepted_covid19_policy",
|
||||||
|
models.BooleanField(db_column="Accepted COVID-19 Policy"),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"db_table": "members",
|
"db_table": "members",
|
||||||
"ordering": ("first_name", "last_name"),
|
"ordering": ("first_name", "last_name"),
|
||||||
"indexes": [
|
|
||||||
models.Index(fields=["account_name"], name="account_name_idx"),
|
|
||||||
models.Index(fields=["first_name"], name="first_name_idx"),
|
|
||||||
models.Index(fields=["last_name"], name="last_name_idx"),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
@ -367,7 +386,6 @@ class Migration(migrations.Migration):
|
|||||||
"member",
|
"member",
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
db_column="uid",
|
db_column="uid",
|
||||||
db_constraint=False,
|
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
to="membershipworks.member",
|
to="membershipworks.member",
|
||||||
),
|
),
|
||||||
@ -375,11 +393,6 @@ class Migration(migrations.Migration):
|
|||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
"db_table": "memberflag",
|
"db_table": "memberflag",
|
||||||
"constraints": [
|
|
||||||
models.UniqueConstraint(
|
|
||||||
fields=("member", "flag"), name="unique_member_flag"
|
|
||||||
)
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
@ -403,7 +416,7 @@ class Migration(migrations.Migration):
|
|||||||
verbose_name="ID",
|
verbose_name="ID",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("sid", models.CharField(blank=True, max_length=256, null=True)),
|
("sid", models.CharField(blank=True, max_length=27, null=True)),
|
||||||
("timestamp", models.DateTimeField()),
|
("timestamp", models.DateTimeField()),
|
||||||
("type", models.TextField(blank=True, null=True)),
|
("type", models.TextField(blank=True, null=True)),
|
||||||
(
|
(
|
||||||
@ -456,7 +469,6 @@ class Migration(migrations.Migration):
|
|||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
blank=True,
|
blank=True,
|
||||||
db_column="uid",
|
db_column="uid",
|
||||||
db_constraint=False,
|
|
||||||
null=True,
|
null=True,
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
related_name="transactions",
|
related_name="transactions",
|
||||||
@ -468,350 +480,22 @@ class Migration(migrations.Migration):
|
|||||||
"db_table": "transactions",
|
"db_table": "transactions",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.AddConstraint(
|
||||||
name="EventCategory",
|
model_name="memberflag",
|
||||||
fields=[
|
constraint=models.UniqueConstraint(
|
||||||
("id", models.IntegerField(primary_key=True, serialize=False)),
|
fields=("member", "flag"), name="unique_member_flag"
|
||||||
("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)),
|
migrations.AddIndex(
|
||||||
(
|
model_name="member",
|
||||||
"category",
|
index=models.Index(fields=["account_name"], name="account_name_idx"),
|
||||||
models.ForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
|
||||||
to="membershipworks.eventcategory",
|
|
||||||
),
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name="member",
|
||||||
|
index=models.Index(fields=["first_name"], name="first_name_idx"),
|
||||||
),
|
),
|
||||||
(
|
migrations.AddIndex(
|
||||||
"occurred",
|
model_name="member",
|
||||||
models.GeneratedField(
|
index=models.Index(fields=["last_name"], name="last_name_idx"),
|
||||||
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.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 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.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,
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -1,453 +0,0 @@
|
|||||||
# Generated by Django 5.1 on 2024-08-28 19:20
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
import simple_history.models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("membershipworks", "0001_initial"),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="HistoricalFlag",
|
|
||||||
fields=[
|
|
||||||
("id", models.CharField(db_index=True, max_length=24)),
|
|
||||||
("name", models.TextField(blank=True, null=True)),
|
|
||||||
("type", models.CharField(max_length=6)),
|
|
||||||
("history_id", models.AutoField(primary_key=True, serialize=False)),
|
|
||||||
("history_date", models.DateTimeField(db_index=True)),
|
|
||||||
("history_change_reason", models.CharField(max_length=100, null=True)),
|
|
||||||
(
|
|
||||||
"history_type",
|
|
||||||
models.CharField(
|
|
||||||
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
|
|
||||||
max_length=1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"history_user",
|
|
||||||
models.ForeignKey(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="+",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "historical flag",
|
|
||||||
"verbose_name_plural": "historical flags",
|
|
||||||
"ordering": ("-history_date", "-history_id"),
|
|
||||||
"get_latest_by": ("history_date", "history_id"),
|
|
||||||
},
|
|
||||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="HistoricalMember",
|
|
||||||
fields=[
|
|
||||||
("uid", models.CharField(db_index=True, max_length=24)),
|
|
||||||
(
|
|
||||||
"year_of_birth",
|
|
||||||
models.TextField(blank=True, db_column="Year of Birth", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"account_name",
|
|
||||||
models.TextField(blank=True, db_column="Account Name", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"first_name",
|
|
||||||
models.TextField(blank=True, db_column="First Name", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"last_name",
|
|
||||||
models.TextField(blank=True, db_column="Last Name", null=True),
|
|
||||||
),
|
|
||||||
("phone", models.TextField(blank=True, db_column="Phone", null=True)),
|
|
||||||
("email", models.TextField(blank=True, db_column="Email", null=True)),
|
|
||||||
(
|
|
||||||
"volunteer_email",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Volunteer Email", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"address_street",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Address (Street)", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"address_city",
|
|
||||||
models.TextField(blank=True, db_column="Address (City)", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"address_state_province",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Address (State/Province)", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"address_postal_code",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Address (Postal Code)", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"address_country",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Address (Country)", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"profile_description",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Profile description", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"website",
|
|
||||||
models.TextField(blank=True, db_column="Website", null=True),
|
|
||||||
),
|
|
||||||
("fax", models.TextField(blank=True, db_column="Fax", null=True)),
|
|
||||||
(
|
|
||||||
"contact_person",
|
|
||||||
models.TextField(blank=True, db_column="Contact Person", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"password",
|
|
||||||
models.TextField(blank=True, db_column="Password", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"position_relation",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Position/relation", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"parent_account_id",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Parent Account ID", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"closet_storage",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Closet Storage #", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"storage_shelf",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Storage Shelf #", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"personal_studio_space",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Personal Studio Space #", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"access_permitted_shops_during_extended_hours",
|
|
||||||
models.BooleanField(
|
|
||||||
db_column="Access Permitted Shops During Extended Hours?"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"access_front_door_and_studio_space_during_extended_hours",
|
|
||||||
models.BooleanField(
|
|
||||||
db_column="Access Front Door and Studio Space During Extended Hours?"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"access_wood_shop",
|
|
||||||
models.BooleanField(db_column="Access Wood Shop?"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"access_metal_shop",
|
|
||||||
models.BooleanField(db_column="Access Metal Shop?"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"access_storage_closet",
|
|
||||||
models.BooleanField(db_column="Access Storage Closet?"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"access_studio_space",
|
|
||||||
models.BooleanField(db_column="Access Studio Space?"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"access_front_door",
|
|
||||||
models.BooleanField(db_column="Access Front Door?"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"access_card_number",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Access Card Number", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"access_card_facility_code",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Access Card Facility Code", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"auto_billing_id",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Auto Billing ID", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"billing_method",
|
|
||||||
models.TextField(blank=True, db_column="Billing Method", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"renewal_date",
|
|
||||||
models.DateField(blank=True, db_column="Renewal Date", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"join_date",
|
|
||||||
models.DateField(blank=True, db_column="Join Date", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"admin_note",
|
|
||||||
models.TextField(blank=True, db_column="Admin note", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"profile_gallery_image_url",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Profile gallery image URL", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"business_card_image_url",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Business card image URL", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"instagram",
|
|
||||||
models.TextField(blank=True, db_column="Instagram", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"pinterest",
|
|
||||||
models.TextField(blank=True, db_column="Pinterest", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"youtube",
|
|
||||||
models.TextField(blank=True, db_column="Youtube", null=True),
|
|
||||||
),
|
|
||||||
("yelp", models.TextField(blank=True, db_column="Yelp", null=True)),
|
|
||||||
(
|
|
||||||
"google",
|
|
||||||
models.TextField(blank=True, db_column="Google+", null=True),
|
|
||||||
),
|
|
||||||
("bbb", models.TextField(blank=True, db_column="BBB", null=True)),
|
|
||||||
(
|
|
||||||
"twitter",
|
|
||||||
models.TextField(blank=True, db_column="Twitter", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"facebook",
|
|
||||||
models.TextField(blank=True, db_column="Facebook", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"linked_in",
|
|
||||||
models.TextField(blank=True, db_column="LinkedIn", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"do_not_show_street_address_in_profile",
|
|
||||||
models.TextField(
|
|
||||||
blank=True,
|
|
||||||
db_column="Do not show street address in profile",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"do_not_list_in_directory",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Do not list in directory", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"how_did_you_hear",
|
|
||||||
models.TextField(blank=True, db_column="HowDidYouHear", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"authorize_charge",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="authorizeCharge", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"policy_agreement",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="policyAgreement", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"waiver_form_signed_and_on_file_date",
|
|
||||||
models.DateField(
|
|
||||||
blank=True,
|
|
||||||
db_column="Waiver form signed and on file date.",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"membership_agreement_signed_and_on_file_date",
|
|
||||||
models.DateField(
|
|
||||||
blank=True,
|
|
||||||
db_column="Membership Agreement signed and on file date.",
|
|
||||||
null=True,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"ip_address",
|
|
||||||
models.TextField(blank=True, db_column="IP Address", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"audit_date",
|
|
||||||
models.DateField(blank=True, db_column="Audit Date", null=True),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"agreement_version",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Agreement Version", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"paperwork_status",
|
|
||||||
models.TextField(
|
|
||||||
blank=True, db_column="Paperwork status", null=True
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"membership_agreement_dated",
|
|
||||||
models.BooleanField(db_column="Membership agreement dated"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"membership_agreement_acknowledgement_page_filled_out",
|
|
||||||
models.BooleanField(
|
|
||||||
db_column="Membership Agreement Acknowledgement Page Filled Out"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"membership_agreement_signed",
|
|
||||||
models.BooleanField(db_column="Membership Agreement Signed"),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"liability_form_filled_out",
|
|
||||||
models.BooleanField(db_column="Liability Form Filled Out"),
|
|
||||||
),
|
|
||||||
("history_id", models.AutoField(primary_key=True, serialize=False)),
|
|
||||||
("history_date", models.DateTimeField(db_index=True)),
|
|
||||||
("history_change_reason", models.CharField(max_length=100, null=True)),
|
|
||||||
(
|
|
||||||
"history_type",
|
|
||||||
models.CharField(
|
|
||||||
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
|
|
||||||
max_length=1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"history_user",
|
|
||||||
models.ForeignKey(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="+",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "historical member",
|
|
||||||
"verbose_name_plural": "historical members",
|
|
||||||
"ordering": ("-history_date", "-history_id"),
|
|
||||||
"get_latest_by": ("history_date", "history_id"),
|
|
||||||
},
|
|
||||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name="HistoricalMemberFlag",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"id",
|
|
||||||
models.BigIntegerField(
|
|
||||||
auto_created=True, blank=True, db_index=True, verbose_name="ID"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("history_id", models.AutoField(primary_key=True, serialize=False)),
|
|
||||||
("history_date", models.DateTimeField(db_index=True)),
|
|
||||||
("history_change_reason", models.CharField(max_length=100, null=True)),
|
|
||||||
(
|
|
||||||
"history_type",
|
|
||||||
models.CharField(
|
|
||||||
choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")],
|
|
||||||
max_length=1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"flag",
|
|
||||||
simple_history.models.HistoricForeignKey(
|
|
||||||
blank=True,
|
|
||||||
db_constraint=False,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
|
||||||
related_name="+",
|
|
||||||
to="membershipworks.flag",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"history_user",
|
|
||||||
models.ForeignKey(
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.SET_NULL,
|
|
||||||
related_name="+",
|
|
||||||
to=settings.AUTH_USER_MODEL,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"member",
|
|
||||||
simple_history.models.HistoricForeignKey(
|
|
||||||
blank=True,
|
|
||||||
db_column="uid",
|
|
||||||
db_constraint=False,
|
|
||||||
null=True,
|
|
||||||
on_delete=django.db.models.deletion.DO_NOTHING,
|
|
||||||
related_name="+",
|
|
||||||
to="membershipworks.member",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "historical member flag",
|
|
||||||
"verbose_name_plural": "historical member flags",
|
|
||||||
"ordering": ("-history_date", "-history_id"),
|
|
||||||
"get_latest_by": ("history_date", "history_id"),
|
|
||||||
},
|
|
||||||
bases=(simple_history.models.HistoricalChanges, models.Model),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="memberflag",
|
|
||||||
name="flag",
|
|
||||||
field=simple_history.models.HistoricForeignKey(
|
|
||||||
on_delete=django.db.models.deletion.PROTECT, to="membershipworks.flag"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="memberflag",
|
|
||||||
name="member",
|
|
||||||
field=simple_history.models.HistoricForeignKey(
|
|
||||||
db_column="uid",
|
|
||||||
db_constraint=False,
|
|
||||||
on_delete=django.db.models.deletion.PROTECT,
|
|
||||||
to="membershipworks.member",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -0,0 +1,36 @@
|
|||||||
|
# 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",
|
||||||
|
),
|
||||||
|
]
|
17
membershipworks/migrations/0003_alter_transaction_sid.py
Normal file
17
membershipworks/migrations/0003_alter_transaction_sid.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,36 @@
|
|||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,154 @@
|
|||||||
|
# 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,25 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
24
membershipworks/migrations/0007_eventmeetingtime_duration.py
Normal file
24
membershipworks/migrations/0007_eventmeetingtime_duration.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# 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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
27
membershipworks/migrations/0008_event_occurred.py
Normal file
27
membershipworks/migrations/0008_event_occurred.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# 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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,17 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
16
membershipworks/migrations/0010_alter_eventext_options.py
Normal file
16
membershipworks/migrations/0010_alter_eventext_options.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# 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"},
|
||||||
|
),
|
||||||
|
]
|
29
membershipworks/migrations/0011_eventext_details.py
Normal file
29
membershipworks/migrations/0011_eventext_details.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# 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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,79 @@
|
|||||||
|
# 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,
|
||||||
|
),
|
||||||
|
]
|
46
membershipworks/migrations/0013_eventattendee.py
Normal file
46
membershipworks/migrations/0013_eventattendee.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# 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,
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,16 @@
|
|||||||
|
# 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",
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,18 @@
|
|||||||
|
# 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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
41
membershipworks/migrations/0016_eventinvoice.py
Normal file
41
membershipworks/migrations/0016_eventinvoice.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,22 @@
|
|||||||
|
# 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/"),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,25 @@
|
|||||||
|
# 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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,22 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,82 @@
|
|||||||
|
# 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",
|
||||||
|
),
|
||||||
|
]
|
@ -1,18 +1,18 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from decimal import Decimal
|
|
||||||
from typing import TYPE_CHECKING, TypedDict
|
from typing import TYPE_CHECKING, TypedDict
|
||||||
|
|
||||||
import django.core.mail.message
|
import django.core.mail.message
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AbstractBaseUser
|
from django.contrib.auth.models import AbstractBaseUser
|
||||||
from django.db import connection, models
|
from django.db import models
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Case,
|
Case,
|
||||||
Count,
|
Count,
|
||||||
Exists,
|
Exists,
|
||||||
ExpressionWrapper,
|
ExpressionWrapper,
|
||||||
F,
|
F,
|
||||||
|
Func,
|
||||||
OuterRef,
|
OuterRef,
|
||||||
Q,
|
Q,
|
||||||
QuerySet,
|
QuerySet,
|
||||||
@ -21,14 +21,13 @@ from django.db.models import (
|
|||||||
Value,
|
Value,
|
||||||
When,
|
When,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Cast, Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
import nh3
|
import nh3
|
||||||
from django_db_views.db_view import DBView
|
from django_db_views.db_view import DBView
|
||||||
from django_stubs_ext import WithAnnotations
|
from django_stubs_ext import WithAnnotations
|
||||||
from simple_history.models import HistoricalRecords, HistoricForeignKey
|
|
||||||
|
|
||||||
from reservations.models import Reservation
|
from reservations.models import Reservation
|
||||||
|
|
||||||
@ -90,8 +89,6 @@ class Flag(BaseModel):
|
|||||||
name = models.TextField(null=True, blank=True)
|
name = models.TextField(null=True, blank=True)
|
||||||
type = models.CharField(max_length=6)
|
type = models.CharField(max_length=6)
|
||||||
|
|
||||||
history = HistoricalRecords()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "flag"
|
db_table = "flag"
|
||||||
ordering = ("name",)
|
ordering = ("name",)
|
||||||
@ -124,6 +121,7 @@ class MemberQuerySet(models.QuerySet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: is this still a temporal table?
|
||||||
class Member(BaseModel):
|
class Member(BaseModel):
|
||||||
uid = models.CharField(max_length=24, primary_key=True)
|
uid = models.CharField(max_length=24, primary_key=True)
|
||||||
year_of_birth = models.TextField(db_column="Year of Birth", null=True, blank=True)
|
year_of_birth = models.TextField(db_column="Year of Birth", null=True, blank=True)
|
||||||
@ -250,8 +248,6 @@ class Member(BaseModel):
|
|||||||
)
|
)
|
||||||
flags = models.ManyToManyField(Flag, through="MemberFlag", related_name="members")
|
flags = models.ManyToManyField(Flag, through="MemberFlag", related_name="members")
|
||||||
|
|
||||||
history = HistoricalRecords()
|
|
||||||
|
|
||||||
_api_names_override = {
|
_api_names_override = {
|
||||||
"uid": "Account ID",
|
"uid": "Account ID",
|
||||||
"how_did_you_hear": "Please tell us how you heard about the Claremont MakerSpace and what tools or shops you are most excited to start using:",
|
"how_did_you_hear": "Please tell us how you heard about the Claremont MakerSpace and what tools or shops you are most excited to start using:",
|
||||||
@ -304,12 +300,10 @@ class Member(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class MemberFlag(BaseModel):
|
class MemberFlag(BaseModel):
|
||||||
member = HistoricForeignKey(
|
member = models.ForeignKey(
|
||||||
Member, on_delete=models.PROTECT, db_column="uid", db_constraint=False
|
Member, on_delete=models.PROTECT, db_column="uid", db_constraint=False
|
||||||
)
|
)
|
||||||
flag = HistoricForeignKey(Flag, on_delete=models.PROTECT)
|
flag = models.ForeignKey(Flag, on_delete=models.PROTECT)
|
||||||
|
|
||||||
history = HistoricalRecords()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "memberflag"
|
db_table = "memberflag"
|
||||||
@ -415,7 +409,7 @@ class Event(BaseModel):
|
|||||||
occurred = models.GeneratedField(
|
occurred = models.GeneratedField(
|
||||||
expression=~(Q(cap=0) | Q(count=0) | Q(calendar=EventCalendar.HIDDEN)),
|
expression=~(Q(cap=0) | Q(count=0) | Q(calendar=EventCalendar.HIDDEN)),
|
||||||
output_field=models.BooleanField(),
|
output_field=models.BooleanField(),
|
||||||
db_persist=True,
|
db_persist=False,
|
||||||
)
|
)
|
||||||
# TODO:
|
# TODO:
|
||||||
# "lgo": {
|
# "lgo": {
|
||||||
@ -479,7 +473,13 @@ class EventExtQuerySet(models.QuerySet["EventExtAnnotated"]):
|
|||||||
def with_financials(self) -> "QuerySet[EventExtAnnotatedWithFinancials]":
|
def with_financials(self) -> "QuerySet[EventExtAnnotatedWithFinancials]":
|
||||||
return self.annotate(
|
return self.annotate(
|
||||||
**{
|
**{
|
||||||
field: F(f"ticket_aggregates__{field}")
|
field: Subquery(
|
||||||
|
EventTicketType.objects.filter(event=OuterRef("pk"))
|
||||||
|
.values("event__pk")
|
||||||
|
.annotate(d=Sum(field))
|
||||||
|
.values("d"),
|
||||||
|
output_field=models.DecimalField(),
|
||||||
|
)
|
||||||
for field in [
|
for field in [
|
||||||
"quantity",
|
"quantity",
|
||||||
"amount",
|
"amount",
|
||||||
@ -492,12 +492,11 @@ class EventExtQuerySet(models.QuerySet["EventExtAnnotated"]):
|
|||||||
total_due_to_instructor=(
|
total_due_to_instructor=(
|
||||||
F("instructor_amount") + F("instructor_flat_rate")
|
F("instructor_amount") + F("instructor_flat_rate")
|
||||||
),
|
),
|
||||||
gross_revenue=Coalesce(
|
gross_revenue=Coalesce(F("attendee_stats__gross_revenue"), 0.0),
|
||||||
F("attendee_stats__gross_revenue"),
|
net_revenue=ExpressionWrapper(
|
||||||
0,
|
F("gross_revenue") - F("total_due_to_instructor"),
|
||||||
output_field=models.DecimalField(),
|
output_field=models.DecimalField(),
|
||||||
),
|
),
|
||||||
net_revenue=F("gross_revenue") - F("total_due_to_instructor"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -550,12 +549,12 @@ class EventExt(Event):
|
|||||||
)
|
)
|
||||||
details = models.JSONField(null=True, blank=True)
|
details = models.JSONField(null=True, blank=True)
|
||||||
details_timestamp = models.GeneratedField(
|
details_timestamp = models.GeneratedField(
|
||||||
expression=models.Func(
|
expression=Func(
|
||||||
Cast(models.F("details___ts"), models.IntegerField()),
|
Func(F("details___ts"), function="FROM_UNIXTIME"),
|
||||||
function="to_timestamp",
|
template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')",
|
||||||
),
|
),
|
||||||
output_field=models.DateTimeField(),
|
output_field=models.DateTimeField(),
|
||||||
db_persist=True,
|
db_persist=False,
|
||||||
verbose_name="Last details fetch",
|
verbose_name="Last details fetch",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -664,7 +663,7 @@ class EventInvoice(models.Model):
|
|||||||
|
|
||||||
class EventTicketTypeQuerySet(models.QuerySet["EventTicketType"]):
|
class EventTicketTypeQuerySet(models.QuerySet["EventTicketType"]):
|
||||||
def group_by_ticket_type(self):
|
def group_by_ticket_type(self):
|
||||||
return self.values(is_members_ticket=Q(restrict_to__isnull=False)).annotate(
|
return self.values("is_members_ticket").annotate(
|
||||||
label=Case(
|
label=Case(
|
||||||
When(Q(is_members_ticket=True), Value("Members")),
|
When(Q(is_members_ticket=True), Value("Members")),
|
||||||
default=Value("Non-Members"),
|
default=Value("Non-Members"),
|
||||||
@ -686,8 +685,17 @@ class EventTicketTypeQuerySet(models.QuerySet["EventTicketType"]):
|
|||||||
|
|
||||||
class EventTicketTypeManager(models.Manager["EventTicketType"]):
|
class EventTicketTypeManager(models.Manager["EventTicketType"]):
|
||||||
def get_queryset(self) -> models.QuerySet["EventTicketType"]:
|
def get_queryset(self) -> models.QuerySet["EventTicketType"]:
|
||||||
|
members_folder = Subquery(
|
||||||
|
Flag.objects.filter(name="Members", type="folder").values("id")[:1]
|
||||||
|
)
|
||||||
qs = super().get_queryset()
|
qs = super().get_queryset()
|
||||||
return qs.annotate(
|
return qs.annotate(
|
||||||
|
members_price=Subquery(
|
||||||
|
qs.filter(event=OuterRef("event"), restrict_to=members_folder).values(
|
||||||
|
"list_price"
|
||||||
|
),
|
||||||
|
output_field=models.FloatField(),
|
||||||
|
),
|
||||||
# Before 2024-07-01, use Members ticket price for any
|
# Before 2024-07-01, use Members ticket price for any
|
||||||
# restricted ticket, but list price for unrestricted
|
# restricted ticket, but list price for unrestricted
|
||||||
# (Non-Members) ticket. After, use Members ticket price
|
# (Non-Members) ticket. After, use Members ticket price
|
||||||
@ -695,7 +703,7 @@ class EventTicketTypeManager(models.Manager["EventTicketType"]):
|
|||||||
actual_price=Case(
|
actual_price=Case(
|
||||||
When(
|
When(
|
||||||
# member ticket
|
# member ticket
|
||||||
Q(restrict_to__has_key=settings.MW_MEMBERS_FOLDER_ID)
|
Q(restrict_to=members_folder)
|
||||||
| (
|
| (
|
||||||
# non-member ticket
|
# non-member ticket
|
||||||
Q(restrict_to__isnull=True)
|
Q(restrict_to__isnull=True)
|
||||||
@ -715,6 +723,7 @@ class EventTicketTypeManager(models.Manager["EventTicketType"]):
|
|||||||
),
|
),
|
||||||
default="members_price",
|
default="members_price",
|
||||||
),
|
),
|
||||||
|
is_members_ticket=(Q(restrict_to__isnull=False)),
|
||||||
materials=Case(
|
materials=Case(
|
||||||
When(
|
When(
|
||||||
(
|
(
|
||||||
@ -755,28 +764,37 @@ class EventTicketType(DBView):
|
|||||||
event = models.ForeignKey(
|
event = models.ForeignKey(
|
||||||
EventExt, on_delete=models.DO_NOTHING, related_name="ticket_types"
|
EventExt, on_delete=models.DO_NOTHING, related_name="ticket_types"
|
||||||
)
|
)
|
||||||
label = models.TextField(db_column="lbl")
|
label = models.TextField()
|
||||||
list_price = models.DecimalField(db_column="amt", max_digits=13, decimal_places=4)
|
restrict_to = models.TextField(null=True, blank=True)
|
||||||
members_price = models.DecimalField(max_digits=13, decimal_places=4)
|
list_price = models.FloatField()
|
||||||
quantity = models.IntegerField(db_column="cnt")
|
quantity = models.IntegerField()
|
||||||
restrict_to = models.JSONField(db_column="dsp")
|
|
||||||
|
|
||||||
view_definition = f"""
|
# 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 = """
|
||||||
SELECT
|
SELECT
|
||||||
row_number() over () as id,
|
row_number() over () as id,
|
||||||
eventext.event_ptr_id as event_id,
|
eventext.event_ptr_id AS event_id,
|
||||||
tkt.*,
|
tkt.label,
|
||||||
jsonb_path_query_first(
|
tkt.list_price,
|
||||||
eventext.details,
|
tkt.quantity,
|
||||||
'$.tkt[*] ? (exists (@.dsp ? (@[*] == "{settings.MW_MEMBERS_FOLDER_ID}"))).amt'
|
GROUP_CONCAT(tkt.restrict_to SEPARATOR ',') as restrict_to
|
||||||
)::numeric as members_price
|
FROM
|
||||||
FROM membershipworks_eventext AS eventext,
|
membershipworks_eventext AS eventext,
|
||||||
jsonb_to_recordset(eventext.details -> 'tkt') AS tkt (
|
JSON_TABLE (eventext.details, '$.tkt[*]' COLUMNS (
|
||||||
lbl TEXT,
|
id FOR ORDINALITY,
|
||||||
amt NUMERIC,
|
label VARCHAR(256) PATH '$.lbl' ERROR ON ERROR,
|
||||||
cnt INT,
|
list_price DOUBLE PATH '$.amt' ERROR ON ERROR,
|
||||||
dsp JSONB
|
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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
@ -787,59 +805,19 @@ class EventTicketType(DBView):
|
|||||||
base_manager_name = "objects"
|
base_manager_name = "objects"
|
||||||
|
|
||||||
|
|
||||||
class EventTicketAggregate(DBView):
|
|
||||||
event = models.OneToOneField(
|
|
||||||
EventExt,
|
|
||||||
on_delete=models.DO_NOTHING,
|
|
||||||
related_name="ticket_aggregates",
|
|
||||||
primary_key=True,
|
|
||||||
)
|
|
||||||
quantity = models.IntegerField()
|
|
||||||
amount = models.DecimalField(max_digits=13, decimal_places=4)
|
|
||||||
materials = models.DecimalField(max_digits=13, decimal_places=4)
|
|
||||||
amount_without_materials = models.DecimalField(max_digits=13, decimal_places=4)
|
|
||||||
instructor_revenue = models.DecimalField(max_digits=13, decimal_places=4)
|
|
||||||
instructor_amount = models.DecimalField(max_digits=13, decimal_places=4)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def view_definition():
|
|
||||||
qs = EventTicketType.objects.values("event").annotate(
|
|
||||||
**{
|
|
||||||
field: Sum(field)
|
|
||||||
for field in [
|
|
||||||
"quantity",
|
|
||||||
"amount",
|
|
||||||
"materials",
|
|
||||||
"amount_without_materials",
|
|
||||||
"instructor_revenue",
|
|
||||||
"instructor_amount",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
with connection.cursor() as cursor:
|
|
||||||
return cursor.mogrify(*qs.query.sql_with_params())
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return f"{self.event}: {self.quantity}, {self.amount}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
managed = False
|
|
||||||
|
|
||||||
|
|
||||||
class EventAttendeeStats(DBView):
|
class EventAttendeeStats(DBView):
|
||||||
event = models.ForeignKey(
|
event = models.ForeignKey(
|
||||||
EventExt, on_delete=models.DO_NOTHING, related_name="attendee_stats"
|
EventExt, on_delete=models.DO_NOTHING, related_name="attendee_stats"
|
||||||
)
|
)
|
||||||
gross_revenue = models.DecimalField(max_digits=13, decimal_places=4)
|
gross_revenue = models.FloatField()
|
||||||
|
|
||||||
view_definition = """
|
view_definition = """
|
||||||
SELECT eventext.event_ptr_id as event_id, SUM(usr.sum) as gross_revenue
|
SELECT eventext.event_ptr_id as event_id, SUM(tkt.s) as gross_revenue
|
||||||
FROM
|
FROM
|
||||||
membershipworks_eventext as eventext,
|
membershipworks_eventext as eventext,
|
||||||
jsonb_to_recordset(eventext.details -> 'usr') AS usr (
|
JSON_TABLE(eventext.details, '$.usr[*]' COLUMNS (
|
||||||
sum NUMERIC
|
s DOUBLE PATH '$.sum' DEFAULT 0 ON EMPTY
|
||||||
)
|
)) as tkt
|
||||||
GROUP BY event_id
|
GROUP BY event_id
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -852,20 +830,20 @@ class EventAttendee(DBView):
|
|||||||
EventExt, on_delete=models.DO_NOTHING, related_name="attendees"
|
EventExt, on_delete=models.DO_NOTHING, related_name="attendees"
|
||||||
)
|
)
|
||||||
uid = models.ForeignKey(Member, on_delete=models.DO_NOTHING)
|
uid = models.ForeignKey(Member, on_delete=models.DO_NOTHING)
|
||||||
name = models.CharField(max_length=256, db_column="nam")
|
name = models.CharField(max_length=256)
|
||||||
email = models.CharField(max_length=256, db_column="eml")
|
email = models.CharField(max_length=256)
|
||||||
sum = models.DecimalField(max_digits=13, decimal_places=4)
|
sum = models.FloatField()
|
||||||
|
|
||||||
view_definition = """
|
view_definition = """
|
||||||
SELECT eventext.event_ptr_id as event_id, usr.*
|
SELECT eventext.event_ptr_id as event_id, tkt.uid, tkt.name, tkt.email, tkt.sum
|
||||||
FROM
|
FROM
|
||||||
membershipworks_eventext AS eventext,
|
membershipworks_eventext as eventext,
|
||||||
jsonb_to_recordset(eventext.details -> 'usr') AS usr (
|
JSON_TABLE(eventext.details, '$.usr[*]' COLUMNS (
|
||||||
uid TEXT,
|
uid VARCHAR(24) PATH '$.uid',
|
||||||
nam TEXT,
|
name VARCHAR(256) PATH '$.nam',
|
||||||
eml TEXT,
|
email VARCHAR(256) PATH '$.eml',
|
||||||
sum NUMERIC
|
sum DOUBLE PATH '$.sum'
|
||||||
)
|
)) as tkt
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -137,7 +137,6 @@ def scrape_events():
|
|||||||
events = Event.objects.bulk_create(
|
events = Event.objects.bulk_create(
|
||||||
[Event.from_api_dict(event_data) for event_data in data["evt"]],
|
[Event.from_api_dict(event_data) for event_data in data["evt"]],
|
||||||
update_conflicts=True,
|
update_conflicts=True,
|
||||||
unique_fields=["eid"],
|
|
||||||
update_fields=[
|
update_fields=[
|
||||||
field.attname
|
field.attname
|
||||||
for field in Event._meta.get_fields()
|
for field in Event._meta.get_fields()
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import django.core.management
|
|
||||||
|
|
||||||
from cmsmanage.django_q2_helper import q_task_group
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: this probably should be more global, instead of owned by membershipworks app
|
|
||||||
@q_task_group("Clean Duplicate History")
|
|
||||||
def q_clean_duplicate_history():
|
|
||||||
django.core.management.call_command("clean_duplicate_history", "--auto")
|
|
@ -10,7 +10,6 @@ from django.contrib.auth.mixins import (
|
|||||||
AccessMixin,
|
AccessMixin,
|
||||||
PermissionRequiredMixin,
|
PermissionRequiredMixin,
|
||||||
)
|
)
|
||||||
from django.contrib.postgres.aggregates import StringAgg
|
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.db.models import OuterRef, Q, Subquery
|
from django.db.models import OuterRef, Q, Subquery
|
||||||
@ -34,6 +33,7 @@ import django_tables2 as tables
|
|||||||
import weasyprint
|
import weasyprint
|
||||||
from dal import autocomplete
|
from dal import autocomplete
|
||||||
from django_filters.views import BaseFilterView
|
from django_filters.views import BaseFilterView
|
||||||
|
from django_mysql.models.aggregates import GroupConcat
|
||||||
from django_sendfile import sendfile
|
from django_sendfile import sendfile
|
||||||
from django_tables2 import A, SingleTableMixin
|
from django_tables2 import A, SingleTableMixin
|
||||||
from django_tables2.export.views import ExportMixin
|
from django_tables2.export.views import ExportMixin
|
||||||
@ -538,7 +538,7 @@ class MissingPaperworkReport(
|
|||||||
membership=Subquery(
|
membership=Subquery(
|
||||||
qs.filter(
|
qs.filter(
|
||||||
pk=OuterRef("pk"), flags__type__in=("level", "addon")
|
pk=OuterRef("pk"), flags__type__in=("level", "addon")
|
||||||
).values(m=StringAgg("flags__name", ", "))
|
).values(m=GroupConcat("flags__name"))
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import logging
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
@ -61,17 +60,10 @@ class PermissionRequiredViewTestCaseMixin:
|
|||||||
cls.user_with_permission.user_permissions.add(*resolved_permissions)
|
cls.user_with_permission.user_permissions.add(*resolved_permissions)
|
||||||
|
|
||||||
def test_missing_permission(self) -> None:
|
def test_missing_permission(self) -> None:
|
||||||
# suppress PermissionDenied messages
|
|
||||||
logger = logging.getLogger("django.request")
|
|
||||||
previous_log_level = logger.getEffectiveLevel()
|
|
||||||
logger.setLevel(logging.ERROR)
|
|
||||||
|
|
||||||
self.client.force_login(self.user_without_permission)
|
self.client.force_login(self.user_without_permission)
|
||||||
response = self.client.get(self.path)
|
response = self.client.get(self.path)
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
logger.setLevel(previous_log_level)
|
|
||||||
|
|
||||||
|
|
||||||
class WaiverReportTestCase(PermissionRequiredViewTestCaseMixin, TestCase):
|
class WaiverReportTestCase(PermissionRequiredViewTestCaseMixin, TestCase):
|
||||||
permissions = [{"model": Waiver, "codename": "view_waiver"}]
|
permissions = [{"model": Waiver, "codename": "view_waiver"}]
|
||||||
|
@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.contrib.postgres.aggregates import StringAgg
|
|
||||||
from django.contrib.staticfiles import finders as staticfiles_finders
|
from django.contrib.staticfiles import finders as staticfiles_finders
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
@ -17,13 +16,14 @@ from django.db.models import (
|
|||||||
Value,
|
Value,
|
||||||
When,
|
When,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Cast, Concat
|
from django.db.models.functions import Concat
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
|
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import weasyprint
|
import weasyprint
|
||||||
|
from django_mysql.models.aggregates import GroupConcat
|
||||||
from django_tables2 import SingleTableMixin
|
from django_tables2 import SingleTableMixin
|
||||||
from django_tables2.export.views import ExportMixin
|
from django_tables2.export.views import ExportMixin
|
||||||
|
|
||||||
@ -158,16 +158,14 @@ class InstructorOrVendorReport(
|
|||||||
.get_table_data()
|
.get_table_data()
|
||||||
.values("name")
|
.values("name")
|
||||||
.annotate(
|
.annotate(
|
||||||
instructor_agreement_date=StringAgg(
|
instructor_agreement_date=GroupConcat(
|
||||||
Cast("instructor_agreement_date", models.TextField()),
|
"instructor_agreement_date", distinct=True, ordering="asc"
|
||||||
delimiter=", ",
|
|
||||||
distinct=True,
|
|
||||||
),
|
),
|
||||||
w9_date=StringAgg(
|
w9_date=GroupConcat("w9_date", distinct=True, ordering="asc"),
|
||||||
Cast("w9_date", models.TextField()), ", ", distinct=True
|
phone=GroupConcat("phone", distinct=True, ordering="asc"),
|
||||||
|
email_address=GroupConcat(
|
||||||
|
"email_address", distinct=True, ordering="asc"
|
||||||
),
|
),
|
||||||
phone=StringAgg("phone", ", ", distinct=True),
|
|
||||||
email_address=StringAgg("email_address", ", ", distinct=True),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
222
pdm.lock
222
pdm.lock
@ -5,7 +5,7 @@
|
|||||||
groups = ["default", "debug", "dev", "lint", "server", "typing"]
|
groups = ["default", "debug", "dev", "lint", "server", "typing"]
|
||||||
strategy = ["inherit_metadata"]
|
strategy = ["inherit_metadata"]
|
||||||
lock_version = "4.5.0"
|
lock_version = "4.5.0"
|
||||||
content_hash = "sha256:5d6778ee41d2095320769ec21bd878f60d2bafbdcf9bc24ab484929311118978"
|
content_hash = "sha256:fbe86ed2e7a1ce164ed8c00ecc3c51fa6a2b8a14209f3c60663fcc12d0367444"
|
||||||
|
|
||||||
[[metadata.targets]]
|
[[metadata.targets]]
|
||||||
requires_python = "==3.11.*"
|
requires_python = "==3.11.*"
|
||||||
@ -192,7 +192,7 @@ name = "cachetools"
|
|||||||
version = "5.4.0"
|
version = "5.4.0"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "Extensible memoizing collections and decorators"
|
summary = "Extensible memoizing collections and decorators"
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"},
|
{file = "cachetools-5.4.0-py3-none-any.whl", hash = "sha256:3ae3b49a3d5e28a77a0be2b37dbcb89005058959cb2323858c2657c4a8cab474"},
|
||||||
@ -204,7 +204,7 @@ name = "certifi"
|
|||||||
version = "2024.7.4"
|
version = "2024.7.4"
|
||||||
requires_python = ">=3.6"
|
requires_python = ">=3.6"
|
||||||
summary = "Python package for providing Mozilla's CA Bundle."
|
summary = "Python package for providing Mozilla's CA Bundle."
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev", "typing"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
||||||
@ -231,7 +231,7 @@ name = "charset-normalizer"
|
|||||||
version = "3.3.2"
|
version = "3.3.2"
|
||||||
requires_python = ">=3.7.0"
|
requires_python = ">=3.7.0"
|
||||||
summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev", "typing"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
|
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
|
||||||
@ -561,6 +561,21 @@ files = [
|
|||||||
{file = "django_model_utils-4.5.1.tar.gz", hash = "sha256:1220f22d9a467d53a1e0f4cda4857df0b2f757edf9a29955c42461988caa648a"},
|
{file = "django_model_utils-4.5.1.tar.gz", hash = "sha256:1220f22d9a467d53a1e0f4cda4857df0b2f757edf9a29955c42461988caa648a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-mysql"
|
||||||
|
version = "4.14.0"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Django-MySQL extends Django's built-in MySQL and MariaDB support their specific features not available on other databases."
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
dependencies = [
|
||||||
|
"django>=3.2",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "django_mysql-4.14.0-py3-none-any.whl", hash = "sha256:c8ae4b8004bd2e1b74999f0254d255771043913273216a8514cf09aa4bd937bb"},
|
||||||
|
{file = "django_mysql-4.14.0.tar.gz", hash = "sha256:77cb615afb8f2a92636617d46dbe11b97b28e2b97d8373cf7752c3e1f2c619f1"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-nh3"
|
name = "django-nh3"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@ -603,21 +618,6 @@ files = [
|
|||||||
{file = "django_picklefield-3.2-py3-none-any.whl", hash = "sha256:e9a73539d110f69825d9320db18bcb82e5189ff48dbed41821c026a20497764c"},
|
{file = "django_picklefield-3.2-py3-none-any.whl", hash = "sha256:e9a73539d110f69825d9320db18bcb82e5189ff48dbed41821c026a20497764c"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "django-postgres-metrics"
|
|
||||||
version = "0.15.0"
|
|
||||||
requires_python = ">=3.7"
|
|
||||||
summary = "A Django app that exposes a bunch of PostgreSQL database metrics."
|
|
||||||
groups = ["default"]
|
|
||||||
marker = "python_version == \"3.11\""
|
|
||||||
dependencies = [
|
|
||||||
"django-rich>=1.0",
|
|
||||||
]
|
|
||||||
files = [
|
|
||||||
{file = "django-postgres-metrics-0.15.0.tar.gz", hash = "sha256:98127ef55cf9d435b4dd0fdf1d160ea5b97690197d4d8e6148fc2cc2bb57ae12"},
|
|
||||||
{file = "django_postgres_metrics-0.15.0-py3-none-any.whl", hash = "sha256:1710d44e910b230e3b7c7ce77304d68a40b1fa3711b18418387455b8e114d97a"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-q2"
|
name = "django-q2"
|
||||||
version = "1.6.2"
|
version = "1.6.2"
|
||||||
@ -651,22 +651,6 @@ files = [
|
|||||||
{file = "django_recurrence-1.11.1-py3-none-any.whl", hash = "sha256:0c65f30872599b5813a9bab6952dada23c55894f28674490a753ada559f14bc5"},
|
{file = "django_recurrence-1.11.1-py3-none-any.whl", hash = "sha256:0c65f30872599b5813a9bab6952dada23c55894f28674490a753ada559f14bc5"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "django-rich"
|
|
||||||
version = "1.11.0"
|
|
||||||
requires_python = ">=3.8"
|
|
||||||
summary = "Extensions for using Rich with Django."
|
|
||||||
groups = ["default"]
|
|
||||||
marker = "python_version == \"3.11\""
|
|
||||||
dependencies = [
|
|
||||||
"django>=3.2",
|
|
||||||
"rich>=10",
|
|
||||||
]
|
|
||||||
files = [
|
|
||||||
{file = "django_rich-1.11.0-py3-none-any.whl", hash = "sha256:7e25f97c3b072df96f58bb76bdc0a7c40001315c3644da33a390efc95cd6cbdc"},
|
|
||||||
{file = "django_rich-1.11.0.tar.gz", hash = "sha256:d07fa5c57921240673b84f8fe57940e6ade034c1f7b481517e72bec524a0e126"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-sendfile2"
|
name = "django-sendfile2"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@ -681,21 +665,6 @@ files = [
|
|||||||
{file = "django_sendfile2-0.7.1-py3-none-any.whl", hash = "sha256:81971df1db77f688c4834b8540930902d0d929c64cf374bd604b81f5ac52c04a"},
|
{file = "django_sendfile2-0.7.1-py3-none-any.whl", hash = "sha256:81971df1db77f688c4834b8540930902d0d929c64cf374bd604b81f5ac52c04a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "django-simple-history"
|
|
||||||
version = "3.7.0"
|
|
||||||
requires_python = ">=3.8"
|
|
||||||
summary = "Store model history and view/revert changes from admin site."
|
|
||||||
groups = ["default"]
|
|
||||||
marker = "python_version == \"3.11\""
|
|
||||||
dependencies = [
|
|
||||||
"django>=4.2",
|
|
||||||
]
|
|
||||||
files = [
|
|
||||||
{file = "django_simple_history-3.7.0-py3-none-any.whl", hash = "sha256:282cb2c4aa63f51547f17da7f2130abaa81ba01694676d19b88d52c94a57a52c"},
|
|
||||||
{file = "django_simple_history-3.7.0.tar.gz", hash = "sha256:ac3b7ca8b0d33f7ea6be8fe7fc98cf43415efa500ff5dfe736fbd1ebc0cf39f9"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-stubs"
|
name = "django-stubs"
|
||||||
version = "5.0.2"
|
version = "5.0.2"
|
||||||
@ -1002,7 +971,7 @@ name = "google-api-core"
|
|||||||
version = "2.19.1"
|
version = "2.19.1"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "Google API client core library"
|
summary = "Google API client core library"
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"google-auth<3.0.dev0,>=2.14.1",
|
"google-auth<3.0.dev0,>=2.14.1",
|
||||||
@ -1018,10 +987,10 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "google-api-python-client"
|
name = "google-api-python-client"
|
||||||
version = "2.142.0"
|
version = "2.141.0"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "Google API Client Library for Python"
|
summary = "Google API Client Library for Python"
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"google-api-core!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0.dev0,>=1.31.5",
|
"google-api-core!=2.0.*,!=2.1.*,!=2.2.*,!=2.3.0,<3.0.0.dev0,>=1.31.5",
|
||||||
@ -1031,8 +1000,8 @@ dependencies = [
|
|||||||
"uritemplate<5,>=3.0.1",
|
"uritemplate<5,>=3.0.1",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "google_api_python_client-2.142.0-py2.py3-none-any.whl", hash = "sha256:266799082bb8301f423ec204dffbffb470b502abbf29efd1f83e644d36eb5a8f"},
|
{file = "google_api_python_client-2.141.0-py2.py3-none-any.whl", hash = "sha256:43c05322b91791204465291b3852718fae38d4f84b411d8be847c4f86882652a"},
|
||||||
{file = "google_api_python_client-2.142.0.tar.gz", hash = "sha256:a1101ac9e24356557ca22f07ff48b7f61fa5d4b4e7feeef3bda16e5dcb86350e"},
|
{file = "google_api_python_client-2.141.0.tar.gz", hash = "sha256:0f225b1f45d5a6f8c2a400f48729f5d6da9a81138e81e0478d61fdd8edf6563a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1040,7 +1009,7 @@ name = "google-api-python-client-stubs"
|
|||||||
version = "1.27.0"
|
version = "1.27.0"
|
||||||
requires_python = "<4.0,>=3.7"
|
requires_python = "<4.0,>=3.7"
|
||||||
summary = "Type stubs for google-api-python-client"
|
summary = "Type stubs for google-api-python-client"
|
||||||
groups = ["typing"]
|
groups = ["dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"google-api-python-client>=2.141.0",
|
"google-api-python-client>=2.141.0",
|
||||||
@ -1057,7 +1026,7 @@ name = "google-auth"
|
|||||||
version = "2.32.0"
|
version = "2.32.0"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "Google Authentication Library"
|
summary = "Google Authentication Library"
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cachetools<6.0,>=2.0.0",
|
"cachetools<6.0,>=2.0.0",
|
||||||
@ -1073,7 +1042,7 @@ files = [
|
|||||||
name = "google-auth-httplib2"
|
name = "google-auth-httplib2"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
summary = "Google Authentication Library: httplib2 transport"
|
summary = "Google Authentication Library: httplib2 transport"
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"google-auth",
|
"google-auth",
|
||||||
@ -1105,7 +1074,7 @@ name = "googleapis-common-protos"
|
|||||||
version = "1.63.2"
|
version = "1.63.2"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "Common protobufs used in Google APIs"
|
summary = "Common protobufs used in Google APIs"
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"protobuf!=3.20.0,!=3.20.1,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.20.2",
|
"protobuf!=3.20.0,!=3.20.1,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<6.0.0.dev0,>=3.20.2",
|
||||||
@ -1175,7 +1144,7 @@ name = "httplib2"
|
|||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
summary = "A comprehensive HTTP client library."
|
summary = "A comprehensive HTTP client library."
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyparsing!=3.0.0,!=3.0.1,!=3.0.2,!=3.0.3,<4,>=2.4.2; python_version > \"3.0\"",
|
"pyparsing!=3.0.0,!=3.0.1,!=3.0.2,!=3.0.3,<4,>=2.4.2; python_version > \"3.0\"",
|
||||||
@ -1237,7 +1206,7 @@ name = "idna"
|
|||||||
version = "3.7"
|
version = "3.7"
|
||||||
requires_python = ">=3.5"
|
requires_python = ">=3.5"
|
||||||
summary = "Internationalized Domain Names in Applications (IDNA)"
|
summary = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
groups = ["default", "server", "typing"]
|
groups = ["default", "dev", "server", "typing"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||||
@ -1470,6 +1439,17 @@ files = [
|
|||||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mysqlclient"
|
||||||
|
version = "2.2.4"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Python interface to MySQL"
|
||||||
|
groups = ["default"]
|
||||||
|
marker = "python_version == \"3.11\""
|
||||||
|
files = [
|
||||||
|
{file = "mysqlclient-2.2.4.tar.gz", hash = "sha256:33bc9fb3464e7d7c10b1eaf7336c5ff8f2a3d3b88bab432116ad2490beb3bf41"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nh3"
|
name = "nh3"
|
||||||
version = "0.2.18"
|
version = "0.2.18"
|
||||||
@ -1621,7 +1601,7 @@ name = "proto-plus"
|
|||||||
version = "1.24.0"
|
version = "1.24.0"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "Beautiful, Pythonic protocol buffers."
|
summary = "Beautiful, Pythonic protocol buffers."
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"protobuf<6.0.0dev,>=3.19.0",
|
"protobuf<6.0.0dev,>=3.19.0",
|
||||||
@ -1636,7 +1616,7 @@ name = "protobuf"
|
|||||||
version = "5.27.3"
|
version = "5.27.3"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = ""
|
summary = ""
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "protobuf-5.27.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:a55c48f2a2092d8e213bd143474df33a6ae751b781dd1d1f4d953c128a415b25"},
|
{file = "protobuf-5.27.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:a55c48f2a2092d8e213bd143474df33a6ae751b781dd1d1f4d953c128a415b25"},
|
||||||
@ -1644,67 +1624,6 @@ files = [
|
|||||||
{file = "protobuf-5.27.3.tar.gz", hash = "sha256:82460903e640f2b7e34ee81a947fdaad89de796d324bcbc38ff5430bcdead82c"},
|
{file = "protobuf-5.27.3.tar.gz", hash = "sha256:82460903e640f2b7e34ee81a947fdaad89de796d324bcbc38ff5430bcdead82c"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "psycopg"
|
|
||||||
version = "3.2.1"
|
|
||||||
requires_python = ">=3.8"
|
|
||||||
summary = "PostgreSQL database adapter for Python"
|
|
||||||
groups = ["default"]
|
|
||||||
marker = "python_version == \"3.11\""
|
|
||||||
dependencies = [
|
|
||||||
"backports-zoneinfo>=0.2.0; python_version < \"3.9\"",
|
|
||||||
"typing-extensions>=4.4",
|
|
||||||
"tzdata; sys_platform == \"win32\"",
|
|
||||||
]
|
|
||||||
files = [
|
|
||||||
{file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"},
|
|
||||||
{file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "psycopg-binary"
|
|
||||||
version = "3.2.1"
|
|
||||||
requires_python = ">=3.8"
|
|
||||||
summary = "PostgreSQL database adapter for Python -- C optimisation distribution"
|
|
||||||
groups = ["default"]
|
|
||||||
marker = "implementation_name != \"pypy\" and python_version == \"3.11\""
|
|
||||||
files = [
|
|
||||||
{file = "psycopg_binary-3.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa3931f308ab4a479d0ee22dc04bea867a6365cac0172e5ddcba359da043854b"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "psycopg-pool"
|
|
||||||
version = "3.2.2"
|
|
||||||
requires_python = ">=3.8"
|
|
||||||
summary = "Connection Pool for Psycopg"
|
|
||||||
groups = ["default"]
|
|
||||||
marker = "python_version == \"3.11\""
|
|
||||||
dependencies = [
|
|
||||||
"typing-extensions>=4.4",
|
|
||||||
]
|
|
||||||
files = [
|
|
||||||
{file = "psycopg_pool-3.2.2-py3-none-any.whl", hash = "sha256:273081d0fbfaced4f35e69200c89cb8fbddfe277c38cc86c235b90a2ec2c8153"},
|
|
||||||
{file = "psycopg_pool-3.2.2.tar.gz", hash = "sha256:9e22c370045f6d7f2666a5ad1b0caf345f9f1912195b0b25d0d3bcc4f3a7389c"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "psycopg"
|
|
||||||
version = "3.2.1"
|
|
||||||
extras = ["binary", "pool"]
|
|
||||||
requires_python = ">=3.8"
|
|
||||||
summary = "PostgreSQL database adapter for Python"
|
|
||||||
groups = ["default"]
|
|
||||||
marker = "python_version == \"3.11\""
|
|
||||||
dependencies = [
|
|
||||||
"psycopg-binary==3.2.1; implementation_name != \"pypy\"",
|
|
||||||
"psycopg-pool",
|
|
||||||
"psycopg==3.2.1",
|
|
||||||
]
|
|
||||||
files = [
|
|
||||||
{file = "psycopg-3.2.1-py3-none-any.whl", hash = "sha256:ece385fb413a37db332f97c49208b36cf030ff02b199d7635ed2fbd378724175"},
|
|
||||||
{file = "psycopg-3.2.1.tar.gz", hash = "sha256:dc8da6dc8729dacacda3cc2f17d2c9397a70a66cf0d2b69c91065d60d5f00cb7"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ptyprocess"
|
name = "ptyprocess"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@ -1732,7 +1651,7 @@ name = "pyasn1"
|
|||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
|
summary = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"},
|
{file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"},
|
||||||
@ -1744,7 +1663,7 @@ name = "pyasn1-modules"
|
|||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "A collection of ASN.1-based protocols modules"
|
summary = "A collection of ASN.1-based protocols modules"
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyasn1<0.7.0,>=0.4.6",
|
"pyasn1<0.7.0,>=0.4.6",
|
||||||
@ -1783,7 +1702,7 @@ name = "pygments"
|
|||||||
version = "2.18.0"
|
version = "2.18.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Pygments is a syntax highlighting package written in Python."
|
summary = "Pygments is a syntax highlighting package written in Python."
|
||||||
groups = ["default", "dev"]
|
groups = ["dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
|
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
|
||||||
@ -1795,7 +1714,7 @@ name = "pyparsing"
|
|||||||
version = "3.1.2"
|
version = "3.1.2"
|
||||||
requires_python = ">=3.6.8"
|
requires_python = ">=3.6.8"
|
||||||
summary = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
summary = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"},
|
{file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"},
|
||||||
@ -1885,7 +1804,7 @@ name = "requests"
|
|||||||
version = "2.32.3"
|
version = "2.32.3"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Python HTTP for Humans."
|
summary = "Python HTTP for Humans."
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev", "typing"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"certifi>=2017.4.17",
|
"certifi>=2017.4.17",
|
||||||
@ -1914,29 +1833,12 @@ files = [
|
|||||||
{file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"},
|
{file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rich"
|
|
||||||
version = "13.8.0"
|
|
||||||
requires_python = ">=3.7.0"
|
|
||||||
summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
|
||||||
groups = ["default"]
|
|
||||||
marker = "python_version == \"3.11\""
|
|
||||||
dependencies = [
|
|
||||||
"markdown-it-py>=2.2.0",
|
|
||||||
"pygments<3.0.0,>=2.13.0",
|
|
||||||
"typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"",
|
|
||||||
]
|
|
||||||
files = [
|
|
||||||
{file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"},
|
|
||||||
{file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "4.9"
|
version = "4.9"
|
||||||
requires_python = ">=3.6,<4"
|
requires_python = ">=3.6,<4"
|
||||||
summary = "Pure-Python RSA implementation"
|
summary = "Pure-Python RSA implementation"
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pyasn1>=0.1.3",
|
"pyasn1>=0.1.3",
|
||||||
@ -1972,14 +1874,14 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "setuptools"
|
name = "setuptools"
|
||||||
version = "73.0.1"
|
version = "72.2.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Easily download, build, install, upgrade, and uninstall Python packages"
|
summary = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||||
groups = ["server", "typing"]
|
groups = ["server", "typing"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "setuptools-73.0.1-py3-none-any.whl", hash = "sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e"},
|
{file = "setuptools-72.2.0-py3-none-any.whl", hash = "sha256:f11dd94b7bae3a156a95ec151f24e4637fb4fa19c878e4d191bfb8b2d82728c4"},
|
||||||
{file = "setuptools-73.0.1.tar.gz", hash = "sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193"},
|
{file = "setuptools-72.2.0.tar.gz", hash = "sha256:80aacbf633704e9c8bfa1d99fa5dd4dc59573efcf9e4042c13d3bcef91ac2ef9"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2200,7 +2102,7 @@ name = "types-httplib2"
|
|||||||
version = "0.22.0.20240310"
|
version = "0.22.0.20240310"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Typing stubs for httplib2"
|
summary = "Typing stubs for httplib2"
|
||||||
groups = ["typing"]
|
groups = ["dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "types-httplib2-0.22.0.20240310.tar.gz", hash = "sha256:1eda99fea18ec8a1dc1a725ead35b889d0836fec1b11ae6f1fe05440724c1d15"},
|
{file = "types-httplib2-0.22.0.20240310.tar.gz", hash = "sha256:1eda99fea18ec8a1dc1a725ead35b889d0836fec1b11ae6f1fe05440724c1d15"},
|
||||||
@ -2238,14 +2140,14 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-psycopg2"
|
name = "types-psycopg2"
|
||||||
version = "2.9.21.20240819"
|
version = "2.9.21.20240417"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Typing stubs for psycopg2"
|
summary = "Typing stubs for psycopg2"
|
||||||
groups = ["typing"]
|
groups = ["typing"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "types-psycopg2-2.9.21.20240819.tar.gz", hash = "sha256:4ed6b47464d6374fa64e5e3b234cea0f710e72123a4596d67ab50b7415a84666"},
|
{file = "types-psycopg2-2.9.21.20240417.tar.gz", hash = "sha256:05db256f4a459fb21a426b8e7fca0656c3539105ff0208eaf6bdaf406a387087"},
|
||||||
{file = "types_psycopg2-2.9.21.20240819-py3-none-any.whl", hash = "sha256:c9192311c27d7ad561eef705f1b2df1074f2cdcf445a98a6a2fcaaaad43278cf"},
|
{file = "types_psycopg2-2.9.21.20240417-py3-none-any.whl", hash = "sha256:644d6644d64ebbe37203229b00771012fb3b3bddd507a129a2e136485990e4f8"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2266,14 +2168,14 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-python-dateutil"
|
name = "types-python-dateutil"
|
||||||
version = "2.9.0.20240821"
|
version = "2.9.0.20240316"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Typing stubs for python-dateutil"
|
summary = "Typing stubs for python-dateutil"
|
||||||
groups = ["typing"]
|
groups = ["dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "types-python-dateutil-2.9.0.20240821.tar.gz", hash = "sha256:9649d1dcb6fef1046fb18bebe9ea2aa0028b160918518c34589a46045f6ebd98"},
|
{file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"},
|
||||||
{file = "types_python_dateutil-2.9.0.20240821-py3-none-any.whl", hash = "sha256:f5889fcb4e63ed4aaa379b44f93c32593d50b9a94c9a60a0c854d8cc3511cd57"},
|
{file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2362,7 +2264,7 @@ name = "uritemplate"
|
|||||||
version = "4.1.1"
|
version = "4.1.1"
|
||||||
requires_python = ">=3.6"
|
requires_python = ">=3.6"
|
||||||
summary = "Implementation of RFC 6570 URI Templates"
|
summary = "Implementation of RFC 6570 URI Templates"
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"},
|
{file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"},
|
||||||
@ -2374,7 +2276,7 @@ name = "urllib3"
|
|||||||
version = "2.2.2"
|
version = "2.2.2"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "HTTP library with thread-safe connection pooling, file post, and more."
|
summary = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
groups = ["default", "typing"]
|
groups = ["default", "dev", "typing"]
|
||||||
marker = "python_version == \"3.11\""
|
marker = "python_version == \"3.11\""
|
||||||
files = [
|
files = [
|
||||||
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
|
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
|
||||||
|
@ -16,6 +16,7 @@ dependencies = [
|
|||||||
"markdownify~=0.13",
|
"markdownify~=0.13",
|
||||||
"mdformat~=0.7",
|
"mdformat~=0.7",
|
||||||
"mdformat-tables~=0.4",
|
"mdformat-tables~=0.4",
|
||||||
|
"mysqlclient~=2.2",
|
||||||
"django-autocomplete-light~=3.11",
|
"django-autocomplete-light~=3.11",
|
||||||
"weasyprint~=62.3",
|
"weasyprint~=62.3",
|
||||||
"requests~=2.32",
|
"requests~=2.32",
|
||||||
@ -33,25 +34,23 @@ dependencies = [
|
|||||||
"tablib[ods,xlsx]~=3.6",
|
"tablib[ods,xlsx]~=3.6",
|
||||||
"django-filter~=24.3",
|
"django-filter~=24.3",
|
||||||
"django-db-views~=0.1",
|
"django-db-views~=0.1",
|
||||||
|
"django-mysql~=4.14",
|
||||||
"django-weasyprint~=2.3",
|
"django-weasyprint~=2.3",
|
||||||
"django-sendfile2~=0.7",
|
"django-sendfile2~=0.7",
|
||||||
"django-bootstrap5~=24.2",
|
"django-bootstrap5~=24.2",
|
||||||
"django-configurations[database,email]~=2.5",
|
"django-configurations[database,email]~=2.5",
|
||||||
"django-vite~=3.0",
|
"django-vite~=3.0",
|
||||||
"django-template-partials~=24.4",
|
"django-template-partials~=24.4",
|
||||||
"google-api-python-client~=2.142",
|
"google-api-python-client~=2.141",
|
||||||
"google-auth-oauthlib~=1.2",
|
"google-auth-oauthlib~=1.2",
|
||||||
"django-model-utils~=4.5",
|
"django-model-utils~=4.5",
|
||||||
"psycopg[binary,pool]~=3.2",
|
|
||||||
"django-simple-history~=3.7",
|
|
||||||
"django-postgres-metrics~=0.15",
|
|
||||||
]
|
]
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
server = [
|
server = [
|
||||||
"uvicorn[standard]~=0.30",
|
"uvicorn[standard]~=0.30",
|
||||||
"setuptools~=73.0",
|
"setuptools~=72.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.entry-points."djangoq.errorreporters"]
|
[project.entry-points."djangoq.errorreporters"]
|
||||||
@ -151,7 +150,7 @@ lint = [
|
|||||||
typing = [
|
typing = [
|
||||||
"mypy~=1.10",
|
"mypy~=1.10",
|
||||||
"django-stubs~=5.0",
|
"django-stubs~=5.0",
|
||||||
"setuptools~=73.0",
|
"setuptools~=72.2",
|
||||||
"types-bleach~=6.1",
|
"types-bleach~=6.1",
|
||||||
"types-requests~=2.32",
|
"types-requests~=2.32",
|
||||||
"types-urllib3~=1.26",
|
"types-urllib3~=1.26",
|
||||||
@ -160,8 +159,6 @@ typing = [
|
|||||||
"types-Pygments~=2.18",
|
"types-Pygments~=2.18",
|
||||||
"types-psycopg2~=2.9",
|
"types-psycopg2~=2.9",
|
||||||
"types-lxml~=2024.8",
|
"types-lxml~=2024.8",
|
||||||
"google-api-python-client-stubs~=1.27",
|
|
||||||
"types-python-dateutil~=2.9",
|
|
||||||
]
|
]
|
||||||
debug = [
|
debug = [
|
||||||
"django-debug-toolbar~=4.4",
|
"django-debug-toolbar~=4.4",
|
||||||
@ -171,6 +168,8 @@ dev = [
|
|||||||
"ipython~=8.26",
|
"ipython~=8.26",
|
||||||
"hypothesis[django]~=6.111",
|
"hypothesis[django]~=6.111",
|
||||||
"tblib~=3.0",
|
"tblib~=3.0",
|
||||||
|
"google-api-python-client-stubs~=1.27",
|
||||||
|
"types-python-dateutil~=2.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pdm.scripts]
|
[tool.pdm.scripts]
|
||||||
|
@ -65,7 +65,7 @@ class Migration(migrations.Migration):
|
|||||||
(
|
(
|
||||||
"duration",
|
"duration",
|
||||||
models.GeneratedField(
|
models.GeneratedField(
|
||||||
db_persist=True,
|
db_persist=False,
|
||||||
expression=django.db.models.expressions.CombinedExpression(
|
expression=django.db.models.expressions.CombinedExpression(
|
||||||
models.F("end"), "-", models.F("start")
|
models.F("end"), "-", models.F("start")
|
||||||
),
|
),
|
||||||
|
@ -87,7 +87,7 @@ class Reservation(models.Model):
|
|||||||
duration = models.GeneratedField(
|
duration = models.GeneratedField(
|
||||||
expression=F("end") - F("start"),
|
expression=F("end") - F("start"),
|
||||||
output_field=models.DurationField(),
|
output_field=models.DurationField(),
|
||||||
db_persist=True,
|
db_persist=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
objects = ReservationQuerySet.as_manager()
|
objects = ReservationQuerySet.as_manager()
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
@ -10,9 +9,6 @@ from google.oauth2 import service_account
|
|||||||
from googleapiclient.discovery import build
|
from googleapiclient.discovery import build
|
||||||
from googleapiclient.errors import HttpError
|
from googleapiclient.errors import HttpError
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from googleapiclient._apis.calendar.v3 import CalendarResource, Event
|
|
||||||
|
|
||||||
from cmsmanage.django_q2_helper import q_task_group
|
from cmsmanage.django_q2_helper import q_task_group
|
||||||
from reservations.models import ExternalReservation, Reservation, Resource
|
from reservations.models import ExternalReservation, Reservation, Resource
|
||||||
|
|
||||||
@ -30,22 +26,9 @@ def parse_google_calendar_datetime(dt) -> date | datetime:
|
|||||||
raise Exception("Google Calendar event with out a start/end date/dateTime")
|
raise Exception("Google Calendar event with out a start/end date/dateTime")
|
||||||
|
|
||||||
|
|
||||||
class GoogleCalendarSynchronizer:
|
def update_calendar_event(
|
||||||
service: "CalendarResource"
|
service, resource: Resource, existing_event, reservation: Reservation
|
||||||
|
):
|
||||||
def __init__(self) -> None:
|
|
||||||
self.service = build(
|
|
||||||
"calendar",
|
|
||||||
"v3",
|
|
||||||
credentials=service_account.Credentials.from_service_account_file(
|
|
||||||
settings.GOOGLE_SERVICE_ACCOUNT_FILE,
|
|
||||||
scopes=SCOPES,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_calendar_event(
|
|
||||||
self, resource: Resource, existing_event: "Event", reservation: Reservation
|
|
||||||
):
|
|
||||||
changes = reservation.make_google_calendar_event()
|
changes = reservation.make_google_calendar_event()
|
||||||
# skip update if no changes are needed
|
# skip update if no changes are needed
|
||||||
if (
|
if (
|
||||||
@ -59,16 +42,17 @@ class GoogleCalendarSynchronizer:
|
|||||||
):
|
):
|
||||||
logger.debug("Updating event")
|
logger.debug("Updating event")
|
||||||
new_event = existing_event | changes
|
new_event = existing_event | changes
|
||||||
self.service.events().update(
|
service.events().update(
|
||||||
calendarId=resource.google_calendar,
|
calendarId=resource.google_calendar,
|
||||||
eventId=reservation.google_calendar_event_id,
|
eventId=reservation.google_calendar_event_id,
|
||||||
body=new_event,
|
body=new_event,
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
def insert_calendar_event(self, resource: Resource, reservation: Reservation):
|
|
||||||
|
def insert_calendar_event(service, resource: Resource, reservation: Reservation):
|
||||||
new_gcal_event = reservation.make_google_calendar_event()
|
new_gcal_event = reservation.make_google_calendar_event()
|
||||||
created_event = (
|
created_event = (
|
||||||
self.service.events()
|
service.events()
|
||||||
.insert(
|
.insert(
|
||||||
calendarId=resource.google_calendar,
|
calendarId=resource.google_calendar,
|
||||||
body=new_gcal_event,
|
body=new_gcal_event,
|
||||||
@ -78,49 +62,12 @@ class GoogleCalendarSynchronizer:
|
|||||||
reservation.google_calendar_event_id = created_event["id"]
|
reservation.google_calendar_event_id = created_event["id"]
|
||||||
reservation.save()
|
reservation.save()
|
||||||
|
|
||||||
def insert_or_update_calendar_event(
|
|
||||||
self, resource: Resource, reservation: Reservation
|
|
||||||
):
|
|
||||||
if not reservation.google_calendar_event_id:
|
|
||||||
logger.info(
|
|
||||||
"Event in database has no Google Calendar event ID: inserting | %s",
|
|
||||||
reservation.google_calendar_event_id,
|
|
||||||
)
|
|
||||||
self.insert_calendar_event(resource, reservation)
|
|
||||||
|
|
||||||
else:
|
def sync_resource_from_google_calendar(
|
||||||
# this event was in Google Calendar at some point (possibly for a different
|
service, resource: Resource, now: datetime
|
||||||
# resource/calendar), but did not appear in list(). Try to update it, then
|
) -> set[str]:
|
||||||
# fall back to insert
|
|
||||||
logger.info(
|
|
||||||
"Reservation with event id not in Google Calendar: trying update | %s",
|
|
||||||
reservation.google_calendar_event_id,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
event = (
|
|
||||||
self.service.events()
|
|
||||||
.get(
|
|
||||||
calendarId=resource.google_calendar,
|
|
||||||
eventId=reservation.google_calendar_event_id,
|
|
||||||
)
|
|
||||||
.execute()
|
|
||||||
)
|
|
||||||
self.update_calendar_event(resource, event, reservation)
|
|
||||||
except HttpError as error:
|
|
||||||
if error.status_code == HTTPStatus.NOT_FOUND:
|
|
||||||
logger.info(
|
|
||||||
"Event in database not in Google Calendar: inserting | %s",
|
|
||||||
reservation.google_calendar_event_id,
|
|
||||||
)
|
|
||||||
self.insert_calendar_event(resource, reservation)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def sync_resource_from_google_calendar(
|
|
||||||
self, resource: Resource, now: datetime
|
|
||||||
) -> set[str]:
|
|
||||||
request = (
|
request = (
|
||||||
self.service.events()
|
service.events()
|
||||||
.list(
|
.list(
|
||||||
calendarId=resource.google_calendar,
|
calendarId=resource.google_calendar,
|
||||||
timeMin=now.isoformat(timespec="seconds"),
|
timeMin=now.isoformat(timespec="seconds"),
|
||||||
@ -150,14 +97,14 @@ class GoogleCalendarSynchronizer:
|
|||||||
"Event in Google Calendar found in database: checking for update | %s",
|
"Event in Google Calendar found in database: checking for update | %s",
|
||||||
event["id"],
|
event["id"],
|
||||||
)
|
)
|
||||||
self.update_calendar_event(resource, event, reservation)
|
update_calendar_event(service, resource, event, reservation)
|
||||||
except Reservation.DoesNotExist:
|
except Reservation.DoesNotExist:
|
||||||
# reservation deleted in database, so remove from Google Calendar
|
# reservation deleted in database, so remove from Google Calendar
|
||||||
logger.info(
|
logger.info(
|
||||||
"Event in Google Calendar not found in database: deleting | %s",
|
"Event in Google Calendar not found in database: deleting | %s",
|
||||||
event["id"],
|
event["id"],
|
||||||
)
|
)
|
||||||
self.service.events().delete(
|
service.events().delete(
|
||||||
calendarId=resource.google_calendar,
|
calendarId=resource.google_calendar,
|
||||||
eventId=event["id"],
|
eventId=event["id"],
|
||||||
sendUpdates="none",
|
sendUpdates="none",
|
||||||
@ -181,44 +128,78 @@ class GoogleCalendarSynchronizer:
|
|||||||
|
|
||||||
return {event["id"] for event in events}
|
return {event["id"] for event in events}
|
||||||
|
|
||||||
def sync_reservation_from_database(self, reservation: Reservation):
|
|
||||||
for resource in reservation.resources.all():
|
|
||||||
self.insert_or_update_calendar_event(resource, reservation)
|
|
||||||
|
|
||||||
def sync_resource_from_database(
|
def sync_resource_from_database(
|
||||||
self, resource: Resource, now: datetime, existing_event_ids: set[str]
|
service, resource: Resource, now: datetime, existing_event_ids: set[str]
|
||||||
):
|
):
|
||||||
reservations = (
|
reservations = resource.reservation_set.filter(end__gt=now).select_subclasses()
|
||||||
resource.reservation_set.filter(end__gt=now)
|
|
||||||
# skip events we already pulled from Google Calendar during this sync
|
|
||||||
.exclude(google_calendar_event_id__in=existing_event_ids)
|
|
||||||
.select_subclasses()
|
|
||||||
)
|
|
||||||
# TODO: this could probably be more efficient?
|
# TODO: this could probably be more efficient?
|
||||||
for reservation in reservations:
|
for reservation in reservations:
|
||||||
|
if not reservation.google_calendar_event_id:
|
||||||
|
logger.info(
|
||||||
|
"Event in database has no Google Calendar event ID: inserting | %s",
|
||||||
|
reservation.google_calendar_event_id,
|
||||||
|
)
|
||||||
|
insert_calendar_event(service, resource, reservation)
|
||||||
|
|
||||||
|
# reservation has an event id, so check if we already handled it earlier
|
||||||
|
elif reservation.google_calendar_event_id not in existing_event_ids:
|
||||||
if isinstance(reservation, ExternalReservation):
|
if isinstance(reservation, ExternalReservation):
|
||||||
logger.info(
|
logger.info(
|
||||||
"External event in database did not exist in future of Google Calendar: deleting locally | %s",
|
"External event in database did not exist in future of Google Calendar: deleting locally | %s",
|
||||||
reservation.google_calendar_event_id,
|
reservation.google_calendar_event_id,
|
||||||
)
|
)
|
||||||
reservation.delete()
|
reservation.delete()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.insert_or_update_calendar_event(resource, reservation)
|
# this event was in Google Calendar at some point (possibly for a different
|
||||||
|
# resource/calendar), but did not appear in list(). Try to update it, then
|
||||||
|
# fall back to insert
|
||||||
|
logger.info(
|
||||||
|
"Reservation with event id not in Google Calendar: trying update | %s",
|
||||||
|
reservation.google_calendar_event_id,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
event = (
|
||||||
|
service.events()
|
||||||
|
.get(
|
||||||
|
calendarId=resource.google_calendar,
|
||||||
|
eventId=reservation.google_calendar_event_id,
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
update_calendar_event(service, resource, event, reservation)
|
||||||
|
except HttpError as error:
|
||||||
|
if error.status_code == HTTPStatus.NOT_FOUND:
|
||||||
|
logger.info(
|
||||||
|
"Event in database not in Google Calendar: inserting | %s",
|
||||||
|
reservation.google_calendar_event_id,
|
||||||
|
)
|
||||||
|
insert_calendar_event(service, resource, reservation)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
def sync_resource(self, resource: Resource, now: datetime):
|
|
||||||
|
def sync_resource(service, resource: Resource, now: datetime):
|
||||||
logger.info(
|
logger.info(
|
||||||
"Checking calendar %s for resource %s", resource.google_calendar, resource
|
"Checking calendar %s for resource %s", resource.google_calendar, resource
|
||||||
)
|
)
|
||||||
|
|
||||||
existing_event_ids = self.sync_resource_from_google_calendar(resource, now)
|
existing_event_ids = sync_resource_from_google_calendar(service, resource, now)
|
||||||
self.sync_resource_from_database(resource, now, existing_event_ids)
|
sync_resource_from_database(service, resource, now, existing_event_ids)
|
||||||
|
|
||||||
|
|
||||||
@q_task_group("Sync Reservations with Google Calendar")
|
@q_task_group("Sync Reservations with Google Calendar")
|
||||||
def sync_reservations_with_google_calendar():
|
def sync_reservations_with_google_calendar():
|
||||||
synchronizer = GoogleCalendarSynchronizer()
|
service = build(
|
||||||
|
"calendar",
|
||||||
|
"v3",
|
||||||
|
credentials=service_account.Credentials.from_service_account_file(
|
||||||
|
settings.GOOGLE_SERVICE_ACCOUNT_FILE,
|
||||||
|
scopes=SCOPES,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
|
|
||||||
for resource in Resource.objects.all():
|
for resource in Resource.objects.all():
|
||||||
synchronizer.sync_resource(resource, now)
|
sync_resource(service, resource, now)
|
||||||
|
Loading…
Reference in New Issue
Block a user