Compare commits

...

7 Commits

15 changed files with 676 additions and 189 deletions

View File

@ -37,7 +37,7 @@ INSTALLED_APPS = [
"rest_framework",
"rest_framework.authtoken",
"django_q",
"django_bleach",
"django_nh3",
"tasks.apps.TasksConfig",
"rentals.apps.RentalsConfig",
"membershipworks.apps.MembershipworksConfig",

View File

@ -14,6 +14,7 @@ class DoorAdmin(admin.ModelAdmin):
@admin.register(HIDEvent)
class HIDEventAdmin(DjangoObjectActions, admin.ModelAdmin):
search_fields = ["forename", "surname", "cardholder_id"]
date_hierarchy = "timestamp"
list_display = ["timestamp", "door", "event_type", "description", "_is_red"]
list_filter = [
"timestamp",

View File

@ -5,7 +5,15 @@ from django_object_actions import DjangoObjectActions, action
from django_q.tasks import async_task
from django_q.models import Task
from .models import Member, Flag, Transaction
from .models import (
Member,
Flag,
Transaction,
Event,
EventExt,
EventMeetingTime,
EventInstructor,
)
from .tasks.scrape import scrape_membershipworks
@ -71,3 +79,57 @@ class TransactionAdmin(BaseMembershipWorksAdmin):
list_display = ["timestamp", "member", "name", "type", "sum", "note"]
list_filter = ["type"]
show_facets = admin.ShowFacets.ALWAYS
search_fields = ["member", "name", "type", "note"]
date_hierarchy = "timestamp"
class EventMeetingTimeInline(admin.TabularInline):
model = EventMeetingTime
extra = 0
min_num = 1
readonly_fields = ["duration"]
# TODO: remove when switched to GeneratedField
@admin.display()
def duration(self, obj):
return obj.duration
@admin.register(EventInstructor)
class EventInstructorAdmin(admin.ModelAdmin):
autocomplete_fields = ["member"]
@admin.register(EventExt)
class EventAdmin(admin.ModelAdmin):
inlines = [EventMeetingTimeInline]
list_display = [
"title",
"start",
"end",
"duration",
"count",
"cap",
"category",
"venue",
]
list_filter = ["category", "calendar", "venue"]
show_facets = admin.ShowFacets.ALWAYS
search_fields = ["eid", "title", "url"]
date_hierarchy = "start"
readonly_fields = [
field.name
for field in Event._meta.get_fields()
if not (field.auto_created or field.many_to_many or not field.concrete)
] + ["duration"]
@admin.display()
def duration(self, obj):
return obj.duration
def has_add_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return False

View File

@ -7,7 +7,7 @@ def post_migrate_callback(sender, **kwargs):
from cmsmanage.django_q2_helper import ensure_scheduled
from .tasks.scrape import scrape_membershipworks
from .tasks.scrape import scrape_membershipworks, scrape_events
from .tasks.ucsAccounts import sync_accounts
ensure_scheduled(
@ -16,6 +16,12 @@ def post_migrate_callback(sender, **kwargs):
schedule_type=Schedule.HOURLY,
)
ensure_scheduled(
"Scrape MembershipWorks Events",
scrape_events,
schedule_type=Schedule.HOURLY,
)
ensure_scheduled(
"Sync UCS Accounts",
sync_accounts,

View File

@ -0,0 +1,8 @@
from django.core.management.base import BaseCommand
from membershipworks.tasks.scrape import scrape_events
class Command(BaseCommand):
def handle(self, *args, **options):
scrape_events()

View File

@ -238,14 +238,25 @@ class MembershipWorks:
members = self.get_members(folders, ",".join(fields.keys()))
return members
def get_events_list(self, start_date: datetime.datetime):
"""Retrive a list of events since start_date"""
r = self.sess.get(
BASE_URL + "/v2/events",
params={
"sdp": start_date.strftime("%s"),
},
)
def get_events_list(
self,
start_date: datetime.datetime = None,
end_date: datetime.datetime = None,
categories=False,
):
"""Retrive a list of events between `start_date` and `end_date`, optionally including category information"""
if start_date is None and end_date is None:
raise ValueError("Must specify one of start_date or end_date")
params = {}
if start_date is not None:
params["sdp"] = start_date.strftime("%s")
if end_date is not None:
params["edp"] = end_date.strftime("%s")
if categories is not None:
params["_st"] = ""
r = self.sess.get(BASE_URL + "/v2/events", params=params)
return r.json()
def get_event_by_eid(self, eid: str):

View File

@ -0,0 +1,154 @@
# Generated by Django 5.0 on 2023-12-30 19:28
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0004_alter_memberflag_member_alter_transaction_member"),
]
operations = [
migrations.CreateModel(
name="Event",
fields=[
(
"eid",
models.CharField(max_length=255, primary_key=True, serialize=False),
),
("url", models.TextField()),
("title", models.TextField()),
("start", models.DateTimeField()),
("end", models.DateTimeField(blank=True, null=True)),
("cap", models.IntegerField(blank=True, null=True)),
("count", models.IntegerField()),
(
"calendar",
models.IntegerField(
choices=[
(0, "Hidden"),
(1, "Green"),
(2, "Red"),
(3, "Yellow"),
(4, "Blue"),
(5, "Purple"),
(6, "Magenta"),
(7, "Grey"),
(8, "Teal"),
]
),
),
("venue", models.TextField(blank=True, null=True)),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="EventCategory",
fields=[
("id", models.IntegerField(primary_key=True, serialize=False)),
("title", models.TextField()),
],
),
migrations.CreateModel(
name="EventExt",
fields=[
(
"event_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="membershipworks.event",
),
),
(
"materials_fee",
models.DecimalField(
blank=True, decimal_places=4, max_digits=13, null=True
),
),
],
options={
"verbose_name": "event",
},
bases=("membershipworks.event",),
),
migrations.AddField(
model_name="event",
name="category",
field=models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.eventcategory",
),
),
migrations.CreateModel(
name="EventInstructor",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.TextField(blank=True)),
(
"member",
models.OneToOneField(
blank=True,
db_constraint=False,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.member",
),
),
],
),
migrations.CreateModel(
name="EventMeetingTime",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("start", models.DateTimeField()),
("end", models.DateTimeField()),
(
"event",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="meeting_times",
to="membershipworks.eventext",
),
),
],
),
migrations.AddField(
model_name="eventext",
name="instructor",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to="membershipworks.eventinstructor",
),
),
migrations.AddConstraint(
model_name="eventmeetingtime",
constraint=models.UniqueConstraint(
fields=("event", "start", "end"), name="unique_event_start_end"
),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 5.0 on 2024-01-01 17:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
(
"membershipworks",
"0005_event_eventcategory_eventext_event_category_and_more",
),
]
operations = [
migrations.AddField(
model_name="eventext",
name="instructor_flat_rate",
field=models.DecimalField(decimal_places=4, default=0, max_digits=13),
),
migrations.AddField(
model_name="eventext",
name="instructor_percentage",
field=models.DecimalField(decimal_places=4, default=0.5, max_digits=5),
),
]

