Compare commits
17 Commits
270e6c7837
...
bfa04be2d9
Author | SHA1 | Date | |
---|---|---|---|
bfa04be2d9 | |||
cbe8d24fe4 | |||
4561e317b8 | |||
be68946dcb | |||
37cb41af1b | |||
3728442680 | |||
27974e7de6 | |||
de0db9ac5a | |||
02777265b0 | |||
978024c538 | |||
8498d311d5 | |||
b8c2792f0a | |||
0633e4ecef | |||
aa143febeb | |||
27c705668c | |||
1fe097ca86 | |||
44692d8d9b |
12
.gitea/workflows/ruff.yml
Normal file
12
.gitea/workflows/ruff.yml
Normal file
@ -0,0 +1,12 @@
|
||||
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,12 +7,13 @@ repos:
|
||||
- id: check-yaml
|
||||
- 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
|
||||
rev: v1.34.0
|
||||
rev: v1.34.1
|
||||
hooks:
|
||||
- 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 traceback
|
||||
|
||||
from django.views.debug import ExceptionReporter
|
||||
from django.core import mail
|
||||
from django.views.debug import ExceptionReporter
|
||||
|
||||
|
||||
class AdminEmailReporter:
|
||||
|
@ -1,4 +1,4 @@
|
||||
from .dev_base import *
|
||||
from .dev_base import * # noqa: F403
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
||||
|
@ -1,4 +1,4 @@
|
||||
from .base import *
|
||||
from .base import * # noqa: F403
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||
@ -9,7 +9,7 @@ DEBUG = True
|
||||
|
||||
INTERNAL_IPS = ["127.0.0.1"]
|
||||
|
||||
INSTALLED_APPS.append("debug_toolbar")
|
||||
INSTALLED_APPS.append("django_extensions")
|
||||
INSTALLED_APPS.append("debug_toolbar") # noqa: F405
|
||||
INSTALLED_APPS.append("django_extensions") # noqa: F405
|
||||
|
||||
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware")
|
||||
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa: F405
|
||||
|
@ -1,7 +1,7 @@
|
||||
import ldap
|
||||
from django_auth_ldap.config import LDAPSearch, PosixGroupType, LDAPGroupQuery
|
||||
from django_auth_ldap.config import LDAPGroupQuery, LDAPSearch, PosixGroupType
|
||||
|
||||
from .base import *
|
||||
from .base import * # noqa: F403
|
||||
|
||||
DEBUG = False
|
||||
|
||||
|
@ -13,16 +13,15 @@ Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import include, path
|
||||
from django.contrib.auth.views import LoginView, LogoutView
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.views import LoginView, LogoutView
|
||||
from django.urls import include, path
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from paperwork.api import router as paperwork_router
|
||||
from membershipworks.api import router as membershipworks_router
|
||||
from paperwork.api import router as paperwork_router
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.registry.extend(paperwork_router.registry)
|
||||
|
@ -1,3 +1 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
@ -1,6 +1,7 @@
|
||||
from typing import Any
|
||||
|
||||
import dashboard
|
||||
|
||||
from .views import REPORTS
|
||||
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import contextlib
|
||||
import csv
|
||||
from datetime import datetime
|
||||
from io import StringIO
|
||||
@ -50,7 +51,7 @@ class DoorController:
|
||||
) # ignore insecure SSL
|
||||
xml = etree.XML(r.content)
|
||||
if (
|
||||
r.status_code != 200
|
||||
r.status_code != requests.codes.ok
|
||||
or len(xml.findall("{http://www.hidglobal.com/VertX}Error")) > 0
|
||||
):
|
||||
raise RemoteError(r)
|
||||
@ -75,7 +76,7 @@ class DoorController:
|
||||
)
|
||||
resp_xml = etree.XML(r.content)
|
||||
# probably meed to be more sane about this
|
||||
if r.status_code != 200 or len(resp_xml.findall("{*}Error")) > 0:
|
||||
if r.status_code != requests.codes.ok or len(resp_xml.findall("{*}Error")) > 0:
|
||||
raise RemoteError(r)
|
||||
return resp_xml
|
||||
|
||||
@ -120,11 +121,8 @@ class DoorController:
|
||||
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
|
||||
pass
|
||||
# don't care about failure to delete, they probably just didn't exist
|
||||
contextlib.suppress(self.doXMLRequest(delXML))
|
||||
|
||||
# load new schedules
|
||||
self.doXMLRequest(schedules)
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-19 04:20
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def link_events_to_doors(apps, schema_editor):
|
||||
|
@ -1,3 +1 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
@ -2,23 +2,19 @@ import calendar
|
||||
import datetime
|
||||
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.core.paginator import Page
|
||||
from django.core.exceptions import BadRequest
|
||||
from django.db.models import Count
|
||||
from django.db.models.functions import Trunc
|
||||
from django.urls import reverse_lazy, path
|
||||
from django.core.paginator import Page
|
||||
from django.db.models import Count, F, FloatField, Window
|
||||
from django.db.models.functions import Lead, Trunc
|
||||
from django.urls import path, reverse_lazy
|
||||
from django.utils import dateparse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.text import slugify
|
||||
from django.utils.timezone import localtime
|
||||
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
|
||||
|
||||
|
||||
REPORTS = []
|
||||
|
||||
|
||||
@ -217,7 +213,7 @@ class DeniedAccess(BaseAccessReport):
|
||||
"door name": event.door.name,
|
||||
"event type": HIDEvent.EventType(event.event_type).label,
|
||||
"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": (
|
||||
event.raw_card_number if event.raw_card_number is not None else ""
|
||||
@ -246,7 +242,7 @@ class MostActiveMembers(BaseAccessReport):
|
||||
{
|
||||
"cardholder id": count["cardholder_id"],
|
||||
"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"],
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
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_object_actions import DjangoObjectActions, action
|
||||
from django_q.tasks import async_task
|
||||
from django_q.models import Task
|
||||
from django_q.tasks import async_task
|
||||
|
||||
from .models import (
|
||||
Member,
|
||||
Flag,
|
||||
Transaction,
|
||||
Event,
|
||||
EventExt,
|
||||
EventMeetingTime,
|
||||
EventInstructor,
|
||||
EventMeetingTime,
|
||||
Flag,
|
||||
Member,
|
||||
Transaction,
|
||||
)
|
||||
from .tasks.scrape import scrape_membershipworks
|
||||
|
||||
@ -36,14 +36,14 @@ class BaseMembershipWorksAdmin(DjangoObjectActions, ReadOnlyAdmin):
|
||||
def _get_tool_dict(self, tool_name):
|
||||
tool = super(DjangoObjectActions, self)._get_tool_dict(tool_name)
|
||||
if tool_name == "refresh_membershipworks_data":
|
||||
last_run = (
|
||||
Task.objects.filter(group="Scrape Data from MembershipWorks")
|
||||
.order_by("started")
|
||||
.last()
|
||||
)
|
||||
last_run_time = (
|
||||
naturaltime(last_run.started) if last_run is not None else "Never"
|
||||
)
|
||||
try:
|
||||
last_run_time = naturaltime(
|
||||
Task.objects.filter(group="Scrape Data from MembershipWorks")
|
||||
.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
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from rest_framework import routers, serializers, viewsets
|
||||
|
||||
from .models import Member, Flag
|
||||
from .models import Flag, Member
|
||||
|
||||
|
||||
class MemberSerializer(serializers.HyperlinkedModelSerializer):
|
||||
|
@ -7,7 +7,7 @@ def post_migrate_callback(sender, **kwargs):
|
||||
|
||||
from cmsmanage.django_q2_helper import ensure_scheduled
|
||||
|
||||
from .tasks.scrape import scrape_membershipworks, scrape_events
|
||||
from .tasks.scrape import scrape_events, scrape_membershipworks
|
||||
from .tasks.ucsAccounts import sync_accounts
|
||||
|
||||
ensure_scheduled(
|
||||
|
@ -1,5 +1,4 @@
|
||||
from typing import Any
|
||||
from datetime import datetime
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
@ -16,11 +15,7 @@ class MembershipworksDashboardFragment(dashboard.DashboardFragment):
|
||||
links = {}
|
||||
|
||||
if self.request.user.has_perm("membershipworks.view_event"):
|
||||
now = datetime.now()
|
||||
links["Event Report"] = reverse(
|
||||
"membershipworks:event-report",
|
||||
kwargs={"year": now.year, "month": now.month},
|
||||
)
|
||||
links["Event Report"] = reverse("membershipworks:event-index-report")
|
||||
|
||||
return {"links": links}
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import csv
|
||||
import datetime
|
||||
from enum import Enum
|
||||
from io import StringIO
|
||||
|
||||
import requests
|
||||
import datetime
|
||||
|
||||
BASE_URL = "https://api.membershipworks.com"
|
||||
|
||||
@ -32,18 +33,20 @@ CRM = {
|
||||
27: "Payment",
|
||||
}
|
||||
|
||||
# Types of fields, extracted from a html snippet in all.js + some guessing
|
||||
typ = {
|
||||
1: "Text input",
|
||||
2: "Password", # inferred from data
|
||||
3: "Simple text area",
|
||||
4: "Rich text area",
|
||||
7: "Address",
|
||||
8: "Check box",
|
||||
9: "Select",
|
||||
11: "Display value stored in field (ie. read only)",
|
||||
12: "Required waiver/terms",
|
||||
}
|
||||
|
||||
# Types of fields ("typ"), extracted from a html snippet in all.js + some guessing
|
||||
class FieldType(Enum):
|
||||
TEXT_INPUT = 1
|
||||
PASSWORD = 2 # inferred from data
|
||||
SIMPLE_TEXT_AREA = 3
|
||||
RICH_TEXT_AREA = 4
|
||||
ADDRESS = 7
|
||||
CHECKBOX = 8
|
||||
SELECT = 9
|
||||
# Display value stored in field
|
||||
READ_ONLY = 11
|
||||
REQUIRED_WAIVER_TERMS = 12
|
||||
|
||||
|
||||
# more constants, this time extracted from the members csv export in all.js
|
||||
staticFlags = {
|
||||
@ -85,7 +88,7 @@ class MembershipWorks:
|
||||
data={"eml": username, "pwd": password},
|
||||
headers={"X-Org": "10000"},
|
||||
)
|
||||
if r.status_code != 200 or "SF" not in r.json():
|
||||
if r.status_code != requests.codes.ok or "SF" not in r.json():
|
||||
raise MembershipWorksRemoteError("login", r)
|
||||
self.org_info = r.json()
|
||||
self.auth_token = self.org_info["SF"]
|
||||
@ -133,11 +136,10 @@ class MembershipWorks:
|
||||
for screen_type in ["anm", "acc", "adm"]:
|
||||
for box in self.org_info["tpl"][screen_type]:
|
||||
for element in box["box"]:
|
||||
if type(element["dat"]) != str:
|
||||
if not isinstance(element["dat"], str):
|
||||
for field in element["dat"]:
|
||||
if "_id" in field:
|
||||
if field["_id"] not in fields:
|
||||
fields[field["_id"]] = field
|
||||
if "_id" in field and field["_id"] not in fields:
|
||||
fields[field["_id"]] = field
|
||||
|
||||
return fields
|
||||
|
||||
@ -168,13 +170,13 @@ class MembershipWorks:
|
||||
BASE_URL + "/v2/accounts",
|
||||
params={"dek": ",".join([folder_map[f] for f in folders])},
|
||||
)
|
||||
if r.status_code != 200 or "usr" not in r.json():
|
||||
if r.status_code != requests.codes.ok or "usr" not in r.json():
|
||||
raise MembershipWorksRemoteError("user listing", r)
|
||||
|
||||
# get list of member ID matching the search
|
||||
# dedup with set() to work around people with alt uids
|
||||
# TODO: figure out why people have alt uids
|
||||
return set(user["uid"] for user in r.json()["usr"])
|
||||
return {user["uid"] for user in r.json()["usr"]}
|
||||
|
||||
# TODO: has issues with aliasing header names:
|
||||
# ex: "Personal Studio Space" Label vs Membership Addon/Field
|
||||
@ -196,7 +198,7 @@ class MembershipWorks:
|
||||
"var": columns,
|
||||
},
|
||||
)
|
||||
if r.status_code != 200:
|
||||
if r.status_code != requests.codes.ok:
|
||||
raise MembershipWorksRemoteError("csv generation", r)
|
||||
|
||||
if r.text[0] == "\ufeff":
|
||||
@ -215,13 +217,13 @@ class MembershipWorks:
|
||||
r = self._get_v1(
|
||||
BASE_URL + "/v1/csv",
|
||||
params={
|
||||
"crm": ",".join(str(k) for k in CRM.keys()),
|
||||
"crm": ",".join(str(k) for k in CRM),
|
||||
**({"txl": ""} if json else {}),
|
||||
"sdp": start_date.strftime("%s"),
|
||||
"edp": end_date.strftime("%s"),
|
||||
},
|
||||
)
|
||||
if r.status_code != 200:
|
||||
if r.status_code != requests.codes.ok:
|
||||
raise MembershipWorksRemoteError("csv generation", r)
|
||||
if json:
|
||||
return r.json()
|
||||
|
@ -4,7 +4,6 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("membershipworks", "0001_initial"),
|
||||
]
|
||||
|
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(),
|
||||
),
|
||||
),
|
||||
]
|
@ -1,10 +1,19 @@
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import django.core.mail.message
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.db.models import Exists, F, OuterRef, Sum
|
||||
from django.db.models import (
|
||||
Count,
|
||||
Exists,
|
||||
ExpressionWrapper,
|
||||
F,
|
||||
OuterRef,
|
||||
Q,
|
||||
Subquery,
|
||||
Sum,
|
||||
)
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
@ -376,6 +385,11 @@ class Event(BaseModel):
|
||||
category = models.ForeignKey(EventCategory, on_delete=models.PROTECT)
|
||||
calendar = models.IntegerField(choices=EventCalendar)
|
||||
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:
|
||||
# "lgo": {
|
||||
# "l": "https://d1tif55lvfk8gc.cloudfront.net/656e3842ae3975908b05e304.jpg?1673405126",
|
||||
@ -413,9 +427,36 @@ class EventInstructor(models.Model):
|
||||
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"]):
|
||||
def get_queryset(self) -> models.QuerySet["EventExt"]:
|
||||
return super().get_queryset().annotate(duration=Sum("meeting_times__duration"))
|
||||
return EventExtQuerySet(self.model, using=self._db).annotate(
|
||||
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):
|
||||
@ -436,14 +477,6 @@ class EventExt(Event):
|
||||
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:
|
||||
verbose_name = "event"
|
||||
|
||||
|
@ -18,9 +18,6 @@ class MembershipWorksRouter:
|
||||
return None
|
||||
|
||||
def allow_relation(self, obj1, obj2, **hints):
|
||||
if (
|
||||
obj1._meta.app_label == self.app_label
|
||||
or obj2._meta.app_label == self.app_label
|
||||
):
|
||||
if self.app_label in (obj1._meta.app_label, obj2._meta.app_label):
|
||||
return True
|
||||
return None
|
||||
|
@ -1,18 +1,18 @@
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
||||
from membershipworks.membershipworks_api import FieldType, MembershipWorks
|
||||
from membershipworks.models import (
|
||||
Member,
|
||||
Flag,
|
||||
Transaction,
|
||||
Event,
|
||||
EventExt,
|
||||
EventCategory,
|
||||
EventExt,
|
||||
Flag,
|
||||
Member,
|
||||
Transaction,
|
||||
)
|
||||
from membershipworks import MembershipWorks
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -24,9 +24,8 @@ def flags_for_member(csv_member, all_flags, folders):
|
||||
if flag.type == "folder":
|
||||
if csv_member["Account ID"] in folders[flag.id]:
|
||||
yield flag
|
||||
else:
|
||||
if csv_member[flag.name] == flag.name:
|
||||
yield flag
|
||||
elif csv_member[flag.name] == flag.name:
|
||||
yield flag
|
||||
|
||||
|
||||
def update_flags(mw_flags) -> list[Flag]:
|
||||
@ -50,12 +49,13 @@ def scrape_members(membershipworks: MembershipWorks):
|
||||
logger.info("Getting/Updating members...")
|
||||
members = membershipworks.get_all_members()
|
||||
for csv_member in members:
|
||||
for field_id, field in membershipworks._all_fields().items():
|
||||
for field in membershipworks._all_fields().values():
|
||||
# convert checkboxes to real booleans
|
||||
if field.get("typ") == 8 and field["lbl"] in csv_member:
|
||||
csv_member[field["lbl"]] = (
|
||||
True if csv_member[field["lbl"]] == "Y" else False
|
||||
)
|
||||
if (
|
||||
field.get("typ") == FieldType.CHECKBOX.value
|
||||
and field["lbl"] in csv_member
|
||||
):
|
||||
csv_member[field["lbl"]] = csv_member[field["lbl"]] == "Y"
|
||||
|
||||
# create/update member
|
||||
member = Member.from_api_dict(csv_member)
|
||||
@ -80,10 +80,8 @@ def scrape_transactions(membershipworks: MembershipWorks):
|
||||
# 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)]
|
||||
assert all(
|
||||
[
|
||||
t["Account ID"] == t.get("uid", "") and t["Payment ID"] == t.get("sid", "")
|
||||
for t in transactions
|
||||
]
|
||||
t["Account ID"] == t.get("uid", "") and t["Payment ID"] == t.get("sid", "")
|
||||
for t in transactions
|
||||
)
|
||||
|
||||
for csv_transaction in transactions:
|
||||
|
@ -4,10 +4,11 @@ import re
|
||||
import string
|
||||
|
||||
from django.conf import settings
|
||||
from udm_rest_client.udm import UDM
|
||||
from udm_rest_client.exceptions import NoObject, UdmError
|
||||
|
||||
from membershipworks.models import Member, Flag
|
||||
from udm_rest_client.exceptions import NoObject, UdmError
|
||||
from udm_rest_client.udm import UDM
|
||||
|
||||
from membershipworks.models import Flag, Member
|
||||
|
||||
USER_BASE = "cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||
GROUP_BASE = "cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||
|
@ -0,0 +1,42 @@
|
||||
{% 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,6 +3,13 @@
|
||||
{% 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">
|
||||
<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 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
@ -21,7 +28,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for event in object_list %}
|
||||
<tr>
|
||||
<tr {% if not event.occurred %}class="text-decoration-line-through table-danger"{% endif %}>
|
||||
<td>
|
||||
<a href="https://membershipworks.com/admin/#!event/admin/{{ event.url }}">{{ event.title }}</a>
|
||||
</td>
|
||||
@ -30,7 +37,7 @@
|
||||
<td>{{ event.category }}</td>
|
||||
<td>{{ event.count }}</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.person_hours|duration_as_hours }}</td>
|
||||
</tr>
|
||||
@ -43,7 +50,7 @@
|
||||
{% if previous_month %}
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
href="{% url 'membershipworks:event-report' previous_month|date:"Y" previous_month|date:"m" %}">
|
||||
href="{% url 'membershipworks:event-month-report' previous_month|date:"Y" previous_month|date:"m" %}">
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
{{ previous_month|date:"F Y" }}
|
||||
</a>
|
||||
@ -55,7 +62,7 @@
|
||||
{% if next_month %}
|
||||
<li class="page-item">
|
||||
<a class="page-link"
|
||||
href="{% url 'membershipworks:event-report' next_month|date:"Y" next_month|date:"m" %}">
|
||||
href="{% url 'membershipworks:event-month-report' next_month|date:"Y" next_month|date:"m" %}">
|
||||
{{ next_month|date:"F Y" }}
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</a>
|
@ -0,0 +1,68 @@
|
||||
{% 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,3 +1 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
@ -1,6 +1,12 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import MemberAutocomplete, upcoming_events, EventReport
|
||||
from .views import (
|
||||
EventIndexReport,
|
||||
EventMonthReport,
|
||||
EventYearReport,
|
||||
MemberAutocomplete,
|
||||
upcoming_events,
|
||||
)
|
||||
|
||||
app_name = "membershipworks"
|
||||
|
||||
@ -15,9 +21,19 @@ urlpatterns = [
|
||||
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(
|
||||
"event-report/<int:year>/<int:month>/",
|
||||
EventReport.as_view(month_format="%m"),
|
||||
name="event-report",
|
||||
EventMonthReport.as_view(month_format="%m"),
|
||||
name="event-month-report",
|
||||
),
|
||||
]
|
||||
|
@ -2,15 +2,21 @@ from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
from django.db.models.functions import TruncMonth, TruncYear
|
||||
from django.shortcuts import render
|
||||
from django.views.generic.dates import MonthArchiveView
|
||||
from django.views.generic.dates import (
|
||||
ArchiveIndexView,
|
||||
MonthArchiveView,
|
||||
YearArchiveView,
|
||||
)
|
||||
|
||||
from dal import autocomplete
|
||||
|
||||
from .models import Member, EventExt
|
||||
from membershipworks import MembershipWorks
|
||||
from membershipworks.membershipworks_api import MembershipWorks
|
||||
|
||||
from .models import EventExt, Member
|
||||
|
||||
|
||||
class MemberAutocomplete(autocomplete.Select2QuerySetView):
|
||||
@ -24,8 +30,7 @@ class MemberAutocomplete(autocomplete.Select2QuerySetView):
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
@login_required
|
||||
# TODO: permission required?
|
||||
@permission_required("membershipworks.view_eventext")
|
||||
def upcoming_events(request):
|
||||
now = datetime.now()
|
||||
|
||||
@ -104,8 +109,42 @@ def upcoming_events(request):
|
||||
return render(request, "membershipworks/upcoming_events.dj.html", context)
|
||||
|
||||
|
||||
class EventReport(PermissionRequiredMixin, MonthArchiveView):
|
||||
class EventIndexReport(PermissionRequiredMixin, ArchiveIndexView):
|
||||
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"
|
||||
queryset = EventExt.objects.select_related("category", "instructor").all()
|
||||
date_field = "start"
|
||||
template_name = "membershipworks/event_report.dj.html"
|
||||
template_name = "membershipworks/event_month_report.dj.html"
|
||||
|
@ -1,29 +1,29 @@
|
||||
from typing import Optional, Any, Type, cast
|
||||
from typing import Any, cast
|
||||
|
||||
from django import forms
|
||||
from django.core import mail
|
||||
from django.contrib import admin, messages
|
||||
from django.core import mail
|
||||
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.functions import Now, Concat, LPad
|
||||
from django.http import HttpRequest
|
||||
|
||||
from .certification_emails import all_certification_emails
|
||||
from .forms import CertificationForm
|
||||
from .models import (
|
||||
AbstractAudit,
|
||||
CmsRedRiverVeteransScholarship,
|
||||
Department,
|
||||
CertificationDefinition,
|
||||
Certification,
|
||||
CertificationAudit,
|
||||
CertificationDefinition,
|
||||
CertificationVersion,
|
||||
CertificationVersionAnnotated,
|
||||
CmsRedRiverVeteransScholarship,
|
||||
Department,
|
||||
InstructorOrVendor,
|
||||
SpecialProgram,
|
||||
Waiver,
|
||||
WaiverAudit,
|
||||
)
|
||||
from .forms import CertificationForm
|
||||
from .certification_emails import all_certification_emails
|
||||
|
||||
|
||||
class AlwaysChangedModelForm(forms.models.ModelForm):
|
||||
@ -38,8 +38,8 @@ class AbstractAuditInline(admin.TabularInline):
|
||||
form = AlwaysChangedModelForm
|
||||
|
||||
def get_formset(
|
||||
self, request: HttpRequest, obj: Optional[AbstractAudit] = None, **kwargs: Any
|
||||
) -> Type[
|
||||
self, request: HttpRequest, obj: AbstractAudit | None = None, **kwargs: Any
|
||||
) -> type[
|
||||
"forms.models.BaseInlineFormSet[AbstractAudit, Any, forms.models.ModelForm[Any]]"
|
||||
]:
|
||||
formset = super().get_formset(request, obj, **kwargs)
|
||||
|
@ -1,14 +1,16 @@
|
||||
from rest_framework import routers, serializers, viewsets
|
||||
from django.db.models import Prefetch, Q
|
||||
|
||||
from rest_framework import routers, serializers, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from membershipworks.models import Member
|
||||
|
||||
from .models import (
|
||||
Department,
|
||||
Certification,
|
||||
CertificationDefinition,
|
||||
CertificationVersion,
|
||||
Department,
|
||||
)
|
||||
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
from dal import autocomplete
|
||||
from django.db.models import CharField, Q, Value
|
||||
from django.db.models.functions import Concat
|
||||
from django.db.models import Q, Value, CharField
|
||||
|
||||
from dal import autocomplete
|
||||
|
||||
from .models import CertificationVersion
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
from itertools import groupby
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core import mail
|
||||
from django.core.mail.message import sanitize_address
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.template import loader
|
||||
|
||||
from markdownify import markdownify
|
||||
import mdformat
|
||||
from markdownify import markdownify
|
||||
|
||||
|
||||
def make_multipart_email(subject, html_body, to):
|
||||
|
@ -4,6 +4,7 @@ from django.urls import reverse
|
||||
|
||||
import dashboard
|
||||
from membershipworks.models import Member
|
||||
|
||||
from .models import Department
|
||||
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
from dal import autocomplete
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from .models import CertificationDefinition, Certification
|
||||
from dal import autocomplete
|
||||
|
||||
from .models import Certification, CertificationDefinition
|
||||
|
||||
|
||||
class CertificationForm(forms.ModelForm):
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-03 21:12
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,7 +1,7 @@
|
||||
# (Partially) Generated by Django 4.0.2 on 2022-02-04 18:01
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def migrate_certification_version_forward(apps, schema_editor):
|
||||
|
@ -1,8 +1,8 @@
|
||||
# Generated by Django 4.1.3 on 2023-01-18 03:31
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def link_departments(apps, schema_editor):
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Generated by Django 4.1.3 on 2023-01-24 02:02
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -3,7 +3,6 @@
|
||||
from datetime import date
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
from semver import VersionInfo
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
# Generated by Django 4.2 on 2023-04-10 05:34
|
||||
|
||||
import datetime
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Generated by Django 4.2 on 2023-04-10 18:37
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,16 +1,18 @@
|
||||
import datetime
|
||||
import re
|
||||
from typing import TypedDict, TYPE_CHECKING, Optional
|
||||
from typing import TYPE_CHECKING, TypedDict
|
||||
|
||||
from semver import VersionInfo
|
||||
from django.conf import settings
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q, Window
|
||||
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 membershipworks.models import Member, Flag as MembershipWorksFlag
|
||||
from django_stubs_ext import WithAnnotations
|
||||
from semver import VersionInfo
|
||||
|
||||
from membershipworks.models import Flag as MembershipWorksFlag
|
||||
from membershipworks.models import Member
|
||||
|
||||
|
||||
class AbstractAudit(models.Model):
|
||||
@ -104,14 +106,14 @@ class Department(models.Model):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def list_name(self) -> Optional[str]:
|
||||
def list_name(self) -> str | None:
|
||||
if self.has_mailing_list:
|
||||
return self.name.replace(" ", "_") + "-info"
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def list_address(self) -> Optional[str]:
|
||||
def list_address(self) -> str | None:
|
||||
if self.list_name:
|
||||
return self.list_name + "@claremontmakerspace.org"
|
||||
else:
|
||||
|
@ -1,3 +1 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
@ -1,7 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
from . import autocomplete_views
|
||||
from . import autocomplete_views, views
|
||||
|
||||
app_name = "paperwork"
|
||||
|
||||
|
@ -9,6 +9,7 @@ import requests
|
||||
import weasyprint
|
||||
|
||||
from membershipworks.models import Member
|
||||
|
||||
from .models import Certification, Department
|
||||
|
||||
WIKI_URL = settings.WIKI_URL
|
||||
|
62
pdm.lock
62
pdm.lock
@ -5,7 +5,7 @@
|
||||
groups = ["default", "debug", "lint", "server", "typing", "dev"]
|
||||
strategy = ["cross_platform"]
|
||||
lock_version = "4.4.1"
|
||||
content_hash = "sha256:91f554bae127245b4082d069629400706b8b43daf3bf1fb8fd963eee120ff449"
|
||||
content_hash = "sha256:73715d6c541091f09cb8dea4f2baba6b58b0972a615b44e6f85869f918fdb360"
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
@ -164,31 +164,6 @@ files = [
|
||||
{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]]
|
||||
name = "brotli"
|
||||
version = "1.0.9"
|
||||
@ -1291,16 +1266,6 @@ files = [
|
||||
{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]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.41"
|
||||
@ -1554,6 +1519,31 @@ files = [
|
||||
{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]]
|
||||
name = "semver"
|
||||
version = "3.0.2"
|
||||
|
@ -41,8 +41,24 @@ server = [
|
||||
[project.entry-points."djangoq.errorreporters"]
|
||||
admin_email = "cmsmanage.django_q2_admin_email_reporter:AdminEmailReporter"
|
||||
|
||||
[tool.black]
|
||||
[tool.ruff]
|
||||
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]
|
||||
profile="django"
|
||||
@ -82,8 +98,8 @@ include_packages = ["openapi-client-udm"]
|
||||
|
||||
[tool.pdm.dev-dependencies]
|
||||
lint = [
|
||||
"black~=23.12",
|
||||
"djlint~=1.34",
|
||||
"ruff~=0.1",
|
||||
]
|
||||
typing = [
|
||||
"mypy~=1.7",
|
||||
|
@ -1,8 +1,9 @@
|
||||
from django.db.models import Prefetch
|
||||
from django.contrib import admin
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.db.models import Prefetch
|
||||
|
||||
from membershipworks.models import Member
|
||||
|
||||
from .models import LockerBank, LockerInfo, LockerUnit
|
||||
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
# Generated by Django 4.0.2 on 2022-02-16 21:19
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -3,8 +3,7 @@ from django.core import validators
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models, transaction
|
||||
from django.db.models import F, Q
|
||||
from django.db.models.functions import Chr, Ord, Concat
|
||||
|
||||
from django.db.models.functions import Chr, Concat, Ord
|
||||
|
||||
from membershipworks.models import Member
|
||||
|
||||
|
@ -1,3 +1 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
@ -1,12 +1,11 @@
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
|
||||
from .models import LockerBank, LockerInfo
|
||||
|
||||
from .forms import LockerInfoForm
|
||||
from .models import LockerBank, LockerInfo
|
||||
|
||||
|
||||
def lockerIndex(request):
|
||||
@ -32,10 +31,7 @@ def lockerIndex(request):
|
||||
@permission_required("rentals.change_lockerinfo", raise_exception=True)
|
||||
def lockerUpdate(request, locker_id: int):
|
||||
if request.method == "POST":
|
||||
try:
|
||||
instance = LockerInfo.objects.get(pk=locker_id)
|
||||
except LockerInfo.DoesNotExist:
|
||||
pass # TODO
|
||||
instance = get_object_or_404(LockerInfo, pk=locker_id)
|
||||
|
||||
form = LockerInfoForm(request.POST, instance=instance)
|
||||
if form.is_valid():
|
||||
|
@ -1,7 +1,8 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from markdownx.admin import MarkdownxModelAdmin
|
||||
|
||||
from .models import Tool, Task, Event, GroupTaskSubscription, GroupToolSubscription
|
||||
from .models import Event, GroupTaskSubscription, GroupToolSubscription, Task, Tool
|
||||
|
||||
|
||||
class GroupTaskSubscriptionInline(admin.TabularInline):
|
||||
|
@ -1,6 +1,7 @@
|
||||
from datetime import datetime
|
||||
|
||||
from django import forms
|
||||
|
||||
from markdownx.widgets import MarkdownxWidget
|
||||
|
||||
from .models import Event
|
||||
|
@ -1,10 +1,8 @@
|
||||
from itertools import groupby
|
||||
|
||||
from django.core.mail import send_mail
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.template import loader
|
||||
|
||||
from tasks.models import Tool, Task, Event, GroupToolSubscription, GroupTaskSubscription
|
||||
from tasks.models import GroupTaskSubscription, GroupToolSubscription
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
@ -1,8 +1,9 @@
|
||||
# Generated by Django 3.2.3 on 2021-05-19 18:05
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
import markdownx.models
|
||||
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Generated by Django 3.2.3 on 2021-05-19 21:46
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
import recurrence
|
||||
import recurrence.fields
|
||||
|
||||
|
@ -4,6 +4,7 @@ from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from markdownx.models import MarkdownxField
|
||||
from recurrence.fields import RecurrenceField
|
||||
|
||||
|
@ -120,9 +120,7 @@
|
||||
<tr>
|
||||
<td class="text-nowrap">{{ event.date }}</td>
|
||||
<td>{{ event.user }}</td>
|
||||
<td>
|
||||
{{ event.notes_html|safe }}
|
||||
</td>
|
||||
<td>{{ event.notes_html|safe }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -1,3 +1 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
@ -1,8 +1,7 @@
|
||||
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 .models import Event, Task, Tool
|
||||
|
||||
|
||||
def index(request):
|
||||
|
@ -21,6 +21,11 @@
|
||||
<nav class="navbar navbar-expand-sm bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<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"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
@ -34,27 +39,43 @@
|
||||
<div class="navbar-nav">
|
||||
{% block nav_extra %}{% endblock %}
|
||||
<div class="nav-item dropdown">
|
||||
<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)">
|
||||
<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)">
|
||||
<i class="bi theme-icon-active bi-circle-half"></i>
|
||||
<span class="d-none ms-2" id="bd-theme-text">Toggle theme</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="bd-theme-text">
|
||||
<ul class="dropdown-menu dropdown-menu-end"
|
||||
aria-labelledby="bd-theme-text">
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="light" aria-pressed="false">
|
||||
<button type="button"
|
||||
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>
|
||||
Light
|
||||
<i class="bi ms-auto d-none bi-check2"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center" data-bs-theme-value="dark" aria-pressed="false">
|
||||
<button type="button"
|
||||
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>
|
||||
Dark
|
||||
<i class="bi ms-auto d-none bi-check2"></i>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item d-flex align-items-center active" data-bs-theme-value="auto" aria-pressed="true">
|
||||
<button type="button"
|
||||
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>
|
||||
Auto
|
||||
<i class="bi ms-auto d-none bi-check2"></i>
|
||||
@ -103,5 +124,6 @@
|
||||
<script src="{% static 'bootstrap.bundle.min.js' %}"></script>
|
||||
<!-- Tabulator JS -->
|
||||
<script src="{% static 'tabulator.min.js' %}"></script>
|
||||
{% block script %}{% endblock %}
|
||||
{% block script %}
|
||||
{% endblock %}
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue
Block a user