Compare commits

..

17 Commits

Author SHA1 Message Date
bfa04be2d9 membershipworks: Add event index and year reports 2024-01-19 15:41:27 -05:00
cbe8d24fe4 membershipworks: Add generated field to check if an event occured 2024-01-19 15:22:05 -05:00
4561e317b8 Enable/apply ruff's "flake6-simplify" rules 2024-01-19 15:16:47 -05:00
be68946dcb Add Gitea action to check ruff linting/formatting 2024-01-19 15:04:08 -05:00
37cb41af1b Enable ruff's "pylint" rules 2024-01-18 14:21:36 -05:00
3728442680 Enable/apply ruff's "perflint" rules 2024-01-18 14:21:36 -05:00
27974e7de6 Enable/apply ruff's "pyupgrade" rules 2024-01-18 14:21:36 -05:00
de0db9ac5a Enable ruff's "comprehensions" rules 2024-01-18 14:21:36 -05:00
02777265b0 Switch from Black to Ruff for formatting, add linting/import sorting 2024-01-18 14:21:36 -05:00
978024c538 membershipworks: Annotate EventExt.meeting_times__count 2024-01-18 13:58:28 -05:00
8498d311d5 membershipworks: Convert EventExt.person_hours to annotation 2024-01-18 13:58:28 -05:00
b8c2792f0a membershipworks: Convert EventExt.duration annotation to a Subquery
should be somewhat less performant, but allows for easier aggregation
2024-01-18 13:58:28 -05:00
0633e4ecef membershipworks: Add breadcrumbs for EventMonthReport 2024-01-18 13:58:28 -05:00
aa143febeb Add breadcrumbs to base template header 2024-01-18 13:58:28 -05:00
27c705668c membershipworks: Slightly simplify admin task "last run time" logic 2024-01-18 13:58:28 -05:00
1fe097ca86 membershipworks: Require "view EventExt" permission for upcoming events 2024-01-18 13:58:28 -05:00
44692d8d9b membershipworks: Use more specific name for EventMonthReport 2024-01-18 13:58:28 -05:00
65 changed files with 498 additions and 250 deletions

12
.gitea/workflows/ruff.yml Normal file
View 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

View File

@ -7,12 +7,13 @@ 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.0 rev: v1.34.1
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

View File

@ -1,8 +1,8 @@
import sys import sys
import traceback import traceback
from django.views.debug import ExceptionReporter
from django.core import mail from django.core import mail
from django.views.debug import ExceptionReporter
class AdminEmailReporter: class AdminEmailReporter:

View File

@ -1,4 +1,4 @@
from .dev_base import * from .dev_base import * # noqa: F403
# 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/

View File

@ -1,4 +1,4 @@
from .base import * from .base import * # noqa: F403
# 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") INSTALLED_APPS.append("debug_toolbar") # noqa: F405
INSTALLED_APPS.append("django_extensions") INSTALLED_APPS.append("django_extensions") # noqa: F405
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa: F405

View File

@ -1,7 +1,7 @@
import ldap 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 DEBUG = False

View File

@ -13,16 +13,15 @@ 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.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.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 rest_framework import routers
from paperwork.api import router as paperwork_router
from membershipworks.api import router as membershipworks_router from membershipworks.api import router as membershipworks_router
from paperwork.api import router as paperwork_router
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.registry.extend(paperwork_router.registry) router.registry.extend(paperwork_router.registry)

View File

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View File

@ -1,6 +1,7 @@
from typing import Any from typing import Any
import dashboard import dashboard
from .views import REPORTS from .views import REPORTS

View File