View File

@ -4,13 +4,14 @@ from datetime import datetime
import django.core.mail.message
from django.conf import settings
from django.db import models
from django.db.models import Exists, OuterRef
from django.db.models import Exists, F, OuterRef, Sum
from django.utils import timezone
class BaseModel(models.Model):
_csv_headers_override = {}
_api_names_override = {}
_date_fields = {}
_allowed_missing_fields = []
class Meta:
abstract = True
@ -22,21 +23,31 @@ class BaseModel(models.Model):
if field.auto_created or field.many_to_many or not field.concrete:
continue
field_name, csv_header = field.get_attname_column()
field_name, api_name = field.get_attname_column()
if field_name in cls._csv_headers_override:
csv_header = cls._csv_headers_override[field.name]
if field_name in cls._api_names_override:
api_name = cls._api_names_override[field_name]
yield field_name, data[csv_header]
yield field_name, data[api_name]
@classmethod
def from_csv_dict(cls, data):
def from_api_dict(cls, data):
data = data.copy()
for field in cls._allowed_missing_fields:
if field not in data:
data[field] = None
# parse date fields to datetime objects
for field, fmt in cls._date_fields.items():
if data[field]:
data[field] = datetime.strptime(str(data[field]), fmt)
if fmt is None:
# can't use '%s' format string, have to use the special function
data[field] = datetime.fromtimestamp(
data[field], tz=timezone.get_current_timezone()
)
else:
data[field] = datetime.strptime(str(data[field]), fmt)
else:
# convert empty string to None to make NULL in SQL
data[field] = None
@ -210,7 +221,7 @@ class Member(BaseModel):
)
flags = models.ManyToManyField(Flag, through="MemberFlag", related_name="members")
_csv_headers_override = {
_api_names_override = {
"uid": "Account ID",
"how_did_you_hear": "Please tell us how you heard about the Claremont MakerSpace and what tools or shops you are most excited to start using:",
"authorize_charge": "Yes - I authorize TwinState MakerSpaces, Inc. to charge my credit card for the membership and other options that I have selected.",
@ -307,34 +318,161 @@ class Transaction(BaseModel):
phone = models.TextField(db_column="Phone", null=True, blank=True)
email = models.TextField(db_column="Email", null=True, blank=True)
@classmethod
def from_csv_dict(cls, data):
txn = data.copy()
# can't use '%s' format string, have to use the special function
txn["_dp"] = datetime.fromtimestamp(
txn["_dp"], tz=timezone.get_current_timezone()
)
allowed_missing_fields = [
"sid",
"uid",
"eid",
"fee",
"sum",
]
for field in allowed_missing_fields:
if field not in txn:
txn[field] = None
return super().from_csv_dict(txn)
_csv_headers_override = {
_allowed_missing_fields = [
"sid",
"uid",
"eid",
"fee",
"sum",
]
_api_names_override = {
"event_id": "eid",
"timestamp": "_dp",
"type": "Transaction Type",
"for_what": "Event/Form Name",
}
_date_fields = {
"_dp": None,
}
def __str__(self):
return f"{self.type} [{self.member if self.member else self.name}] {self.timestamp}"
class Meta:
db_table = "transactions"
class EventCategory(models.Model):
id = models.IntegerField(primary_key=True)
title = models.TextField()
@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):
HIDDEN = 0
GREEN = 1
RED = 2
YELLOW = 3
BLUE = 4
PURPLE = 5
MAGENTA = 6
GREY = 7
TEAL = 8
eid = models.CharField(max_length=255, primary_key=True)
url = models.TextField()
title = models.TextField()
start = models.DateTimeField()
end = models.DateTimeField(null=True, blank=True)
cap = models.IntegerField(null=True, blank=True)
count = models.IntegerField()
category = models.ForeignKey(EventCategory, on_delete=models.PROTECT)
calendar = models.IntegerField(choices=EventCalendar)
venue = models.TextField(null=True, blank=True)
# TODO:
# "lgo": {
# "l": "https://d1tif55lvfk8gc.cloudfront.net/656e3842ae3975908b05e304.jpg?1673405126",
# "s": "https://d1tif55lvfk8gc.cloudfront.net/656e3842ae3975908b05e304s.jpg?1673405126"
# },
_api_names_override = {
"title": "ttl",
"category_id": "grp",
"start": "sdp",
"end": "edp",
"count": "cnt",
"calendar": "cal",
"venue": "adn",
}
_date_fields = {
"sdp": None,
"edp": None,
}
_allowed_missing_fields = ["cap", "edp", "adn"]
def __str__(self):
return self.title
class EventInstructor(models.Model):
name = models.TextField(blank=True)
member = models.OneToOneField(
Member, on_delete=models.PROTECT, null=True, blank=True, db_constraint=False
)
def __str__(self):
return str(self.member) if self.member else self.name
class EventExtManager(models.Manager["EventExt"]):
def get_queryset(self) -> models.QuerySet["EventExt"]:
return (
super()
.get_queryset()
.annotate(duration=Sum(F("meeting_times__end") - F("meeting_times__start")))
)
# TODO: use simpler expression when GeneratedField fixed
# return super().get_queryset().annotate(duration=Sum("meeting_times__duration"))
class EventExt(Event):
"""Extension of `Event` to capture some fields not supported in MembershipWorks"""
objects = EventExtManager()
instructor = models.ForeignKey(
EventInstructor, on_delete=models.PROTECT, null=True, blank=True
)
materials_fee = models.DecimalField(
max_digits=13, decimal_places=4, null=True, blank=True
)
instructor_percentage = models.DecimalField(
max_digits=5, decimal_places=4, default=0.5
)
instructor_flat_rate = models.DecimalField(
max_digits=13, decimal_places=4, default=0
)
class Meta:
verbose_name = "event"
class EventMeetingTimeManager(models.Manager["EventMeetingTime"]):
def get_queryset(self) -> models.QuerySet["EventMeetingTime"]:
return super().get_queryset().annotate(duration=F("end") - F("start"))
class EventMeetingTime(models.Model):
objects = EventMeetingTimeManager()
event = models.ForeignKey(
EventExt, on_delete=models.CASCADE, related_name="meeting_times"
)
start = models.DateTimeField()
end = models.DateTimeField()
# TODO: Should use generated field instead of manager, but this is
# broken due to current Django bug, pending next release (> 5.0)
# ref: https://code.djangoproject.com/ticket/35019
# duration = models.GeneratedField(
# expression=F("end") - F("start"),
# output_field=models.DurationField(),
# db_persist=False,
# )
class Meta:
constraints = [
models.UniqueConstraint(
fields=["event", "start", "end"], name="unique_event_start_end"
)
]

View File

@ -4,12 +4,20 @@ import logging
from django.conf import settings
from django.db import transaction
from membershipworks.models import Member, Flag, Transaction
from membershipworks.models import (
Member,
Flag,
Transaction,
Event,
EventExt,
EventCategory,
)
from membershipworks import MembershipWorks
logger = logging.getLogger(__name__)
MAX_MEETING_TIME = timedelta(hours=6)
def flags_for_member(csv_member, all_flags, folders):
for flag in all_flags:
@ -50,7 +58,7 @@ def scrape_members(membershipworks: MembershipWorks):
)
# create/update member
member = Member.from_csv_dict(csv_member)
member = Member.from_api_dict(csv_member)
member.clean_fields()
member.save()
member.flags.set(flags_for_member(csv_member, flags, folders))
@ -79,7 +87,7 @@ def scrape_transactions(membershipworks: MembershipWorks):
)
for csv_transaction in transactions:
Transaction.from_csv_dict(csv_transaction).save()
Transaction.from_api_dict(csv_transaction).save()
@transaction.atomic
@ -91,3 +99,45 @@ def scrape_membershipworks(*args, **options):
scrape_members(membershipworks)
scrape_transactions(membershipworks)
def scrape_events():
membershipworks = MembershipWorks()
membershipworks.login(
settings.MEMBERSHIPWORKS_USERNAME, settings.MEMBERSHIPWORKS_PASSWORD
)
data = membershipworks.get_events_list(
datetime.fromtimestamp(0), datetime.now() + timedelta(weeks=52), categories=True
)
logger.info(f"{len(data)} events retrieved!")
for category_id, category_data in enumerate(data["_st"]["evg"]):
category = EventCategory.from_api_dict(category_id, category_data)
category.clean_fields()
category.save()
for event_data in data["evt"]:
logger.debug(event_data)
event = Event.from_api_dict(event_data)
event.clean_fields()
event.save()
try:
event_ext = EventExt.objects.get(event_ptr=event)
except EventExt.DoesNotExist:
event_ext = EventExt(event_ptr=event)
# create extension model instance
event_ext.save_base(raw=True)
event_ext.refresh_from_db()
if (
event_ext.end is not None
and event_ext.end - event_ext.start < MAX_MEETING_TIME
):
meeting_times_count = event_ext.meeting_times.count()
if meeting_times_count == 0:
event_ext.meeting_times.create(start=event_ext.start, end=event_ext.end)
# if there is exactly one meeting time, it should match the event start/end
elif meeting_times_count == 1:
event_ext.meeting_times.update(start=event_ext.start, end=event_ext.end)

