Compare commits

...

6 Commits

Author SHA1 Message Date
40eca22eb8 doorcontrol: Grant instructors access for ~1 hour around their class times
Some checks failed
Test / test (push) Has been cancelled
Ruff / ruff (push) Has been cancelled
Also the required foundation for #14
2024-06-07 09:39:01 -04:00
d8e6a69719 Apply ruff lint rule DJ012 for model internals ordering 2024-06-07 09:36:49 -04:00
c3f3294f2a membershipworks: Add a __str__ method for EventMeetingTime 2024-06-07 09:36:49 -04:00
521c6cd52a membershipworks: Allow setting verbosity in scrape_events command 2024-06-07 09:36:49 -04:00
31c9a9b13d Disable django-debug-toolbar when running tests 2024-06-07 09:36:49 -04:00
9f24286701 Bump dependencies 2024-06-07 09:36:49 -04:00
10 changed files with 221 additions and 186 deletions

View File

@ -1,4 +1,5 @@
import os
import sys
from pathlib import Path
from django.core import validators
@ -303,8 +304,15 @@ class Dev(NonCIBase):
DEBUG = values.BooleanValue(True)
INTERNAL_IPS = ["127.0.0.1"]
INSTALLED_APPS = NonCIBase.INSTALLED_APPS + ["debug_toolbar", "django_extensions"]
MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + Base.MIDDLEWARE
INSTALLED_APPS = NonCIBase.INSTALLED_APPS + ["django_extensions"]
# bit of a hack to disable debug toolbar when running tests
if DEBUG and "test" not in sys.argv:
INSTALLED_APPS += ["debug_toolbar"]
MIDDLEWARE = NonCIBase.MIDDLEWARE + [
"debug_toolbar.middleware.DebugToolbarMiddleware",
]
EMAIL = values.EmailURLValue("smtp://localhost:1025") # for local `mailpit`
SENDFILE_BACKEND = "django_sendfile.backends.development"

View File