@ -1,3 +1,4 @@
import contextlib
import csv import csv
from datetime import datetime from datetime import datetime
from io import StringIO from io import StringIO
@ -50,7 +51,7 @@ class DoorController:
) # ignore insecure SSL ) # ignore insecure SSL
xml = etree.XML(r.content) xml = etree.XML(r.content)
if ( if (
r.status_code != 200 r.status_code != requests.codes.ok
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)
@ -75,7 +76,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 != 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) raise RemoteError(r)
return resp_xml return resp_xml
@ -120,11 +121,8 @@ 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
pass contextlib.suppress(self.doXMLRequest(delXML))
# load new schedules # load new schedules
self.doXMLRequest(schedules) self.doXMLRequest(schedules)

View File

@ -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
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
def link_events_to_doors(apps, schema_editor): def link_events_to_doors(apps, schema_editor):

View File

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View File

@ -2,23 +2,19 @@ import calendar
import datetime import datetime
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.paginator import Page
from django.core.exceptions import BadRequest from django.core.exceptions import BadRequest
from django.db.models import Count from django.core.paginator import Page
from django.db.models.functions import Trunc from django.db.models import Count, F, FloatField, Window
from django.urls import reverse_lazy, path from django.db.models.functions import Lead, Trunc
from django.urls import path, reverse_lazy
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 = []
@ -217,7 +213,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 ""
@ -246,7 +242,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"],
} }

View File

@ -1 +0,0 @@
from .membershipworks_api import MembershipWorks, MembershipWorksRemoteError

View File

@ -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.tasks import async_task
from django_q.models import Task from django_q.models import Task
from django_q.tasks import async_task
from .models import ( from .models import (
Member,
Flag,
Transaction,
Event, Event,
EventExt, EventExt,
EventMeetingTime,
EventInstructor, EventInstructor,
EventMeetingTime,
Flag,
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":
last_run = ( try:
last_run_time = naturaltime(
Task.objects.filter(group="Scrape Data from MembershipWorks") Task.objects.filter(group="Scrape Data from MembershipWorks")
.order_by("started") .values_list("started", flat=True)
.last() .latest("started")
)
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

View File

@ -1,6 +1,6 @@
from rest_framework import routers, serializers, viewsets from rest_framework import routers, serializers, viewsets
from .models import Member, Flag from .models import Flag, Member
class MemberSerializer(serializers.HyperlinkedModelSerializer): class MemberSerializer(serializers.HyperlinkedModelSerializer):

View File

@ -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_membershipworks, scrape_events from .tasks.scrape import scrape_events, scrape_membershipworks
from .tasks.ucsAccounts import sync_accounts from .tasks.ucsAccounts import sync_accounts
ensure_scheduled( ensure_scheduled(

View File

@ -1,5 +1,4 @@
from typing import Any from typing import Any
from datetime import datetime
from django.urls import reverse from django.urls import reverse
@ -16,11 +15,7 @@ class MembershipworksDashboardFragment(dashboard.DashboardFragment):
links = {} links = {}
if self.request.user.has_perm("membershipworks.view_event"): if self.request.user.has_perm("membershipworks.view_event"):
now = datetime.now() links["Event Report"] = reverse("membershipworks:event-index-report")
links["Event Report"] = reverse(
"membershipworks:event-report",
kwargs={"year": now.year, "month": now.month},
)
return {"links": links} return {"links": links}

View File

@ -1,8 +1,9 @@
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"
@ -32,18 +33,20 @@ CRM = {
27: "Payment", 27: "Payment",
} }
# Types of fields, extracted from a html snippet in all.js + some guessing
typ = { # Types of fields ("typ"), extracted from a html snippet in all.js + some guessing
1: "Text input", class FieldType(Enum):
2: "Password", # inferred from data TEXT_INPUT = 1
3: "Simple text area", PASSWORD = 2 # inferred from data
4: "Rich text area", SIMPLE_TEXT_AREA = 3
7: "Address", RICH_TEXT_AREA = 4
8: "Check box", ADDRESS = 7
9: "Select", CHECKBOX = 8
11: "Display value stored in field (ie. read only)", SELECT = 9
12: "Required waiver/terms", # 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 # more constants, this time extracted from the members csv export in all.js
staticFlags = { staticFlags = {
@ -85,7 +88,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 != 200 or "SF" not in r.json(): if r.status_code != requests.codes.ok 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"]
@ -133,10 +136,9 @@ 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 type(element["dat"]) != str: if not isinstance(element["dat"], str):
for field in element["dat"]: for field in element["dat"]:
if "_id" in field: if "_id" in field and field["_id"] not in fields:
if field["_id"] not in fields:
fields[field["_id"]] = field fields[field["_id"]] = field
return fields return fields
@ -168,13 +170,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 != 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) 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 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: # 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
@ -196,7 +198,7 @@ class MembershipWorks:
"var": columns, "var": columns,
}, },
) )
if r.status_code != 200: if r.status_code != requests.codes.ok:
raise MembershipWorksRemoteError("csv generation", r) raise MembershipWorksRemoteError("csv generation", r)
if r.text[0] == "\ufeff": if r.text[0] == "\ufeff":
@ -215,13 +217,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.keys()), "crm": ",".join(str(k) for k in CRM),
**({"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 != 200: if r.status_code != requests.codes.ok:
raise MembershipWorksRemoteError("csv generation", r) raise MembershipWorksRemoteError("csv generation", r)
if json: if json:
return r.json() return r.json()

View File

@ -4,7 +4,6 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("membershipworks", "0001_initial"), ("membershipworks", "0001_initial"),
] ]

View 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(),
),
),
]