View File

@ -1,6 +1,6 @@
{% extends "base.dj.html" %}
{% load bleach_tags %}
{% load nh3_tags %}
{% block title %}Upcoming Events{% endblock %}
{% block content %}
@ -73,11 +73,11 @@
{# djlint:off H006 #}
<img class="{% cycle 'alignleft' 'alignright' %}"
width="400"
alt="Image for {{ event.ttl|bleach }}"
alt="Image for {{ event.ttl|nh3 }}"
src="{{ event.lgo.l }}">
{# djlint:on #}
{% endif %}
<span>{{ event.ttl|bleach }}</span>
<span>{{ event.ttl|nh3 }}</span>
</a>
</h2>
<!-- /wp:heading -->
@ -92,7 +92,7 @@
<!-- /wp:paragraph -->
{% if not section.truncate %}
<!-- wp:tadv/classic-paragraph -->
<div>{{ event.dtl|bleach:"a,abbr,acronym,b,blockquote,code,em,i,li,ol,strong,ul,p,span,br,div" }}</div>
<div>{{ event.dtl|nh3:"a,abbr,acronym,b,blockquote,code,em,i,li,ol,strong,ul,p,span,br,div" }}</div>
<!-- /wp:tadv/classic-paragraph -->
<!-- wp:paragraph -->
<p>

View File

@ -107,6 +107,7 @@ class CertificationAdmin(admin.ModelAdmin):
"certification_version__definition__name",
"certification_version__definition__department__name",
]
date_hierarchy = "date"
autocomplete_fields = ["member"]
exclude = ["shop_lead_notified"]
inlines = [CertificationAuditInline]

293
pdm.lock generated
View File

@ -5,7 +5,7 @@
groups = ["default", "debug", "lint", "server", "typing", "dev"]
strategy = ["cross_platform"]
lock_version = "4.4"
content_hash = "sha256:ce53f1fbeaf17cf8d491af18f7584e7d221bd17d074dfaec9df0099aefbc979c"
content_hash = "sha256:91f554bae127245b4082d069629400706b8b43daf3bf1fb8fd963eee120ff449"
[[package]]
name = "aiohttp"
@ -166,7 +166,7 @@ files = [
[[package]]
name = "black"
version = "23.11.0"
version = "23.12.1"
requires_python = ">=3.8"
summary = "The uncompromising code formatter."
dependencies = [
@ -177,26 +177,16 @@ dependencies = [
"platformdirs>=2",
]
files = [
{file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"},
{file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"},
{file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"},
{file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"},
{file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"},
{file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"},
]
[[package]]
name = "bleach"
version = "6.1.0"
requires_python = ">=3.8"
summary = "An easy safelist-based HTML-sanitizing tool."
dependencies = [
"six>=1.9.0",
"webencodings",
]
files = [
{file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"},
{file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"},
{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]]
@ -420,15 +410,15 @@ files = [
[[package]]
name = "django-admin-logs"
version = "1.0.2"
requires_python = ">=3.5"
version = "1.1.0"
requires_python = ">=3.6"
summary = "View, delete or disable Django admin log entries."
dependencies = [
"Django>=2.1",
"Django>=3.2",
]
files = [
{file = "django-admin-logs-1.0.2.tar.gz", hash = "sha256:aedb5df940d32c10423d65136343bc009727df8a5a49ed0196e65241d823a890"},
{file = "django_admin_logs-1.0.2-py3-none-any.whl", hash = "sha256:81753c20d372bc5562fe4a09090418bbb61b308388e851b19192873a472fa3d1"},
{file = "django-admin-logs-1.1.0.tar.gz", hash = "sha256:bb87cd944cfa14b6d90c93584fbcdc3ffde9410fd999c65a0b524b94518a5c64"},
{file = "django_admin_logs-1.1.0-py3-none-any.whl", hash = "sha256:bb139a99a08a4b08a98731efe9112a6ba269ab0af5efcdba435d1c79706fde16"},
]
[[package]]
@ -457,19 +447,6 @@ files = [
{file = "django-autocomplete-light-3.9.7.tar.gz", hash = "sha256:a34f192ac438c4df056dbfd399550799ddc631c4661960134ded924648770373"},
]
[[package]]
name = "django-bleach"
version = "1.0.0"
summary = "Easily use bleach with Django models and templates"
dependencies = [
"Django>=1.11",
"bleach>=1.5.0",
]
files = [
{file = "django-bleach-1.0.0.tar.gz", hash = "sha256:2586b90d641d4d7e70ee353570ad33d3625ed4b97036a3ea5b03ea1bb5bbeccd"},
{file = "django_bleach-1.0.0-py2.py3-none-any.whl", hash = "sha256:60074a4f4bc8d5200fdb2e03dce16fb4913427698b64570bc3e1a7ea1b8c3cf7"},
]
[[package]]
name = "django-debug-toolbar"
version = "4.2.0"
@ -499,7 +476,7 @@ files = [
[[package]]
name = "django-markdownx"
version = "4.0.5"
version = "4.0.7"
summary = "A comprehensive Markdown editor built for Django."
dependencies = [
"Django",
@ -507,8 +484,21 @@ dependencies = [
"Pillow",
]
files = [
{file = "django-markdownx-4.0.5.tar.gz", hash = "sha256:b6007790363743aad06c70a2fa49158b4149a6226f52213b3ed3f40d790cb4d3"},
{file = "django_markdownx-4.0.5-py2.py3-none-any.whl", hash = "sha256:31cf644e38720439eb48978ea4cef8d942067408019ea8b2db802233a6377455"},
{file = "django-markdownx-4.0.7.tar.gz", hash = "sha256:38aa331c2ca0bee218b77f462361b5393e4727962bc6021939c09048363cb6ea"},
{file = "django_markdownx-4.0.7-py2.py3-none-any.whl", hash = "sha256:c1975ae3053481d4c111abd38997a5b5bb89235a1e3215f995d835942925fe7b"},
]
[[package]]
name = "django-nh3"
version = "0.1.1"
requires_python = ">=3.10"
summary = "Django integration with for nh3, Python binding to Ammonia HTML sanitizer Rust crate."
dependencies = [
"Django>=3.2",
"nh3",
]
files = [
{file = "django_nh3-0.1.1-py3-none-any.whl", hash = "sha256:10df44fd9c1d1bc5d88739094826c636c2c256ba9d89d17e4356280bb8e159a0"},
]
[[package]]
@ -564,24 +554,24 @@ files = [
[[package]]
name = "django-stubs"
version = "4.2.6"
version = "4.2.7"
requires_python = ">=3.8"
summary = "Mypy stubs for Django"
dependencies = [
"django",
"django-stubs-ext>=4.2.5",
"django-stubs-ext>=4.2.7",
"types-PyYAML",
"types-pytz",
"typing-extensions",
]
files = [
{file = "django-stubs-4.2.6.tar.gz", hash = "sha256:e60b43de662a199db4b15c803c06669e0ac5035614af291cbd3b91591f7dcc94"},
{file = "django_stubs-4.2.6-py3-none-any.whl", hash = "sha256:2fcd257884a68dfa02de41ee5410ec805264d9b07d9b5b119e4dea82c7b8345e"},
{file = "django-stubs-4.2.7.tar.gz", hash = "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b"},
{file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"},
]
[[package]]
name = "django-stubs-ext"
version = "4.2.5"
version = "4.2.7"
requires_python = ">=3.8"
summary = "Monkey-patching and extensions for django-stubs"
dependencies = [
@ -589,8 +579,23 @@ dependencies = [
"typing-extensions",
]
files = [
{file = "django-stubs-ext-4.2.5.tar.gz", hash = "sha256:8c4d1fb5f68419b3b2474c659681a189803e27d6a5e5abf5aa0da57601b58633"},
{file = "django_stubs_ext-4.2.5-py3-none-any.whl", hash = "sha256:921cd7ae4614e74c234bc0fe86ee75537d163addfe1fc6f134bf03e29d86c01e"},
{file = "django-stubs-ext-4.2.7.tar.gz", hash = "sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3"},
{file = "django_stubs_ext-4.2.7-py3-none-any.whl", hash = "sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c"},
]
[[package]]
name = "django-stubs"
version = "4.2.7"
extras = ["compatible-mypy"]
requires_python = ">=3.8"
summary = "Mypy stubs for Django"
dependencies = [
"django-stubs==4.2.7",
"mypy~=1.7.0",
]
files = [
{file = "django-stubs-4.2.7.tar.gz", hash = "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b"},
{file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"},
]
[[package]]
@ -619,40 +624,40 @@ files = [
[[package]]
name = "djangorestframework-stubs"
version = "3.14.4"
version = "3.14.5"
requires_python = ">=3.8"
summary = "PEP-484 stubs for django-rest-framework"
dependencies = [
"django-stubs>=4.2.5",
"mypy>=0.991",
"django-stubs>=4.2.7",
"requests>=2.0.0",
"types-PyYAML>=5.4.3",
"types-requests>=0.1.12",
"typing-extensions>=3.10.0",
]
files = [
{file = "djangorestframework-stubs-3.14.4.tar.gz", hash = "sha256:8ee8719bfeb647b92cc200e15b3cc9813d2e4468c8190777a55a121542a4b2d4"},
{file = "djangorestframework_stubs-3.14.4-py3-none-any.whl", hash = "sha256:5be8275dd05d6629b3d1688929586ef7b6bc66b4f3f728b5e0389305f07c7a7f"},
{file = "djangorestframework-stubs-3.14.5.tar.gz", hash = "sha256:5dd6f638aa5291fb7863e6166128a6ed20bf4986e2fc5cf334e6afc841797a09"},
{file = "djangorestframework_stubs-3.14.5-py3-none-any.whl", hash = "sha256:43d788fd50cda49b922cd411e59c5b8cdc3f3de49c02febae12ce42139f0269b"},
]
[[package]]
name = "djangorestframework-stubs"
version = "3.14.4"
version = "3.14.5"
extras = ["compatible-mypy"]
requires_python = ">=3.8"
summary = "PEP-484 stubs for django-rest-framework"
dependencies = [
"djangorestframework-stubs==3.14.4",
"mypy~=1.6.0",
"django-stubs[compatible-mypy]",
"djangorestframework-stubs==3.14.5",
"mypy~=1.7.0",
]
files = [
{file = "djangorestframework-stubs-3.14.4.tar.gz", hash = "sha256:8ee8719bfeb647b92cc200e15b3cc9813d2e4468c8190777a55a121542a4b2d4"},
{file = "djangorestframework_stubs-3.14.4-py3-none-any.whl", hash = "sha256:5be8275dd05d6629b3d1688929586ef7b6bc66b4f3f728b5e0389305f07c7a7f"},
{file = "djangorestframework-stubs-3.14.5.tar.gz", hash = "sha256:5dd6f638aa5291fb7863e6166128a6ed20bf4986e2fc5cf334e6afc841797a09"},
{file = "djangorestframework_stubs-3.14.5-py3-none-any.whl", hash = "sha256:43d788fd50cda49b922cd411e59c5b8cdc3f3de49c02febae12ce42139f0269b"},
]
[[package]]
name = "djlint"
version = "1.34.0"
version = "1.34.1"
requires_python = ">=3.8.0,<4.0.0"
summary = "HTML Template Linter and Formatter"
dependencies = [
@ -664,13 +669,13 @@ dependencies = [
"html-void-elements<0.2.0,>=0.1.0",
"jsbeautifier<2.0.0,>=1.14.4",
"json5<0.10.0,>=0.9.11",
"pathspec<0.12.0,>=0.11.0",
"pathspec<0.13.0,>=0.12.0",
"regex<2024.0.0,>=2023.0.0",
"tqdm<5.0.0,>=4.62.2",
]
files = [
{file = "djlint-1.34.0-py3-none-any.whl", hash = "sha256:bdc26cc607dee8b46e262654eb0fbac7862c34d68172c8adc25a0b56fc7d8173"},
{file = "djlint-1.34.0.tar.gz", hash = "sha256:60b4f4ca99fd83106603bdd466f35314fda33776f3a6e70ea9d674da9d0ad053"},
{file = "djlint-1.34.1-py3-none-any.whl", hash = "sha256:96ff1c464fb6f061130ebc88663a2ea524d7ec51f4b56221a2b3f0320a3cfce8"},
{file = "djlint-1.34.1.tar.gz", hash = "sha256:db93fa008d19eaadb0454edf1704931d14469d48508daba2df9941111f408346"},
]
[[package]]
@ -869,8 +874,8 @@ files = [
[[package]]
name = "ipython"
version = "8.18.1"
requires_python = ">=3.9"
version = "8.19.0"
requires_python = ">=3.10"
summary = "IPython: Productive Interactive Computing"
dependencies = [
"colorama; sys_platform == \"win32\"",
@ -884,8 +889,8 @@ dependencies = [
"traitlets>=5",
]
files = [
{file = "ipython-8.18.1-py3-none-any.whl", hash = "sha256:e8267419d72d81955ec1177f8a29aaa90ac80ad647499201119e2f05e99aa397"},
{file = "ipython-8.18.1.tar.gz", hash = "sha256:ca6f079bb33457c66e233e4580ebfc4128855b4cf6370dddd73842a9563e8a27"},
{file = "ipython-8.19.0-py3-none-any.whl", hash = "sha256:2f55d59370f59d0d2b2212109fe0e6035cfea436b1c0e6150ad2244746272ec5"},
{file = "ipython-8.19.0.tar.gz", hash = "sha256:ac4da4ecf0042fb4e0ce57c60430c2db3c719fa8bdf92f8631d6bd8a5785d1f0"},
]
[[package]]
@ -924,40 +929,41 @@ files = [
[[package]]
name = "lxml"
version = "4.9.3"
version = "5.0.0"
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*"
summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
files = [
{file = "lxml-4.9.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5"},
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf"},
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a"},
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f"},
{file = "lxml-4.9.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b"},
{file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120"},
{file = "lxml-4.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6"},
{file = "lxml-4.9.3-cp311-cp311-win32.whl", hash = "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305"},
{file = "lxml-4.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc"},
{file = "lxml-4.9.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4"},
{file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be"},
{file = "lxml-4.9.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13"},
{file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9"},
{file = "lxml-4.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5"},
{file = "lxml-4.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8"},
{file = "lxml-4.9.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35"},
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0"},
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3"},
{file = "lxml-4.9.3-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b"},
{file = "lxml-4.9.3-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b"},
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7"},
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d"},
{file = "lxml-4.9.3-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b"},
{file = "lxml-4.9.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a"},
{file = "lxml-4.9.3-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0"},
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694"},
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7"},
{file = "lxml-4.9.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4"},
{file = "lxml-4.9.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9"},
{file = "lxml-4.9.3.tar.gz", hash = "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"},
{file = "lxml-5.0.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5382612ba2424cea5d2c89e2c29077023d8de88f8d60d5ceff5f76334516df9e"},
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:07a900735bad9af7be3085480bf384f68ed5580ba465b39a098e6a882c060d6b"},
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:980ba47c8db4b9d870014c7040edb230825b79017a6a27aa54cdb6fcc02d8cc0"},
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6507c58431dbd95b50654b3313c5ad54f90e54e5f2cdacf733de61eae478eec5"},
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:4a45a278518e4308865c1e9dbb2c42ce84fb154efb03adeb16fdae3c1687c7c9"},
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:59cea9ba1c675fbd6867ca1078fc717a113e7f5b7644943b74137b7cc55abebf"},
{file = "lxml-5.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dd39ef87fd1f7bb5c4aa53454936e6135cbfe03fe3744e8218be193f9e4fef16"},
{file = "lxml-5.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e6bb39d91bf932e7520cb5718ae3c2f498052aca53294d5d59fdd9068fe1a7f2"},
{file = "lxml-5.0.0-cp311-cp311-win32.whl", hash = "sha256:21af2c3862db6f4f486cddf73ec1157b40d5828876c47cd880edcbad8240ea1b"},
{file = "lxml-5.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:c1249aa4eaced30b59ecf8b8cae0b1ccede04583c74ca7d10b6f8bbead908b2c"},
{file = "lxml-5.0.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f30e697b6215e759d0824768b2c5b0618d2dc19abe6c67eeed2b0460f52470d1"},
{file = "lxml-5.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d1bb64646480c36a4aa1b6a44a5b6e33d0fcbeab9f53f1b39072cd3bb2c6243a"},
{file = "lxml-5.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4e69c36c8618707a90ed3fb6f48a6cc9254ffcdbf7b259e439a5ae5fbf9c5206"},
{file = "lxml-5.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9ca498f8554a09fbc3a2f8fc4b23261e07bc27bef99b3df98e2570688033f6fc"},
{file = "lxml-5.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0326e9b8176ea77269fb39e7af4010906e73e9496a9f8eaf06d253b1b1231ceb"},
{file = "lxml-5.0.0-cp312-cp312-win32.whl", hash = "sha256:5fb988e15378d6e905ca8f60813950a0c56da9469d0e8e5d8fe785b282684ec5"},
{file = "lxml-5.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:bb58e8f4b2cfe012cd312239b8d5139995fe8f5945c7c26d5fbbbb1ddb9acd47"},
{file = "lxml-5.0.0-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:96095bfc0c02072fc89afa67626013a253596ea5118b8a7f4daaae049dafa096"},
{file = "lxml-5.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:992029258ed719f130d5a9c443d142c32843046f1263f2c492862b2a853be570"},
{file = "lxml-5.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:db40e85cffd22f7d65dcce30e85af565a66401a6ed22fc0c56ed342cfa4ffc43"},
{file = "lxml-5.0.0-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:cfa8a4cdc3765574b7fd0c7cfa5fbd1e2108014c9dfd299c679e5152bea9a55e"},
{file = "lxml-5.0.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:049fef98d02513c34f5babd07569fc1cf1ed14c0f2fbff18fe72597f977ef3c2"},
{file = "lxml-5.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a85136d0ee18a41c91cc3e2844c683be0e72e6dda4cb58da9e15fcaef3726af7"},
{file = "lxml-5.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:766868f729f3ab84125350f1a0ea2594d8b1628a608a574542a5aff7355b9941"},
{file = "lxml-5.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:99cad5c912f359e59e921689c04e54662cdd80835d80eeaa931e22612f515df7"},
{file = "lxml-5.0.0-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:c90c593aa8dd57d5dab0ef6d7d64af894008971d98e6a41b320fdd75258fbc6e"},
{file = "lxml-5.0.0-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8134d5441d1ed6a682e3de3d7a98717a328dce619ee9c4c8b3b91f0cb0eb3e28"},
{file = "lxml-5.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f298ac9149037d6a3d5c74991bded39ac46292520b9c7c182cb102486cc87677"},
{file = "lxml-5.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:894c5f71186b410679aaab5774543fcb9cbabe8893f0b31d11cf28a0740e80be"},
{file = "lxml-5.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9cd3d6c2c67d4fdcd795e4945e2ba5434909c96640b4cc09453bd0dc7e8e1bac"},
{file = "lxml-5.0.0.zip", hash = "sha256:2219cbf790e701acf9a21a31ead75f983e73daf0eceb9da6990212e4d20ebefe"},
]
[[package]]
@ -1110,7 +1116,7 @@ files = [
[[package]]
name = "mypy"
version = "1.6.1"
version = "1.7.1"
requires_python = ">=3.8"
summary = "Optional static typing for Python"
dependencies = [
@ -1118,18 +1124,18 @@ dependencies = [
"typing-extensions>=4.1.0",
]
files = [
{file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"},
{file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"},
{file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"},
{file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"},
{file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"},
{file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"},
{file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"},
{file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"},
{file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"},
{file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"},
{file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"},
{file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"},
{file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"},
{file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"},
{file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"},
{file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"},
{file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"},
{file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"},
{file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"},
{file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"},
{file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"},
{file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"},
{file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"},
{file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"},
]
[[package]]
@ -1144,14 +1150,39 @@ files = [
[[package]]
name = "mysqlclient"
version = "2.2.0"
version = "2.2.1"
requires_python = ">=3.8"
summary = "Python interface to MySQL"
files = [
{file = "mysqlclient-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5670679ff1be1cc3fef0fa81bf39f0cd70605ba121141050f02743eb878ac114"},
{file = "mysqlclient-2.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:955dba905a7443ce4788c63fdb9f8d688316260cf60b20ff51ac3b1c77616ede"},
{file = "mysqlclient-2.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:530ece9995a36cadb6211b9787f0c9e05cdab6702549bdb4236af5e9b535ed6a"},
{file = "mysqlclient-2.2.0.tar.gz", hash = "sha256:04368445f9c487d8abb7a878e3d23e923e6072c04a6c320f9e0dc8a82efba14e"},
{file = "mysqlclient-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:8f40c872f19639366e3df27bef2ff087be0e3ee0bd3453470bd29f46b54a90f6"},
{file = "mysqlclient-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:45600f4f321096bd1ead3355bc62cfcf8d97dc78df94e4ab5db72ecb5db1bd04"},
{file = "mysqlclient-2.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:97eee76818774bb695e018ff4c3dafaab74b9a0b0cf32c90b02caeec3b19cd8e"},
{file = "mysqlclient-2.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4fabe1f4b545ed6244ad0ff426e6b27054b7e5c5b1392be0de2e5f2f59be0392"},
{file = "mysqlclient-2.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:641a7c9de443ddef186a0e89f24b4251ad44f4ddc5e7094332bf2d286d7c9e33"},
{file = "mysqlclient-2.2.1.tar.gz", hash = "sha256:2c7ad15b87293b12fd44b47c46879ec95ec647f4567e866ccd70b8337584e9b2"},
]
[[package]]
name = "nh3"
version = "0.2.15"
summary = "Python bindings to the ammonia HTML sanitization library."
files = [
{file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0"},
{file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3"},
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97"},
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28"},
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf"},
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911"},
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf"},
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7"},
{file = "nh3-0.2.15-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305"},
{file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770"},
{file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6"},
{file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d"},
{file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5"},
{file = "nh3-0.2.15-cp37-abi3-win32.whl", hash = "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601"},
{file = "nh3-0.2.15-cp37-abi3-win_amd64.whl", hash = "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e"},
{file = "nh3-0.2.15.tar.gz", hash = "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3"},
]
[[package]]
@ -1191,12 +1222,12 @@ files = [
[[package]]
name = "pathspec"
version = "0.11.0"
requires_python = ">=3.7"
version = "0.12.1"
requires_python = ">=3.8"
summary = "Utility library for gitignore style pattern matching of file paths."
files = [
{file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"},
{file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"},
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
@ -1535,12 +1566,12 @@ files = [
[[package]]
name = "setuptools"
version = "69.0.2"
version = "69.0.3"
requires_python = ">=3.8"
summary = "Easily download, build, install, upgrade, and uninstall Python packages"
files = [
{file = "setuptools-69.0.2-py3-none-any.whl", hash = "sha256:1e8fdff6797d3865f37397be788a4e3cba233608e9b509382a2777d25ebde7f2"},
{file = "setuptools-69.0.2.tar.gz", hash = "sha256:735896e78a4742605974de002ac60562d286fa8051a7e2299445e8e8fbb01aa6"},
{file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"},
{file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"},
]
[[package]]
@ -1599,15 +1630,15 @@ files = [
[[package]]
name = "tinycss2"
version = "1.2.1"
requires_python = ">=3.7"
version = "1.1.1"
requires_python = ">=3.6"
summary = "A tiny CSS parser"
dependencies = [
"webencodings>=0.4",
]
files = [
{file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"},
{file = "tinycss2-1.2.1.tar.gz", hash = "sha256:8cff3a8f066c2ec677c06dbc7b45619804a6938478d9d73c284b29d14ecb0627"},
{file = "tinycss2-1.1.1-py3-none-any.whl", hash = "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8"},
{file = "tinycss2-1.1.1.tar.gz", hash = "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf"},
]
[[package]]
@ -1847,7 +1878,7 @@ files = [
[[package]]
name = "weasyprint"
version = "60.1"
version = "60.2"
requires_python = ">=3.7"
summary = "The Awesome Document Factory"
dependencies = [
@ -1861,8 +1892,8 @@ dependencies = [
"tinycss2>=1.0.0",
]
files = [
{file = "weasyprint-60.1-py3-none-any.whl", hash = "sha256:55227e5e44f5f34bc9cec651329bd38d063ef7d29151d4b058d4af1ca943d4a7"},
{file = "weasyprint-60.1.tar.gz", hash = "sha256:56b9812280118357b0f63b1efe18199e08343d4a56a3393c1d475ab878cea26a"},
{file = "weasyprint-60.2-py3-none-any.whl", hash = "sha256:3e98eedcc1c5a14cb310c293c6d59a479f59a13f0d705ff07106482827fa5705"},
{file = "weasyprint-60.2.tar.gz", hash = "sha256:0c0cdd617a78699262b80026e67fa1692e3802cfa966395436eeaf6f787dd126"},
]
[[package]]

View File

@ -7,7 +7,7 @@ authors = [
]
dependencies = [
"django~=5.0",
"django-admin-logs~=1.0",
"django-admin-logs~=1.1",
"django-auth-ldap~=4.6",
"django-markdownx~=4.0",
"django-recurrence~=1.11",
@ -17,18 +17,18 @@ dependencies = [
"mdformat~=0.7",
"mdformat-tables~=0.4",
"mysqlclient~=2.2",
"bleach~=6.1",
"django-autocomplete-light~=3.9",
"weasyprint~=60.1",
"weasyprint~=60.2",
"requests~=2.31",
"semver~=3.0",
"djangorestframework~=3.14",
"django-q2~=1.6",
"lxml~=4.9",
"lxml~=5.0",
"django-object-actions~=4.2",
"udm-rest-client~=1.2",
"openapi-client-udm~=1.0",
"django-bleach~=1.0",
"django-nh3~=0.1",
"nh3~=0.2",
]
requires-python = ">=3.11"
@ -82,11 +82,11 @@ include_packages = ["openapi-client-udm"]
[tool.pdm.dev-dependencies]
lint = [
"black~=23.11",
"black~=23.12",
"djlint~=1.34",
]
typing = [
"mypy~=1.6",
"mypy~=1.7",
"django-stubs~=4.2",
"setuptools~=69.0",
"types-bleach~=6.1",
@ -100,7 +100,7 @@ debug = [
]
dev = [
"django-extensions~=3.2",
"ipython~=8.18",
"ipython~=8.19",
]
[tool.pdm.scripts]

View File

@ -1,4 +1,4 @@
import bleach
import nh3
from markdownx.utils import markdownify
# fmt: off
@ -23,5 +23,5 @@ MARKDOWN_ATTRS = {
def markdown_to_clean_html(md: str) -> str:
x = bleach.clean(markdownify(md), tags=MARKDOWN_TAGS, attributes=MARKDOWN_ATTRS)
x = nh3.clean(markdownify(md), tags=MARKDOWN_TAGS, attributes=MARKDOWN_ATTRS)
return x