Compare commits
No commits in common. "dcf483d19ee38e01e2fa89bd99eb271e04f4a7f0" and "a7e7fafedd0ee622dc0f9782cc58cd0feb58a23e" have entirely different histories.
dcf483d19e
...
a7e7fafedd
@ -1,81 +0,0 @@
|
|||||||
from collections.abc import Sequence
|
|
||||||
from typing import Any, ClassVar, TypeAlias
|
|
||||||
|
|
||||||
from django.core import mail
|
|
||||||
from django.template import loader
|
|
||||||
|
|
||||||
import mdformat
|
|
||||||
from markdownify import markdownify
|
|
||||||
|
|
||||||
Context: TypeAlias = dict[str, Any]
|
|
||||||
|
|
||||||
|
|
||||||
class EmailBase:
|
|
||||||
subject: ClassVar[str]
|
|
||||||
from_email: ClassVar[str | None] = None
|
|
||||||
reply_to: ClassVar[Sequence[str] | None] = None
|
|
||||||
|
|
||||||
context: Context
|
|
||||||
|
|
||||||
def __init__(self, context: Context) -> None:
|
|
||||||
self.context = context
|
|
||||||
|
|
||||||
def render_body(self) -> str:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def render(
|
|
||||||
cls,
|
|
||||||
context: Context,
|
|
||||||
to: list[str] | None = None,
|
|
||||||
cc: list[str] | None = None,
|
|
||||||
bcc: list[str] | None = None,
|
|
||||||
) -> mail.EmailMessage:
|
|
||||||
self = cls(context)
|
|
||||||
|
|
||||||
body = self.render_body()
|
|
||||||
|
|
||||||
return mail.EmailMessage(
|
|
||||||
self.subject,
|
|
||||||
body,
|
|
||||||
from_email=self.from_email,
|
|
||||||
reply_to=self.reply_to,
|
|
||||||
to=to,
|
|
||||||
cc=cc,
|
|
||||||
bcc=bcc,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TemplatedMultipartEmail(EmailBase):
|
|
||||||
template: ClassVar[str]
|
|
||||||
|
|
||||||
def render_html_body(self) -> str:
|
|
||||||
template = loader.get_template(self.template)
|
|
||||||
return template.render(self.context)
|
|
||||||
|
|
||||||
def render_body(self, html_body: str) -> str:
|
|
||||||
return mdformat.text(markdownify(html_body), extensions={"tables"})
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def render(
|
|
||||||
cls,
|
|
||||||
context: Context,
|
|
||||||
to: list[str] | None = None,
|
|
||||||
cc: list[str] | None = None,
|
|
||||||
bcc: list[str] | None = None,
|
|
||||||
) -> mail.EmailMessage:
|
|
||||||
self = cls(context)
|
|
||||||
|
|
||||||
html_body = self.render_html_body()
|
|
||||||
plain_body = self.render_body(html_body)
|
|
||||||
|
|
||||||
return mail.EmailMultiAlternatives(
|
|
||||||
self.subject,
|
|
||||||
plain_body,
|
|
||||||
from_email=self.from_email,
|
|
||||||
reply_to=self.reply_to,
|
|
||||||
to=to,
|
|
||||||
cc=cc,
|
|
||||||
bcc=bcc,
|
|
||||||
alternatives=[(html_body, "text/html")],
|
|
||||||
)
|
|
@ -7,7 +7,6 @@ def post_migrate_callback(sender, **kwargs):
|
|||||||
|
|
||||||
from cmsmanage.django_q2_helper import ensure_scheduled
|
from cmsmanage.django_q2_helper import ensure_scheduled
|
||||||
|
|
||||||
from .tasks.event_survey_emails import send_survey_emails
|
|
||||||
from .tasks.scrape import scrape_events, scrape_membershipworks
|
from .tasks.scrape import scrape_events, scrape_membershipworks
|
||||||
from .tasks.ucsAccounts import sync_accounts
|
from .tasks.ucsAccounts import sync_accounts
|
||||||
|
|
||||||
@ -30,12 +29,6 @@ def post_migrate_callback(sender, **kwargs):
|
|||||||
minutes=15,
|
minutes=15,
|
||||||
)
|
)
|
||||||
|
|
||||||
ensure_scheduled(
|
|
||||||
sync_accounts.q_task_group,
|
|
||||||
send_survey_emails,
|
|
||||||
schedule_type=Schedule.HOURLY,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MembershipworksConfig(AppConfig):
|
class MembershipworksConfig(AppConfig):
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
@ -1,53 +1,58 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage, EmailMultiAlternatives
|
||||||
|
from django.template import loader
|
||||||
|
|
||||||
|
import mdformat
|
||||||
|
from markdownify import markdownify
|
||||||
|
|
||||||
from cmsmanage.email import TemplatedMultipartEmail
|
|
||||||
from membershipworks.models import EventInvoice
|
from membershipworks.models import EventInvoice
|
||||||
|
|
||||||
|
|
||||||
class InvoiceEmailBase(TemplatedMultipartEmail):
|
def make_multipart_email(
|
||||||
from_email = "CMS Invoices <invoices@claremontmakerspace.org>"
|
subject: str, html_body: str, to: tuple[str]
|
||||||
reply_to = ["Claremont MakerSpace <Info@ClaremontMakerSpace.org>"]
|
) -> EmailMultiAlternatives:
|
||||||
|
plain_body = mdformat.text(markdownify(html_body), extensions={"tables"})
|
||||||
|
|
||||||
|
email = EmailMultiAlternatives(
|
||||||
|
subject,
|
||||||
|
plain_body,
|
||||||
|
from_email="CMS Invoices <invoices@claremontmakerspace.org>",
|
||||||
|
to=to,
|
||||||
|
reply_to=["Claremont MakerSpace <Info@ClaremontMakerSpace.org>"],
|
||||||
|
)
|
||||||
|
|
||||||
|
email.attach_alternative(html_body, "text/html")
|
||||||
|
return email
|
||||||
|
|
||||||
|
|
||||||
class InstructorInvoiceEmail(InvoiceEmailBase):
|
def make_instructor_email(
|
||||||
template = "membershipworks/email/event_invoice_instructor.dj.html"
|
invoice: EventInvoice, pdf: bytes, event_url: str
|
||||||
|
|
||||||
@property
|
|
||||||
def subject(self) -> str:
|
|
||||||
event = self.context["invoice"].event
|
|
||||||
return f'Your CMS instructor invoice has been received for event "{event}" {event.start} - {event.end}'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def render_for_invoice(
|
|
||||||
cls, invoice: EventInvoice, pdf: bytes, event_url: str
|
|
||||||
) -> EmailMessage:
|
) -> EmailMessage:
|
||||||
if invoice.event.instructor is None or invoice.event.instructor.member is None:
|
if invoice.event.instructor is None or invoice.event.instructor.member is None:
|
||||||
raise ValueError("Event Instructor not defined or is not member")
|
raise ValueError("Event Instructor not defined or is not member")
|
||||||
message = cls.render(
|
template = loader.get_template(
|
||||||
{"invoice": invoice, "event_url": event_url},
|
"membershipworks/email/event_invoice_instructor.dj.html"
|
||||||
to=[invoice.event.instructor.member.sanitized_mailbox()],
|
)
|
||||||
|
html_body = template.render({"invoice": invoice, "event_url": event_url})
|
||||||
|
|
||||||
|
message = make_multipart_email(
|
||||||
|
f'Your CMS instructor invoice has been received for event "{invoice.event}" {invoice.event.start} - {invoice.event.end}',
|
||||||
|
html_body,
|
||||||
|
(invoice.event.instructor.member.sanitized_mailbox(),),
|
||||||
)
|
)
|
||||||
message.attach(f"CMS_event_invoice_{invoice.uuid}.pdf", pdf, "application/pdf")
|
message.attach(f"CMS_event_invoice_{invoice.uuid}.pdf", pdf, "application/pdf")
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
|
||||||
class AdminInvoiceEmail(InvoiceEmailBase):
|
def make_admin_email(invoice: EventInvoice, pdf: bytes, event_url: str) -> EmailMessage:
|
||||||
template = "membershipworks/email/event_invoice_admin.dj.html"
|
template = loader.get_template("membershipworks/email/event_invoice_admin.dj.html")
|
||||||
|
html_body = template.render({"invoice": invoice, "event_url": event_url})
|
||||||
|
|
||||||
@property
|
message = make_multipart_email(
|
||||||
def subject(self) -> str:
|
f'CMS instructor invoice created for event "{invoice.event}" {invoice.event.start} - {invoice.event.end}',
|
||||||
event = self.context["invoice"].event
|
html_body,
|
||||||
return f'CMS instructor invoice created for event "{event}" {event.start} - {event.end}'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def render_for_invoice(
|
|
||||||
cls, invoice: EventInvoice, pdf: bytes, event_url: str
|
|
||||||
) -> EmailMessage:
|
|
||||||
message = cls.render(
|
|
||||||
{"invoice": invoice, "event_url": event_url},
|
|
||||||
# TODO: should this be in database instead?
|
# TODO: should this be in database instead?
|
||||||
to=settings.INVOICE_HANDLERS,
|
settings.INVOICE_HANDLERS,
|
||||||
)
|
)
|
||||||
message.attach(f"CMS_event_invoice_{invoice.uuid}.pdf", pdf, "application/pdf")
|
message.attach(f"CMS_event_invoice_{invoice.uuid}.pdf", pdf, "application/pdf")
|
||||||
return message
|
return message
|
||||||
@ -57,6 +62,6 @@ def make_invoice_emails(
|
|||||||
invoice: EventInvoice, pdf: bytes, event_url: str
|
invoice: EventInvoice, pdf: bytes, event_url: str
|
||||||
) -> list[EmailMessage]:
|
) -> list[EmailMessage]:
|
||||||
return [
|
return [
|
||||||
InstructorInvoiceEmail.render_for_invoice(invoice, pdf, event_url),
|
make_instructor_email(invoice, pdf, event_url),
|
||||||
AdminInvoiceEmail.render_for_invoice(invoice, pdf, event_url),
|
make_admin_email(invoice, pdf, event_url),
|
||||||
]
|
]
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
|
|
||||||
from membershipworks.tasks.event_survey_emails import logger, send_survey_emails
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
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))
|
|
||||||
send_survey_emails()
|
|
@ -1,22 +0,0 @@
|
|||||||
# Generated by Django 5.0.6 on 2024-05-20 22:07
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("membershipworks", "0018_eventext_details_timestamp"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="eventext",
|
|
||||||
name="should_survey",
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="eventext",
|
|
||||||
name="survey_email_sent",
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
@ -554,9 +554,6 @@ class EventExt(Event):
|
|||||||
|
|
||||||
registrations = models.JSONField(null=True, blank=True)
|
registrations = models.JSONField(null=True, blank=True)
|
||||||
|
|
||||||
should_survey = models.BooleanField(default=False)
|
|
||||||
survey_email_sent = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
def get_absolute_url(self) -> str:
|
def get_absolute_url(self) -> str:
|
||||||
return reverse("membershipworks:event-detail", kwargs={"eid": self.eid})
|
return reverse("membershipworks:event-detail", kwargs={"eid": self.eid})
|
||||||
|
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import logging
|
|
||||||
from collections.abc import Iterable
|
|
||||||
from urllib.parse import quote, urlencode
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core import mail
|
|
||||||
from django.db.models.functions import Now
|
|
||||||
|
|
||||||
from cmsmanage.email import TemplatedMultipartEmail
|
|
||||||
from membershipworks.models import EventExt
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class EventSurveyEmail(TemplatedMultipartEmail):
|
|
||||||
# TODO: better wording
|
|
||||||
subject = (
|
|
||||||
"[Claremont MakerSpace] Please fill out a survey for your recent CMS class!"
|
|
||||||
)
|
|
||||||
from_email = "CMS Classes <classes@claremontmakerspace.org>"
|
|
||||||
|
|
||||||
template = "membershipworks/email/event_survey.dj.html"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def survey_url(event: EventExt, attendee_name: str, attendee_email: str) -> str:
|
|
||||||
return "https://claremontmakerspace.org/class-evaluation-form?" + urlencode(
|
|
||||||
{
|
|
||||||
"event_id": event.eid,
|
|
||||||
"instructor_name": str(event.instructor) if event.instructor else "",
|
|
||||||
"event_name": event.title,
|
|
||||||
"event_date": event.start.strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
"participant_name": attendee_name,
|
|
||||||
"participant_email": attendee_email,
|
|
||||||
},
|
|
||||||
quote_via=quote,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def render_for_event(cls, event: EventExt) -> Iterable[mail.EmailMessage]:
|
|
||||||
for name, email in event.attendees.values_list("name", "email"):
|
|
||||||
sanitized_email = mail.message.sanitize_address(
|
|
||||||
(name, email), settings.DEFAULT_CHARSET
|
|
||||||
)
|
|
||||||
survey_url = cls.survey_url(event, name, email)
|
|
||||||
yield cls.render(
|
|
||||||
{"event": event, "attendee_name": name, "survey_url": survey_url},
|
|
||||||
to=[sanitized_email],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def send_survey_emails():
|
|
||||||
with mail.get_connection() as conn:
|
|
||||||
for event in EventExt.objects.filter(
|
|
||||||
occurred=True, should_survey=True, survey_email_sent=False, end__lt=Now()
|
|
||||||
):
|
|
||||||
logger.info("Sending survey messages for event: %s", event)
|
|
||||||
|
|
||||||
# mark as sent even if we don't finish, to prevent sending duplicates
|
|
||||||
event.survey_email_sent = True
|
|
||||||
event.save()
|
|
||||||
|
|
||||||
conn.send_messages(list(EventSurveyEmail.render_for_event(event)))
|
|
@ -1,19 +0,0 @@
|
|||||||
{% load nh3_tags %}
|
|
||||||
|
|
||||||
<p>Dear {{ attendee_name }},</p>
|
|
||||||
<p>
|
|
||||||
Thank you for recently attending "{{ event.details.ttl|nh3 }}" at CMS on {{ event.start|date }}! We hope you enjoyed the experience and found it both informative and inspiring.
|
|
||||||
To help us continue to offer high-quality classes and improve our programs, we would greatly appreciate your feedback.
|
|
||||||
We kindly ask you to take a few minutes to complete a brief survey about your experience.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a href="{{ survey_url }}">Click here to fill out the survey</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Your insights are invaluable to us and will directly contribute to enhancing our offerings and ensuring that we meet the needs and expectations of our community.
|
|
||||||
Thank you in advance for your time and feedback. If you have any additional comments or questions, please feel free to reach out to us at <a href="mailto:info@claremontmakerspace.org">info@claremontmakerspace.org</a>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<div>Best regards,</div>
|
|
||||||
<div>The Claremont MakerSpace Team</div>
|
|
||||||
</p>
|
|
@ -1,114 +1,93 @@
|
|||||||
from abc import abstractmethod
|
|
||||||
from collections.abc import Iterable, Iterator
|
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
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.db.models import QuerySet
|
from django.template import loader
|
||||||
|
|
||||||
from cmsmanage.email import TemplatedMultipartEmail
|
import mdformat
|
||||||
from membershipworks.models import Member
|
from markdownify import markdownify
|
||||||
from paperwork.models import Certification, Department
|
|
||||||
|
|
||||||
|
|
||||||
class CertificationEmailBase(TemplatedMultipartEmail):
|
def make_multipart_email(subject, html_body, to):
|
||||||
from_email = "Claremont MakerSpace Member Certification System <Certifications@ClaremontMakerSpace.org>"
|
plain_body = mdformat.text(markdownify(html_body), extensions={"tables"})
|
||||||
reply_to = ["Claremont MakerSpace <Info@ClaremontMakerSpace.org>"]
|
|
||||||
|
|
||||||
@classmethod
|
email = mail.EmailMultiAlternatives(
|
||||||
@abstractmethod
|
subject,
|
||||||
def render_for_certifications(
|
plain_body,
|
||||||
cls, ordered_certifications: QuerySet[Certification]
|
"Claremont MakerSpace Member Certification System <Certifications@ClaremontMakerSpace.org>",
|
||||||
) -> Iterable[mail.EmailMessage]:
|
to,
|
||||||
raise NotImplementedError
|
reply_to=["Claremont MakerSpace <Info@ClaremontMakerSpace.org>"],
|
||||||
|
)
|
||||||
|
|
||||||
|
email.attach_alternative(html_body, "text/html")
|
||||||
|
return email
|
||||||
|
|
||||||
|
|
||||||
class DepartmentCertificationEmail(CertificationEmailBase):
|
def make_department_email(department, certifications):
|
||||||
template = "paperwork/email/department_certifications.dj.html"
|
template = loader.get_template("paperwork/email/department_certifications.dj.html")
|
||||||
|
|
||||||
@property
|
|
||||||
def subject(self) -> str:
|
|
||||||
certification_count = len(self.context["certifications"])
|
|
||||||
return f"{certification_count} new CMS Certifications issued for {self.context['department']}"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def render_for_department(
|
|
||||||
cls, department: Department, certifications: Iterator[Certification]
|
|
||||||
) -> Iterable[mail.EmailMessage]:
|
|
||||||
if department.shop_lead_flag is not None:
|
|
||||||
shop_leads = department.shop_lead_flag.members.values_list(
|
shop_leads = department.shop_lead_flag.members.values_list(
|
||||||
"first_name", "account_name", "email", named=True
|
"first_name", "account_name", "email", named=True
|
||||||
)
|
)
|
||||||
yield cls.render(
|
|
||||||
|
html_body = template.render(
|
||||||
{
|
{
|
||||||
"shop_lead_names": [
|
"shop_lead_names": [shop_lead.first_name for shop_lead in shop_leads],
|
||||||
shop_lead.first_name for shop_lead in shop_leads
|
|
||||||
],
|
|
||||||
"department": department,
|
"department": department,
|
||||||
"certifications": list(certifications),
|
"certifications": certifications,
|
||||||
},
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return make_multipart_email(
|
||||||
|
f"{len(certifications)} new CMS Certifications issued for {department}",
|
||||||
|
html_body,
|
||||||
to=[
|
to=[
|
||||||
sanitize_address((shop_lead.account_name, shop_lead.email), "ascii")
|
sanitize_address((shop_lead.account_name, shop_lead.email), "ascii")
|
||||||
for shop_lead in shop_leads
|
for shop_lead in shop_leads
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def render_for_certifications(
|
def department_emails(ordered_queryset):
|
||||||
cls, ordered_certifications: QuerySet[Certification]
|
|
||||||
) -> Iterable[mail.EmailMessage]:
|
|
||||||
certifications_by_department = groupby(
|
certifications_by_department = groupby(
|
||||||
ordered_certifications,
|
ordered_queryset, lambda c: c.certification_version.definition.department
|
||||||
lambda c: c.certification_version.definition.department,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for department, certifications in certifications_by_department:
|
for department, certifications in certifications_by_department:
|
||||||
yield from cls.render_for_department(department, certifications)
|
if department.shop_lead_flag is not None:
|
||||||
|
yield make_department_email(department, list(certifications))
|
||||||
|
|
||||||
|
|
||||||
class MemberCertificationEmail(CertificationEmailBase):
|
def make_member_email(member, certifications):
|
||||||
template = "paperwork/email/member_certifications.dj.html"
|
template = loader.get_template("paperwork/email/member_certifications.dj.html")
|
||||||
|
|
||||||
@property
|
html_body = template.render({"member": member, "certifications": certifications})
|
||||||
def subject(self) -> str:
|
|
||||||
return f"You have been issued {len(self.context['certifications'])} new CMS Certifications"
|
|
||||||
|
|
||||||
@classmethod
|
return make_multipart_email(
|
||||||
def render_for_member(
|
f"You have been issued {len(certifications)} new CMS Certifications",
|
||||||
cls, member: Member, certifications: list[Certification]
|
html_body,
|
||||||
) -> mail.EmailMessage:
|
|
||||||
return cls.render(
|
|
||||||
{"member": member, "certifications": certifications},
|
|
||||||
to=[sanitize_address((member.account_name, member.email), "ascii")],
|
to=[sanitize_address((member.account_name, member.email), "ascii")],
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def render_for_certifications(
|
def member_emails(ordered_queryset):
|
||||||
cls, ordered_certifications: QuerySet[Certification]
|
|
||||||
) -> Iterable[mail.EmailMessage]:
|
|
||||||
certifications_by_member = groupby(
|
certifications_by_member = groupby(
|
||||||
ordered_certifications.filter(member__isnull=False), lambda c: c.member
|
ordered_queryset.filter(member__isnull=False), lambda c: c.member
|
||||||
)
|
)
|
||||||
|
|
||||||
for member, certifications in certifications_by_member:
|
for member, certifications in certifications_by_member:
|
||||||
yield cls.render_for_member(member, list(certifications))
|
yield make_member_email(member, list(certifications))
|
||||||
|
|
||||||
|
|
||||||
class AdminCertificationEmail(CertificationEmailBase):
|
def admin_email(ordered_queryset):
|
||||||
template = "paperwork/email/admin_certifications.dj.html"
|
template = loader.get_template("paperwork/email/admin_certifications.dj.html")
|
||||||
|
html_body = template.render({"certifications": ordered_queryset})
|
||||||
|
|
||||||
@property
|
return make_multipart_email(
|
||||||
def subject(self) -> str:
|
f"{len(ordered_queryset)} new CMS Certifications issued",
|
||||||
return f"{len(self.context['certifications'])} new CMS Certifications issued"
|
html_body,
|
||||||
|
to=(
|
||||||
@classmethod
|
get_user_model()
|
||||||
def render_for_certifications(
|
|
||||||
cls, ordered_certifications: QuerySet[Certification]
|
|
||||||
) -> Iterable[mail.EmailMessage]:
|
|
||||||
yield cls.render(
|
|
||||||
{"certifications": ordered_certifications},
|
|
||||||
to=get_user_model()
|
|
||||||
.objects.with_perm(
|
.objects.with_perm(
|
||||||
"paperwork.receive_certification_emails",
|
"paperwork.receive_certification_emails",
|
||||||
include_superusers=False,
|
include_superusers=False,
|
||||||
@ -116,18 +95,16 @@ class AdminCertificationEmail(CertificationEmailBase):
|
|||||||
# TODO: LDAPBackend does not support with_perm() directly
|
# TODO: LDAPBackend does not support with_perm() directly
|
||||||
backend="django.contrib.auth.backends.ModelBackend",
|
backend="django.contrib.auth.backends.ModelBackend",
|
||||||
)
|
)
|
||||||
.values_list("email", flat=True),
|
.values_list("email", flat=True)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def all_certification_emails(queryset: QuerySet[Certification]):
|
def all_certification_emails(queryset):
|
||||||
ordered_queryset = queryset.select_related(
|
ordered_queryset = queryset.select_related(
|
||||||
"certification_version__definition"
|
"certification_version__definition"
|
||||||
).order_by("certification_version__definition__department")
|
).order_by("certification_version__definition__department")
|
||||||
|
|
||||||
for email_type in [
|
yield from department_emails(ordered_queryset)
|
||||||
DepartmentCertificationEmail,
|
yield from member_emails(ordered_queryset)
|
||||||
MemberCertificationEmail,
|
yield admin_email(ordered_queryset)
|
||||||
AdminCertificationEmail,
|
|
||||||
]:
|
|
||||||
yield from email_type.render_for_certifications(ordered_queryset)
|
|
||||||
|
194
pdm.lock
194
pdm.lock
@ -5,7 +5,7 @@
|
|||||||
groups = ["default", "debug", "lint", "server", "typing", "dev"]
|
groups = ["default", "debug", "lint", "server", "typing", "dev"]
|
||||||
strategy = ["cross_platform"]
|
strategy = ["cross_platform"]
|
||||||
lock_version = "4.4.1"
|
lock_version = "4.4.1"
|
||||||
content_hash = "sha256:8b3bb37d6fbada119262035c0e94ada756c2f43ca8863b50b51f47a232ee84b9"
|
content_hash = "sha256:651200bd58f4159fe99a599564e0f83a89fd149e5e7300abd151d6a3bb6477a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohttp"
|
name = "aiohttp"
|
||||||
@ -224,15 +224,15 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitstring"
|
name = "bitstring"
|
||||||
version = "4.2.2"
|
version = "4.2.1"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Simple construction, analysis and modification of binary data."
|
summary = "Simple construction, analysis and modification of binary data."
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitarray<3.0.0,>=2.9.0",
|
"bitarray<3.0.0,>=2.9.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "bitstring-4.2.2-py3-none-any.whl", hash = "sha256:8b784373e78e953879c8192589e1ecbcda8f111841633a2aaf88d2b073fd6c35"},
|
{file = "bitstring-4.2.1-py3-none-any.whl", hash = "sha256:9ae5d89072b065d640d645d37c0efcd27284b2f79f1c48cc1cd38b54e1932b4f"},
|
||||||
{file = "bitstring-4.2.2.tar.gz", hash = "sha256:b40b01d911eebaea6efff40d826580806dced5e04b9d3cbad6aebf9422f4b643"},
|
{file = "bitstring-4.2.1.tar.gz", hash = "sha256:8abb5a661588c764bacf1a23d64c7bb57517d2841e3e6f54fb8c057119e0540d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1079,7 +1079,7 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hypothesis"
|
name = "hypothesis"
|
||||||
version = "6.102.4"
|
version = "6.100.5"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "A library for property-based testing"
|
summary = "A library for property-based testing"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -1087,23 +1087,23 @@ dependencies = [
|
|||||||
"sortedcontainers<3.0.0,>=2.1.0",
|
"sortedcontainers<3.0.0,>=2.1.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "hypothesis-6.102.4-py3-none-any.whl", hash = "sha256:013df31b04a4daede13756f497e60e451963d86f426395a79f99c5d692919bbd"},
|
{file = "hypothesis-6.100.5-py3-none-any.whl", hash = "sha256:d2f875a8791abdf68599e85cc9238f7239a73b72362d34be95e532e811766723"},
|
||||||
{file = "hypothesis-6.102.4.tar.gz", hash = "sha256:59b4d144346d5cffb482cc1bafbd21b13ff31608e8c4b3e4630339aee3e87763"},
|
{file = "hypothesis-6.100.5.tar.gz", hash = "sha256:14e06081459ee96ca8f1ed996b6fc19f71910281e01f6a9fa3d9d6e68bbe4a25"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hypothesis"
|
name = "hypothesis"
|
||||||
version = "6.102.4"
|
version = "6.100.5"
|
||||||
extras = ["django"]
|
extras = ["django"]
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "A library for property-based testing"
|
summary = "A library for property-based testing"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"django>=3.2",
|
"django>=3.2",
|
||||||
"hypothesis==6.102.4",
|
"hypothesis==6.100.5",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "hypothesis-6.102.4-py3-none-any.whl", hash = "sha256:013df31b04a4daede13756f497e60e451963d86f426395a79f99c5d692919bbd"},
|
{file = "hypothesis-6.100.5-py3-none-any.whl", hash = "sha256:d2f875a8791abdf68599e85cc9238f7239a73b72362d34be95e532e811766723"},
|
||||||
{file = "hypothesis-6.102.4.tar.gz", hash = "sha256:59b4d144346d5cffb482cc1bafbd21b13ff31608e8c4b3e4630339aee3e87763"},
|
{file = "hypothesis-6.100.5.tar.gz", hash = "sha256:14e06081459ee96ca8f1ed996b6fc19f71910281e01f6a9fa3d9d6e68bbe4a25"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1174,77 +1174,77 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lxml"
|
name = "lxml"
|
||||||
version = "5.2.2"
|
version = "5.2.1"
|
||||||
requires_python = ">=3.6"
|
requires_python = ">=3.6"
|
||||||
summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
|
||||||
files = [
|
files = [
|
||||||
{file = "lxml-5.2.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:45f9494613160d0405682f9eee781c7e6d1bf45f819654eb249f8f46a2c22545"},
|
{file = "lxml-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0b3f2df149efb242cee2ffdeb6674b7f30d23c9a7af26595099afaf46ef4e88"},
|
{file = "lxml-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d28cb356f119a437cc58a13f8135ab8a4c8ece18159eb9194b0d269ec4e28083"},
|
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:657a972f46bbefdbba2d4f14413c0d079f9ae243bd68193cb5061b9732fa54c1"},
|
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b9ea10063efb77a965a8d5f4182806fbf59ed068b3c3fd6f30d2ac7bee734"},
|
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07542787f86112d46d07d4f3c4e7c760282011b354d012dc4141cc12a68cef5f"},
|
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:303f540ad2dddd35b92415b74b900c749ec2010e703ab3bfd6660979d01fd4ed"},
|
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2eb2227ce1ff998faf0cd7fe85bbf086aa41dfc5af3b1d80867ecfe75fb68df3"},
|
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:1d8a701774dfc42a2f0b8ccdfe7dbc140500d1049e0632a611985d943fcf12df"},
|
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:56793b7a1a091a7c286b5f4aa1fe4ae5d1446fe742d00cdf2ffb1077865db10d"},
|
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb00b549b13bd6d884c863554566095bf6fa9c3cecb2e7b399c4bc7904cb33b5"},
|
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a2569a1f15ae6c8c64108a2cd2b4a858fc1e13d25846be0666fc144715e32ab"},
|
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:8cf85a6e40ff1f37fe0f25719aadf443686b1ac7652593dc53c7ef9b8492b115"},
|
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d237ba6664b8e60fd90b8549a149a74fcc675272e0e95539a00522e4ca688b04"},
|
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0b3f5016e00ae7630a4b83d0868fca1e3d494c78a75b1c7252606a3a1c5fc2ad"},
|
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23441e2b5339bc54dc949e9e675fa35efe858108404ef9aa92f0456929ef6fe8"},
|
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2fb0ba3e8566548d6c8e7dd82a8229ff47bd8fb8c2da237607ac8e5a1b8312e5"},
|
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:79d1fb9252e7e2cfe4de6e9a6610c7cbb99b9708e2c3e29057f487de5a9eaefa"},
|
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6dcc3d17eac1df7859ae01202e9bb11ffa8c98949dcbeb1069c8b9a75917e01b"},
|
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-win32.whl", hash = "sha256:4c30a2f83677876465f44c018830f608fa3c6a8a466eb223535035fbc16f3438"},
|
{file = "lxml-5.2.1-cp311-cp311-win32.whl", hash = "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be"},
|
||||||
{file = "lxml-5.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:49095a38eb333aaf44c06052fd2ec3b8f23e19747ca7ec6f6c954ffea6dbf7be"},
|
{file = "lxml-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7429e7faa1a60cad26ae4227f4dd0459efde239e494c7312624ce228e04f6391"},
|
{file = "lxml-5.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:50ccb5d355961c0f12f6cf24b7187dbabd5433f29e15147a67995474f27d1776"},
|
{file = "lxml-5.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc911208b18842a3a57266d8e51fc3cfaccee90a5351b92079beed912a7914c2"},
|
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33ce9e786753743159799fdf8e92a5da351158c4bfb6f2db0bf31e7892a1feb5"},
|
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec87c44f619380878bd49ca109669c9f221d9ae6883a5bcb3616785fa8f94c97"},
|
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08ea0f606808354eb8f2dfaac095963cb25d9d28e27edcc375d7b30ab01abbf6"},
|
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75a9632f1d4f698b2e6e2e1ada40e71f369b15d69baddb8968dcc8e683839b18"},
|
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74da9f97daec6928567b48c90ea2c82a106b2d500f397eeb8941e47d30b1ca85"},
|
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:0969e92af09c5687d769731e3f39ed62427cc72176cebb54b7a9d52cc4fa3b73"},
|
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:9164361769b6ca7769079f4d426a41df6164879f7f3568be9086e15baca61466"},
|
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d26a618ae1766279f2660aca0081b2220aca6bd1aa06b2cf73f07383faf48927"},
|
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab67ed772c584b7ef2379797bf14b82df9aa5f7438c5b9a09624dd834c1c1aaf"},
|
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3d1e35572a56941b32c239774d7e9ad724074d37f90c7a7d499ab98761bd80cf"},
|
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:8268cbcd48c5375f46e000adb1390572c98879eb4f77910c6053d25cc3ac2c67"},
|
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e282aedd63c639c07c3857097fc0e236f984ceb4089a8b284da1c526491e3f3d"},
|
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfdc2bfe69e9adf0df4915949c22a25b39d175d599bf98e7ddf620a13678585"},
|
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4aefd911793b5d2d7a921233a54c90329bf3d4a6817dc465f12ffdfe4fc7b8fe"},
|
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8b8df03a9e995b6211dafa63b32f9d405881518ff1ddd775db4e7b98fb545e1c"},
|
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f11ae142f3a322d44513de1018b50f474f8f736bc3cd91d969f464b5bfef8836"},
|
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-win32.whl", hash = "sha256:16a8326e51fcdffc886294c1e70b11ddccec836516a343f9ed0f82aac043c24a"},
|
{file = "lxml-5.2.1-cp312-cp312-win32.whl", hash = "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134"},
|
||||||
{file = "lxml-5.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:bbc4b80af581e18568ff07f6395c02114d05f4865c2812a1f02f2eaecf0bfd48"},
|
{file = "lxml-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a"},
|
||||||
{file = "lxml-5.2.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b537bd04d7ccd7c6350cdaaaad911f6312cbd61e6e6045542f781c7f8b2e99d2"},
|
{file = "lxml-5.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11"},
|
||||||
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4820c02195d6dfb7b8508ff276752f6b2ff8b64ae5d13ebe02e7667e035000b9"},
|
{file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1"},
|
||||||
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a09f6184f17a80897172863a655467da2b11151ec98ba8d7af89f17bf63dae"},
|
{file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94"},
|
||||||
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76acba4c66c47d27c8365e7c10b3d8016a7da83d3191d053a58382311a8bf4e1"},
|
{file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98"},
|
||||||
{file = "lxml-5.2.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b128092c927eaf485928cec0c28f6b8bead277e28acf56800e972aa2c2abd7a2"},
|
{file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e"},
|
||||||
{file = "lxml-5.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ae791f6bd43305aade8c0e22f816b34f3b72b6c820477aab4d18473a37e8090b"},
|
{file = "lxml-5.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66"},
|
||||||
{file = "lxml-5.2.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a2f6a1bc2460e643785a2cde17293bd7a8f990884b822f7bca47bee0a82fc66b"},
|
{file = "lxml-5.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c"},
|
||||||
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e8d351ff44c1638cb6e980623d517abd9f580d2e53bfcd18d8941c052a5a009"},
|
{file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa"},
|
||||||
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bec4bd9133420c5c52d562469c754f27c5c9e36ee06abc169612c959bd7dbb07"},
|
{file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817"},
|
||||||
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:55ce6b6d803890bd3cc89975fca9de1dff39729b43b73cb15ddd933b8bc20484"},
|
{file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460"},
|
||||||
{file = "lxml-5.2.2-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ab6a358d1286498d80fe67bd3d69fcbc7d1359b45b41e74c4a26964ca99c3f8"},
|
{file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96"},
|
||||||
{file = "lxml-5.2.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:06668e39e1f3c065349c51ac27ae430719d7806c026fec462e5693b08b95696b"},
|
{file = "lxml-5.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860"},
|
||||||
{file = "lxml-5.2.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9cd5323344d8ebb9fb5e96da5de5ad4ebab993bbf51674259dbe9d7a18049525"},
|
{file = "lxml-5.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d"},
|
||||||
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89feb82ca055af0fe797a2323ec9043b26bc371365847dbe83c7fd2e2f181c34"},
|
{file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f"},
|
||||||
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e481bba1e11ba585fb06db666bfc23dbe181dbafc7b25776156120bf12e0d5a6"},
|
{file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138"},
|
||||||
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d6c6ea6a11ca0ff9cd0390b885984ed31157c168565702959c25e2191674a14"},
|
{file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b"},
|
||||||
{file = "lxml-5.2.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3d98de734abee23e61f6b8c2e08a88453ada7d6486dc7cdc82922a03968928db"},
|
{file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85"},
|
||||||
{file = "lxml-5.2.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:69ab77a1373f1e7563e0fb5a29a8440367dec051da6c7405333699d07444f511"},
|
{file = "lxml-5.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144"},
|
||||||
{file = "lxml-5.2.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34e17913c431f5ae01d8658dbf792fdc457073dcdfbb31dc0cc6ab256e664a8d"},
|
{file = "lxml-5.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc"},
|
||||||
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f8757b03208c3f50097761be2dea0aba02e94f0dc7023ed73a7bb14ff11eb0"},
|
{file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354"},
|
||||||
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a520b4f9974b0a0a6ed73c2154de57cdfd0c8800f4f15ab2b73238ffed0b36e"},
|
{file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9"},
|
||||||
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5e097646944b66207023bc3c634827de858aebc226d5d4d6d16f0b77566ea182"},
|
{file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5"},
|
||||||
{file = "lxml-5.2.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b5e4ef22ff25bfd4ede5f8fb30f7b24446345f3e79d9b7455aef2836437bc38a"},
|
{file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246"},
|
||||||
{file = "lxml-5.2.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ff69a9a0b4b17d78170c73abe2ab12084bdf1691550c5629ad1fe7849433f324"},
|
{file = "lxml-5.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704"},
|
||||||
{file = "lxml-5.2.2.tar.gz", hash = "sha256:bb2dc4898180bea79863d5487e5f9c7c34297414bad54bcd0f0852aee9cfdb87"},
|
{file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1833,27 +1833,27 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.4.4"
|
version = "0.4.3"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"},
|
{file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"},
|
||||||
{file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"},
|
{file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"},
|
||||||
{file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"},
|
||||||
{file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"},
|
||||||
{file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"},
|
||||||
{file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"},
|
||||||
{file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"},
|
||||||
{file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"},
|
||||||
{file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"},
|
||||||
{file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"},
|
{file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"},
|
||||||
{file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"},
|
{file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"},
|
||||||
{file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"},
|
{file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"},
|
||||||
{file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"},
|
{file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"},
|
||||||
{file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"},
|
{file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"},
|
||||||
{file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"},
|
{file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"},
|
||||||
{file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"},
|
{file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"},
|
||||||
{file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"},
|
{file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -134,7 +134,7 @@ debug = [
|
|||||||
dev = [
|
dev = [
|
||||||
"django-extensions~=3.2",
|
"django-extensions~=3.2",
|
||||||
"ipython~=8.24",
|
"ipython~=8.24",
|
||||||
"hypothesis[django]~=6.102",
|
"hypothesis[django]~=6.100",
|
||||||
"tblib~=3.0",
|
"tblib~=3.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user