Compare commits
No commits in common. "bfa04be2d9b4c123f176635ca9d59f47b3f26169" and "270e6c78375eabd6f72025b76daa363abd8279cc" have entirely different histories.
bfa04be2d9
...
270e6c7837
@ -1,12 +0,0 @@
|
|||||||
name: Ruff
|
|
||||||
on: [ push, pull_request ]
|
|
||||||
jobs:
|
|
||||||
ruff:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container: catthehacker/ubuntu:act-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: chartboost/ruff-action@v1
|
|
||||||
- uses: chartboost/ruff-action@v1
|
|
||||||
with:
|
|
||||||
args: format --check --diff
|
|
@ -7,13 +7,12 @@ repos:
|
|||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
|
|
||||||
|
- repo: https://github.com/psf/black
|
||||||
|
rev: 23.11.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
|
||||||
- repo: https://github.com/Riverside-Healthcare/djLint
|
- repo: https://github.com/Riverside-Healthcare/djLint
|
||||||
rev: v1.34.1
|
rev: v1.34.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: djlint-django
|
- id: djlint-django
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
||||||
rev: v0.1.13
|
|
||||||
hooks:
|
|
||||||
- id: ruff
|
|
||||||
- id: ruff-format
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from django.core import mail
|
|
||||||
from django.views.debug import ExceptionReporter
|
from django.views.debug import ExceptionReporter
|
||||||
|
from django.core import mail
|
||||||
|
|
||||||
|
|
||||||
class AdminEmailReporter:
|
class AdminEmailReporter:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from .dev_base import * # noqa: F403
|
from .dev_base import *
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from .base import * # noqa: F403
|
from .base import *
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||||
@ -9,7 +9,7 @@ DEBUG = True
|
|||||||
|
|
||||||
INTERNAL_IPS = ["127.0.0.1"]
|
INTERNAL_IPS = ["127.0.0.1"]
|
||||||
|
|
||||||
INSTALLED_APPS.append("debug_toolbar") # noqa: F405
|
INSTALLED_APPS.append("debug_toolbar")
|
||||||
INSTALLED_APPS.append("django_extensions") # noqa: F405
|
INSTALLED_APPS.append("django_extensions")
|
||||||
|
|
||||||
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa: F405
|
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import ldap
|
import ldap
|
||||||
from django_auth_ldap.config import LDAPGroupQuery, LDAPSearch, PosixGroupType
|
from django_auth_ldap.config import LDAPSearch, PosixGroupType, LDAPGroupQuery
|
||||||
|
|
||||||
from .base import * # noqa: F403
|
from .base import *
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
|
@ -13,15 +13,16 @@ Including another URLconf
|
|||||||
1. Import the include() function: from django.urls import include, path
|
1. Import the include() function: from django.urls import include, path
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.views import LoginView, LogoutView
|
from django.shortcuts import redirect
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
|
from django.contrib.auth.views import LoginView, LogoutView
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from membershipworks.api import router as membershipworks_router
|
|
||||||
from paperwork.api import router as paperwork_router
|
from paperwork.api import router as paperwork_router
|
||||||
|
from membershipworks.api import router as membershipworks_router
|
||||||
|
|
||||||
router = routers.DefaultRouter()
|
router = routers.DefaultRouter()
|
||||||
router.registry.extend(paperwork_router.registry)
|
router.registry.extend(paperwork_router.registry)
|
||||||
|
@ -1 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import dashboard
|
import dashboard
|
||||||
|
|
||||||
from .views import REPORTS
|
from .views import REPORTS
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import contextlib
|
|
||||||
import csv
|
import csv
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
@ -51,7 +50,7 @@ class DoorController:
|
|||||||
) # ignore insecure SSL
|
) # ignore insecure SSL
|
||||||
xml = etree.XML(r.content)
|
xml = etree.XML(r.content)
|
||||||
if (
|
if (
|
||||||
r.status_code != requests.codes.ok
|
r.status_code != 200
|
||||||
or len(xml.findall("{http://www.hidglobal.com/VertX}Error")) > 0
|
or len(xml.findall("{http://www.hidglobal.com/VertX}Error")) > 0
|
||||||
):
|
):
|
||||||
raise RemoteError(r)
|
raise RemoteError(r)
|
||||||
@ -76,7 +75,7 @@ class DoorController:
|
|||||||
)
|
)
|
||||||
resp_xml = etree.XML(r.content)
|
resp_xml = etree.XML(r.content)
|
||||||
# probably meed to be more sane about this
|
# probably meed to be more sane about this
|
||||||
if r.status_code != requests.codes.ok or len(resp_xml.findall("{*}Error")) > 0:
|
if r.status_code != 200 or len(resp_xml.findall("{*}Error")) > 0:
|
||||||
raise RemoteError(r)
|
raise RemoteError(r)
|
||||||
return resp_xml
|
return resp_xml
|
||||||
|
|
||||||
@ -121,8 +120,11 @@ class DoorController:
|
|||||||
for ii in range(1, 8)
|
for ii in range(1, 8)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
self.doXMLRequest(delXML)
|
||||||
|
except RemoteError:
|
||||||
# don't care about failure to delete, they probably just didn't exist
|
# don't care about failure to delete, they probably just didn't exist
|
||||||
contextlib.suppress(self.doXMLRequest(delXML))
|
pass
|
||||||
|
|
||||||
# load new schedules
|
# load new schedules
|
||||||
self.doXMLRequest(schedules)
|
self.doXMLRequest(schedules)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 4.2.5 on 2023-09-19 04:20
|
# Generated by Django 4.2.5 on 2023-09-19 04:20
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
def link_events_to_doors(apps, schema_editor):
|
def link_events_to_doors(apps, schema_editor):
|
||||||
|
@ -1 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
@ -2,19 +2,23 @@ import calendar
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.core.exceptions import BadRequest
|
|
||||||
from django.core.paginator import Page
|
from django.core.paginator import Page
|
||||||
from django.db.models import Count, F, FloatField, Window
|
from django.core.exceptions import BadRequest
|
||||||
from django.db.models.functions import Lead, Trunc
|
from django.db.models import Count
|
||||||
from django.urls import path, reverse_lazy
|
from django.db.models.functions import Trunc
|
||||||
|
from django.urls import reverse_lazy, path
|
||||||
from django.utils import dateparse
|
from django.utils import dateparse
|
||||||
from django.utils.formats import date_format
|
from django.utils.formats import date_format
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.timezone import localtime
|
from django.utils.timezone import localtime
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
|
from django.db.models import Window, F, FloatField
|
||||||
|
from django.db.models.functions import Lead
|
||||||
|
|
||||||
|
|
||||||
from .models import HIDEvent
|
from .models import HIDEvent
|
||||||
|
|
||||||
|
|
||||||
REPORTS = []
|
REPORTS = []
|
||||||
|
|
||||||
|
|
||||||
@ -213,7 +217,7 @@ class DeniedAccess(BaseAccessReport):
|
|||||||
"door name": event.door.name,
|
"door name": event.door.name,
|
||||||
"event type": HIDEvent.EventType(event.event_type).label,
|
"event type": HIDEvent.EventType(event.event_type).label,
|
||||||
"name": " ".join(
|
"name": " ".join(
|
||||||
n for n in [event.forename, event.surname] if n is not None
|
(n for n in [event.forename, event.surname] if n is not None)
|
||||||
),
|
),
|
||||||
"raw card number": (
|
"raw card number": (
|
||||||
event.raw_card_number if event.raw_card_number is not None else ""
|
event.raw_card_number if event.raw_card_number is not None else ""
|
||||||
@ -242,7 +246,7 @@ class MostActiveMembers(BaseAccessReport):
|
|||||||
{
|
{
|
||||||
"cardholder id": count["cardholder_id"],
|
"cardholder id": count["cardholder_id"],
|
||||||
"name": " ".join(
|
"name": " ".join(
|
||||||
n for n in [count["forename"], count["surname"]] if n is not None
|
(n for n in [count["forename"], count["surname"]] if n is not None)
|
||||||
),
|
),
|
||||||
"access count": count["access_count"],
|
"access count": count["access_count"],
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
from .membershipworks_api import MembershipWorks, MembershipWorksRemoteError
|
@ -3,17 +3,17 @@ from django.contrib.humanize.templatetags.humanize import naturaltime
|
|||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
|
||||||
from django_object_actions import DjangoObjectActions, action
|
from django_object_actions import DjangoObjectActions, action
|
||||||
from django_q.models import Task
|
|
||||||
from django_q.tasks import async_task
|
from django_q.tasks import async_task
|
||||||
|
from django_q.models import Task
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
|
Member,
|
||||||
|
Flag,
|
||||||
|
Transaction,
|
||||||
Event,
|
Event,
|
||||||
EventExt,
|
EventExt,
|
||||||
EventInstructor,
|
|
||||||
EventMeetingTime,
|
EventMeetingTime,
|
||||||
Flag,
|
EventInstructor,
|
||||||
Member,
|
|
||||||
Transaction,
|
|
||||||
)
|
)
|
||||||
from .tasks.scrape import scrape_membershipworks
|
from .tasks.scrape import scrape_membershipworks
|
||||||
|
|
||||||
@ -36,14 +36,14 @@ class BaseMembershipWorksAdmin(DjangoObjectActions, ReadOnlyAdmin):
|
|||||||
def _get_tool_dict(self, tool_name):
|
def _get_tool_dict(self, tool_name):
|
||||||
tool = super(DjangoObjectActions, self)._get_tool_dict(tool_name)
|
tool = super(DjangoObjectActions, self)._get_tool_dict(tool_name)
|
||||||
if tool_name == "refresh_membershipworks_data":
|
if tool_name == "refresh_membershipworks_data":
|
||||||
try:
|
last_run = (
|
||||||
last_run_time = naturaltime(
|
|
||||||
Task.objects.filter(group="Scrape Data from MembershipWorks")
|
Task.objects.filter(group="Scrape Data from MembershipWorks")
|
||||||
.values_list("started", flat=True)
|
.order_by("started")
|
||||||
.latest("started")
|
.last()
|
||||||
|
)
|
||||||
|
last_run_time = (
|
||||||
|
naturaltime(last_run.started) if last_run is not None else "Never"
|
||||||
)
|
)
|
||||||
except Task.DoesNotExist:
|
|
||||||
last_run_time = "Never"
|
|
||||||
tool["label"] = f"Refresh Data [Last Run {last_run_time}]"
|
tool["label"] = f"Refresh Data [Last Run {last_run_time}]"
|
||||||
return tool
|
return tool
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from rest_framework import routers, serializers, viewsets
|
from rest_framework import routers, serializers, viewsets
|
||||||
|
|
||||||
from .models import Flag, Member
|
from .models import Member, Flag
|
||||||
|
|
||||||
|
|
||||||
class MemberSerializer(serializers.HyperlinkedModelSerializer):
|
class MemberSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
@ -7,7 +7,7 @@ def post_migrate_callback(sender, **kwargs):
|
|||||||
|
|
||||||
from cmsmanage.django_q2_helper import ensure_scheduled
|
from cmsmanage.django_q2_helper import ensure_scheduled
|
||||||
|
|
||||||
from .tasks.scrape import scrape_events, scrape_membershipworks
|
from .tasks.scrape import scrape_membershipworks, scrape_events
|
||||||
from .tasks.ucsAccounts import sync_accounts
|
from .tasks.ucsAccounts import sync_accounts
|
||||||
|
|
||||||
ensure_scheduled(
|
ensure_scheduled(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
@ -15,7 +16,11 @@ class MembershipworksDashboardFragment(dashboard.DashboardFragment):
|
|||||||
links = {}
|
links = {}
|
||||||
|
|
||||||
if self.request.user.has_perm("membershipworks.view_event"):
|
if self.request.user.has_perm("membershipworks.view_event"):
|
||||||
links["Event Report"] = reverse("membershipworks:event-index-report")
|
now = datetime.now()
|
||||||
|
links["Event Report"] = reverse(
|
||||||
|
"membershipworks:event-report",
|
||||||
|
kwargs={"year": now.year, "month": now.month},
|
||||||
|
)
|
||||||
|
|
||||||
return {"links": links}
|
return {"links": links}
|
||||||
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import csv
|
import csv
|
||||||
import datetime
|
|
||||||
from enum import Enum
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import datetime
|
||||||
|
|
||||||
BASE_URL = "https://api.membershipworks.com"
|
BASE_URL = "https://api.membershipworks.com"
|
||||||
|
|
||||||
@ -33,20 +32,18 @@ CRM = {
|
|||||||
27: "Payment",
|
27: "Payment",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Types of fields, extracted from a html snippet in all.js + some guessing
|
||||||
# Types of fields ("typ"), extracted from a html snippet in all.js + some guessing
|
typ = {
|
||||||
class FieldType(Enum):
|
1: "Text input",
|
||||||
TEXT_INPUT = 1
|
2: "Password", # inferred from data
|
||||||
PASSWORD = 2 # inferred from data
|
3: "Simple text area",
|
||||||
SIMPLE_TEXT_AREA = 3
|
4: "Rich text area",
|
||||||
RICH_TEXT_AREA = 4
|
7: "Address",
|
||||||
ADDRESS = 7
|
8: "Check box",
|
||||||
CHECKBOX = 8
|
9: "Select",
|
||||||
SELECT = 9
|
11: "Display value stored in field (ie. read only)",
|
||||||
# Display value stored in field
|
12: "Required waiver/terms",
|
||||||
READ_ONLY = 11
|
}
|
||||||
REQUIRED_WAIVER_TERMS = 12
|
|
||||||
|
|
||||||
|
|
||||||
# more constants, this time extracted from the members csv export in all.js
|
# more constants, this time extracted from the members csv export in all.js
|
||||||
staticFlags = {
|
staticFlags = {
|
||||||
@ -88,7 +85,7 @@ class MembershipWorks:
|
|||||||
data={"eml": username, "pwd": password},
|
data={"eml": username, "pwd": password},
|
||||||
headers={"X-Org": "10000"},
|
headers={"X-Org": "10000"},
|
||||||
)
|
)
|
||||||
if r.status_code != requests.codes.ok or "SF" not in r.json():
|
if r.status_code != 200 or "SF" not in r.json():
|
||||||
raise MembershipWorksRemoteError("login", r)
|
raise MembershipWorksRemoteError("login", r)
|
||||||
self.org_info = r.json()
|
self.org_info = r.json()
|
||||||
self.auth_token = self.org_info["SF"]
|
self.auth_token = self.org_info["SF"]
|
||||||
@ -136,9 +133,10 @@ class MembershipWorks:
|
|||||||
for screen_type in ["anm", "acc", "adm"]:
|
for screen_type in ["anm", "acc", "adm"]:
|
||||||
for box in self.org_info["tpl"][screen_type]:
|
for box in self.org_info["tpl"][screen_type]:
|
||||||
for element in box["box"]:
|
for element in box["box"]:
|
||||||
if not isinstance(element["dat"], str):
|
if type(element["dat"]) != str:
|
||||||
for field in element["dat"]:
|
for field in element["dat"]:
|
||||||
if "_id" in field and field["_id"] not in fields:
|
if "_id" in field:
|
||||||
|
if field["_id"] not in fields:
|
||||||
fields[field["_id"]] = field
|
fields[field["_id"]] = field
|
||||||
|
|
||||||
return fields
|
return fields
|
||||||
@ -170,13 +168,13 @@ class MembershipWorks:
|
|||||||
BASE_URL + "/v2/accounts",
|
BASE_URL + "/v2/accounts",
|
||||||
params={"dek": ",".join([folder_map[f] for f in folders])},
|
params={"dek": ",".join([folder_map[f] for f in folders])},
|
||||||
)
|
)
|
||||||
if r.status_code != requests.codes.ok or "usr" not in r.json():
|
if r.status_code != 200 or "usr" not in r.json():
|
||||||
raise MembershipWorksRemoteError("user listing", r)
|
raise MembershipWorksRemoteError("user listing", r)
|
||||||
|
|
||||||
# get list of member ID matching the search
|
# get list of member ID matching the search
|
||||||
# dedup with set() to work around people with alt uids
|
# dedup with set() to work around people with alt uids
|
||||||
# TODO: figure out why people have alt uids
|
# TODO: figure out why people have alt uids
|
||||||
return {user["uid"] for user in r.json()["usr"]}
|
return set(user["uid"] for user in r.json()["usr"])
|
||||||
|
|
||||||
# TODO: has issues with aliasing header names:
|
# TODO: has issues with aliasing header names:
|
||||||
# ex: "Personal Studio Space" Label vs Membership Addon/Field
|
# ex: "Personal Studio Space" Label vs Membership Addon/Field
|
||||||
@ -198,7 +196,7 @@ class MembershipWorks:
|
|||||||
"var": columns,
|
"var": columns,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if r.status_code != requests.codes.ok:
|
if r.status_code != 200:
|
||||||
raise MembershipWorksRemoteError("csv generation", r)
|
raise MembershipWorksRemoteError("csv generation", r)
|
||||||
|
|
||||||
if r.text[0] == "\ufeff":
|
if r.text[0] == "\ufeff":
|
||||||
@ -217,13 +215,13 @@ class MembershipWorks:
|
|||||||
r = self._get_v1(
|
r = self._get_v1(
|
||||||
BASE_URL + "/v1/csv",
|
BASE_URL + "/v1/csv",
|
||||||
params={
|
params={
|
||||||
"crm": ",".join(str(k) for k in CRM),
|
"crm": ",".join(str(k) for k in CRM.keys()),
|
||||||
**({"txl": ""} if json else {}),
|
**({"txl": ""} if json else {}),
|
||||||
"sdp": start_date.strftime("%s"),
|
"sdp": start_date.strftime("%s"),
|
||||||
"edp": end_date.strftime("%s"),
|
"edp": end_date.strftime("%s"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if r.status_code != requests.codes.ok:
|
if r.status_code != 200:
|
||||||
raise MembershipWorksRemoteError("csv generation", r)
|
raise MembershipWorksRemoteError("csv generation", r)
|
||||||
if json:
|
if json:
|
||||||
return r.json()
|
return r.json()
|
||||||
|
@ -4,6 +4,7 @@ from django.db import migrations
|
|||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("membershipworks", "0001_initial"),
|
("membershipworks", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
# Generated by Django 5.0.1 on 2024-01-19 20:21
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("membershipworks", "0007_eventmeetingtime_duration"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="event",
|
|
||||||
name="occurred",
|
|
||||||
field=models.GeneratedField(
|
|
||||||
db_persist=False,
|
|
||||||
expression=models.Q(
|
|
||||||
("cap", 0),
|
|
||||||
("count", 0),
|
|
||||||
("calendar", 0),
|
|
||||||
_connector="OR",
|
|
||||||
_negated=True,
|
|
||||||
),
|
|
||||||
output_field=models.BooleanField(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,19 +1,10 @@
|
|||||||
from datetime import datetime
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import django.core.mail.message
|
import django.core.mail.message
|
||||||
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 (
|
from django.db.models import Exists, F, OuterRef, Sum
|
||||||
Count,
|
|
||||||
Exists,
|
|
||||||
ExpressionWrapper,
|
|
||||||
F,
|
|
||||||
OuterRef,
|
|
||||||
Q,
|
|
||||||
Subquery,
|
|
||||||
Sum,
|
|
||||||
)
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
@ -385,11 +376,6 @@ class Event(BaseModel):
|
|||||||
category = models.ForeignKey(EventCategory, on_delete=models.PROTECT)
|
category = models.ForeignKey(EventCategory, on_delete=models.PROTECT)
|
||||||
calendar = models.IntegerField(choices=EventCalendar)
|
calendar = models.IntegerField(choices=EventCalendar)
|
||||||
venue = models.TextField(null=True, blank=True)
|
venue = models.TextField(null=True, blank=True)
|
||||||
occurred = models.GeneratedField(
|
|
||||||
expression=~(Q(cap=0) | Q(count=0) | Q(calendar=EventCalendar.HIDDEN)),
|
|
||||||
output_field=models.BooleanField(),
|
|
||||||
db_persist=False,
|
|
||||||
)
|
|
||||||
# TODO:
|
# TODO:
|
||||||
# "lgo": {
|
# "lgo": {
|
||||||
# "l": "https://d1tif55lvfk8gc.cloudfront.net/656e3842ae3975908b05e304.jpg?1673405126",
|
# "l": "https://d1tif55lvfk8gc.cloudfront.net/656e3842ae3975908b05e304.jpg?1673405126",
|
||||||
@ -427,36 +413,9 @@ class EventInstructor(models.Model):
|
|||||||
return str(self.member) if self.member else self.name
|
return str(self.member) if self.member else self.name
|
||||||
|
|
||||||
|
|
||||||
class EventExtQuerySet(models.QuerySet["EventExt"]):
|
|
||||||
def summarize(self, aggregate: bool = False):
|
|
||||||
method = self.aggregate if aggregate else self.annotate
|
|
||||||
return method(
|
|
||||||
count__sum=Sum("count", filter=F("occurred")),
|
|
||||||
instructor__count=Count("instructor", distinct=True, filter=F("occurred")),
|
|
||||||
meeting_times__count=Count("meeting_times", filter=F("occurred")),
|
|
||||||
duration__sum=Sum("duration", filter=F("occurred")),
|
|
||||||
person_hours__sum=Sum("person_hours", filter=F("occurred")),
|
|
||||||
event_count=Count("eid", filter=F("occurred")),
|
|
||||||
canceled_event_count=Count("eid", filter=~F("occurred")),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EventExtManager(models.Manager["EventExt"]):
|
class EventExtManager(models.Manager["EventExt"]):
|
||||||
def get_queryset(self) -> models.QuerySet["EventExt"]:
|
def get_queryset(self) -> models.QuerySet["EventExt"]:
|
||||||
return EventExtQuerySet(self.model, using=self._db).annotate(
|
return super().get_queryset().annotate(duration=Sum("meeting_times__duration"))
|
||||||
Count("meeting_times"),
|
|
||||||
duration=Subquery(
|
|
||||||
EventMeetingTime.objects.filter(event=OuterRef("pk"))
|
|
||||||
.values("event__pk")
|
|
||||||
.annotate(d=Sum("duration"))
|
|
||||||
.values("d")[:1],
|
|
||||||
output_field=models.DurationField(),
|
|
||||||
),
|
|
||||||
person_hours=ExpressionWrapper(
|
|
||||||
ExpressionWrapper(F("duration"), models.IntegerField()) * F("count"),
|
|
||||||
models.DurationField(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EventExt(Event):
|
class EventExt(Event):
|
||||||
@ -477,6 +436,14 @@ class EventExt(Event):
|
|||||||
max_digits=13, decimal_places=4, default=0
|
max_digits=13, decimal_places=4, default=0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: ideally this would be a generated column or annotation,
|
||||||
|
# but I couldn't get the types to work out
|
||||||
|
@property
|
||||||
|
def person_hours(self):
|
||||||
|
if self.duration is None:
|
||||||
|
return None
|
||||||
|
return self.count * self.duration
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "event"
|
verbose_name = "event"
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ class MembershipWorksRouter:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def allow_relation(self, obj1, obj2, **hints):
|
def allow_relation(self, obj1, obj2, **hints):
|
||||||
if self.app_label in (obj1._meta.app_label, obj2._meta.app_label):
|
if (
|
||||||
|
obj1._meta.app_label == self.app_label
|
||||||
|
or obj2._meta.app_label == self.app_label
|
||||||
|
):
|
||||||
return True
|
return True
|
||||||
return None
|
return None
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
from membershipworks.membershipworks_api import FieldType, MembershipWorks
|
|
||||||
from membershipworks.models import (
|
from membershipworks.models import (
|
||||||
Event,
|
|
||||||
EventCategory,
|
|
||||||
EventExt,
|
|
||||||
Flag,
|
|
||||||
Member,
|
Member,
|
||||||
|
Flag,
|
||||||
Transaction,
|
Transaction,
|
||||||
|
Event,
|
||||||
|
EventExt,
|
||||||
|
EventCategory,
|
||||||
)
|
)
|
||||||
|
from membershipworks import MembershipWorks
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -24,7 +24,8 @@ def flags_for_member(csv_member, all_flags, folders):
|
|||||||
if flag.type == "folder":
|
if flag.type == "folder":
|
||||||
if csv_member["Account ID"] in folders[flag.id]:
|
if csv_member["Account ID"] in folders[flag.id]:
|
||||||
yield flag
|
yield flag
|
||||||
elif csv_member[flag.name] == flag.name:
|
else:
|
||||||
|
if csv_member[flag.name] == flag.name:
|
||||||
yield flag
|
yield flag
|
||||||
|
|
||||||
|
|
||||||
@ -49,13 +50,12 @@ def scrape_members(membershipworks: MembershipWorks):
|
|||||||
logger.info("Getting/Updating members...")
|
logger.info("Getting/Updating members...")
|
||||||
members = membershipworks.get_all_members()
|
members = membershipworks.get_all_members()
|
||||||
for csv_member in members:
|
for csv_member in members:
|
||||||
for field in membershipworks._all_fields().values():
|
for field_id, field in membershipworks._all_fields().items():
|
||||||
# convert checkboxes to real booleans
|
# convert checkboxes to real booleans
|
||||||
if (
|
if field.get("typ") == 8 and field["lbl"] in csv_member:
|
||||||
field.get("typ") == FieldType.CHECKBOX.value
|
csv_member[field["lbl"]] = (
|
||||||
and field["lbl"] in csv_member
|
True if csv_member[field["lbl"]] == "Y" else False
|
||||||
):
|
)
|
||||||
csv_member[field["lbl"]] = csv_member[field["lbl"]] == "Y"
|
|
||||||
|
|
||||||
# create/update member
|
# create/update member
|
||||||
member = Member.from_api_dict(csv_member)
|
member = Member.from_api_dict(csv_member)
|
||||||
@ -80,8 +80,10 @@ def scrape_transactions(membershipworks: MembershipWorks):
|
|||||||
# this is terrible, but as long as the dates are the same, should be fiiiine
|
# this is terrible, but as long as the dates are the same, should be fiiiine
|
||||||
transactions = [{**j, **v} for j, v in zip(transactions_csv, transactions_json)]
|
transactions = [{**j, **v} for j, v in zip(transactions_csv, transactions_json)]
|
||||||
assert all(
|
assert all(
|
||||||
|
[
|
||||||
t["Account ID"] == t.get("uid", "") and t["Payment ID"] == t.get("sid", "")
|
t["Account ID"] == t.get("uid", "") and t["Payment ID"] == t.get("sid", "")
|
||||||
for t in transactions
|
for t in transactions
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
for csv_transaction in transactions:
|
for csv_transaction in transactions:
|
||||||
|
@ -4,11 +4,10 @@ import re
|
|||||||
import string
|
import string
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from udm_rest_client.exceptions import NoObject, UdmError
|
|
||||||
from udm_rest_client.udm import UDM
|
from udm_rest_client.udm import UDM
|
||||||
|
from udm_rest_client.exceptions import NoObject, UdmError
|
||||||
|
|
||||||
from membershipworks.models import Flag, Member
|
from membershipworks.models import Member, Flag
|
||||||
|
|
||||||
USER_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
USER_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||||
GROUP_BASE = "cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
GROUP_BASE = "cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
{% extends "base.dj.html" %}
|
|
||||||
|
|
||||||
{% load membershipworks_tags %}
|
|
||||||
|
|
||||||
{% block title %}Event Report{% endblock %}
|
|
||||||
{% block breadcrumbs %}
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">MW Event Reports</li>
|
|
||||||
{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th for="column">Month</th>
|
|
||||||
<th for="column">Events</th>
|
|
||||||
<th for="column">Canceled Events</th>
|
|
||||||
<th for="column">Tickets</th>
|
|
||||||
<th for="column">Unique Instructors</th>
|
|
||||||
<th for="column">Meetings</th>
|
|
||||||
<th for="column">Total Hours</th>
|
|
||||||
<th for="column">Total Person Hours</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for year in object_list %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'membershipworks:event-year-report' year.year|date:"Y" %}">{{ year.year|date:"Y" }}</a>
|
|
||||||
</td>
|
|
||||||
<td>{{ year.event_count }}</td>
|
|
||||||
<td>{{ year.canceled_event_count }}</td>
|
|
||||||
<td>{{ year.count__sum }}</td>
|
|
||||||
<td>{{ year.instructor__count }}</td>
|
|
||||||
<td>{{ year.meeting_times__count }}</td>
|
|
||||||
<td>{{ year.duration__sum|duration_as_hours }}</td>
|
|
||||||
<td>{{ year.person_hours__sum|duration_as_hours }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -3,13 +3,6 @@
|
|||||||
{% load membershipworks_tags %}
|
{% load membershipworks_tags %}
|
||||||
|
|
||||||
{% block title %}Event Report{% endblock %}
|
{% block title %}Event Report{% endblock %}
|
||||||
{% block breadcrumbs %}
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'membershipworks:event-index-report' %}">MW Event Reports</a></li>
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="{% url 'membershipworks:event-year-report' month|date:"Y" %}">{{ month|date:"Y" }}</a>
|
|
||||||
</li>
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">{{ month|date:"F" }}</li>
|
|
||||||
{% endblock %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
@ -28,7 +21,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for event in object_list %}
|
{% for event in object_list %}
|
||||||
<tr {% if not event.occurred %}class="text-decoration-line-through table-danger"{% endif %}>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a href="https://membershipworks.com/admin/#!event/admin/{{ event.url }}">{{ event.title }}</a>
|
<a href="https://membershipworks.com/admin/#!event/admin/{{ event.url }}">{{ event.title }}</a>
|
||||||
</td>
|
</td>
|
||||||
@ -37,7 +30,7 @@
|
|||||||
<td>{{ event.category }}</td>
|
<td>{{ event.category }}</td>
|
||||||
<td>{{ event.count }}</td>
|
<td>{{ event.count }}</td>
|
||||||
<td>{{ event.cap }}</td>
|
<td>{{ event.cap }}</td>
|
||||||
<td>{{ event.meeting_times__count }}</td>
|
<td>{{ event.meeting_times.count }}</td>
|
||||||
<td>{{ event.duration|duration_as_hours }}</td>
|
<td>{{ event.duration|duration_as_hours }}</td>
|
||||||
<td>{{ event.person_hours|duration_as_hours }}</td>
|
<td>{{ event.person_hours|duration_as_hours }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -50,7 +43,7 @@
|
|||||||
{% if previous_month %}
|
{% if previous_month %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link"
|
<a class="page-link"
|
||||||
href="{% url 'membershipworks:event-month-report' previous_month|date:"Y" previous_month|date:"m" %}">
|
href="{% url 'membershipworks:event-report' previous_month|date:"Y" previous_month|date:"m" %}">
|
||||||
<i class="bi bi-arrow-left"></i>
|
<i class="bi bi-arrow-left"></i>
|
||||||
{{ previous_month|date:"F Y" }}
|
{{ previous_month|date:"F Y" }}
|
||||||
</a>
|
</a>
|
||||||
@ -62,7 +55,7 @@
|
|||||||
{% if next_month %}
|
{% if next_month %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a class="page-link"
|
<a class="page-link"
|
||||||
href="{% url 'membershipworks:event-month-report' next_month|date:"Y" next_month|date:"m" %}">
|
href="{% url 'membershipworks:event-report' next_month|date:"Y" next_month|date:"m" %}">
|
||||||
{{ next_month|date:"F Y" }}
|
{{ next_month|date:"F Y" }}
|
||||||
<i class="bi bi-arrow-right"></i>
|
<i class="bi bi-arrow-right"></i>
|
||||||
</a>
|
</a>
|
@ -1,68 +0,0 @@
|
|||||||
{% extends "base.dj.html" %}
|
|
||||||
|
|
||||||
{% load membershipworks_tags %}
|
|
||||||
|
|
||||||
{% block title %}Event Report{% endblock %}
|
|
||||||
{% block breadcrumbs %}
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'membershipworks:event-index-report' %}">MW Event Reports</a></li>
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">{{ year|date:"Y" }}</li>
|
|
||||||
{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th for="column">Month</th>
|
|
||||||
<th for="column">Events</th>
|
|
||||||
<th for="column">Canceled Events</th>
|
|
||||||
<th for="column">Tickets</th>
|
|
||||||
<th for="column">Unique Instructors</th>
|
|
||||||
<th for="column">Meetings</th>
|
|
||||||
<th for="column">Total Hours</th>
|
|
||||||
<th for="column">Total Person Hours</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for month in object_list %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'membershipworks:event-month-report' month.month|date:"Y" month.month|date:"m" %}">{{ month.month|date:"F Y" }}</a>
|
|
||||||
</td>
|
|
||||||
<td>{{ month.event_count }}</td>
|
|
||||||
<td>{{ month.canceled_event_count }}</td>
|
|
||||||
<td>{{ month.count__sum }}</td>
|
|
||||||
<td>{{ month.instructor__count }}</td>
|
|
||||||
<td>{{ month.meeting_times__count }}</td>
|
|
||||||
<td>{{ month.duration__sum|duration_as_hours }}</td>
|
|
||||||
<td>{{ month.person_hours__sum|duration_as_hours }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<nav aria-label="Page navigation">
|
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
{% if previous_year %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link"
|
|
||||||
href="{% url 'membershipworks:event-year-report' previous_year|date:"Y" %}">
|
|
||||||
<i class="bi bi-arrow-left"></i>
|
|
||||||
{{ previous_year|date:"Y" }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
<li class="page-item active">
|
|
||||||
<a class="page-link" href="#">{{ year|date:"Y" }}</a>
|
|
||||||
</li>
|
|
||||||
{% if next_year %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link"
|
|
||||||
href="{% url 'membershipworks:event-year-report' next_year|date:"Y" %}">
|
|
||||||
{{ next_year|date:"Y" }}
|
|
||||||
<i class="bi bi-arrow-right"></i>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% endblock %}
|
|
@ -1 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import (
|
from .views import MemberAutocomplete, upcoming_events, EventReport
|
||||||
EventIndexReport,
|
|
||||||
EventMonthReport,
|
|
||||||
EventYearReport,
|
|
||||||
MemberAutocomplete,
|
|
||||||
upcoming_events,
|
|
||||||
)
|
|
||||||
|
|
||||||
app_name = "membershipworks"
|
app_name = "membershipworks"
|
||||||
|
|
||||||
@ -21,19 +15,9 @@ urlpatterns = [
|
|||||||
upcoming_events,
|
upcoming_events,
|
||||||
name="upcoming-events",
|
name="upcoming-events",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"event-report/",
|
|
||||||
EventIndexReport.as_view(),
|
|
||||||
name="event-index-report",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"event-report/<int:year>/",
|
|
||||||
EventYearReport.as_view(),
|
|
||||||
name="event-year-report",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"event-report/<int:year>/<int:month>/",
|
"event-report/<int:year>/<int:month>/",
|
||||||
EventMonthReport.as_view(month_format="%m"),
|
EventReport.as_view(month_format="%m"),
|
||||||
name="event-month-report",
|
name="event-report",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -2,21 +2,15 @@ from datetime import datetime
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import permission_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.db.models.functions import TruncMonth, TruncYear
|
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.views.generic.dates import (
|
from django.views.generic.dates import MonthArchiveView
|
||||||
ArchiveIndexView,
|
|
||||||
MonthArchiveView,
|
|
||||||
YearArchiveView,
|
|
||||||
)
|
|
||||||
|
|
||||||
from dal import autocomplete
|
from dal import autocomplete
|
||||||
|
|
||||||
from membershipworks.membershipworks_api import MembershipWorks
|
from .models import Member, EventExt
|
||||||
|
from membershipworks import MembershipWorks
|
||||||
from .models import EventExt, Member
|
|
||||||
|
|
||||||
|
|
||||||
class MemberAutocomplete(autocomplete.Select2QuerySetView):
|
class MemberAutocomplete(autocomplete.Select2QuerySetView):
|
||||||
@ -30,7 +24,8 @@ class MemberAutocomplete(autocomplete.Select2QuerySetView):
|
|||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
|
|
||||||
|
|
||||||
@permission_required("membershipworks.view_eventext")
|
@login_required
|
||||||
|
# TODO: permission required?
|
||||||
def upcoming_events(request):
|
def upcoming_events(request):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
@ -109,42 +104,8 @@ def upcoming_events(request):
|
|||||||
return render(request, "membershipworks/upcoming_events.dj.html", context)
|
return render(request, "membershipworks/upcoming_events.dj.html", context)
|
||||||
|
|
||||||
|
|
||||||
class EventIndexReport(PermissionRequiredMixin, ArchiveIndexView):
|
class EventReport(PermissionRequiredMixin, MonthArchiveView):
|
||||||
permission_required = "membershipworks.view_eventext"
|
|
||||||
queryset = EventExt.objects.all()
|
|
||||||
date_field = "start"
|
|
||||||
template_name = "membershipworks/event_index_report.dj.html"
|
|
||||||
make_object_list = True
|
|
||||||
|
|
||||||
def get_dated_queryset(self, **lookup):
|
|
||||||
return (
|
|
||||||
super()
|
|
||||||
.get_dated_queryset(**lookup)
|
|
||||||
.values(year=TruncYear("start"))
|
|
||||||
.summarize()
|
|
||||||
.order_by("year")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EventYearReport(PermissionRequiredMixin, YearArchiveView):
|
|
||||||
permission_required = "membershipworks.view_eventext"
|
|
||||||
queryset = EventExt.objects.all()
|
|
||||||
date_field = "start"
|
|
||||||
template_name = "membershipworks/event_year_report.dj.html"
|
|
||||||
make_object_list = True
|
|
||||||
|
|
||||||
def get_dated_queryset(self, **lookup):
|
|
||||||
return (
|
|
||||||
super()
|
|
||||||
.get_dated_queryset(**lookup)
|
|
||||||
.values(month=TruncMonth("start"))
|
|
||||||
.summarize()
|
|
||||||
.order_by("month")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EventMonthReport(PermissionRequiredMixin, MonthArchiveView):
|
|
||||||
permission_required = "membershipworks.view_eventext"
|
permission_required = "membershipworks.view_eventext"
|
||||||
queryset = EventExt.objects.select_related("category", "instructor").all()
|
queryset = EventExt.objects.select_related("category", "instructor").all()
|
||||||
date_field = "start"
|
date_field = "start"
|
||||||
template_name = "membershipworks/event_month_report.dj.html"
|
template_name = "membershipworks/event_report.dj.html"
|
||||||
|
@ -1,29 +1,29 @@
|
|||||||
from typing import Any, cast
|
from typing import Optional, Any, Type, cast
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib import admin, messages
|
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
|
from django.contrib import admin, messages
|
||||||
from django.db.models import Value
|
from django.db.models import Value
|
||||||
from django.db.models.functions import Concat, LPad, Now
|
|
||||||
from django.db.models.query import QuerySet
|
from django.db.models.query import QuerySet
|
||||||
|
from django.db.models.functions import Now, Concat, LPad
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from .certification_emails import all_certification_emails
|
|
||||||
from .forms import CertificationForm
|
|
||||||
from .models import (
|
from .models import (
|
||||||
AbstractAudit,
|
AbstractAudit,
|
||||||
Certification,
|
|
||||||
CertificationAudit,
|
|
||||||
CertificationDefinition,
|
|
||||||
CertificationVersion,
|
|
||||||
CertificationVersionAnnotated,
|
|
||||||
CmsRedRiverVeteransScholarship,
|
CmsRedRiverVeteransScholarship,
|
||||||
Department,
|
Department,
|
||||||
|
CertificationDefinition,
|
||||||
|
Certification,
|
||||||
|
CertificationAudit,
|
||||||
|
CertificationVersion,
|
||||||
|
CertificationVersionAnnotated,
|
||||||
InstructorOrVendor,
|
InstructorOrVendor,
|
||||||
SpecialProgram,
|
SpecialProgram,
|
||||||
Waiver,
|
Waiver,
|
||||||
WaiverAudit,
|
WaiverAudit,
|
||||||
)
|
)
|
||||||
|
from .forms import CertificationForm
|
||||||
|
from .certification_emails import all_certification_emails
|
||||||
|
|
||||||
|
|
||||||
class AlwaysChangedModelForm(forms.models.ModelForm):
|
class AlwaysChangedModelForm(forms.models.ModelForm):
|
||||||
@ -38,8 +38,8 @@ class AbstractAuditInline(admin.TabularInline):
|
|||||||
form = AlwaysChangedModelForm
|
form = AlwaysChangedModelForm
|
||||||
|
|
||||||
def get_formset(
|
def get_formset(
|
||||||
self, request: HttpRequest, obj: AbstractAudit | None = None, **kwargs: Any
|
self, request: HttpRequest, obj: Optional[AbstractAudit] = None, **kwargs: Any
|
||||||
) -> type[
|
) -> Type[
|
||||||
"forms.models.BaseInlineFormSet[AbstractAudit, Any, forms.models.ModelForm[Any]]"
|
"forms.models.BaseInlineFormSet[AbstractAudit, Any, forms.models.ModelForm[Any]]"
|
||||||
]:
|
]:
|
||||||
formset = super().get_formset(request, obj, **kwargs)
|
formset = super().get_formset(request, obj, **kwargs)
|
||||||
|
@ -1,16 +1,14 @@
|
|||||||
from django.db.models import Prefetch, Q
|
|
||||||
|
|
||||||
from rest_framework import routers, serializers, viewsets
|
from rest_framework import routers, serializers, viewsets
|
||||||
|
from django.db.models import Prefetch, Q
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from membershipworks.models import Member
|
from membershipworks.models import Member
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
|
Department,
|
||||||
Certification,
|
Certification,
|
||||||
CertificationDefinition,
|
CertificationDefinition,
|
||||||
CertificationVersion,
|
CertificationVersion,
|
||||||
Department,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from django.db.models import CharField, Q, Value
|
|
||||||
from django.db.models.functions import Concat
|
|
||||||
|
|
||||||
from dal import autocomplete
|
from dal import autocomplete
|
||||||
|
from django.db.models.functions import Concat
|
||||||
|
from django.db.models import Q, Value, CharField
|
||||||
|
|
||||||
from .models import CertificationVersion
|
from .models import CertificationVersion
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.core.mail.message import sanitize_address
|
from django.core.mail.message import sanitize_address
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
|
|
||||||
import mdformat
|
|
||||||
from markdownify import markdownify
|
from markdownify import markdownify
|
||||||
|
import mdformat
|
||||||
|
|
||||||
|
|
||||||
def make_multipart_email(subject, html_body, to):
|
def make_multipart_email(subject, html_body, to):
|
||||||
|
@ -4,7 +4,6 @@ from django.urls import reverse
|
|||||||
|
|
||||||
import dashboard
|
import dashboard
|
||||||
from membershipworks.models import Member
|
from membershipworks.models import Member
|
||||||
|
|
||||||
from .models import Department
|
from .models import Department
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
|
from dal import autocomplete
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from dal import autocomplete
|
from .models import CertificationDefinition, Certification
|
||||||
|
|
||||||
from .models import Certification, CertificationDefinition
|
|
||||||
|
|
||||||
|
|
||||||
class CertificationForm(forms.ModelForm):
|
class CertificationForm(forms.ModelForm):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 4.0.2 on 2022-02-03 21:12
|
# Generated by Django 4.0.2 on 2022-02-03 21:12
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# (Partially) Generated by Django 4.0.2 on 2022-02-04 18:01
|
# (Partially) Generated by Django 4.0.2 on 2022-02-04 18:01
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
def migrate_certification_version_forward(apps, schema_editor):
|
def migrate_certification_version_forward(apps, schema_editor):
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Generated by Django 4.1.3 on 2023-01-18 03:31
|
# Generated by Django 4.1.3 on 2023-01-18 03:31
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
def link_departments(apps, schema_editor):
|
def link_departments(apps, schema_editor):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 4.1.3 on 2023-01-24 02:02
|
# Generated by Django 4.1.3 on 2023-01-24 02:02
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
from semver import VersionInfo
|
from semver import VersionInfo
|
||||||
|
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
# Generated by Django 4.2 on 2023-04-10 05:34
|
# Generated by Django 4.2 on 2023-04-10 05:34
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Generated by Django 4.2 on 2023-04-10 18:37
|
# Generated by Django 4.2 on 2023-04-10 18:37
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -1,18 +1,16 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import re
|
import re
|
||||||
from typing import TYPE_CHECKING, TypedDict
|
from typing import TypedDict, TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from semver import VersionInfo
|
||||||
from django.core.validators import RegexValidator
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q, Window
|
from django.db.models import Q, Window
|
||||||
from django.db.models.functions import FirstValue
|
from django.db.models.functions import FirstValue
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.validators import RegexValidator
|
||||||
from django_stubs_ext import WithAnnotations
|
from django_stubs_ext import WithAnnotations
|
||||||
from semver import VersionInfo
|
|
||||||
|
|
||||||
from membershipworks.models import Flag as MembershipWorksFlag
|
from membershipworks.models import Member, Flag as MembershipWorksFlag
|
||||||
from membershipworks.models import Member
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractAudit(models.Model):
|
class AbstractAudit(models.Model):
|
||||||
@ -106,14 +104,14 @@ class Department(models.Model):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def list_name(self) -> str | None:
|
def list_name(self) -> Optional[str]:
|
||||||
if self.has_mailing_list:
|
if self.has_mailing_list:
|
||||||
return self.name.replace(" ", "_") + "-info"
|
return self.name.replace(" ", "_") + "-info"
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def list_address(self) -> str | None:
|
def list_address(self) -> Optional[str]:
|
||||||
if self.list_name:
|
if self.list_name:
|
||||||
return self.list_name + "@claremontmakerspace.org"
|
return self.list_name + "@claremontmakerspace.org"
|
||||||
else:
|
else:
|
||||||
|
@ -1 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from . import autocomplete_views, views
|
from . import views
|
||||||
|
from . import autocomplete_views
|
||||||
|
|
||||||
app_name = "paperwork"
|
app_name = "paperwork"
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ import requests
|
|||||||
import weasyprint
|
import weasyprint
|
||||||
|
|
||||||
from membershipworks.models import Member
|
from membershipworks.models import Member
|
||||||
|
|
||||||
from .models import Certification, Department
|
from .models import Certification, Department
|
||||||
|
|
||||||
WIKI_URL = settings.WIKI_URL
|
WIKI_URL = settings.WIKI_URL
|
||||||
|
62
pdm.lock
62
pdm.lock
@ -5,7 +5,7 @@
|
|||||||
groups = ["default", "debug", "lint", "server", "typing", "dev"]
|
groups = ["default", "debug", "lint", "server", "typing", "dev"]
|
||||||
strategy = ["cross_platform"]
|
strategy = ["cross_platform"]
|
||||||
lock_version = "4.4.1"
|
lock_version = "4.4.1"
|
||||||
content_hash = "sha256:73715d6c541091f09cb8dea4f2baba6b58b0972a615b44e6f85869f918fdb360"
|
content_hash = "sha256:91f554bae127245b4082d069629400706b8b43daf3bf1fb8fd963eee120ff449"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohttp"
|
name = "aiohttp"
|
||||||
@ -164,6 +164,31 @@ files = [
|
|||||||
{file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
|
{file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "23.12.1"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "The uncompromising code formatter."
|
||||||
|
dependencies = [
|
||||||
|
"click>=8.0.0",
|
||||||
|
"mypy-extensions>=0.4.3",
|
||||||
|
"packaging>=22.0",
|
||||||
|
"pathspec>=0.9.0",
|
||||||
|
"platformdirs>=2",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"},
|
||||||
|
{file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"},
|
||||||
|
{file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"},
|
||||||
|
{file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"},
|
||||||
|
{file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"},
|
||||||
|
{file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"},
|
||||||
|
{file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"},
|
||||||
|
{file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"},
|
||||||
|
{file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"},
|
||||||
|
{file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
@ -1266,6 +1291,16 @@ files = [
|
|||||||
{file = "Pillow-9.3.0.tar.gz", hash = "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f"},
|
{file = "Pillow-9.3.0.tar.gz", hash = "sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "2.5.3"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
|
files = [
|
||||||
|
{file = "platformdirs-2.5.3-py3-none-any.whl", hash = "sha256:0cb405749187a194f444c25c82ef7225232f11564721eabffc6ec70df83b11cb"},
|
||||||
|
{file = "platformdirs-2.5.3.tar.gz", hash = "sha256:6e52c21afff35cb659c6e52d8b4d61b9bd544557180440538f255d9382c8cbe0"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prompt-toolkit"
|
name = "prompt-toolkit"
|
||||||
version = "3.0.41"
|
version = "3.0.41"
|
||||||
@ -1519,31 +1554,6 @@ files = [
|
|||||||
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
|
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ruff"
|
|
||||||
version = "0.1.13"
|
|
||||||
requires_python = ">=3.7"
|
|
||||||
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
|
||||||
files = [
|
|
||||||
{file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e3fd36e0d48aeac672aa850045e784673449ce619afc12823ea7868fcc41d8ba"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9fb6b3b86450d4ec6a6732f9f60c4406061b6851c4b29f944f8c9d91c3611c7a"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b13ba5d7156daaf3fd08b6b993360a96060500aca7e307d95ecbc5bb47a69296"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ebb40442f7b531e136d334ef0851412410061e65d61ca8ce90d894a094feb22"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226b517f42d59a543d6383cfe03cccf0091e3e0ed1b856c6824be03d2a75d3b6"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f0312ba1061e9b8c724e9a702d3c8621e3c6e6c2c9bd862550ab2951ac75c16"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f59bcf5217c661254bd6bc42d65a6fd1a8b80c48763cb5c2293295babd945dd"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6894b00495e00c27b6ba61af1fc666f17de6140345e5ef27dd6e08fb987259d"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1600942485c6e66119da294c6294856b5c86fd6df591ce293e4a4cc8e72989"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ee3febce7863e231a467f90e681d3d89210b900d49ce88723ce052c8761be8c7"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dcaab50e278ff497ee4d1fe69b29ca0a9a47cd954bb17963628fa417933c6eb1"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f57de973de4edef3ad3044d6a50c02ad9fc2dff0d88587f25f1a48e3f72edf5e"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a36fa90eb12208272a858475ec43ac811ac37e91ef868759770b71bdabe27b6"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-win32.whl", hash = "sha256:a623349a505ff768dad6bd57087e2461be8db58305ebd5577bd0e98631f9ae69"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-win_amd64.whl", hash = "sha256:f988746e3c3982bea7f824c8fa318ce7f538c4dfefec99cd09c8770bd33e6539"},
|
|
||||||
{file = "ruff-0.1.13-py3-none-win_arm64.whl", hash = "sha256:6bbbc3042075871ec17f28864808540a26f0f79a4478c357d3e3d2284e832998"},
|
|
||||||
{file = "ruff-0.1.13.tar.gz", hash = "sha256:e261f1baed6291f434ffb1d5c6bd8051d1c2a26958072d38dfbec39b3dda7352"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "semver"
|
name = "semver"
|
||||||
version = "3.0.2"
|
version = "3.0.2"
|
||||||
|
@ -41,24 +41,8 @@ server = [
|
|||||||
[project.entry-points."djangoq.errorreporters"]
|
[project.entry-points."djangoq.errorreporters"]
|
||||||
admin_email = "cmsmanage.django_q2_admin_email_reporter:AdminEmailReporter"
|
admin_email = "cmsmanage.django_q2_admin_email_reporter:AdminEmailReporter"
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.black]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
select = ["E4", "E7", "E9", "F", "I", "C4", "UP", "PERF", "PL", "SIM"]
|
|
||||||
|
|
||||||
[tool.ruff.lint.isort]
|
|
||||||
known-first-party = [
|
|
||||||
"cmsmanage",
|
|
||||||
"dashboard",
|
|
||||||
"doorcontrol",
|
|
||||||
"membershipworks",
|
|
||||||
"paperwork",
|
|
||||||
"rentals",
|
|
||||||
"tasks",
|
|
||||||
]
|
|
||||||
section-order = ["future", "standard-library", "django", "third-party", "first-party", "local-folder"]
|
|
||||||
|
|
||||||
[tool.ruff.lint.isort.sections]
|
|
||||||
"django" = ["django"]
|
|
||||||
|
|
||||||
[tool.djlint]
|
[tool.djlint]
|
||||||
profile="django"
|
profile="django"
|
||||||
@ -98,8 +82,8 @@ include_packages = ["openapi-client-udm"]
|
|||||||
|
|
||||||
[tool.pdm.dev-dependencies]
|
[tool.pdm.dev-dependencies]
|
||||||
lint = [
|
lint = [
|
||||||
|
"black~=23.12",
|
||||||
"djlint~=1.34",
|
"djlint~=1.34",
|
||||||
"ruff~=0.1",
|
|
||||||
]
|
]
|
||||||
typing = [
|
typing = [
|
||||||
"mypy~=1.7",
|
"mypy~=1.7",
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
from django import forms
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
|
from django.contrib import admin
|
||||||
|
from django import forms
|
||||||
|
|
||||||
from membershipworks.models import Member
|
from membershipworks.models import Member
|
||||||
|
|
||||||
from .models import LockerBank, LockerInfo, LockerUnit
|
from .models import LockerBank, LockerInfo, LockerUnit
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Generated by Django 4.0.2 on 2022-02-16 21:19
|
# Generated by Django 4.0.2 on 2022-02-16 21:19
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -3,7 +3,8 @@ from django.core import validators
|
|||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.db.models.functions import Chr, Concat, Ord
|
from django.db.models.functions import Chr, Ord, Concat
|
||||||
|
|
||||||
|
|
||||||
from membershipworks.models import Member
|
from membershipworks.models import Member
|
||||||
|
|
||||||
|
@ -1 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
|
from django.contrib import messages
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .forms import LockerInfoForm
|
|
||||||
from .models import LockerBank, LockerInfo
|
from .models import LockerBank, LockerInfo
|
||||||
|
|
||||||
|
from .forms import LockerInfoForm
|
||||||
|
|
||||||
|
|
||||||
def lockerIndex(request):
|
def lockerIndex(request):
|
||||||
locker_banks = {
|
locker_banks = {
|
||||||
@ -31,7 +32,10 @@ def lockerIndex(request):
|
|||||||
@permission_required("rentals.change_lockerinfo", raise_exception=True)
|
@permission_required("rentals.change_lockerinfo", raise_exception=True)
|
||||||
def lockerUpdate(request, locker_id: int):
|
def lockerUpdate(request, locker_id: int):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
instance = get_object_or_404(LockerInfo, pk=locker_id)
|
try:
|
||||||
|
instance = LockerInfo.objects.get(pk=locker_id)
|
||||||
|
except LockerInfo.DoesNotExist:
|
||||||
|
pass # TODO
|
||||||
|
|
||||||
form = LockerInfoForm(request.POST, instance=instance)
|
form = LockerInfoForm(request.POST, instance=instance)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from markdownx.admin import MarkdownxModelAdmin
|
from markdownx.admin import MarkdownxModelAdmin
|
||||||
|
|
||||||
from .models import Event, GroupTaskSubscription, GroupToolSubscription, Task, Tool
|
from .models import Tool, Task, Event, GroupTaskSubscription, GroupToolSubscription
|
||||||
|
|
||||||
|
|
||||||
class GroupTaskSubscriptionInline(admin.TabularInline):
|
class GroupTaskSubscriptionInline(admin.TabularInline):
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from markdownx.widgets import MarkdownxWidget
|
from markdownx.widgets import MarkdownxWidget
|
||||||
|
|
||||||
from .models import Event
|
from .models import Event
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
|
from itertools import groupby
|
||||||
|
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
|
|
||||||
from tasks.models import GroupTaskSubscription, GroupToolSubscription
|
from tasks.models import Tool, Task, Event, GroupToolSubscription, GroupTaskSubscription
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
# Generated by Django 3.2.3 on 2021-05-19 18:05
|
# Generated by Django 3.2.3 on 2021-05-19 18:05
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
import markdownx.models
|
import markdownx.models
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# Generated by Django 3.2.3 on 2021-05-19 21:46
|
# Generated by Django 3.2.3 on 2021-05-19 21:46
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
|
|
||||||
import recurrence
|
import recurrence
|
||||||
import recurrence.fields
|
import recurrence.fields
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ from django.contrib.auth import get_user_model
|
|||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from markdownx.models import MarkdownxField
|
from markdownx.models import MarkdownxField
|
||||||
from recurrence.fields import RecurrenceField
|
from recurrence.fields import RecurrenceField
|
||||||
|
|
||||||
|
@ -120,7 +120,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="text-nowrap">{{ event.date }}</td>
|
<td class="text-nowrap">{{ event.date }}</td>
|
||||||
<td>{{ event.user }}</td>
|
<td>{{ event.user }}</td>
|
||||||
<td>{{ event.notes_html|safe }}</td>
|
<td>
|
||||||
|
{{ event.notes_html|safe }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -1 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
from .models import Tool, Task, Event
|
||||||
from .forms import EventForm
|
from .forms import EventForm
|
||||||
from .models import Event, Task, Tool
|
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
|
@ -21,11 +21,6 @@
|
|||||||
<nav class="navbar navbar-expand-sm bg-body-tertiary">
|
<nav class="navbar navbar-expand-sm bg-body-tertiary">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand" href="{% url 'dashboard:dashboard' %}">Claremont MakerSpace</a>
|
<a class="navbar-brand" href="{% url 'dashboard:dashboard' %}">Claremont MakerSpace</a>
|
||||||
<nav aria-label="breadcrumb" class="d-none d-sm-block">
|
|
||||||
<ol class="breadcrumb" style="--bs-breadcrumb-margin-bottom: 0;">
|
|
||||||
{% block breadcrumbs %}{% endblock %}
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
<button class="navbar-toggler"
|
<button class="navbar-toggler"
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="collapse"
|
data-bs-toggle="collapse"
|
||||||
@ -39,43 +34,27 @@
|
|||||||
<div class="navbar-nav">
|
<div class="navbar-nav">
|
||||||
{% block nav_extra %}{% endblock %}
|
{% block nav_extra %}{% endblock %}
|
||||||
<div class="nav-item dropdown">
|
<div class="nav-item dropdown">
|
||||||
<button class="btn btn-link nav-link dropdown-toggle d-flex align-items-center"
|
<button class="btn btn-link nav-link dropdown-toggle d-flex align-items-center" id="bd-theme" type="button" aria-expanded="false" data-bs-toggle="dropdown" data-bs-display="static" aria-label="Toggle theme (auto)">
|
||||||
id="bd-theme"
|
|
||||||
type="button"
|
|
||||||
aria-expanded="false"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
data-bs-display="static"
|
|
||||||
aria-label="Toggle theme (auto)">
|
|
||||||
<i class="bi theme-icon-active bi-circle-half"></i>
|
<i class="bi theme-icon-active bi-circle-half"></i>
|
||||||
<span class="d-none ms-2" id="bd-theme-text">Toggle theme</span>
|
<span class="d-none ms-2" id="bd-theme-text">Toggle theme</span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end"
|
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme-text">
|
||||||
aria-labelledby="bd-theme-text">
|
|
||||||
<li>
|
<li>
|
||||||
<button type="button"
|
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false">
|
||||||
class="dropdown-item d-flex align-items-center"
|
|
||||||
data-bs-theme-value="light"
|
|
||||||
aria-pressed="false">
|
|
||||||
<i class="bi me-2 opacity-50 theme-icon bi-sun-fill"></i>
|
<i class="bi me-2 opacity-50 theme-icon bi-sun-fill"></i>
|
||||||
Light
|
Light
|
||||||
<i class="bi ms-auto d-none bi-check2"></i>
|
<i class="bi ms-auto d-none bi-check2"></i>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button type="button"
|
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false">
|
||||||
class="dropdown-item d-flex align-items-center"
|
|
||||||
data-bs-theme-value="dark"
|
|
||||||
aria-pressed="false">
|
|
||||||
<i class="bi me-2 opacity-50 theme-icon bi-moon-stars-fill"></i>
|
<i class="bi me-2 opacity-50 theme-icon bi-moon-stars-fill"></i>
|
||||||
Dark
|
Dark
|
||||||
<i class="bi ms-auto d-none bi-check2"></i>
|
<i class="bi ms-auto d-none bi-check2"></i>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button type="button"
|
<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto" aria-pressed="true">
|
||||||
class="dropdown-item d-flex align-items-center active"
|
|
||||||
data-bs-theme-value="auto"
|
|
||||||
aria-pressed="true">
|
|
||||||
<i class="bi me-2 opacity-50 theme-icon bi-circle-half"></i>
|
<i class="bi me-2 opacity-50 theme-icon bi-circle-half"></i>
|
||||||
Auto
|
Auto
|
||||||
<i class="bi ms-auto d-none bi-check2"></i>
|
<i class="bi ms-auto d-none bi-check2"></i>
|
||||||
@ -124,6 +103,5 @@
|
|||||||
<script src="{% static 'bootstrap.bundle.min.js' %}"></script>
|
<script src="{% static 'bootstrap.bundle.min.js' %}"></script>
|
||||||
<!-- Tabulator JS -->
|
<!-- Tabulator JS -->
|
||||||
<script src="{% static 'tabulator.min.js' %}"></script>
|
<script src="{% static 'tabulator.min.js' %}"></script>
|
||||||
{% block script %}
|
{% block script %}{% endblock %}
|
||||||
{% endblock %}
|
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
Reference in New Issue
Block a user