View File

@ -1,10 +1,19 @@
from typing import Optional
from datetime import datetime from datetime import datetime
from typing import Optional
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 Exists, F, OuterRef, Sum from django.db.models import (
Count,
Exists,
ExpressionWrapper,
F,
OuterRef,
Q,
Subquery,
Sum,
)
from django.utils import timezone from django.utils import timezone
@ -376,6 +385,11 @@ 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",
@ -413,9 +427,36 @@ 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 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): class EventExt(Event):
@ -436,14 +477,6 @@ 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"

View File

@ -18,9 +18,6 @@ class MembershipWorksRouter:
return None return None
def allow_relation(self, obj1, obj2, **hints): def allow_relation(self, obj1, obj2, **hints):
if ( if self.app_label in (obj1._meta.app_label, obj2._meta.app_label):
obj1._meta.app_label == self.app_label
or obj2._meta.app_label == self.app_label
):
return True return True
return None return None

View File

@ -1,18 +1,18 @@
from datetime import datetime, timedelta
import logging import logging
from datetime import datetime, timedelta
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 (
Member,
Flag,
Transaction,
Event, Event,
EventExt,
EventCategory, EventCategory,
EventExt,
Flag,
Member,
Transaction,
) )
from membershipworks import MembershipWorks
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -24,8 +24,7 @@ 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
else: elif csv_member[flag.name] == flag.name:
if csv_member[flag.name] == flag.name:
yield flag yield flag
@ -50,12 +49,13 @@ 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_id, field in membershipworks._all_fields().items(): for field in membershipworks._all_fields().values():
# convert checkboxes to real booleans # convert checkboxes to real booleans
if field.get("typ") == 8 and field["lbl"] in csv_member: if (
csv_member[field["lbl"]] = ( field.get("typ") == FieldType.CHECKBOX.value
True if csv_member[field["lbl"]] == "Y" else False and field["lbl"] in csv_member
) ):
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,10 +80,8 @@ 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:

View File

@ -4,10 +4,11 @@ import re
import string import string
from django.conf import settings 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" 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"

View File

@ -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 %}

View File