@ -22,6 +22,9 @@ class Door(models.Model):
help_text="Membershipworks field that grants members access to this door",
)
def __str__(self):
return self.name
@property
def controller(self) -> DoorController:
return DoorController(
@ -30,9 +33,6 @@ class Door(models.Model):
settings.HID_DOOR_PASSWORD,
)
def __str__(self):
return self.name
@cached_property
def card_formats(self):
return self.controller.get_cardFormats()
@ -149,8 +149,6 @@ class HIDEventQuerySet(models.QuerySet):
class HIDEvent(models.Model):
objects = HIDEventQuerySet.as_manager()
class EventType(models.IntegerChoices):
DENIED_ACCESS_CARD_NOT_FOUND = 1022, "Denied Access: Card Not Found"
DENIED_ACCESS_ACCESS_PIN_NOT_FOUND = 1023, "Denied Access Access: PIN Not Found"
@ -224,6 +222,20 @@ class HIDEvent(models.Model):
db_persist=False,
)
objects = HIDEventQuerySet.as_manager()
class Meta:
constraints = [
models.UniqueConstraint(
fields=["door", "timestamp", "event_type"], name="unique_hidevent"
)
]
db_table = "hidevent"
ordering = ("-timestamp",)
def __str__(self):
return f"{self.door.name} {self.timestamp} - {self.description}"
@classmethod
def from_xml_attributes(cls, door: Door, attrib: dict[str, str]):
field_lookup = {
@ -289,9 +301,6 @@ class HIDEvent(models.Model):
return event_types.get(self.event_type, f"Unknown Event Type {self.event_type}")
def __str__(self):
return f"{self.door.name} {self.timestamp} - {self.description}"
def decoded_card_number(self) -> str | None:
"""Requires annotations from `with_decoded_card_number`"""
if self.raw_card_number is None:
@ -303,12 +312,3 @@ class HIDEvent(models.Model):
return "Invalid"
else:
return "Not 26 bit card"
class Meta:
constraints = [
models.UniqueConstraint(
fields=["door", "timestamp", "event_type"], name="unique_hidevent"
)
]
db_table = "hidevent"
ordering = ("-timestamp",)

View File

@ -1,7 +1,10 @@
import dataclasses
import logging
from datetime import timedelta
from typing import TypedDict
from django.utils import timezone
from django_q.tasks import async_task
from cmsmanage.django_q2_helper import q_task_group
@ -62,6 +65,17 @@ class DoorMember:
if getattr(member, attribute_rule.access_field)
}
# grant instructors access for ~1 hour around their class times
if hasattr(member, "eventinstructor") and getattr(member, door.access_field):
now = timezone.now()
margin = timedelta(hours=1)
if member.eventinstructor.eventext_set.filter(
occurred=True,
meeting_times__start__lt=now + margin,
meeting_times__end__gt=now - margin,
).exists():
reasons_and_schedules["Instructor for Active Class"] = "Active Class"
reasons = sorted(reasons_and_schedules.keys())
return cls(

View File

@ -1,8 +1,18 @@
import logging
from django.core.management.base import BaseCommand
from membershipworks.tasks.scrape import scrape_events
from membershipworks.tasks.scrape import logger, scrape_events
class Command(BaseCommand):
def handle(self, *args, **options):
def handle(self, *args, verbosity: int, **options):
verbosity_levels = {
0: logging.ERROR,
1: logging.WARNING,
2: logging.INFO,
3: logging.DEBUG,
}
logger.setLevel(verbosity_levels.get(verbosity, logging.WARNING))
scrape_events()

View File

@ -88,13 +88,13 @@ class Flag(BaseModel):
name = models.TextField(null=True, blank=True)
type = models.CharField(max_length=6)
def __str__(self):
return f"{self.name} ({self.type})"
class Meta:
db_table = "flag"
ordering = ("name",)
def __str__(self):
return f"{self.name} ({self.type})"
class MemberQuerySet(models.QuerySet):
# TODO: maybe rename to reflect EXISTS?
@ -122,8 +122,6 @@ class MemberQuerySet(models.QuerySet):
# TODO: is this still a temporal table?
class Member(BaseModel):
objects = MemberQuerySet.as_manager()
uid = models.CharField(max_length=24, primary_key=True)
year_of_birth = models.TextField(db_column="Year of Birth", null=True, blank=True)
account_name = models.TextField(db_column="Account Name", null=True, blank=True)
@ -264,8 +262,7 @@ class Member(BaseModel):
"Waiver form signed and on file date.": "%m/%d/%Y",
}
def __str__(self):
return f"{self.account_name}"
objects = MemberQuerySet.as_manager()
class Meta:
db_table = "members"
@ -276,6 +273,9 @@ class Member(BaseModel):
models.Index(fields=["last_name"], name="last_name_idx"),
]
def __str__(self):
return f"{self.account_name}"
@classmethod
def from_user(cls, user) -> "Member | None":
if hasattr(user, "ldap_user"):
@ -304,9 +304,6 @@ class MemberFlag(BaseModel):
)
flag = models.ForeignKey(Flag, on_delete=models.PROTECT)
def __str__(self):
return f"{self.member} - {self.flag}"
class Meta:
db_table = "memberflag"
constraints = [
@ -315,6 +312,9 @@ class MemberFlag(BaseModel):
)
]
def __str__(self):
return f"{self.member} - {self.flag}"
class Transaction(BaseModel):
sid = models.CharField(max_length=256, null=True, blank=True)
@ -364,24 +364,24 @@ class Transaction(BaseModel):
"_dp": None,
}
def __str__(self):
return f"{self.type} [{self.member if self.member else self.name}] {self.timestamp}"
class Meta:
db_table = "transactions"
def __str__(self):
return f"{self.type} [{self.member if self.member else self.name}] {self.timestamp}"
class EventCategory(models.Model):
id = models.IntegerField(primary_key=True)
title = models.TextField()
def __str__(self):
return self.title
@classmethod
def from_api_dict(cls, id: int, data):
return cls(id=id, title=data["ttl"])
def __str__(self):
return self.title
class Event(BaseModel):
class EventCalendar(models.IntegerChoices):
@ -433,13 +433,13 @@ class Event(BaseModel):
_allowed_missing_fields = ["cap", "edp", "adn"]
def __str__(self):
return self.unescaped_title
@property
def unescaped_title(self):
return nh3.clean(self.title, tags=set())
def __str__(self):
return self.unescaped_title
class EventInstructor(models.Model):
name = models.TextField(blank=True)
@ -526,8 +526,6 @@ class EventExtManager(models.Manager):
class EventExt(Event):
"""Extension of `Event` to capture some fields not supported in MembershipWorks"""
objects = EventExtManager.from_queryset(EventExtQuerySet)()
instructor = models.ForeignKey(
EventInstructor, on_delete=models.PROTECT, null=True, blank=True
)
@ -557,6 +555,12 @@ class EventExt(Event):
should_survey = models.BooleanField(default=False)
survey_email_sent = models.BooleanField(default=False)
objects = EventExtManager.from_queryset(EventExtQuerySet)()
class Meta:
verbose_name = "event"
ordering = ["-start"]
def get_absolute_url(self) -> str:
return reverse("membershipworks:event-detail", kwargs={"eid": self.eid})
@ -581,10 +585,6 @@ class EventExt(Event):
and getattr(self, "total_due_to_instructor") is not None
)
class Meta:
verbose_name = "event"
ordering = ["-start"]
if TYPE_CHECKING:
@ -633,6 +633,9 @@ class EventMeetingTime(models.Model):
models.CheckConstraint(check=Q(end__gt=F("start")), name="end_after_start"),
]
def __str__(self) -> str:
return f"{self.start} - {self.end}"
class EventInvoice(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

View File

@ -20,14 +20,14 @@ class AbstractAudit(models.Model):
good = models.BooleanField(default=False)
notes = models.CharField(max_length=255, blank=True)
def __str__(self) -> str:
return f"{'Good' if self.good else 'Bad'} audit at {self.date} by {self.user}"
class Meta:
abstract = True
ordering = ["date"]
get_latest_by = ["date"]
def __str__(self) -> str:
return f"{'Good' if self.good else 'Bad'} audit at {self.date} by {self.user}"
class CmsRedRiverVeteransScholarship(models.Model):
serial = models.AutoField(primary_key=True)
@ -61,12 +61,12 @@ class CmsRedRiverVeteransScholarship(models.Model):
db_column="Program Status", max_length=16, blank=True, null=True
)
def __str__(self) -> str:
return f"{self.program_name} {self.member_name}"
class Meta:
db_table = "CMS Red River Veterans Scholarship"
def __str__(self) -> str:
return f"{self.program_name} {self.member_name}"
class DepartmentQuerySet(models.QuerySet):
def filter_by_shop_lead(self, member: Member) -> models.QuerySet["Department"]:
@ -80,8 +80,6 @@ class DepartmentQuerySet(models.QuerySet):
class Department(models.Model):
objects = DepartmentQuerySet.as_manager()
name = models.CharField(
max_length=64,
validators=[RegexValidator("^[-_ A-Za-z0-9]*$")],
@ -101,6 +99,8 @@ class Department(models.Model):
)
list_reply_to_address = models.EmailField(max_length=254, blank=True)
objects = DepartmentQuerySet.as_manager()
def __str__(self) -> str:
return self.name
@ -123,13 +123,13 @@ class CertificationDefinition(models.Model):
name = models.CharField(max_length=255)
department = models.ForeignKey(Department, models.PROTECT)
def __str__(self) -> str:
return f"{self.name} <{self.department}>"
class Meta:
db_table = "Certification Definitions"
ordering = ("name", "department")
def __str__(self) -> str:
return f"{self.name} <{self.department}>"
def latest_version(self) -> "CertificationVersion":
return self.certificationversion_set.latest()
@ -157,8 +157,6 @@ class CertificationVersionManager(models.Manager["CertificationVersion"]):
class CertificationVersion(models.Model):
objects = CertificationVersionManager()
definition = models.ForeignKey(CertificationDefinition, on_delete=models.PROTECT)
major = models.PositiveSmallIntegerField()
minor = models.PositiveSmallIntegerField()
@ -166,8 +164,7 @@ class CertificationVersion(models.Model):
prerelease = models.CharField(max_length=255, blank=True)
approval_date = models.DateField(blank=True, null=True)
def __str__(self) -> str:
return f"{self.definition} [{self.semantic_version()}]"
objects = CertificationVersionManager()
class Meta:
constraints = [
@ -187,6 +184,9 @@ class CertificationVersion(models.Model):
get_latest_by = ("major", "minor", "patch", "prerelease", "approval_date")
base_manager_name = "objects"
def __str__(self) -> str:
return f"{self.definition} [{self.semantic_version()}]"
def semantic_version(self) -> VersionInfo:
return VersionInfo(
self.major or 0,
@ -224,9 +224,6 @@ class Certification(models.Model):
shop_lead_notified = models.DateTimeField(blank=True, null=True)
notes = models.CharField(max_length=255, blank=True, null=True)
def __str__(self) -> str:
return f"{self.name} - {self.certification_version}"
class Meta:
db_table = "Certifications"
permissions = [
@ -236,6 +233,9 @@ class Certification(models.Model):
),
]
def __str__(self) -> str:
return f"{self.name} - {self.certification_version}"
class CertificationAudit(AbstractAudit):
certification = models.ForeignKey(
@ -255,12 +255,12 @@ class InstructorOrVendor(models.Model):
db_column="email address", max_length=255, blank=True, null=True
)
def __str__(self) -> str:
return f"{self.name}"
class Meta:
db_table = "Instructors and Vendors"
def __str__(self) -> str:
return f"{self.name}"
class SpecialProgram(models.Model):
program_name = models.CharField(
@ -292,12 +292,12 @@ class SpecialProgram(models.Model):
db_column="Program Status", max_length=16, blank=True, null=True
)
def __str__(self) -> str:
return self.program_name
class Meta:
db_table = "Special_Programs"
def __str__(self) -> str:
return self.program_name
class Waiver(models.Model):
number = models.AutoField(db_column="Number", primary_key=True)
@ -319,12 +319,12 @@ class Waiver(models.Model):
guardian_date = models.DateField(db_column="Guardian Date", blank=True, null=True)
notes = models.CharField(max_length=255, blank=True, null=True)
def __str__(self) -> str:
return f"{self.name} {self.date}"
class Meta:
db_table = "Waivers"
def __str__(self) -> str:
return f"{self.name} {self.date}"
class WaiverAudit(AbstractAudit):
waiver = models.ForeignKey(Waiver, on_delete=models.CASCADE, related_name="audits")

144
pdm.lock
View File

@ -5,7 +5,7 @@
groups = ["default", "debug", "lint", "server", "typing", "dev"]
strategy = ["cross_platform"]
lock_version = "4.4.1"
content_hash = "sha256:8b3bb37d6fbada119262035c0e94ada756c2f43ca8863b50b51f47a232ee84b9"
content_hash = "sha256:b32d3518b2ef3d797f260f44adff417959def44b75fb60134fcdd93351c92581"
[[package]]
name = "aiohttp"
@ -224,15 +224,15 @@ files = [
[[package]]
name = "bitstring"
version = "4.2.2"
version = "4.2.3"
requires_python = ">=3.8"
summary = "Simple construction, analysis and modification of binary data."
dependencies = [
"bitarray<3.0.0,>=2.9.0",
]
files = [
{file = "bitstring-4.2.2-py3-none-any.whl", hash = "sha256:8b784373e78e953879c8192589e1ecbcda8f111841633a2aaf88d2b073fd6c35"},
{file = "bitstring-4.2.2.tar.gz", hash = "sha256:b40b01d911eebaea6efff40d826580806dced5e04b9d3cbad6aebf9422f4b643"},
{file = "bitstring-4.2.3-py3-none-any.whl", hash = "sha256:20ed0036e2fcf0323acb0f92f0b7b178516a080f3e91061470aa019ac4ede404"},
{file = "bitstring-4.2.3.tar.gz", hash = "sha256:e0c447af3fda0d114f77b88c2d199f02f97ee7e957e6d719f40f41cf15fbb897"},
]
[[package]]
@ -578,29 +578,29 @@ files = [
[[package]]
name = "django-db-views"
version = "0.1.7"
version = "0.1.8"
summary = "Handle database views. Allow to create migrations for database views. View migrations using django code. They can be reversed. Changes in model view definition are detected automatically. Support almost all options as regular makemigrations command"
dependencies = [
"Django",
"django>=2.2",
"six",
]
files = [
{file = "django-db-views-0.1.7.tar.gz", hash = "sha256:7c0dc78aa5f53cc4eefc4d880450a8cb61bb8376b5494356e20383f71a3e1657"},
{file = "django_db_views-0.1.7-py3-none-any.whl", hash = "sha256:ffa399af1678e60f532f8c2b531927e94e8249e1012a83fc865234384419bff8"},
{file = "django_db_views-0.1.8-py2.py3-none-any.whl", hash = "sha256:089c9f193c265f956c0f06d0695b54ff57343bbb25f6c5d546f816e7cb9b456c"},
{file = "django_db_views-0.1.8.tar.gz", hash = "sha256:b737b01b782d859a90f4d579147a5fa34f4b5376606443abe35c5fdc00313bcc"},
]
[[package]]
name = "django-debug-toolbar"
version = "4.3.0"
version = "4.4.2"
requires_python = ">=3.8"
summary = "A configurable set of panels that display various debug information about the current request/response."
dependencies = [
"django>=3.2.4",
"django>=4.2.9",
"sqlparse>=0.2",
]
files = [
{file = "django_debug_toolbar-4.3.0-py3-none-any.whl", hash = "sha256:e09b7dcb8417b743234dfc57c95a7c1d1d87a88844abd13b4c5387f807b31bf6"},
{file = "django_debug_toolbar-4.3.0.tar.gz", hash = "sha256:0b0dddee5ea29b9cb678593bc0d7a6d76b21d7799cb68e091a2148341a80f3c4"},
{file = "django_debug_toolbar-4.4.2-py3-none-any.whl", hash = "sha256:5d7afb2ea5f8730241e5b0735396e16cd1fd8c6b53a2f3e1e30bbab9abb23728"},
{file = "django_debug_toolbar-4.4.2.tar.gz", hash = "sha256:9204050fcb1e4f74216c5b024bc76081451926a6303993d6c513f5e142675927"},
]
[[package]]
@ -734,24 +734,24 @@ files = [
[[package]]
name = "django-stubs"
version = "5.0.0"
version = "5.0.2"
requires_python = ">=3.8"
summary = "Mypy stubs for Django"
dependencies = [
"asgiref",
"django",
"django-stubs-ext>=5.0.0",
"django-stubs-ext>=5.0.2",
"types-PyYAML",
"typing-extensions",
"typing-extensions>=4.11.0",
]
files = [
{file = "django_stubs-5.0.0-py3-none-any.whl", hash = "sha256:084484cbe16a6d388e80ec687e46f529d67a232f3befaf55c936b3b476be289d"},
{file = "django_stubs-5.0.0.tar.gz", hash = "sha256:b8a792bee526d6cab31e197cb414ee7fa218abd931a50948c66a80b3a2548621"},
{file = "django_stubs-5.0.2-py3-none-any.whl", hash = "sha256:cb0c506cb5c54c64612e4a2ee8d6b913c6178560ec168009fe847c09747c304b"},
{file = "django_stubs-5.0.2.tar.gz", hash = "sha256:236bc5606e5607cb968f92b648471f9edaa461a774bc013bf9e6bff8730f6bdf"},
]
[[package]]
name = "django-stubs-ext"
version = "5.0.0"
version = "5.0.2"
requires_python = ">=3.8"
summary = "Monkey-patching and extensions for django-stubs"
dependencies = [
@ -759,23 +759,23 @@ dependencies = [
"typing-extensions",
]
files = [
{file = "django_stubs_ext-5.0.0-py3-none-any.whl", hash = "sha256:8e1334fdf0c8bff87e25d593b33d4247487338aaed943037826244ff788b56a8"},
{file = "django_stubs_ext-5.0.0.tar.gz", hash = "sha256:5bacfbb498a206d5938454222b843d81da79ea8b6fcd1a59003f529e775bc115"},
{file = "django_stubs_ext-5.0.2-py3-none-any.whl", hash = "sha256:8d8efec5a86241266bec94a528fe21258ad90d78c67307f3ae5f36e81de97f12"},
{file = "django_stubs_ext-5.0.2.tar.gz", hash = "sha256:409c62585d7f996cef5c760e6e27ea3ff29f961c943747e67519c837422cad32"},
]
[[package]]
name = "django-stubs"
version = "5.0.0"
version = "5.0.2"
extras = ["compatible-mypy"]
requires_python = ">=3.8"
summary = "Mypy stubs for Django"
dependencies = [
"django-stubs==5.0.0",
"django-stubs==5.0.2",
"mypy~=1.10.0",
]
files = [
{file = "django_stubs-5.0.0-py3-none-any.whl", hash = "sha256:084484cbe16a6d388e80ec687e46f529d67a232f3befaf55c936b3b476be289d"},
{file = "django_stubs-5.0.0.tar.gz", hash = "sha256:b8a792bee526d6cab31e197cb414ee7fa218abd931a50948c66a80b3a2548621"},
{file = "django_stubs-5.0.2-py3-none-any.whl", hash = "sha256:cb0c506cb5c54c64612e4a2ee8d6b913c6178560ec168009fe847c09747c304b"},
{file = "django_stubs-5.0.2.tar.gz", hash = "sha256:236bc5606e5607cb968f92b648471f9edaa461a774bc013bf9e6bff8730f6bdf"},
]
[[package]]
@ -1079,7 +1079,7 @@ files = [
[[package]]
name = "hypothesis"
version = "6.102.4"
version = "6.103.1"
requires_python = ">=3.8"
summary = "A library for property-based testing"
dependencies = [
@ -1087,23 +1087,23 @@ dependencies = [
"sortedcontainers<3.0.0,>=2.1.0",
]
files = [
{file = "hypothesis-6.102.4-py3-none-any.whl", hash = "sha256:013df31b04a4daede13756f497e60e451963d86f426395a79f99c5d692919bbd"},
{file = "hypothesis-6.102.4.tar.gz", hash = "sha256:59b4d144346d5cffb482cc1bafbd21b13ff31608e8c4b3e4630339aee3e87763"},
{file = "hypothesis-6.103.1-py3-none-any.whl", hash = "sha256:d3c959fab6233e78867499e2117ae9db8dc40eeed936d71a2cfc7b6094972e74"},
{file = "hypothesis-6.103.1.tar.gz", hash = "sha256:d299d5c21d6408eab3be670c94c974f3acf0b511c61fe81804b09091e393ee1f"},
]
[[package]]
name = "hypothesis"
version = "6.102.4"
version = "6.103.1"
extras = ["django"]
requires_python = ">=3.8"
summary = "A library for property-based testing"
dependencies = [
"django>=3.2",
"hypothesis==6.102.4",
"hypothesis==6.103.1",
]
files = [
{file = "hypothesis-6.102.4-py3-none-any.whl", hash = "sha256:013df31b04a4daede13756f497e60e451963d86f426395a79f99c5d692919bbd"},
{file = "hypothesis-6.102.4.tar.gz", hash = "sha256:59b4d144346d5cffb482cc1bafbd21b13ff31608e8c4b3e4630339aee3e87763"},
{file = "hypothesis-6.103.1-py3-none-any.whl", hash = "sha256:d3c959fab6233e78867499e2117ae9db8dc40eeed936d71a2cfc7b6094972e74"},
{file = "hypothesis-6.103.1.tar.gz", hash = "sha256:d299d5c21d6408eab3be670c94c974f3acf0b511c61fe81804b09091e393ee1f"},
]
[[package]]
@ -1118,7 +1118,7 @@ files = [
[[package]]
name = "ipython"
version = "8.24.0"
version = "8.25.0"
requires_python = ">=3.10"
summary = "IPython: Productive Interactive Computing"
dependencies = [
@ -1134,8 +1134,8 @@ dependencies = [
"typing-extensions>=4.6; python_version < \"3.12\"",
]
files = [
{file = "ipython-8.24.0-py3-none-any.whl", hash = "sha256:d7bf2f6c4314984e3e02393213bab8703cf163ede39672ce5918c51fe253a2a3"},
{file = "ipython-8.24.0.tar.gz", hash = "sha256:010db3f8a728a578bb641fdd06c063b9fb8e96a9464c63aec6310fbcb5e80501"},
{file = "ipython-8.25.0-py3-none-any.whl", hash = "sha256:53eee7ad44df903a06655871cbab66d156a051fd86f3ec6750470ac9604ac1ab"},
{file = "ipython-8.25.0.tar.gz", hash = "sha256:c6ed726a140b6e725b911528f80439c534fac915246af3efc39440a6b0f9d716"},
]
[[package]]
@ -1817,8 +1817,8 @@ files = [
[[package]]
name = "requests"
version = "2.31.0"
requires_python = ">=3.7"
version = "2.32.3"
requires_python = ">=3.8"
summary = "Python HTTP for Humans."
dependencies = [
"certifi>=2017.4.17",
@ -1827,33 +1827,33 @@ dependencies = [
"urllib3<3,>=1.21.1",
]
files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[[package]]
name = "ruff"
version = "0.4.4"
version = "0.4.8"
requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust."
files = [
{file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"},
{file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"},
{file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"},
{file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"},
{file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"},
{file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"},
{file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"},
{file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"},
{file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"},
{file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"},
{file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"},
{file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"},
]
[[package]]
@ -1868,12 +1868,12 @@ files = [
[[package]]
name = "setuptools"
version = "69.5.1"
version = "70.0.0"
requires_python = ">=3.8"
summary = "Easily download, build, install, upgrade, and uninstall Python packages"
files = [
{file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"},
{file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"},
{file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"},
{file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"},
]
[[package]]
@ -2117,15 +2117,15 @@ files = [
[[package]]
name = "types-requests"
version = "2.31.0.20240406"
version = "2.32.0.20240602"
requires_python = ">=3.8"
summary = "Typing stubs for requests"
dependencies = [
"urllib3>=2",
]
files = [
{file = "types-requests-2.31.0.20240406.tar.gz", hash = "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1"},
{file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"},
{file = "types-requests-2.32.0.20240602.tar.gz", hash = "sha256:3f98d7bbd0dd94ebd10ff43a7fbe20c3b8528acace6d8efafef0b6a184793f06"},
{file = "types_requests-2.32.0.20240602-py3-none-any.whl", hash = "sha256:ed3946063ea9fbc6b5fc0c44fa279188bae42d582cb63760be6cb4b9d06c3de8"},
]
[[package]]
@ -2196,7 +2196,7 @@ files = [
[[package]]
name = "uvicorn"
version = "0.29.0"
version = "0.30.1"
requires_python = ">=3.8"
summary = "The lightning-fast ASGI server."
dependencies = [
@ -2204,13 +2204,13 @@ dependencies = [
"h11>=0.8",
]
files = [
{file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"},
{file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"},
{file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"},
{file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"},
]
[[package]]
name = "uvicorn"
version = "0.29.0"
version = "0.30.1"
extras = ["standard"]
requires_python = ">=3.8"
summary = "The lightning-fast ASGI server."
@ -2219,14 +2219,14 @@ dependencies = [
"httptools>=0.5.0",
"python-dotenv>=0.13",
"pyyaml>=5.1",
"uvicorn==0.29.0",
"uvicorn==0.30.1",
"uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"",
"watchfiles>=0.13",
"websockets>=10.4",
]
files = [
{file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"},
{file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"},
{file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"},
{file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"},
]
[[package]]
@ -2311,7 +2311,7 @@ files = [
[[package]]
name = "weasyprint"
version = "62.1"
version = "62.2"
requires_python = ">=3.9"
summary = "The Awesome Document Factory"
dependencies = [
@ -2325,8 +2325,8 @@ dependencies = [
"tinycss2>=1.3.0",
]
files = [
{file = "weasyprint-62.1-py3-none-any.whl", hash = "sha256:654d4c266336cbf9acc4da118c7778ef5839717e6055d5b8f995cf50be200c46"},
{file = "weasyprint-62.1.tar.gz", hash = "sha256:bf3c1a9ac4194271a7cf117229c093744105b50ac2fa64c0a6e44e68ae742992"},
{file = "weasyprint-62.2-py3-none-any.whl", hash = "sha256:6fd84e9f55ac239c5657845eae117fd43916c3c5986fe98f69ea13fdab8ec9ad"},
{file = "weasyprint-62.2.tar.gz", hash = "sha256:a08ac400e11919d996d76becaa33160d7c1ac55ba160628c42ce7586574c1a51"},
]
[[package]]

View File

@ -18,8 +18,8 @@ dependencies = [
"mdformat-tables~=0.4",
"mysqlclient~=2.2",
"django-autocomplete-light~=3.11",
"weasyprint~=62.1",
"requests~=2.31",
"weasyprint~=62.2",
"requests~=2.32",
"semver~=3.0",
"djangorestframework~=3.15",
"django-q2~=1.6",
@ -44,8 +44,8 @@ requires-python = ">=3.11"
[project.optional-dependencies]
server = [
"uvicorn[standard]~=0.29",
"setuptools~=69.5",
"uvicorn[standard]~=0.30",
"setuptools~=70.0",
]
[project.entry-points."djangoq.errorreporters"]
@ -55,7 +55,7 @@ admin_email = "cmsmanage.django_q2_admin_email_reporter:AdminEmailReporter"
line-length = 88
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I", "C4", "UP", "PERF", "PL", "SIM", "FIX003"]
select = ["E4", "E7", "E9", "F", "I", "C4", "UP", "PERF", "PL", "SIM", "FIX003", "DJ012"]
[tool.ruff.lint.isort]
known-first-party = [
@ -118,9 +118,9 @@ lint = [
typing = [
"mypy~=1.10",
"django-stubs~=5.0",
"setuptools~=69.5",
"setuptools~=70.0",
"types-bleach~=6.1",
"types-requests~=2.31",
"types-requests~=2.32",
"types-urllib3~=1.26",
"djangorestframework-stubs[compatible-mypy]~=3.15",
"types-Markdown~=3.6",
@ -129,12 +129,12 @@ typing = [
"types-lxml~=2024.4",
]
debug = [
"django-debug-toolbar~=4.3",
"django-debug-toolbar~=4.4",
]
dev = [
"django-extensions~=3.2",
"ipython~=8.24",
"hypothesis[django]~=6.102",
"ipython~=8.25",
"hypothesis[django]~=6.103",
"tblib~=3.0",
]

View File

@ -33,17 +33,6 @@ class LockerUnit(models.Model):
rows = models.PositiveIntegerField(default=5)
columns = models.PositiveIntegerField(default=2)
def save(self, *args, **kwargs):
if self._state.adding:
# Create LockerInfo for each locker
with transaction.atomic():
super().save(self, *args, **kwargs)
for column in range(self.columns):
for row in range(self.rows):
self.lockers.create(column=column + 1, row=row + 1)
else:
super().save(self, *args, **kwargs)
class Meta:
# TODO: add constraint to check for letter overlaps
constraints = [
@ -56,6 +45,17 @@ class LockerUnit(models.Model):
last_number = self.first_number + self.columns * self.rows
return f"{self.bank.name} (Unit {last_letter}{self.first_number}-{self.first_letter}{last_number})"
def save(self, *args, **kwargs):
if self._state.adding:
# Create LockerInfo for each locker
with transaction.atomic():
super().save(self, *args, **kwargs)
for column in range(self.columns):
for row in range(self.rows):
self.lockers.create(column=column + 1, row=row + 1)
else:
super().save(self, *args, **kwargs)
def letter_for_column(self, column: int) -> str:
return chr(column + ord(self.first_letter))
@ -91,10 +91,6 @@ class LockerInfo(models.Model):
)
notes = models.TextField(blank=True)
def clean(self):
if self.reserved and self.renter is not None:
raise ValidationError("Locker cannot both be reserved and rented!")
class Meta:
constraints = [
models.UniqueConstraint(
@ -106,6 +102,13 @@ class LockerInfo(models.Model):
),
]
def __str__(self):
return f"{self.locker_unit}-{self.address} [{self.renter}]"
def clean(self):
if self.reserved and self.renter is not None:
raise ValidationError("Locker cannot both be reserved and rented!")
@property
def available(self) -> bool:
return self.renter is None and not self.reserved
@ -132,6 +135,3 @@ class LockerInfo(models.Model):
)
def address(self) -> str:
return f"{self.letter}{self.number}"
def __str__(self):
return f"{self.locker_unit}-{self.address} [{self.renter}]"

View File

@ -90,18 +90,18 @@ class GroupToolSubscription(SubscriptionSettings):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
tool = models.ForeignKey(Tool, on_delete=models.CASCADE)
def get_task_subscriptions(self):
for task in self.tool.task_set.all():
yield GroupTaskSubscription(
days_before=self.days_before, group=self.group, task=task
)
class Meta:
unique_together = (("group", "tool"),)
def __str__(self):
return f"{self.group}-{self.tool}, {super().__str__()}"
def get_task_subscriptions(self):
for task in self.tool.task_set.all():
yield GroupTaskSubscription(
days_before=self.days_before, group=self.group, task=task
)
class GroupTaskSubscription(SubscriptionSettings):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
@ -110,6 +110,9 @@ class GroupTaskSubscription(SubscriptionSettings):
class Meta:
unique_together = (("group", "task"),)
def __str__(self):
return f"{self.group}-{self.task}, {super().__str__()}"
@property
def should_remind(self):
next_recurrence = self.task.next_recurrence
@ -118,9 +121,6 @@ class GroupTaskSubscription(SubscriptionSettings):
time_until_overdue = next_recurrence - datetime.datetime.now()
return self.task.is_overdue or (time_until_overdue.days <= self.days_before)
def __str__(self):
return f"{self.group}-{self.task}, {super().__str__()}"
class Event(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE)
@ -128,9 +128,9 @@ class Event(models.Model):
date = models.DateField()
notes = MarkdownxField(blank=True)
def __str__(self):
return f"{self.task}: {self.user} {self.date}"
@property
def notes_html(self):
return markdown_to_clean_html(self.notes)
def __str__(self):
return f"{self.task}: {self.user} {self.date}"