@ -3,6 +3,13 @@
{% 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">
@ -21,7 +28,7 @@
</thead> </thead>
<tbody> <tbody>
{% for event in object_list %} {% for event in object_list %}
<tr> <tr {% if not event.occurred %}class="text-decoration-line-through table-danger"{% endif %}>
<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>
@ -30,7 +37,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>
@ -43,7 +50,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-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> <i class="bi bi-arrow-left"></i>
{{ previous_month|date:"F Y" }} {{ previous_month|date:"F Y" }}
</a> </a>
@ -55,7 +62,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-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" }} {{ next_month|date:"F Y" }}
<i class="bi bi-arrow-right"></i> <i class="bi bi-arrow-right"></i>
</a> </a>

View File

@ -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 %}

View File

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View File

@ -1,6 +1,12 @@
from django.urls import path from django.urls import path
from .views import MemberAutocomplete, upcoming_events, EventReport from .views import (
EventIndexReport,
EventMonthReport,
EventYearReport,
MemberAutocomplete,
upcoming_events,
)
app_name = "membershipworks" app_name = "membershipworks"
@ -15,9 +21,19 @@ 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>/",
EventReport.as_view(month_format="%m"), EventMonthReport.as_view(month_format="%m"),
name="event-report", name="event-month-report",
), ),
] ]

View File

@ -2,15 +2,21 @@ 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 login_required from django.contrib.auth.decorators import permission_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 MonthArchiveView from django.views.generic.dates import (
ArchiveIndexView,
MonthArchiveView,
YearArchiveView,
)
from dal import autocomplete from dal import autocomplete
from .models import Member, EventExt from membershipworks.membershipworks_api import MembershipWorks
from membershipworks import MembershipWorks
from .models import EventExt, Member
class MemberAutocomplete(autocomplete.Select2QuerySetView): class MemberAutocomplete(autocomplete.Select2QuerySetView):
@ -24,8 +30,7 @@ class MemberAutocomplete(autocomplete.Select2QuerySetView):
return super().get_queryset() return super().get_queryset()
@login_required @permission_required("membershipworks.view_eventext")
# TODO: permission required?
def upcoming_events(request): def upcoming_events(request):
now = datetime.now() now = datetime.now()
@ -104,8 +109,42 @@ def upcoming_events(request):
return render(request, "membershipworks/upcoming_events.dj.html", context) 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" 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_report.dj.html" template_name = "membershipworks/event_month_report.dj.html"

View File

@ -1,29 +1,29 @@
from typing import Optional, Any, Type, cast from typing import Any, cast
from django import forms from django import forms
from django.core import mail
from django.contrib import admin, messages from django.contrib import admin, messages
from django.core import mail
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,
CmsRedRiverVeteransScholarship,
Department,
CertificationDefinition,
Certification, Certification,
CertificationAudit, CertificationAudit,
CertificationDefinition,
CertificationVersion, CertificationVersion,
CertificationVersionAnnotated, CertificationVersionAnnotated,
CmsRedRiverVeteransScholarship,
Department,
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: Optional[AbstractAudit] = None, **kwargs: Any self, request: HttpRequest, obj: AbstractAudit | None = 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)

View File

@ -1,14 +1,16 @@
from rest_framework import routers, serializers, viewsets
from django.db.models import Prefetch, Q from django.db.models import Prefetch, Q
from rest_framework import routers, serializers, viewsets
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,
) )

View File

@ -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.functions import Concat
from django.db.models import Q, Value, CharField
from dal import autocomplete
from .models import CertificationVersion from .models import CertificationVersion

View File

@ -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
from markdownify import markdownify
import mdformat import mdformat
from markdownify import markdownify
def make_multipart_email(subject, html_body, to): def make_multipart_email(subject, html_body, to):

View File

@ -4,6 +4,7 @@ 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

View File

@ -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 .models import CertificationDefinition, Certification from dal import autocomplete
from .models import Certification, CertificationDefinition
class CertificationForm(forms.ModelForm): class CertificationForm(forms.ModelForm):

View File

@ -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
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -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
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
def migrate_certification_version_forward(apps, schema_editor): def migrate_certification_version_forward(apps, schema_editor):

View File

@ -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
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
def link_departments(apps, schema_editor): def link_departments(apps, schema_editor):

View File

@ -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
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -3,7 +3,6 @@
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

View File

@ -1,9 +1,10 @@
# 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):

View File

@ -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
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,16 +1,18 @@
import datetime import datetime
import re 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 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 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): class AbstractAudit(models.Model):
@ -104,14 +106,14 @@ class Department(models.Model):
return self.name return self.name
@property @property
def list_name(self) -> Optional[str]: def list_name(self) -> str | None:
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) -> Optional[str]: def list_address(self) -> str | None:
if self.list_name: if self.list_name:
return self.list_name + "@claremontmakerspace.org" return self.list_name + "@claremontmakerspace.org"
else: else:

View File

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View File

@ -1,7 +1,6 @@
from django.urls import path from django.urls import path
from . import views from . import autocomplete_views, views
from . import autocomplete_views
app_name = "paperwork" app_name = "paperwork"

View File

@ -9,6 +9,7 @@ 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

View File

@ -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:91f554bae127245b4082d069629400706b8b43daf3bf1fb8fd963eee120ff449" content_hash = "sha256:73715d6c541091f09cb8dea4f2baba6b58b0972a615b44e6f85869f918fdb360"
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@ -164,31 +164,6 @@ 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"
@ -1291,16 +1266,6 @@ 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"
@ -1554,6 +1519,31 @@ 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"

View File

@ -41,8 +41,24 @@ 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.black] [tool.ruff]
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"
@ -82,8 +98,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",

View File

@ -1,8 +1,9 @@
from django.db.models import Prefetch
from django.contrib import admin
from django import forms from django import forms
from django.contrib import admin
from django.db.models import Prefetch
from membershipworks.models import Member from membershipworks.models import Member
from .models import LockerBank, LockerInfo, LockerUnit from .models import LockerBank, LockerInfo, LockerUnit

View File

@ -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
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -3,8 +3,7 @@ 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, Ord, Concat from django.db.models.functions import Chr, Concat, Ord
from membershipworks.models import Member from membershipworks.models import Member

View File

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View File

@ -1,12 +1,11 @@
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.http import HttpResponseRedirect 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 django.urls import reverse
from .models import LockerBank, LockerInfo
from .forms import LockerInfoForm from .forms import LockerInfoForm
from .models import LockerBank, LockerInfo
def lockerIndex(request): def lockerIndex(request):
@ -32,10 +31,7 @@ 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":
try: instance = get_object_or_404(LockerInfo, pk=locker_id)
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():

View File

@ -1,7 +1,8 @@
from django.contrib import admin from django.contrib import admin
from markdownx.admin import MarkdownxModelAdmin 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): class GroupTaskSubscriptionInline(admin.TabularInline):

View File

@ -1,6 +1,7 @@
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

View File

@ -1,10 +1,8 @@
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, CommandError from django.core.management.base import BaseCommand
from django.template import loader from django.template import loader
from tasks.models import Tool, Task, Event, GroupToolSubscription, GroupTaskSubscription from tasks.models import GroupTaskSubscription, GroupToolSubscription
class Command(BaseCommand): class Command(BaseCommand):

View File

@ -1,8 +1,9 @@
# 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

View File

@ -1,6 +1,7 @@
# 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

View File

@ -4,6 +4,7 @@ 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

View File

@ -120,9 +120,7 @@
<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> <td>{{ event.notes_html|safe }}</td>
{{ event.notes_html|safe }}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View File

@ -1,8 +1,7 @@
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):

View File

@ -21,6 +21,11 @@
<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"
@ -34,27 +39,43 @@
<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" 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> <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" aria-labelledby="bd-theme-text"> <ul class="dropdown-menu dropdown-menu-end"
aria-labelledby="bd-theme-text">
<li> <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> <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" 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> <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" 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> <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>
@ -103,5 +124,6 @@
<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 %}{% endblock %} {% block script %}
{% endblock %}
</html> </html>