mirror of
https://github.com/ae-utbm/sith.git
synced 2024-11-22 14:13:21 +00:00
commit
0790ae2298
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@ -8,11 +8,7 @@ updates:
|
||||
- package-ecosystem: "pip" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
# Raise pull requests for version updates
|
||||
# to pip against the `develop` branch
|
||||
interval: "weekly"
|
||||
target-branch: "taiste"
|
||||
reviewers:
|
||||
- "ae-utbm/developpers-v3"
|
||||
commit-message:
|
||||
prefix: "[UPDATE] "
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -17,4 +17,6 @@ sith/settings_custom.py
|
||||
sith/search_indexes/
|
||||
.coverage
|
||||
coverage_report/
|
||||
doc/_build
|
||||
|
||||
# compiled documentation
|
||||
site/
|
||||
|
@ -1,10 +1,21 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.5.1
|
||||
rev: v0.5.5
|
||||
hooks:
|
||||
- id: ruff # just check the code, and print the errors
|
||||
- id: ruff # actually fix the fixable errors, but print nothing
|
||||
args: ["--fix", "--silent"]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/rtts/djhtml
|
||||
rev: 3.0.6
|
||||
hooks:
|
||||
- id: djhtml
|
||||
name: format templates
|
||||
entry: djhtml --tabwidth 2
|
||||
types: ["jinja"]
|
||||
- id: djcss
|
||||
name: format scss files
|
||||
entry: djcss --tabwidth 2
|
||||
types: ["scss"]
|
||||
|
@ -264,33 +264,26 @@ class TestOperation(TestCase):
|
||||
)
|
||||
self.assertContains(response, "Total : 5575.72", status_code=200)
|
||||
self.assertContains(response, "Total : 71.42")
|
||||
self.assertContains(
|
||||
response,
|
||||
"""
|
||||
<td><a href="/user/1/">S' Kia</a></td>
|
||||
|
||||
<td>3.00</td>""",
|
||||
content = response.content.decode()
|
||||
self.assertInHTML(
|
||||
"""<td><a href="/user/1/">S' Kia</a></td><td>3.00</td>""", content
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
"""
|
||||
<td><a href="/user/1/">S' Kia</a></td>
|
||||
|
||||
<td>823.00</td>""",
|
||||
self.assertInHTML(
|
||||
"""<td><a href="/user/1/">S' Kia</a></td><td>823.00</td>""", content
|
||||
)
|
||||
|
||||
def test_accounting_statement(self):
|
||||
response = self.client.get(
|
||||
reverse("accounting:journal_accounting_statement", args=[self.journal.id])
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
assert response.status_code == 200
|
||||
self.assertInHTML(
|
||||
"""
|
||||
<tr>
|
||||
<td>443 - Crédit - Ce code n'existe pas</td>
|
||||
<td>3.00</td>
|
||||
</tr>""",
|
||||
status_code=200,
|
||||
response.content.decode(),
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
|
@ -29,7 +29,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from club.models import Club, Mailing, MailingSubscription, Membership
|
||||
from core.models import User
|
||||
from core.views.forms import SelectDate, TzAwareDateTimeField
|
||||
from core.views.forms import SelectDate, SelectDateTime
|
||||
from counter.models import Counter
|
||||
|
||||
|
||||
@ -149,8 +149,12 @@ class MailingForm(forms.Form):
|
||||
|
||||
|
||||
class SellingsForm(forms.Form):
|
||||
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
|
||||
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||
begin_date = forms.DateTimeField(
|
||||
label=_("Begin date"), widget=SelectDateTime, required=False
|
||||
)
|
||||
end_date = forms.DateTimeField(
|
||||
label=_("End date"), widget=SelectDateTime, required=False
|
||||
)
|
||||
|
||||
counters = forms.ModelMultipleChoiceField(
|
||||
Counter.objects.order_by("name").all(), label=_("Counter"), required=False
|
||||
|
18
com/tests.py
18
com/tests.py
@ -69,11 +69,11 @@ class TestCom(TestCase):
|
||||
},
|
||||
)
|
||||
r = self.client.get(reverse("core:index"))
|
||||
self.assertContains(
|
||||
r,
|
||||
"""<div id="alert_box">
|
||||
<div class="markdown"><h3>ALERTE!</h3>
|
||||
assert r.status_code == 200
|
||||
self.assertInHTML(
|
||||
"""<div id="alert_box"><div class="markdown"><h3>ALERTE!</h3>
|
||||
<p><strong>Caaaataaaapuuuulte!!!!</strong></p>""",
|
||||
r.content.decode(),
|
||||
)
|
||||
|
||||
def test_info_msg(self):
|
||||
@ -86,10 +86,12 @@ class TestCom(TestCase):
|
||||
},
|
||||
)
|
||||
r = self.client.get(reverse("core:index"))
|
||||
self.assertContains(
|
||||
r,
|
||||
"""<div id="info_box">
|
||||
<div class="markdown"><h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>""",
|
||||
|
||||
assert r.status_code == 200
|
||||
self.assertInHTML(
|
||||
"""<div id="info_box"><div class="markdown">
|
||||
<h3>INFO: <strong>Caaaataaaapuuuulte!!!!</strong></h3>""",
|
||||
r.content.decode(),
|
||||
)
|
||||
|
||||
def test_birthday_non_subscribed_user(self):
|
||||
|
21
com/views.py
21
com/views.py
@ -50,7 +50,7 @@ from core.views import (
|
||||
QuickNotifMixin,
|
||||
TabedViewMixin,
|
||||
)
|
||||
from core.views.forms import MarkdownInput, TzAwareDateTimeField
|
||||
from core.views.forms import MarkdownInput, SelectDateTime
|
||||
|
||||
# Sith object
|
||||
|
||||
@ -72,12 +72,15 @@ class PosterForm(forms.ModelForm):
|
||||
widgets = {"screens": forms.CheckboxSelectMultiple}
|
||||
help_texts = {"file": _("Format: 16:9 | Resolution: 1920x1080")}
|
||||
|
||||
date_begin = TzAwareDateTimeField(
|
||||
date_begin = forms.DateTimeField(
|
||||
label=_("Start date"),
|
||||
widget=SelectDateTime,
|
||||
required=True,
|
||||
initial=timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
)
|
||||
date_end = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||
date_end = forms.DateTimeField(
|
||||
label=_("End date"), widget=SelectDateTime, required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop("user", None)
|
||||
@ -191,9 +194,13 @@ class NewsForm(forms.ModelForm):
|
||||
"content": MarkdownInput,
|
||||
}
|
||||
|
||||
start_date = TzAwareDateTimeField(label=_("Start date"), required=False)
|
||||
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||
until = TzAwareDateTimeField(label=_("Until"), required=False)
|
||||
start_date = forms.DateTimeField(
|
||||
label=_("Start date"), widget=SelectDateTime, required=False
|
||||
)
|
||||
end_date = forms.DateTimeField(
|
||||
label=_("End date"), widget=SelectDateTime, required=False
|
||||
)
|
||||
until = forms.DateTimeField(label=_("Until"), widget=SelectDateTime, required=False)
|
||||
|
||||
automoderation = forms.BooleanField(label=_("Automoderation"), required=False)
|
||||
|
||||
@ -258,7 +265,7 @@ class NewsEditView(CanEditMixin, UpdateView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
if form.is_valid() and "preview" not in request.POST.keys():
|
||||
if form.is_valid() and "preview" not in request.POST:
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
@ -21,8 +21,6 @@
|
||||
#
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from core.models import SithFile
|
||||
@ -37,9 +35,6 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
root_path = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
)
|
||||
files = SithFile.objects.filter(id__in=options["ids"]).all()
|
||||
for f in files:
|
||||
f._check_fs()
|
||||
|
@ -22,7 +22,7 @@
|
||||
#
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import sass
|
||||
from django.conf import settings
|
||||
@ -34,44 +34,36 @@ class Command(BaseCommand):
|
||||
|
||||
help = "Compile scss files from static folder"
|
||||
|
||||
def compile(self, filename):
|
||||
args = {"filename": filename, "include_paths": settings.STATIC_ROOT}
|
||||
def compile(self, filename: str):
|
||||
args = {
|
||||
"filename": filename,
|
||||
"include_paths": settings.STATIC_ROOT.name,
|
||||
"output_style": "compressed",
|
||||
}
|
||||
if settings.SASS_PRECISION:
|
||||
args["precision"] = settings.SASS_PRECISION
|
||||
return sass.compile(**args)
|
||||
|
||||
def is_compilable(self, file, ext_list):
|
||||
path, ext = os.path.splitext(file)
|
||||
return ext in ext_list
|
||||
|
||||
def exec_on_folder(self, folder, func):
|
||||
to_exec = []
|
||||
for file in os.listdir(folder):
|
||||
file = os.path.join(folder, file)
|
||||
if os.path.isdir(file):
|
||||
self.exec_on_folder(file, func)
|
||||
elif self.is_compilable(file, [".scss"]):
|
||||
to_exec.append(file)
|
||||
|
||||
for file in to_exec:
|
||||
func(file)
|
||||
|
||||
def compilescss(self, file):
|
||||
print("compiling %s" % file)
|
||||
with open(file.replace(".scss", ".css"), "w") as newfile:
|
||||
newfile.write(self.compile(file))
|
||||
|
||||
def removescss(self, file):
|
||||
print("removing %s" % file)
|
||||
os.remove(file)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if os.path.isdir(settings.STATIC_ROOT):
|
||||
print("---- Compiling scss files ---")
|
||||
self.exec_on_folder(settings.STATIC_ROOT, self.compilescss)
|
||||
print("---- Removing scss files ----")
|
||||
self.exec_on_folder(settings.STATIC_ROOT, self.removescss)
|
||||
else:
|
||||
print(
|
||||
"No static folder avalaible, please use collectstatic before compiling scss"
|
||||
if not settings.STATIC_ROOT.is_dir():
|
||||
raise Exception(
|
||||
"No static folder availaible, please use collectstatic before compiling scss"
|
||||
)
|
||||
to_exec = list(settings.STATIC_ROOT.rglob("*.scss"))
|
||||
if len(to_exec) == 0:
|
||||
self.stdout.write("Nothing to compile.")
|
||||
sys.exit(0)
|
||||
self.stdout.write("---- Compiling scss files ---")
|
||||
for file in to_exec:
|
||||
# remove existing css files that will be replaced
|
||||
# keeping them while compiling the scss would break
|
||||
# import statements resolution
|
||||
css_file = file.with_suffix(".css")
|
||||
if css_file.exists():
|
||||
css_file.unlink()
|
||||
compiled_files = {file: self.compile(str(file.resolve())) for file in to_exec}
|
||||
for file, scss in compiled_files.items():
|
||||
file.replace(file.with_suffix(".css")).write_text(scss)
|
||||
self.stdout.write(
|
||||
"Files compiled : \n" + "\n- ".join(str(f) for f in compiled_files)
|
||||
)
|
||||
|
@ -21,7 +21,6 @@
|
||||
#
|
||||
#
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
@ -33,7 +32,7 @@ class Command(BaseCommand):
|
||||
help = "Output the fully rendered SYNTAX.md file"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
root_path = Path(settings.BASE_DIR)
|
||||
root_path = settings.BASE_DIR
|
||||
with open(root_path / "core/fixtures/SYNTAX.md", "r") as md:
|
||||
result = markdown(md.read())
|
||||
print(result, end="")
|
||||
|
383
core/management/commands/populate_more.py
Normal file
383
core/management/commands/populate_more.py
Normal file
@ -0,0 +1,383 @@
|
||||
import random
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import Iterator
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Exists, F, Min, OuterRef, Subquery, Sum
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.timezone import make_aware, now
|
||||
from faker import Faker
|
||||
|
||||
from club.models import Club, Membership
|
||||
from core.models import RealGroup, User
|
||||
from counter.models import (
|
||||
Counter,
|
||||
Customer,
|
||||
Permanency,
|
||||
Product,
|
||||
ProductType,
|
||||
Refilling,
|
||||
Selling,
|
||||
)
|
||||
from pedagogy.models import UV
|
||||
from subscription.models import Subscription
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Add more fixtures for a more complete development environment"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.faker = Faker("fr_FR")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if not settings.DEBUG:
|
||||
raise Exception("Never call this command in prod. Never.")
|
||||
|
||||
self.stdout.write("Creating users...")
|
||||
users = [
|
||||
User(
|
||||
username=self.faker.user_name(),
|
||||
first_name=self.faker.first_name(),
|
||||
last_name=self.faker.last_name(),
|
||||
date_of_birth=self.faker.date_of_birth(minimum_age=15, maximum_age=25),
|
||||
email=self.faker.email(),
|
||||
phone=self.faker.phone_number(),
|
||||
address=self.faker.address(),
|
||||
)
|
||||
for _ in range(600)
|
||||
]
|
||||
# there may a duplicate or two
|
||||
# Not a problem, we will just have 599 users instead of 600
|
||||
User.objects.bulk_create(users, ignore_conflicts=True)
|
||||
users = list(User.objects.order_by("-id")[: len(users)])
|
||||
|
||||
subscribers = random.sample(users, k=int(0.8 * len(users)))
|
||||
self.stdout.write("Creating subscriptions...")
|
||||
self.create_subscriptions(users)
|
||||
self.stdout.write("Creating club memberships...")
|
||||
users_qs = User.objects.filter(id__in=[s.id for s in subscribers])
|
||||
subscribers_now = list(
|
||||
users_qs.annotate(
|
||||
filter=Exists(
|
||||
Subscription.objects.filter(
|
||||
member_id=OuterRef("pk"), subscription_end__gte=now()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
old_subscribers = list(
|
||||
users_qs.annotate(
|
||||
filter=Exists(
|
||||
Subscription.objects.filter(
|
||||
member_id=OuterRef("pk"), subscription_end__lt=now()
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
self.make_club(
|
||||
Club.objects.get(unix_name="ae"),
|
||||
random.sample(subscribers_now, k=min(30, len(subscribers_now))),
|
||||
random.sample(old_subscribers, k=min(60, len(old_subscribers))),
|
||||
)
|
||||
self.make_club(
|
||||
Club.objects.get(unix_name="troll"),
|
||||
random.sample(subscribers_now, k=min(20, len(subscribers_now))),
|
||||
random.sample(old_subscribers, k=min(80, len(old_subscribers))),
|
||||
)
|
||||
self.stdout.write("Creating uvs...")
|
||||
self.create_uvs()
|
||||
self.stdout.write("Creating products...")
|
||||
self.create_products()
|
||||
self.stdout.write("Creating sales and refills...")
|
||||
sellers = random.sample(list(User.objects.all()), 100)
|
||||
self.create_sales(sellers)
|
||||
self.stdout.write("Creating permanences...")
|
||||
self.create_permanences(sellers)
|
||||
|
||||
self.stdout.write("Done")
|
||||
|
||||
def create_subscriptions(self, users: list[User]):
|
||||
def prepare_subscription(user: User, start_date: date) -> Subscription:
|
||||
payment_method = random.choice(settings.SITH_SUBSCRIPTION_PAYMENT_METHOD)[0]
|
||||
duration = random.randint(1, 4)
|
||||
sub = Subscription(member=user, payment_method=payment_method)
|
||||
sub.subscription_start = sub.compute_start(d=start_date, duration=duration)
|
||||
sub.subscription_end = sub.compute_end(duration)
|
||||
return sub
|
||||
|
||||
subscriptions = []
|
||||
customers = []
|
||||
# first set of subscriptions
|
||||
for i, user in enumerate(users):
|
||||
sub = prepare_subscription(user, self.faker.past_date("-10y"))
|
||||
subscriptions.append(sub)
|
||||
customers.append(
|
||||
Customer(
|
||||
user=user,
|
||||
account_id=f"{9900 + i}{self.faker.random_lowercase_letter()}",
|
||||
)
|
||||
)
|
||||
while sub.subscription_end < now().date() and random.random() > 0.7:
|
||||
# 70% chances to subscribe again
|
||||
# (expect if it would make the subscription start after tomorrow)
|
||||
sub = prepare_subscription(
|
||||
user, self.faker.past_date(sub.subscription_end)
|
||||
)
|
||||
subscriptions.append(sub)
|
||||
Subscription.objects.bulk_create(subscriptions)
|
||||
Customer.objects.bulk_create(customers, ignore_conflicts=True)
|
||||
|
||||
def make_club(self, club: Club, members: list[User], old_members: list[User]):
|
||||
def zip_roles(users: list[User]) -> Iterator[tuple[User, int]]:
|
||||
roles = iter(sorted(settings.SITH_CLUB_ROLES.keys(), reverse=True))
|
||||
user_idx = 0
|
||||
while (role := next(roles)) > 2:
|
||||
# one member for each major role
|
||||
yield users[user_idx], role
|
||||
user_idx += 1
|
||||
for _ in range(int(0.3 * (len(users) - user_idx))):
|
||||
# 30% of the remaining in the board
|
||||
yield users[user_idx], 2
|
||||
user_idx += 1
|
||||
for remaining in users[user_idx + 1 :]:
|
||||
# everything else is a simple member
|
||||
yield remaining, 1
|
||||
|
||||
memberships = []
|
||||
old_members = old_members.copy()
|
||||
random.shuffle(old_members)
|
||||
for old in old_members:
|
||||
start = self.faker.date_between("-3y", "-1y")
|
||||
memberships.append(
|
||||
Membership(
|
||||
start_date=start,
|
||||
end_date=self.faker.past_date(start),
|
||||
user=old,
|
||||
role=random.choice(list(settings.SITH_CLUB_ROLES.keys())),
|
||||
club=club,
|
||||
)
|
||||
)
|
||||
for member, role in zip_roles(members):
|
||||
start = self.faker.past_date("-1y")
|
||||
memberships.append(
|
||||
Membership(
|
||||
start_date=start,
|
||||
user=member,
|
||||
role=role,
|
||||
club=club,
|
||||
)
|
||||
)
|
||||
Membership.objects.bulk_create(memberships)
|
||||
|
||||
def create_uvs(self):
|
||||
root = User.objects.get(username="root")
|
||||
categories = ["CS", "TM", "OM", "QC", "EC"]
|
||||
branches = ["TC", "GMC", "GI", "EDIM", "E", "IMSI", "HUMA"]
|
||||
languages = ["FR", "FR", "EN"]
|
||||
semesters = ["AUTUMN", "SPRING", "AUTUMN_AND_SPRING"]
|
||||
teachers = [self.faker.name() for _ in range(50)]
|
||||
uvs = []
|
||||
for _ in range(1000):
|
||||
code = (
|
||||
self.faker.random_uppercase_letter()
|
||||
+ self.faker.random_uppercase_letter()
|
||||
+ str(random.randint(10, 90))
|
||||
)
|
||||
uvs.append(
|
||||
UV(
|
||||
code=code,
|
||||
author=root,
|
||||
manager=random.choice(teachers),
|
||||
title=self.faker.text(max_nb_chars=50),
|
||||
department=random.choice(branches),
|
||||
credit_type=random.choice(categories),
|
||||
credits=6,
|
||||
semester=random.choice(semesters),
|
||||
language=random.choice(languages),
|
||||
program=self.faker.paragraph(random.randint(3, 10)),
|
||||
skills="\n* ".join(self.faker.sentences(random.randint(3, 10))),
|
||||
key_concepts="\n* ".join(
|
||||
self.faker.sentences(random.randint(3, 10))
|
||||
),
|
||||
hours_CM=random.randint(15, 40),
|
||||
hours_TD=random.randint(15, 40),
|
||||
hours_TP=random.randint(15, 40),
|
||||
hours_THE=random.randint(15, 40),
|
||||
hours_TE=random.randint(15, 40),
|
||||
)
|
||||
)
|
||||
UV.objects.bulk_create(uvs, ignore_conflicts=True)
|
||||
|
||||
def create_products(self):
|
||||
categories = []
|
||||
for _ in range(10):
|
||||
categories.append(ProductType(name=self.faker.text(max_nb_chars=30)))
|
||||
ProductType.objects.bulk_create(categories)
|
||||
categories = list(
|
||||
ProductType.objects.filter(name__in=[c.name for c in categories])
|
||||
)
|
||||
ae = Club.objects.get(unix_name="ae")
|
||||
other_clubs = random.sample(list(Club.objects.all()), k=3)
|
||||
groups = list(
|
||||
RealGroup.objects.filter(
|
||||
name__in=["Subscribers", "Old subscribers", "Public"]
|
||||
)
|
||||
)
|
||||
counters = list(
|
||||
Counter.objects.filter(name__in=["Foyer", "MDE", "La Gommette", "Eboutic"])
|
||||
)
|
||||
# 2/3 of the products are owned by AE
|
||||
clubs = [ae, ae, ae, ae, ae, ae, *other_clubs]
|
||||
products = []
|
||||
buying_groups = []
|
||||
selling_places = []
|
||||
for _ in range(200):
|
||||
price = random.randint(0, 10) + random.choice([0, 0.25, 0.5, 0.75])
|
||||
product = Product(
|
||||
name=self.faker.text(max_nb_chars=30),
|
||||
description=self.faker.text(max_nb_chars=120),
|
||||
product_type=random.choice(categories),
|
||||
code="".join(self.faker.random_letters(length=random.randint(4, 8))),
|
||||
purchase_price=price,
|
||||
selling_price=price,
|
||||
special_selling_price=price - min(0.5, price),
|
||||
club=random.choice(clubs),
|
||||
limit_age=0 if random.random() > 0.2 else 18,
|
||||
archived=bool(random.random() > 0.7),
|
||||
)
|
||||
products.append(product)
|
||||
for group in random.sample(groups, k=random.randint(0, 3)):
|
||||
# there will be products without buying groups
|
||||
# but there are also such products in the real database
|
||||
buying_groups.append(
|
||||
Product.buying_groups.through(product=product, group=group)
|
||||
)
|
||||
for counter in random.sample(counters, random.randint(0, 4)):
|
||||
selling_places.append(
|
||||
Counter.products.through(counter=counter, product=product)
|
||||
)
|
||||
Product.objects.bulk_create(products)
|
||||
Product.buying_groups.through.objects.bulk_create(buying_groups)
|
||||
Counter.products.through.objects.bulk_create(selling_places)
|
||||
|
||||
@staticmethod
|
||||
def _update_balances():
|
||||
customers = Customer.objects.annotate(
|
||||
money_in=Sum(F("refillings__amount"), default=0),
|
||||
money_out=Coalesce(
|
||||
Subquery(
|
||||
Selling.objects.filter(customer=OuterRef("pk"))
|
||||
.values("customer_id") # group by customer
|
||||
.annotate(res=Sum(F("unit_price") * F("quantity"), default=0))
|
||||
.values("res")
|
||||
),
|
||||
Decimal("0"),
|
||||
),
|
||||
).annotate(real_balance=F("money_in") - F("money_out"))
|
||||
for c in customers:
|
||||
c.amount = c.real_balance
|
||||
Customer.objects.bulk_update(customers, fields=["amount"])
|
||||
|
||||
def create_sales(self, sellers: list[User]):
|
||||
customers = list(
|
||||
Customer.objects.annotate(
|
||||
since=Subquery(
|
||||
Subscription.objects.filter(member__customer=OuterRef("pk"))
|
||||
.annotate(res=Min("subscription_start"))
|
||||
.values("res")
|
||||
)
|
||||
)
|
||||
)
|
||||
products = list(Product.objects.all())
|
||||
counters = list(
|
||||
Counter.objects.filter(name__in=["Foyer", "MDE", "La Gommette"])
|
||||
)
|
||||
sales = []
|
||||
reloads = []
|
||||
for customer in customers:
|
||||
# the longer the customer has existed, the higher the mean of nb_products
|
||||
mu = 5 + (now().year - customer.since.year) * 2
|
||||
nb_sales = max(0, int(random.normalvariate(mu=mu, sigma=mu * 5)))
|
||||
favoured_products = random.sample(products, k=(random.randint(1, 5)))
|
||||
favoured_counter = random.choice(counters)
|
||||
this_customer_sales = []
|
||||
for _ in range(nb_sales):
|
||||
product = (
|
||||
random.choice(favoured_products)
|
||||
if random.random() > 0.7
|
||||
else random.choice(products)
|
||||
)
|
||||
counter = (
|
||||
favoured_counter
|
||||
if random.random() > 0.7
|
||||
else random.choice(counters)
|
||||
)
|
||||
this_customer_sales.append(
|
||||
Selling(
|
||||
product=product,
|
||||
counter=counter,
|
||||
club_id=product.club_id,
|
||||
quantity=random.randint(1, 5),
|
||||
unit_price=product.selling_price,
|
||||
seller=random.choice(sellers),
|
||||
customer=customer,
|
||||
date=make_aware(
|
||||
self.faker.date_time_between(customer.since, now().date())
|
||||
),
|
||||
)
|
||||
)
|
||||
total_expanse = sum(s.unit_price * s.quantity for s in this_customer_sales)
|
||||
total_reloaded = 0
|
||||
while total_reloaded < total_expanse:
|
||||
amount = random.choice(list(range(5, 51, 5)))
|
||||
total_reloaded += amount
|
||||
reloads.append(
|
||||
Refilling(
|
||||
counter=random.choice(counters),
|
||||
amount=amount,
|
||||
operator=random.choice(sellers),
|
||||
customer=customer,
|
||||
date=make_aware(
|
||||
self.faker.date_time_between(customer.since, now().date())
|
||||
),
|
||||
is_validated=True,
|
||||
)
|
||||
)
|
||||
sales.extend(this_customer_sales)
|
||||
Refilling.objects.bulk_create(reloads)
|
||||
Selling.objects.bulk_create(sales)
|
||||
self._update_balances()
|
||||
|
||||
def create_permanences(self, sellers: list[User]):
|
||||
counters = list(
|
||||
Counter.objects.filter(name__in=["Foyer", "MDE", "La Gommette"])
|
||||
)
|
||||
perms = []
|
||||
for seller in sellers:
|
||||
favoured_counter = random.choice(counters)
|
||||
nb_perms = abs(int(random.normalvariate(mu=275, sigma=100)))
|
||||
active_period_start = self.faker.past_date("-10y")
|
||||
active_period_end = self.faker.date_between(
|
||||
active_period_start,
|
||||
min(now().date(), active_period_start + relativedelta(years=5)),
|
||||
)
|
||||
for _ in range(nb_perms):
|
||||
counter = (
|
||||
favoured_counter
|
||||
if random.random() > 0.8
|
||||
else random.choice(counters)
|
||||
)
|
||||
duration = self.faker.time_delta(timedelta(hours=1))
|
||||
start = make_aware(
|
||||
self.faker.date_time_between(active_period_start, active_period_end)
|
||||
)
|
||||
perms.append(
|
||||
Permanency(
|
||||
counter=counter, user=seller, start=start, end=start + duration
|
||||
)
|
||||
)
|
||||
Permanency.objects.bulk_create(perms)
|
@ -21,7 +21,6 @@
|
||||
#
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
@ -37,9 +36,6 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
root_path = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
)
|
||||
files = SithFile.objects.filter(id__in=options["ids"]).all()
|
||||
for f in files:
|
||||
f._repair_fs()
|
||||
|
@ -13,28 +13,32 @@
|
||||
#
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Set up a new instance of the Sith AE"
|
||||
help = "Set up the development environment."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
root_path = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
)
|
||||
try:
|
||||
os.mkdir(os.path.join(root_path) + "/data")
|
||||
print("Data dir created")
|
||||
except Exception as e:
|
||||
repr(e)
|
||||
try:
|
||||
os.remove(os.path.join(root_path, "db.sqlite3"))
|
||||
print("db.sqlite3 deleted")
|
||||
except Exception as e:
|
||||
repr(e)
|
||||
if not settings.DEBUG:
|
||||
raise Exception("Never call this command in prod. Never.")
|
||||
data_dir = settings.BASE_DIR / "data"
|
||||
settings.EMAIL_BACKEND = "django.core.mail.backends.dummy.EmailBackend"
|
||||
if not data_dir.is_dir():
|
||||
data_dir.mkdir()
|
||||
db_path = settings.BASE_DIR / "db.sqlite3"
|
||||
if db_path.exists():
|
||||
call_command("flush", "--noinput")
|
||||
self.stdout.write("Existing database reset")
|
||||
call_command("migrate")
|
||||
self.stdout.write("Add the base fixtures.")
|
||||
call_command("populate")
|
||||
self.stdout.write("Generate additional random fixtures")
|
||||
call_command("populate_more")
|
||||
self.stdout.write("Build the xapian index")
|
||||
call_command("rebuild_index", "--noinput")
|
||||
|
||||
settings.EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
self.stdout.write("Setup complete!")
|
||||
|
@ -27,6 +27,7 @@ import importlib
|
||||
import os
|
||||
import unicodedata
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from django.conf import settings
|
||||
@ -56,8 +57,6 @@ from django.utils.html import escape
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
from core import utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from club.models import Club
|
||||
|
||||
@ -377,7 +376,9 @@ class User(AbstractBaseUser):
|
||||
USERNAME_FIELD = "username"
|
||||
|
||||
def promo_has_logo(self):
|
||||
return utils.file_exist("./core/static/core/img/promo_%02d.png" % self.promo)
|
||||
return Path(
|
||||
settings.BASE_DIR / f"core/static/core/img/promo_{self.promo}.png"
|
||||
).exists()
|
||||
|
||||
def has_module_perms(self, package_name):
|
||||
return self.is_active
|
||||
|
@ -22,7 +22,6 @@
|
||||
#
|
||||
#
|
||||
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
@ -37,7 +36,7 @@ class ScssFinder(FileSystemFinder):
|
||||
|
||||
def __init__(self, apps=None, *args, **kwargs):
|
||||
location = settings.STATIC_ROOT
|
||||
if not os.path.isdir(location):
|
||||
if not location.is_dir():
|
||||
return
|
||||
self.locations = [("", location)]
|
||||
self.storages = OrderedDict()
|
||||
|
@ -21,61 +21,35 @@
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
|
||||
import os
|
||||
from urllib.parse import urljoin
|
||||
import functools
|
||||
from pathlib import Path
|
||||
|
||||
import sass
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.templatetags.static import static
|
||||
from django.utils.encoding import force_bytes, iri_to_uri
|
||||
from django_jinja.builtins.filters import static
|
||||
|
||||
from core.scss.storage import ScssFileStorage, find_file
|
||||
|
||||
|
||||
class ScssProcessor(object):
|
||||
"""If DEBUG mode enabled : compile the scss file
|
||||
Else : give the path of the corresponding css supposed to already be compiled
|
||||
Don't forget to use compilestatics to compile scss for production.
|
||||
"""
|
||||
@functools.cache
|
||||
def _scss_storage():
|
||||
return ScssFileStorage()
|
||||
|
||||
prefix = iri_to_uri(getattr(settings, "STATIC_URL", "/static/"))
|
||||
storage = ScssFileStorage()
|
||||
scss_extensions = [".scss"]
|
||||
|
||||
def __init__(self, path=None):
|
||||
self.path = path
|
||||
|
||||
def _convert_scss(self):
|
||||
basename, ext = os.path.splitext(self.path)
|
||||
css_filename = self.path.replace(".scss", ".css")
|
||||
url = urljoin(self.prefix, css_filename)
|
||||
|
||||
if not settings.DEBUG:
|
||||
return url
|
||||
|
||||
if ext not in self.scss_extensions:
|
||||
return static(self.path)
|
||||
|
||||
# Compilation on the fly
|
||||
def process_scss_path(path: Path):
|
||||
css_path = path.with_suffix(".css")
|
||||
if settings.DEBUG:
|
||||
compile_args = {
|
||||
"filename": find_file(self.path),
|
||||
"filename": find_file(path),
|
||||
"include_paths": settings.SASS_INCLUDE_FOLDERS,
|
||||
}
|
||||
if settings.SASS_PRECISION:
|
||||
compile_args["precision"] = settings.SASS_PRECISION
|
||||
content = sass.compile(**compile_args)
|
||||
content = force_bytes(content)
|
||||
|
||||
if self.storage.exists(css_filename):
|
||||
self.storage.delete(css_filename)
|
||||
self.storage.save(css_filename, ContentFile(content))
|
||||
|
||||
return url
|
||||
|
||||
def get_converted_scss(self):
|
||||
if self.path:
|
||||
return self._convert_scss()
|
||||
else:
|
||||
return ""
|
||||
storage = _scss_storage()
|
||||
if storage.exists(css_path):
|
||||
storage.delete(css_path)
|
||||
storage.save(css_path, ContentFile(content))
|
||||
return static(css_path)
|
||||
|
30
core/static/core/colors.scss
Normal file
30
core/static/core/colors.scss
Normal file
@ -0,0 +1,30 @@
|
||||
$first-color: hsl(220, 100%, 50%);
|
||||
$second-color: hsl(48, 100%, 67%);
|
||||
$primary-color: hsl(219.9, 53.7%, 50%);
|
||||
$secondary-color: hsl(204, 64%, 44%);
|
||||
$primary-color-text: hsl(0, 0%, 100%);
|
||||
$secondary-color-text: hsla(0, 0%, 0%, 0.87);
|
||||
|
||||
$primary-light-color: hsl(219.8, 46.4%, 64.9%);
|
||||
$primary-dark-color: hsl(203, 75%, 40%);
|
||||
|
||||
$secondary-light-color: hsl(40, 68%, 65%);
|
||||
$secondary-dark-color: hsl(40, 68%, 35%);
|
||||
|
||||
$primary-neutral-color: hsl(219.6, 20.8%, 50%);
|
||||
$primary-neutral-light-color: hsl(0, 0%, 94%);
|
||||
$primary-neutral-dark-color: hsl(210, 29%, 29%);
|
||||
|
||||
$secondary-neutral-color: hsl(204, 64%, 44%);
|
||||
$secondary-neutral-light-color: hsl(0, 0%, 91%);
|
||||
$secondary-neutral-dark-color: hsl(40, 57.6%, 17%);
|
||||
|
||||
$white-color: hsl(219.6, 20.8%, 98%);
|
||||
$black-color: hsl(0, 0%, 17%);
|
||||
|
||||
$faceblue: hsl(221, 44%, 41%);
|
||||
$twitblue: hsl(206, 82%, 63%);
|
||||
|
||||
$shadow-color: rgb(223, 223, 223);
|
||||
|
||||
$background-button-color: hsl(0, 0%, 95%);
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
core/static/core/js/jszip/jszip-utils.min.js
vendored
Normal file
1
core/static/core/js/jszip/jszip-utils.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.JSZipUtils=e():"undefined"!=typeof global?global.JSZipUtils=e():"undefined"!=typeof self&&(self.JSZipUtils=e())}(function(){return function o(i,f,u){function s(n,e){if(!f[n]){if(!i[n]){var t="function"==typeof require&&require;if(!e&&t)return t(n,!0);if(a)return a(n,!0);throw new Error("Cannot find module '"+n+"'")}var r=f[n]={exports:{}};i[n][0].call(r.exports,function(e){var t=i[n][1][e];return s(t||e)},r,r.exports,o,i,f,u)}return f[n].exports}for(var a="function"==typeof require&&require,e=0;e<u.length;e++)s(u[e]);return s}({1:[function(e,t,n){"use strict";var u={};function r(){try{return new window.XMLHttpRequest}catch(e){}}u._getBinaryFromXHR=function(e){return e.response||e.responseText};var s="undefined"!=typeof window&&window.ActiveXObject?function(){return r()||function(){try{return new window.ActiveXObject("Microsoft.XMLHTTP")}catch(e){}}()}:r;u.getBinaryContent=function(t,n){var e,r,o,i;"function"==typeof(n=n||{})?(i=n,n={}):"function"==typeof n.callback&&(i=n.callback),i||"undefined"==typeof Promise?(r=function(e){i(null,e)},o=function(e){i(e,null)}):e=new Promise(function(e,t){r=e,o=t});try{var f=s();f.open("GET",t,!0),"responseType"in f&&(f.responseType="arraybuffer"),f.overrideMimeType&&f.overrideMimeType("text/plain; charset=x-user-defined"),f.onreadystatechange=function(e){if(4===f.readyState)if(200===f.status||0===f.status)try{r(u._getBinaryFromXHR(f))}catch(e){o(new Error(e))}else o(new Error("Ajax error for "+t+" : "+this.status+" "+this.statusText))},n.progress&&(f.onprogress=function(e){n.progress({path:t,originalEvent:e,percent:e.loaded/e.total*100,loaded:e.loaded,total:e.total})}),f.send()}catch(e){o(new Error(e),null)}return e},t.exports=u},{}]},{},[1])(1)});
|
13
core/static/core/js/jszip/jszip.min.js
vendored
Normal file
13
core/static/core/js/jszip/jszip.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
21
core/static/core/js/native-file-system-adapter/LICENSE
Normal file
21
core/static/core/js/native-file-system-adapter/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Jimmy Wärting
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
1
core/static/core/js/native-file-system-adapter/mod.js
Normal file
1
core/static/core/js/native-file-system-adapter/mod.js
Normal file
@ -0,0 +1 @@
|
||||
export*from"./src/es6.js";
|
0
core/static/core/js/native-file-system-adapter/mod.min.js
vendored
Normal file
0
core/static/core/js/native-file-system-adapter/mod.min.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
import FileSystemHandle from"./FileSystemHandle.js";import{errors}from"./util.js";const{GONE:GONE,MOD_ERR:MOD_ERR}=errors,kAdapter=Symbol("adapter");class FileSystemDirectoryHandle extends FileSystemHandle{[kAdapter];constructor(e){super(e),this[kAdapter]=e}async getDirectoryHandle(e,t={}){if(""===e)throw new TypeError("Name can't be an empty string.");if("."===e||".."===e||e.includes("/"))throw new TypeError("Name contains invalid characters.");t.create=!!t.create;const r=await this[kAdapter].getDirectoryHandle(e,t);return new FileSystemDirectoryHandle(r)}async*entries(){const{FileSystemFileHandle:e}=await import("./FileSystemFileHandle.js");for await(const[t,r]of this[kAdapter].entries())yield[r.name,"file"===r.kind?new e(r):new FileSystemDirectoryHandle(r)]}async*getEntries(){const{FileSystemFileHandle:e}=await import("./FileSystemFileHandle.js");console.warn("deprecated, use .entries() instead");for await(let t of this[kAdapter].entries())yield"file"===t.kind?new e(t):new FileSystemDirectoryHandle(t)}async getFileHandle(e,t={}){const{FileSystemFileHandle:r}=await import("./FileSystemFileHandle.js");if(""===e)throw new TypeError("Name can't be an empty string.");if("."===e||".."===e||e.includes("/"))throw new TypeError("Name contains invalid characters.");t.create=!!t.create;return new r(await this[kAdapter].getFileHandle(e,t))}async removeEntry(e,t={}){if(""===e)throw new TypeError("Name can't be an empty string.");if("."===e||".."===e||e.includes("/"))throw new TypeError("Name contains invalid characters.");return t.recursive=!!t.recursive,this[kAdapter].removeEntry(e,t)}async resolve(e){if(await e.isSameEntry(this))return[];const t=[{handle:this,path:[]}];for(;t.length;){let{handle:r,path:n}=t.pop();for await(const a of r.values()){if(await a.isSameEntry(e))return[...n,a.name];"directory"===a.kind&&t.push({handle:a,path:[...n,a.name]})}}return null}async*keys(){for await(const[e]of this[kAdapter].entries())yield e}async*values(){for await(const[e,t]of this)yield t}[Symbol.asyncIterator](){return this.entries()}}if(Object.defineProperty(FileSystemDirectoryHandle.prototype,Symbol.toStringTag,{value:"FileSystemDirectoryHandle",writable:!1,enumerable:!1,configurable:!0}),Object.defineProperties(FileSystemDirectoryHandle.prototype,{getDirectoryHandle:{enumerable:!0},entries:{enumerable:!0},getFileHandle:{enumerable:!0},removeEntry:{enumerable:!0}}),globalThis.FileSystemDirectoryHandle){const e=globalThis.FileSystemDirectoryHandle.prototype;async function ensureDoActuallyStillExist(e){const t=await navigator.storage.getDirectory();if(null===await t.resolve(e))throw new DOMException(...GONE)}e.resolve=async function(e){if(await e.isSameEntry(this))return[];const t=[{handle:this,path:[]}];for(;t.length;){let{handle:r,path:n}=t.pop();for await(const a of r.values()){if(await a.isSameEntry(e))return[...n,a.name];"directory"===a.kind&&t.push({handle:a,path:[...n,a.name]})}}return null};const t=e.entries;e.entries=async function*(){await ensureDoActuallyStillExist(this),yield*t.call(this)},e[Symbol.asyncIterator]=async function*(){yield*this.entries()};const r=e.removeEntry;e.removeEntry=async function(e,n={}){return r.call(this,e,n).catch((async e=>{if(e instanceof DOMException&&"UnknownError"===e.name&&!n.recursive){if(!(await t.call(this).next()).done)throw new DOMException(...MOD_ERR)}throw e}))}}export default FileSystemDirectoryHandle;export{FileSystemDirectoryHandle};
|
@ -0,0 +1 @@
|
||||
import FileSystemHandle from"./FileSystemHandle.js";import FileSystemWritableFileStream from"./FileSystemWritableFileStream.js";import{errors}from"./util.js";const{INVALID:INVALID,SYNTAX:SYNTAX,GONE:GONE}=errors,kAdapter=Symbol("adapter");class FileSystemFileHandle extends FileSystemHandle{[kAdapter];constructor(e){super(e),this[kAdapter]=e}async createWritable(e={}){return new FileSystemWritableFileStream(await this[kAdapter].createWritable(e))}async getFile(){return this[kAdapter].getFile()}}if(Object.defineProperty(FileSystemFileHandle.prototype,Symbol.toStringTag,{value:"FileSystemFileHandle",writable:!1,enumerable:!1,configurable:!0}),Object.defineProperties(FileSystemFileHandle.prototype,{createWritable:{enumerable:!0},getFile:{enumerable:!0}}),globalThis.FileSystemFileHandle&&!globalThis.FileSystemFileHandle.prototype.createWritable){const e=new WeakMap;let t;const a=()=>{let e,t;onmessage=async a=>{const i=a.ports[0],r=a.data;switch(r.type){case"open":const a=r.name;let i=await navigator.storage.getDirectory();for(const e of r.path)i=await i.getDirectoryHandle(e);e=await i.getFileHandle(a),t=await e.createSyncAccessHandle();break;case"write":t.write(r.data,{at:r.position}),t.flush();break;case"truncate":t.truncate(r.size);break;case"abort":case"close":t.close()}i.postMessage(0)}};globalThis.FileSystemFileHandle.prototype.createWritable=async function(i){if(!t){const e=`(${a.toString()})()`,i=new Blob([e],{type:"text/javascript"});t=URL.createObjectURL(i)}const r=new Worker(t,{type:"module"});let n=0;const s=new TextEncoder;let o=await this.getFile().then((e=>e.size));const l=e=>new Promise(((t,a)=>{const i=new MessageChannel;i.port1.onmessage=e=>{e.data instanceof Error?a(e.data):t(e.data),i.port1.close(),i.port2.close(),i.port1.onmessage=null},r.postMessage(e,[i.port2])})),c=await navigator.storage.getDirectory(),p=await e.get(this),y=await c.resolve(p);if(null===y)throw new DOMException(...GONE);let d;await l({type:"open",path:y,name:this.name}),!1===i?.keepExistingData&&(await l({type:"truncate",size:0}),o=0);return new FileSystemWritableFileStream({start:e=>{d=e},async write(e){if("write"===(e=e?.constructor===Object?{...e}:{type:"write",data:e,position:n}).type){if(!("data"in e))throw await l({type:"close"}),new DOMException(...SYNTAX("write requires a data argument"));if(e.position??=n,"string"==typeof e.data)e.data=s.encode(e.data);else if(e.data instanceof ArrayBuffer)e.data=new Uint8Array(e.data);else if(e.data instanceof Uint8Array||!ArrayBuffer.isView(e.data)){if(!(e.data instanceof Uint8Array)){const t=await new Response(e.data).arrayBuffer();e.data=new Uint8Array(t)}}else e.data=new Uint8Array(e.data.buffer,e.data.byteOffset,e.data.byteLength);Number.isInteger(e.position)&&e.position>=0&&(n=e.position),n+=e.data.byteLength,o+=e.data.byteLength}else{if("seek"===e.type){if(Number.isInteger(e.position)&&e.position>=0){if(o<e.position)throw new DOMException(...INVALID);return console.log("seeking",e),void(n=e.position)}throw await l({type:"close"}),new DOMException(...SYNTAX("seek requires a position argument"))}if("truncate"===e.type){if(!(Number.isInteger(e.size)&&e.size>=0))throw await l({type:"close"}),new DOMException(...SYNTAX("truncate requires a size argument"));o=e.size,n>o&&(n=o)}}await l(e)},async close(){await l({type:"close"}),r.terminate()},async abort(e){await l({type:"abort",reason:e}),r.terminate()}})};const i=FileSystemDirectoryHandle.prototype.getFileHandle;FileSystemDirectoryHandle.prototype.getFileHandle=async function(...t){const a=await i.call(this,...t);return e.set(a,this),a}}export default FileSystemFileHandle;export{FileSystemFileHandle};
|
@ -0,0 +1 @@
|
||||
const kAdapter=Symbol("adapter");class FileSystemHandle{[kAdapter];name;kind;constructor(e){this.kind=e.kind,this.name=e.name,this[kAdapter]=e}async queryPermission(e={}){const{mode:r="read"}=e,t=this[kAdapter];if(t.queryPermission)return t.queryPermission({mode:r});if("read"===r)return"granted";if("readwrite"===r)return t.writable?"granted":"denied";throw new TypeError(`Mode ${r} must be 'read' or 'readwrite'`)}async requestPermission({mode:e="read"}={}){const r=this[kAdapter];if(r.requestPermission)return r.requestPermission({mode:e});if("read"===e)return"granted";if("readwrite"===e)return r.writable?"granted":"denied";throw new TypeError(`Mode ${e} must be 'read' or 'readwrite'`)}async remove(e={}){await this[kAdapter].remove(e)}async isSameEntry(e){return this===e||!(!e||"object"!=typeof e||this.kind!==e.kind||!e[kAdapter])&&this[kAdapter].isSameEntry(e[kAdapter])}}Object.defineProperty(FileSystemHandle.prototype,Symbol.toStringTag,{value:"FileSystemHandle",writable:!1,enumerable:!1,configurable:!0}),globalThis.FileSystemHandle&&(globalThis.FileSystemHandle.prototype.queryPermission??=function(e){return"granted"});export default FileSystemHandle;export{FileSystemHandle};
|
@ -0,0 +1 @@
|
||||
import config from"./config.js";const{WritableStream:WritableStream}=config;class FileSystemWritableFileStream extends WritableStream{#e;constructor(e){super(e),this.#e=e,Object.setPrototypeOf(this,FileSystemWritableFileStream.prototype),this._closed=!1}async close(){this._closed=!0;const e=this.getWriter(),t=e.close();return e.releaseLock(),t}seek(e){return this.write({type:"seek",position:e})}truncate(e){return this.write({type:"truncate",size:e})}write(e){if(this._closed)return Promise.reject(new TypeError("Cannot write to a CLOSED writable stream"));const t=this.getWriter(),r=t.write(e);return t.releaseLock(),r}}Object.defineProperty(FileSystemWritableFileStream.prototype,Symbol.toStringTag,{value:"FileSystemWritableFileStream",writable:!1,enumerable:!1,configurable:!0}),Object.defineProperties(FileSystemWritableFileStream.prototype,{close:{enumerable:!0},seek:{enumerable:!0},truncate:{enumerable:!0},write:{enumerable:!0}}),!globalThis.FileSystemFileHandle||globalThis.FileSystemFileHandle.prototype.createWritable||globalThis.FileSystemWritableFileStream||(globalThis.FileSystemWritableFileStream=FileSystemWritableFileStream);export default FileSystemWritableFileStream;export{FileSystemWritableFileStream};
|
@ -0,0 +1 @@
|
||||
import{errors}from"../util.js";const{INVALID:INVALID,GONE:GONE,MISMATCH:MISMATCH,MOD_ERR:MOD_ERR,SYNTAX:SYNTAX,SECURITY:SECURITY,DISALLOWED:DISALLOWED}=errors;export class Sink{constructor(){}write(e){}close(){}}export class FileHandle{constructor(){this._path=""}async getFile(){return new File([],"")}async createWritable(){}async isSameEntry(e){return e._path===this._path}}export class FolderHandle{constructor(){this._path=""}async*entries(){yield}async isSameEntry(e){return e._path===this._path}async getDirectoryHandle(e,r){return new FolderHandle}async getFileHandle(e,r){return new FileHandle}async removeEntry(e,r){}}const fs=new FolderHandle("");export default()=>fs;
|
@ -0,0 +1 @@
|
||||
import{errors}from"../util.js";const{INVALID:INVALID,GONE:GONE,MISMATCH:MISMATCH,MOD_ERR:MOD_ERR,SYNTAX:SYNTAX}=errors,DIR={headers:{"content-type":"dir"}},FILE=()=>({headers:{"content-type":"file","last-modified":Date.now()}}),hasOwn=Object.prototype.hasOwnProperty;class Sink{constructor(e,t,i){this._cache=e,this.path=t,this.size=i.size,this.position=0,this.file=i}write(e,t){if("object"==typeof e)if("write"===e.type){if(Number.isInteger(e.position)&&e.position>=0&&(this.size<e.position&&(this.file=new Blob([this.file,new ArrayBuffer(e.position-this.size)])),this.position=e.position),!("data"in e))throw new DOMException(...SYNTAX("write requires a data argument"));e=e.data}else{if("seek"===e.type){if(Number.isInteger(e.position)&&e.position>=0){if(this.size<e.position)throw new DOMException(...INVALID);return void(this.position=e.position)}throw new DOMException(...SYNTAX("seek requires a position argument"))}if("truncate"===e.type){if(Number.isInteger(e.size)&&e.size>=0){let t=this.file;return t=e.size<this.size?t.slice(0,e.size):new File([t,new Uint8Array(e.size-this.size)],t.name),this.size=t.size,this.position>t.size&&(this.position=t.size),void(this.file=t)}throw new DOMException(...SYNTAX("truncate requires a size argument"))}}e=new Blob([e]);let i=this.file;const s=i.slice(0,this.position),n=i.slice(this.position+e.size);let a=this.position-s.size;a<0&&(a=0),i=new File([s,new Uint8Array(a),e,n],i.name),this.size=i.size,this.position+=e.size,this.file=i}async close(){const[e]=await this._cache.keys(this.path);if(!e)throw new DOMException(...GONE);return this._cache.put(this.path,new Response(this.file,FILE()))}}export class FileHandle{constructor(e,t){this._cache=t,this.path=e,this.kind="file",this.writable=!0,this.readable=!0}get name(){return this.path.split("/").pop()}async isSameEntry(e){return this.path===e.path}async getFile(){const e=await this._cache.match(this.path);if(!e)throw new DOMException(...GONE);const t=await e.blob();return new File([t],this.name,{lastModified:+e.headers.get("last-modified")})}async createWritable(e){const[t]=await this._cache.keys(this.path);if(!t)throw new DOMException(...GONE);return new Sink(this._cache,this.path,e.keepExistingData?await this.getFile():new File([],this.name))}}export class FolderHandle{constructor(e,t){this._dir=e,this.writable=!0,this.readable=!0,this._cache=t,this.kind="directory",this.name=e.split("/").pop()}async*entries(){for(const[e,t]of Object.entries(await this._tree))yield[e.split("/").pop(),t?new FileHandle(e,this._cache):new FolderHandle(e,this._cache)]}async isSameEntry(e){return this._dir===e._dir}async getDirectoryHandle(e,t){const i=this._dir.endsWith("/")?this._dir+e:`${this._dir}/${e}`,s=await this._tree;if(hasOwn.call(s,i)){if(s[i])throw new DOMException(...MISMATCH);return new FolderHandle(i,this._cache)}if(t.create)return s[i]=!1,await this._cache.put(i,new Response("{}",DIR)),await this._save(s),new FolderHandle(i,this._cache);throw new DOMException(...GONE)}get _tree(){return this._cache.match(this._dir).then((e=>e.json())).catch((e=>{throw new DOMException(...GONE)}))}_save(e){return this._cache.put(this._dir,new Response(JSON.stringify(e),DIR))}async getFileHandle(e,t){const i=this._dir.endsWith("/")?this._dir+e:`${this._dir}/${e}`,s=await this._tree;if(hasOwn.call(s,i)){if(!s[i])throw new DOMException(...MISMATCH);return new FileHandle(i,this._cache)}if(t.create){const e=await this._tree;return e[i]=!0,await this._cache.put(i,new Response("",FILE())),await this._save(e),new FileHandle(i,this._cache)}throw new DOMException(...GONE)}async removeEntry(e,t){const i=await this._tree,s=this._dir.endsWith("/")?this._dir+e:`${this._dir}/${e}`;if(!hasOwn.call(i,s))throw new DOMException(...GONE);if(t.recursive){const e=[...Object.entries(i)];for(;e.length;){const[t,i]=e.pop();if(i)await this._cache.delete(t);else{const i=await this._cache.match(t).then((e=>e.json()));e.push(...Object.entries(i))}}delete i[s]}else{const e=i[s];if(delete i[s],e)await this._cache.delete(s);else{const e=await this._cache.match(s).then((e=>e.json()));if(Object.keys(e).length)throw new DOMException(...MOD_ERR);await this._cache.delete(s)}}await this._save(i)}}export default async function(){const e=await caches.open("sandboxed-fs");return await e.match("/")||await e.put("/",new Response("{}",DIR)),new FolderHandle(location.origin+"/",e)}
|
@ -0,0 +1 @@
|
||||
import{join,basename}from"https://deno.land/std@0.108.0/path/mod.ts";import{errors}from"../util.js";const{INVALID:INVALID,GONE:GONE,MISMATCH:MISMATCH,MOD_ERR:MOD_ERR,SYNTAX:SYNTAX}=errors;async function fileFrom(t){const e=Deno.readFileSync(t),i=await Deno.stat(t);return new File([e],basename(t),{lastModified:Number(i.mtime)})}export class Sink{constructor(t,e){this.fileHandle=t,this.size=e,this.position=0}async abort(){await this.fileHandle.close()}async write(t){if("object"==typeof t)if("write"===t.type){if(Number.isInteger(t.position)&&t.position>=0&&(this.position=t.position),!("data"in t))throw await this.fileHandle.close(),new DOMException(...SYNTAX("write requires a data argument"));t=t.data}else{if("seek"===t.type){if(Number.isInteger(t.position)&&t.position>=0){if(this.size<t.position)throw new DOMException(...INVALID);return void(this.position=t.position)}throw await this.fileHandle.close(),new DOMException(...SYNTAX("seek requires a position argument"))}if("truncate"===t.type){if(Number.isInteger(t.size)&&t.size>=0)return await this.fileHandle.truncate(t.size),this.size=t.size,void(this.position>this.size&&(this.position=this.size));throw await this.fileHandle.close(),new DOMException(...SYNTAX("truncate requires a size argument"))}}if(t instanceof ArrayBuffer)t=new Uint8Array(t);else if("string"==typeof t)t=(new TextEncoder).encode(t);else if(t instanceof Blob){await this.fileHandle.seek(this.position,Deno.SeekMode.Start);for await(const e of t.stream()){const t=await this.fileHandle.write(e);this.position+=t,this.size+=t}return}await this.fileHandle.seek(this.position,Deno.SeekMode.Start);const e=await this.fileHandle.write(t);this.position+=e,this.size+=e}async close(){await this.fileHandle.close()}}export class FileHandle{#t;constructor(t,e){this.#t=t,this.name=e,this.kind="file"}async getFile(){return await Deno.stat(this.#t).catch((t=>{if("NotFound"===t.name)throw new DOMException(...GONE)})),fileFrom(this.#t)}async isSameEntry(t){return this.#t===this.#e.apply(t)}#e(){return this.#t}async createWritable(t){const e=await Deno.open(this.#t,{write:!0,truncate:!t.keepExistingData}).catch((t=>{if("NotFound"===t.name)throw new DOMException(...GONE);throw t})),{size:i}=await e.stat();return new Sink(e,i)}}export class FolderHandle{#t="";constructor(t,e=""){this.name=e,this.kind="directory",this.#t=join(t)}async isSameEntry(t){return this.#t===this.#e.apply(t)}#e(){return this.#t}async*entries(){const t=this.#t;try{for await(const e of Deno.readDir(t)){const{name:i}=e,n=join(t,i),o=await Deno.lstat(n);o.isFile?yield[i,new FileHandle(n,i)]:o.isDirectory&&(yield[i,new FolderHandle(n,i)])}}catch(t){throw"NotFound"===t.name?new DOMException(...GONE):t}}async getDirectoryHandle(t,e){const i=join(this.#t,t),n=await Deno.lstat(i).catch((t=>{if("NotFound"!==t.name)throw t})),o=n?.isDirectory;if(n&&o)return new FolderHandle(i,t);if(n&&!o)throw new DOMException(...MISMATCH);if(!e.create)throw new DOMException(...GONE);return await Deno.mkdir(i),new FolderHandle(i,t)}async getFileHandle(t,e){const i=join(this.#t,t),n=await Deno.lstat(i).catch((t=>{if("NotFound"!==t.name)throw t})),o=n?.isFile;if(n&&o)return new FileHandle(i,t);if(n&&!o)throw new DOMException(...MISMATCH);if(!e.create)throw new DOMException(...GONE);return(await Deno.open(i,{create:!0,write:!0})).close(),new FileHandle(i,t)}async queryPermission(){return"granted"}async removeEntry(t,e){const i=join(this.#t,t);(await Deno.lstat(i).catch((t=>{if("NotFound"===t.name)throw new DOMException(...GONE);throw t}))).isDirectory?e.recursive?await Deno.remove(i,{recursive:!0}).catch((t=>{if("ENOTEMPTY"===t.code)throw new DOMException(...MOD_ERR);throw t})):await Deno.remove(i).catch((()=>{throw new DOMException(...MOD_ERR)})):await Deno.remove(i)}}export default t=>new FolderHandle(join(Deno.cwd(),t));
|
@ -0,0 +1 @@
|
||||
import{errors}from"../util.js";import config from"../config.js";const{WritableStream:WritableStream,TransformStream:TransformStream,DOMException:DOMException,Blob:Blob}=config,{GONE:GONE}=errors,isOldSafari=/constructor/i.test(window.HTMLElement);export class FileHandle{constructor(e="unkown"){this.name=e,this.kind="file"}async getFile(){throw new DOMException(...GONE)}async isSameEntry(e){return this===e}async createWritable(e={}){const t=await(navigator.serviceWorker?.getRegistration()),r=document.createElement("a"),s=new TransformStream,a=s.writable;if(r.download=this.name,isOldSafari||!t){let e=[];s.readable.pipeTo(new WritableStream({write(t){e.push(new Blob([t]))},close(){const t=new Blob(e,{type:"application/octet-stream; charset=utf-8"});e=[],r.href=URL.createObjectURL(t),r.click(),setTimeout((()=>URL.revokeObjectURL(r.href)),1e4)}}))}else{const{writable:r,readablePort:a}=new RemoteWritableStream(WritableStream),o=encodeURIComponent(this.name).replace(/['()]/g,escape).replace(/\*/g,"%2A"),n={"content-disposition":"attachment; filename*=UTF-8''"+o,"content-type":"application/octet-stream; charset=utf-8",...e.size?{"content-length":e.size}:{}},i=setTimeout((()=>t.active.postMessage(0)),1e4);s.readable.pipeThrough(new TransformStream({transform(e,t){if(e instanceof Uint8Array)return t.enqueue(e);const r=new Response(e).body.getReader(),s=e=>r.read().then((e=>e.done?0:s(t.enqueue(e.value))));return s()}})).pipeTo(r).finally((()=>{clearInterval(i)})),t.active.postMessage({url:t.scope+o,headers:n,readablePort:a},[a]);const c=document.createElement("iframe");c.hidden=!0,c.src=t.scope+o,document.body.appendChild(c)}return a.getWriter()}}const WRITE=0,PULL=0,ERROR=1,ABORT=1,CLOSE=2;class MessagePortSink{constructor(e){e.onmessage=e=>this._onMessage(e.data),this._port=e,this._resetReady()}start(e){return this._controller=e,this._readyPromise}write(e){const t={type:0,chunk:e};return this._port.postMessage(t,[e.buffer]),this._resetReady(),this._readyPromise}close(){this._port.postMessage({type:2}),this._port.close()}abort(e){this._port.postMessage({type:1,reason:e}),this._port.close()}_onMessage(e){0===e.type&&this._resolveReady(),1===e.type&&this._onError(e.reason)}_onError(e){this._controller.error(e),this._rejectReady(e),this._port.close()}_resetReady(){this._readyPromise=new Promise(((e,t)=>{this._readyResolve=e,this._readyReject=t})),this._readyPending=!0}_resolveReady(){this._readyResolve(),this._readyPending=!1}_rejectReady(e){this._readyPending||this._resetReady(),this._readyPromise.catch((()=>{})),this._readyReject(e),this._readyPending=!1}}class RemoteWritableStream{constructor(e){const t=new MessageChannel;this.readablePort=t.port1,this.writable=new e(new MessagePortSink(t.port2))}}
|
@ -0,0 +1 @@
|
||||
import{errors}from"../util.js";const{INVALID:INVALID,GONE:GONE,MISMATCH:MISMATCH,MOD_ERR:MOD_ERR,SYNTAX:SYNTAX,ABORT:ABORT}=errors;function setupTxErrorHandler(e,t){e.onerror=()=>t(e.error),e.onabort=()=>t(e.error||new DOMException(...ABORT))}class Sink{constructor(e,t,i,s){this.db=e,this.id=t,this.size=i,this.position=0,this.file=s}write(e){if("object"==typeof e)if("write"===e.type){if(Number.isInteger(e.position)&&e.position>=0&&(this.size<e.position&&(this.file=new File([this.file,new ArrayBuffer(e.position-this.size)],this.file.name,this.file)),this.position=e.position),!("data"in e))throw new DOMException(...SYNTAX("write requires a data argument"));e=e.data}else{if("seek"===e.type){if(Number.isInteger(e.position)&&e.position>=0){if(this.size<e.position)throw new DOMException(...INVALID);return void(this.position=e.position)}throw new DOMException(...SYNTAX("seek requires a position argument"))}if("truncate"===e.type){if(Number.isInteger(e.size)&&e.size>=0){let t=this.file;return t=e.size<this.size?new File([t.slice(0,e.size)],t.name,t):new File([t,new Uint8Array(e.size-this.size)],t.name,t),this.size=t.size,this.position>t.size&&(this.position=t.size),void(this.file=t)}throw new DOMException(...SYNTAX("truncate requires a size argument"))}}e=new Blob([e]);let t=this.file;const i=t.slice(0,this.position),s=t.slice(this.position+e.size);let n=this.position-i.size;n<0&&(n=0),t=new File([i,new Uint8Array(n),e,s],t.name),this.size=t.size,this.position+=e.size,this.file=t}close(){return new Promise(((e,t)=>{const[i,s]=store(this.db);s.get(this.id).onsuccess=e=>{e.target.result?s.put(this.file,this.id):t(new DOMException(...GONE))},i.oncomplete=()=>e(),i.onerror=t,i.onabort=t}))}}class FileHandle{constructor(e,t,i){this._db=e,this._id=t,this.name=i,this.kind="file",this.readable=!0,this.writable=!0}async isSameEntry(e){return this._id===e._id}async getFile(){const e=await new Promise(((e,t)=>{const i=store(this._db)[1].get(this._id);i.onsuccess=t=>e(t.target.result),i.onerror=e=>t(e.target.error)}));if(!e)throw new DOMException(...GONE);return e}async createWritable(e){let t=await this.getFile();return t=e.keepExistingData?t:new File([],this.name),new Sink(this._db,this._id,t.size,t)}}function store(e){const t=e.transaction("entries","readwrite",{durability:"relaxed"});return[t,t.objectStore("entries")]}function rimraf(e,t,i=!0){const{source:s,result:n}=e.target;for(const[e,r]of Object.values(t||n))r?s.delete(e):i?(s.get(e).onsuccess=rimraf,s.delete(e)):s.get(e).onsuccess=t=>{0!==Object.keys(t.target.result).length?t.target.transaction.abort():s.delete(e)}}class FolderHandle{constructor(e,t,i){this._db=e,this._id=t,this.kind="directory",this.name=i,this.readable=!0,this.writable=!0}async*entries(){const e=store(this._db)[1].get(this._id);await new Promise(((t,i)=>{e.onsuccess=()=>t(),e.onerror=()=>i(e.error)}));const t=e.result;if(!t)throw new DOMException(...GONE);for(const[e,[i,s]]of Object.entries(t))yield[e,s?new FileHandle(this._db,i,e):new FolderHandle(this._db,i,e)]}isSameEntry(e){return this._id===e._id}getDirectoryHandle(e,t){return new Promise(((i,s)=>{const n=store(this._db)[1],r=n.get(this._id);r.onsuccess=()=>{const o=r.result,c=o[e];c?c[1]?s(new DOMException(...MISMATCH)):i(new FolderHandle(this._db,c[0],e)):t.create?n.add({}).onsuccess=t=>{const s=t.target.result;o[e]=[s,!1],n.put(o,this._id).onsuccess=()=>i(new FolderHandle(this._db,s,e))}:s(new DOMException(...GONE))}}))}getFileHandle(e,t){return new Promise(((i,s)=>{const n=store(this._db)[1],r=n.get(this._id);r.onsuccess=()=>{const o=r.result,c=o[e];if(c&&c[1]&&i(new FileHandle(this._db,c[0],e)),c&&!c[1]&&s(new DOMException(...MISMATCH)),c||t.create||s(new DOMException(...GONE)),!c&&t.create){const t=n.put(new File([],e));t.onsuccess=()=>{const s=t.result;o[e]=[s,!0];n.put(o,this._id).onsuccess=()=>{i(new FileHandle(this._db,s,e))}}}}}))}async removeEntry(e,t){return new Promise(((i,s)=>{const[n,r]=store(this._db),o=r.get(this._id);o.onsuccess=i=>{const n=o.result,c={_:n[e]};if(!c._)return s(new DOMException(...GONE));delete n[e],r.put(n,this._id),rimraf(i,c,!!t.recursive)},n.oncomplete=i,n.onerror=s,n.onabort=()=>{s(new DOMException(...MOD_ERR))}}))}}export default(e={persistent:!1})=>new Promise((e=>{const t=indexedDB.open("fileSystem");t.onupgradeneeded=()=>{const e=t.result;e.createObjectStore("entries",{autoIncrement:!0}).transaction.oncomplete=t=>{e.transaction("entries","readwrite").objectStore("entries").add({})}},t.onsuccess=()=>{e(new FolderHandle(t.result,1,""))}}));
|
@ -0,0 +1 @@
|
||||
import{errors}from"../util.js";const{GONE:GONE,MISMATCH:MISMATCH,SYNTAX:SYNTAX,DISALLOWED:DISALLOWED}=errors;export class FileHandle{constructor(e,t){this.name=e.name,this.kind="file",this._deleted=!1,this._root=t,this._entry=e,this.writable=!1,this.readable=!0}async getFile(){const e=await fetch(`https://cdn.jsdelivr.net/${this._root}/${this.name}`),t=await e.blob();return new File([t],this.name,{type:t.type,lastModified:this._entry.time})}async createWritable(){throw new DOMException(...DISALLOWED)}async isSameEntry(e){return this===e}}function toDic(e,t){const n={};for(const i of e)i.time=+new Date(i.time),"file"===i.type?n[i.name]=new FileHandle(i,t):n[i.name]=new FolderHandle(i.files,`${t}/${i.name}`,i.name);return n}export class FolderHandle{constructor(e,t,n=""){this.name=n,this.kind="directory",this._deleted=!1,this._entries=toDic(e,t),this.writable=!1,this.readable=!0}async*entries(){yield*Object.entries(this._entries)}async isSameEntry(e){return this===e}async getDirectoryHandle(e,t){if(this._deleted)throw new DOMException(...GONE);const n=this._entries[e];if(n){if(n instanceof FileHandle)throw new DOMException(...MISMATCH);return n}throw t.create?new DOMException(...DISALLOWED):new DOMException(...GONE)}async getFileHandle(e,t){const n=this._entries[e],i=n instanceof FileHandle;if(n&&i)return n;if(n&&!i)throw new DOMException(...MISMATCH);if(!n&&!t.create)throw new DOMException(...GONE);if(!n&&t.create)throw new DOMException(...DISALLOWED)}async removeEntry(e,t){throw new DOMException(...DISALLOWED)}}export default async e=>{const t=await fetch(`https://data.jsdelivr.com/v1/package/${e}`),{files:n}=await t.json();return new FolderHandle(n,e)};
|
@ -0,0 +1 @@
|
||||
import{errors}from"../util.js";import config from"../config.js";const{File:File,Blob:Blob,DOMException:DOMException}=config,{INVALID:INVALID,GONE:GONE,MISMATCH:MISMATCH,MOD_ERR:MOD_ERR,SYNTAX:SYNTAX,SECURITY:SECURITY,DISALLOWED:DISALLOWED}=errors;export class Sink{constructor(e,i){this.fileHandle=e,this.file=i,this.size=i.size,this.position=0}write(e){let i=this.file;if("object"==typeof e)if("write"===e.type){if(Number.isInteger(e.position)&&e.position>=0&&(this.position=e.position,this.size<e.position&&(this.file=new File([this.file,new ArrayBuffer(e.position-this.size)],this.file.name,this.file))),!("data"in e))throw new DOMException(...SYNTAX("write requires a data argument"));e=e.data}else{if("seek"===e.type){if(Number.isInteger(e.position)&&e.position>=0){if(this.size<e.position)throw new DOMException(...INVALID);return void(this.position=e.position)}throw new DOMException(...SYNTAX("seek requires a position argument"))}if("truncate"===e.type){if(Number.isInteger(e.size)&&e.size>=0)return i=e.size<this.size?new File([i.slice(0,e.size)],i.name,i):new File([i,new Uint8Array(e.size-this.size)],i.name),this.size=i.size,this.position>i.size&&(this.position=i.size),void(this.file=i);throw new DOMException(...SYNTAX("truncate requires a size argument"))}}e=new Blob([e]);let t=this.file;const s=t.slice(0,this.position),n=t.slice(this.position+e.size);let o=this.position-s.size;o<0&&(o=0),t=new File([s,new Uint8Array(o),e,n],t.name),this.size=t.size,this.position+=e.size,this.file=t}close(){if(this.fileHandle._deleted)throw new DOMException(...GONE);this.fileHandle._file=this.file,this.file=this.position=this.size=null,this.fileHandle.onclose&&this.fileHandle.onclose(this.fileHandle)}}export class FileHandle{constructor(e="",i=new File([],e),t=!0){this._file=i,this.name=e,this.kind="file",this._deleted=!1,this.writable=t,this.readable=!0}async getFile(){if(this._deleted)throw new DOMException(...GONE);return this._file}async createWritable(e){if(!this.writable)throw new DOMException(...DISALLOWED);if(this._deleted)throw new DOMException(...GONE);const i=e.keepExistingData?await this.getFile():new File([],this.name);return new Sink(this,i)}async isSameEntry(e){return this===e}async _destroy(){this._deleted=!0,this._file=null}}export class FolderHandle{constructor(e,i=!0){this.name=e,this.kind="directory",this._deleted=!1,this._entries={},this.writable=i,this.readable=!0}async*entries(){if(this._deleted)throw new DOMException(...GONE);yield*Object.entries(this._entries)}async isSameEntry(e){return this===e}async getDirectoryHandle(e,i){if(this._deleted)throw new DOMException(...GONE);const t=this._entries[e];if(t){if(t instanceof FileHandle)throw new DOMException(...MISMATCH);return t}if(i.create)return this._entries[e]=new FolderHandle(e);throw new DOMException(...GONE)}async getFileHandle(e,i){const t=this._entries[e],s=t instanceof FileHandle;if(t&&s)return t;if(t&&!s)throw new DOMException(...MISMATCH);if(!t&&!i.create)throw new DOMException(...GONE);return!t&&i.create?this._entries[e]=new FileHandle(e):void 0}async removeEntry(e,i){const t=this._entries[e];if(!t)throw new DOMException(...GONE);await t._destroy(i.recursive),delete this._entries[e]}async _destroy(e){for(let i of Object.values(this._entries)){if(!e)throw new DOMException(...MOD_ERR);await i._destroy(e)}this._entries={},this._deleted=!0}}const fs=new FolderHandle("");export default()=>fs;
|
@ -0,0 +1 @@
|
||||
import fs from"node:fs/promises";import{join}from"node:path";import{errors}from"../util.js";import config from"../config.js";const{DOMException:DOMException}=config,{INVALID:INVALID,GONE:GONE,MISMATCH:MISMATCH,MOD_ERR:MOD_ERR,SYNTAX:SYNTAX}=errors;function isBlob(t){return t&&"object"==typeof t&&"function"==typeof t.constructor&&("function"==typeof t.stream||"function"==typeof t.arrayBuffer)&&/^(Blob|File)$/.test(t[Symbol.toStringTag])}export class Sink{constructor(t,i){this._fileHandle=t,this._size=i,this._position=0}async abort(){await this._fileHandle.close()}async write(t){if("object"==typeof t)if("write"===t.type){if(Number.isInteger(t.position)&&t.position>=0&&(this._position=t.position),!("data"in t))throw await this._fileHandle.close(),new DOMException(...SYNTAX("write requires a data argument"));t=t.data}else{if("seek"===t.type){if(Number.isInteger(t.position)&&t.position>=0){if(this._size<t.position)throw new DOMException(...INVALID);return void(this._position=t.position)}throw await this._fileHandle.close(),new DOMException(...SYNTAX("seek requires a position argument"))}if("truncate"===t.type){if(Number.isInteger(t.size)&&t.size>=0)return await this._fileHandle.truncate(t.size),this._size=t.size,void(this._position>this._size&&(this._position=this._size));throw await this._fileHandle.close(),new DOMException(...SYNTAX("truncate requires a size argument"))}}if(t instanceof ArrayBuffer)t=new Uint8Array(t);else if("string"==typeof t)t=Buffer.from(t);else if(isBlob(t)){for await(const i of t.stream()){const t=await this._fileHandle.writev([i],this._position);this._position+=t.bytesWritten,this._size+=t.bytesWritten}return}const i=await this._fileHandle.writev([t],this._position);this._position+=i.bytesWritten,this._size+=i.bytesWritten}async close(){await this._fileHandle.close()}}export class FileHandle{constructor(t,i){this._path=t,this.name=i,this.kind="file"}async getFile(){await fs.stat(this._path).catch((t=>{if("ENOENT"===t.code)throw new DOMException(...GONE)}));const{fileFrom:t}=await import("fetch-blob/from.js");return t(this._path)}async isSameEntry(t){return this._path===this._getPath.apply(t)}_getPath(){return this._path}async createWritable(t){const i=await fs.open(this._path,t.keepExistingData?"r+":"w+").catch((t=>{if("ENOENT"===t.code)throw new DOMException(...GONE);throw t})),{size:e}=await i.stat();return new Sink(i,e)}}export class FolderHandle{_path="";constructor(t="",i=""){this.name=i,this.kind="directory",this._path=t}async isSameEntry(t){return this._path===t._path}async*entries(){const t=this._path,i=await fs.readdir(t).catch((t=>{if("ENOENT"===t.code)throw new DOMException(...GONE);throw t}));for(let e of i){const i=join(t,e),o=await fs.lstat(i);o.isFile()?yield[e,new FileHandle(i,e)]:o.isDirectory()&&(yield[e,new FolderHandle(i,e)])}}async getDirectoryHandle(t,i){const e=join(this._path,t),o=await fs.lstat(e).catch((t=>{if("ENOENT"!==t.code)throw t})),s=o?.isDirectory();if(o&&s)return new FolderHandle(e,t);if(o&&!s)throw new DOMException(...MISMATCH);if(!i.create)throw new DOMException(...GONE);return await fs.mkdir(e),new FolderHandle(e,t)}async getFileHandle(t,i){const e=join(this._path,t),o=await fs.lstat(e).catch((t=>{if("ENOENT"!==t.code)throw t})),s=o?.isFile();if(o&&s)return new FileHandle(e,t);if(o&&!s)throw new DOMException(...MISMATCH);if(!i.create)throw new DOMException(...GONE);return await(await fs.open(e,"w")).close(),new FileHandle(e,t)}async queryPermission(){return"granted"}async removeEntry(t,i){const e=join(this._path,t);(await fs.lstat(e).catch((t=>{if("ENOENT"===t.code)throw new DOMException(...GONE);throw t}))).isDirectory()?i.recursive?await fs.rm(e,{recursive:!0}).catch((t=>{if("ENOTEMPTY"===t.code)throw new DOMException(...MOD_ERR);throw t})):await fs.rmdir(e).catch((t=>{if("ENOTEMPTY"===t.code)throw new DOMException(...MOD_ERR);throw t})):await fs.unlink(e)}}export default t=>new FolderHandle(t);
|
@ -0,0 +1 @@
|
||||
import{errors}from"../util.js";const{DISALLOWED:DISALLOWED}=errors;class Sink{constructor(e,i){this.writer=e,this.fileEntry=i}async write(e){if("object"==typeof e)if("write"===e.type){if(Number.isInteger(e.position)&&e.position>=0&&(this.writer.seek(e.position),this.writer.position!==e.position&&(await new Promise(((i,t)=>{this.writer.onwriteend=i,this.writer.onerror=t,this.writer.truncate(e.position)})),this.writer.seek(e.position))),!("data"in e))throw new DOMException("Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. write requires a data argument","SyntaxError");e=e.data}else{if("seek"===e.type){if(Number.isInteger(e.position)&&e.position>=0){if(this.writer.seek(e.position),this.writer.position!==e.position)throw new DOMException("seeking position failed","InvalidStateError");return}throw new DOMException("Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. seek requires a position argument","SyntaxError")}if("truncate"===e.type)return new Promise((i=>{if(!(Number.isInteger(e.size)&&e.size>=0))throw new DOMException("Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. truncate requires a size argument","SyntaxError");this.writer.onwriteend=e=>i(),this.writer.truncate(e.size)}))}await new Promise(((i,t)=>{this.writer.onwriteend=i,this.writer.onerror=t,this.writer.write(new Blob([e]))}))}close(){return new Promise(this.fileEntry.file.bind(this.fileEntry))}}export class FileHandle{constructor(e,i=!0){this.file=e,this.kind="file",this.writable=i,this.readable=!0}get name(){return this.file.name}isSameEntry(e){return this.file.toURL()===e.file.toURL()}getFile(){return new Promise(this.file.file.bind(this.file))}createWritable(e){if(!this.writable)throw new DOMException(...DISALLOWED);return new Promise(((i,t)=>this.file.createWriter((t=>{!1===e.keepExistingData?(t.onwriteend=e=>i(new Sink(t,this.file)),t.truncate(0)):i(new Sink(t,this.file))}),t)))}}export class FolderHandle{constructor(e,i=!0){this.dir=e,this.writable=i,this.readable=!0,this.kind="directory",this.name=e.name}isSameEntry(e){return this.dir.fullPath===e.dir.fullPath}async*entries(){const e=this.dir.createReader(),i=await new Promise(e.readEntries.bind(e));for(const e of i)yield[e.name,e.isFile?new FileHandle(e,this.writable):new FolderHandle(e,this.writable)]}getDirectoryHandle(e,i){return new Promise(((t,r)=>{this.dir.getDirectory(e,i,(e=>{t(new FolderHandle(e))}),r)}))}getFileHandle(e,i){return new Promise(((t,r)=>this.dir.getFile(e,i,(e=>t(new FileHandle(e))),r)))}async removeEntry(e,i){const t=await this.getDirectoryHandle(e,{create:!1}).catch((i=>"TypeMismatchError"===i.name?this.getFileHandle(e,{create:!1}):i));if(t instanceof Error)throw t;return new Promise(((e,r)=>{t instanceof FolderHandle?i.recursive?t.dir.removeRecursively((()=>e()),r):t.dir.remove((()=>e()),r):t.file&&t.file.remove((()=>e()),r)}))}}export default(e={})=>new Promise(((i,t)=>window.webkitRequestFileSystem(e._persistent,0,(e=>i(new FolderHandle(e.root))),t)));
|
@ -0,0 +1 @@
|
||||
const config={ReadableStream:globalThis.ReadableStream,WritableStream:globalThis.WritableStream,TransformStream:globalThis.TransformStream,DOMException:globalThis.DOMException,Blob:globalThis.Blob,File:globalThis.File};export default config;
|
@ -0,0 +1 @@
|
||||
import showDirectoryPicker from"./showDirectoryPicker.js";import showOpenFilePicker from"./showOpenFilePicker.js";import showSaveFilePicker from"./showSaveFilePicker.js";import getOriginPrivateDirectory from"./getOriginPrivateDirectory.js";import FileSystemWritableFileStream from"./FileSystemWritableFileStream.js";import FileSystemDirectoryHandle from"./FileSystemDirectoryHandle.js";import FileSystemFileHandle from"./FileSystemFileHandle.js";import FileSystemHandle from"./FileSystemHandle.js";export{FileSystemDirectoryHandle,FileSystemFileHandle,FileSystemHandle,FileSystemWritableFileStream,getOriginPrivateDirectory,showDirectoryPicker,showOpenFilePicker,showSaveFilePicker};
|
@ -0,0 +1 @@
|
||||
async function getOriginPrivateDirectory(e,t={}){if(!e)return globalThis.navigator?.storage?.getDirectory()||globalThis.getOriginPrivateDirectory();const{FileSystemDirectoryHandle:i}=await import("./FileSystemDirectoryHandle.js"),r=await e;return new i(await(r.default?r.default(t):r(t)))}globalThis.DataTransferItem&&!DataTransferItem.prototype.getAsFileSystemHandle&&(DataTransferItem.prototype.getAsFileSystemHandle=async function(){const e=this.webkitGetAsEntry(),[{FileHandle:t,FolderHandle:i},{FileSystemDirectoryHandle:r},{FileSystemFileHandle:a}]=await Promise.all([import("./adapters/sandbox.js"),import("./FileSystemDirectoryHandle.js"),import("./FileSystemFileHandle.js")]);return e.isFile?new a(new t(e,!1)):new r(new i(e,!1))});export default getOriginPrivateDirectory;
|
@ -0,0 +1 @@
|
||||
const native=globalThis.showDirectoryPicker;async function showDirectoryPicker(e={}){if(native&&!e._preferPolyfill)return native(e);const t=document.createElement("input");t.type="file",t.webkitdirectory=!0,t.multiple=!0,t.style.position="fixed",t.style.top="-100000px",t.style.left="-100000px",document.body.appendChild(t);const i=import("./util.js");return await new Promise((e=>{t.addEventListener("change",e),t.click()})),i.then((e=>e.getDirHandlesFromInput(t)))}export default showDirectoryPicker;export{showDirectoryPicker};
|
@ -0,0 +1 @@
|
||||
const def={accepts:[]},native=globalThis.showOpenFilePicker;async function showOpenFilePicker(e={}){const t={...def,...e};if(native&&!e._preferPolyfill)return native(t);const i=document.createElement("input");i.type="file",i.multiple=t.multiple,i.accept=(t.accepts||[]).map((e=>[...(e.extensions||[]).map((e=>"."+e)),...e.mimeTypes||[]])).flat().join(","),Object.assign(i.style,{position:"fixed",top:"-100000px",left:"-100000px"}),document.body.appendChild(i);const n=import("./util.js");return await new Promise((e=>{i.addEventListener("change",e,{once:!0}),i.click()})),i.remove(),n.then((e=>e.getFileHandlesFromInput(i)))}export default showOpenFilePicker;export{showOpenFilePicker};
|
@ -0,0 +1 @@
|
||||
const native=globalThis.showSaveFilePicker;async function showSaveFilePicker(e={}){if(native&&!e._preferPolyfill)return native(e);e._name&&(console.warn("deprecated _name, spec now have `suggestedName`"),e.suggestedName=e._name);const{FileSystemFileHandle:a}=await import("./FileSystemFileHandle.js"),{FileHandle:i}=await import("./adapters/downloader.js");return new a(new i(e.suggestedName))}export default showSaveFilePicker;export{showSaveFilePicker};
|
@ -0,0 +1 @@
|
||||
export const errors={INVALID:["seeking position failed.","InvalidStateError"],GONE:["A requested file or directory could not be found at the time an operation was processed.","NotFoundError"],MISMATCH:["The path supplied exists, but was not an entry of requested type.","TypeMismatchError"],MOD_ERR:["The object can not be modified in this way.","InvalidModificationError"],SYNTAX:e=>[`Failed to execute 'write' on 'UnderlyingSinkBase': Invalid params passed. ${e}`,"SyntaxError"],SECURITY:["It was determined that certain files are unsafe for access within a Web application, or that too many calls are being made on file resources.","SecurityError"],DISALLOWED:["The request is not allowed by the user agent or the platform in the current context.","NotAllowedError"]};export const config={writable:globalThis.WritableStream};export async function fromDataTransfer(e){console.warn("deprecated fromDataTransfer - use `dt.items[0].getAsFileSystemHandle()` instead");const[t,r,a]=await Promise.all([import("./adapters/memory.js"),import("./adapters/sandbox.js"),import("./FileSystemDirectoryHandle.js")]),n=new t.FolderHandle("",!1);return n._entries=e.map((e=>e.isFile?new r.FileHandle(e,!1):new r.FolderHandle(e,!1))),new a.FileSystemDirectoryHandle(n)}export async function getDirHandlesFromInput(e){const{FolderHandle:t,FileHandle:r}=await import("./adapters/memory.js"),{FileSystemDirectoryHandle:a}=await import("./FileSystemDirectoryHandle.js"),n=Array.from(e.files),i=n[0].webkitRelativePath.split("/",1)[0],o=new t(i,!1);return n.forEach((e=>{const a=e.webkitRelativePath.split("/");a.shift();const n=a.pop();a.reduce(((e,r)=>(e._entries[r]||(e._entries[r]=new t(r,!1)),e._entries[r])),o)._entries[n]=new r(e.name,e,!1)})),new a(o)}export async function getFileHandlesFromInput(e){const{FileHandle:t}=await import("./adapters/memory.js"),{FileSystemFileHandle:r}=await import("./FileSystemFileHandle.js");return Array.from(e.files).map((e=>new r(new t(e.name,e,!1))))}
|
1
core/static/core/js/native-file-system-adapter/sw.js
Normal file
1
core/static/core/js/native-file-system-adapter/sw.js
Normal file
@ -0,0 +1 @@
|
||||
const WRITE=0,PULL=0,ERROR=1,ABORT=1,CLOSE=2,PING=3;class MessagePortSource{controller;constructor(e){this.port=e,this.port.onmessage=e=>this.onMessage(e.data)}start(e){this.controller=e}pull(){this.port.postMessage({type:0})}cancel(e){this.port.postMessage({type:1,reason:e.message}),this.port.close()}onMessage(e){0===e.type&&this.controller.enqueue(e.chunk),1===e.type&&(this.controller.error(e.reason),this.port.close()),2===e.type&&(this.controller.close(),this.port.close())}}self.addEventListener("install",(()=>{self.skipWaiting()})),self.addEventListener("activate",(e=>{e.waitUntil(self.clients.claim())}));const map=new Map;globalThis.addEventListener("message",(e=>{const t=e.data;t.url&&t.readablePort&&(t.rs=new ReadableStream(new MessagePortSource(e.data.readablePort),new CountQueuingStrategy({highWaterMark:4})),map.set(t.url,t))})),globalThis.addEventListener("fetch",(e=>{const t=e.request.url,s=map.get(t);if(!s)return null;map.delete(t),e.respondWith(new Response(s.rs,{headers:s.headers}))}));
|
@ -1,5 +1,7 @@
|
||||
@import "colors";
|
||||
|
||||
nav.navbar {
|
||||
background-color: hsl(203, 75%, 40%);
|
||||
background-color: $primary-dark-color;
|
||||
margin: 1em;
|
||||
color: white;
|
||||
border-radius: 0.6em;
|
||||
|
37
core/static/core/pagination.scss
Normal file
37
core/static/core/pagination.scss
Normal file
@ -0,0 +1,37 @@
|
||||
@import "colors";
|
||||
|
||||
.pagination {
|
||||
text-align: center;
|
||||
gap: 10px;
|
||||
|
||||
button {
|
||||
background-color: $secondary-neutral-light-color;
|
||||
min-width: 37px;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($secondary-neutral-light-color, 10%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: #fff;
|
||||
background-color: darken($secondary-neutral-light-color, 5%);
|
||||
}
|
||||
|
||||
&.active,
|
||||
&.active:hover {
|
||||
background-color: $primary-neutral-color;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&:first-of-type,
|
||||
&:last-of-type {
|
||||
// previous and next buttons
|
||||
&:disabled {
|
||||
// Hide the arrows when they are disabled, without
|
||||
// changing the layout of the navigation
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,39 +1,9 @@
|
||||
$first-color: hsl(220, 100%, 50%);
|
||||
$second-color: hsl(48, 100%, 67%);
|
||||
$primary-color: hsl(219.9, 53.7%, 50%);
|
||||
$secondary-color: hsl(204, 64%, 44%);
|
||||
$primary-color-text: hsl(0, 0%, 100%);
|
||||
$secondary-color-text: hsla(0, 0%, 0%, 0.87);
|
||||
|
||||
$primary-light-color: hsl(219.8, 46.4%, 64.9%);
|
||||
$primary-dark-color: hsl(203, 75%, 40%);
|
||||
|
||||
$secondary-light-color: hsl(40, 68%, 65%);
|
||||
$secondary-dark-color: hsl(40, 68%, 35%);
|
||||
|
||||
$primary-neutral-color: hsl(219.6, 20.8%, 50%);
|
||||
$primary-neutral-light-color: hsl(0, 0%, 94%);
|
||||
$primary-neutral-dark-color: hsl(210, 29%, 29%);
|
||||
|
||||
$secondary-neutral-color: hsl(204, 64%, 44%);
|
||||
$secondary-neutral-light-color: hsl(0, 0%, 91%);
|
||||
$secondary-neutral-dark-color: hsl(40, 57.6%, 17%);
|
||||
|
||||
$white-color: hsl(219.6, 20.8%, 98%);
|
||||
$black-color: hsl(0, 0%, 17%);
|
||||
|
||||
$faceblue: hsl(221, 44%, 41%);
|
||||
$twitblue: hsl(206, 82%, 63%);
|
||||
|
||||
$shadow-color: rgb(223, 223, 223);
|
||||
|
||||
$background-button-color: hsl(0, 0%, 95%);
|
||||
@import "colors";
|
||||
|
||||
/*--------------------------MEDIA QUERY HELPERS------------------------*/
|
||||
$small-devices: 576px;
|
||||
$medium-devices: 768px;
|
||||
$large-devices: 992px;
|
||||
$extra-large-devices: 1200px;
|
||||
|
||||
/*--------------------------------GENERAL------------------------------*/
|
||||
|
||||
@ -43,17 +13,6 @@ body {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
button:disabled:hover {
|
||||
color: #fff;
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
button.active,
|
||||
button.active:hover {
|
||||
color: #fff;
|
||||
background-color: $secondary-color;
|
||||
}
|
||||
|
||||
a.button,
|
||||
button,
|
||||
@ -385,11 +344,11 @@ a:not(.button) {
|
||||
background: $second-color;
|
||||
box-shadow: $shadow-color 1px 1px 1px;
|
||||
padding: 0.4em;
|
||||
margin: 0em 0em 0.5em 0em;
|
||||
margin: 0 0 0.5em 0;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.1em;
|
||||
&:not(:first-of-type) {
|
||||
margin: 2em 0em 1em 0em;
|
||||
margin: 2em 0 1em 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -410,7 +369,7 @@ a:not(.button) {
|
||||
margin-bottom: 1em;
|
||||
#agenda_title,
|
||||
#birthdays_title {
|
||||
margin: 0em;
|
||||
margin: 0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
box-shadow: $shadow-color 1px 1px 1px;
|
||||
padding: 0.5em;
|
||||
@ -444,7 +403,7 @@ a:not(.button) {
|
||||
}
|
||||
}
|
||||
ul.birthdays_year {
|
||||
margin: 0em;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
font-weight: bold;
|
||||
> li {
|
||||
@ -454,7 +413,7 @@ a:not(.button) {
|
||||
}
|
||||
}
|
||||
ul {
|
||||
margin: 0em;
|
||||
margin: 0;
|
||||
margin-left: 1em;
|
||||
list-style-type: square;
|
||||
list-style-position: inside;
|
||||
@ -516,14 +475,14 @@ a:not(.button) {
|
||||
float: left;
|
||||
min-width: 7em;
|
||||
max-width: 9em;
|
||||
margin: 0em;
|
||||
margin: 0;
|
||||
margin-right: 1em;
|
||||
margin-top: 0.8em;
|
||||
img {
|
||||
max-height: 6em;
|
||||
max-width: 8em;
|
||||
display: block;
|
||||
margin: 0em auto;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
.news_date {
|
||||
@ -552,7 +511,7 @@ a:not(.button) {
|
||||
list-style-type: square;
|
||||
list-style-position: inside;
|
||||
margin-left: 1em;
|
||||
padding-left: 0em;
|
||||
padding-left: 0;
|
||||
a {
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
@ -565,14 +524,14 @@ a:not(.button) {
|
||||
|
||||
/* NOTICES */
|
||||
.news_notice {
|
||||
margin: 0em 0em 1em 1em;
|
||||
margin: 0 0 1em 1em;
|
||||
padding: 0.4em;
|
||||
padding-left: 1em;
|
||||
background: $secondary-neutral-light-color;
|
||||
box-shadow: $shadow-color 0 0 2px;
|
||||
border-radius: 18px 5px 18px 5px;
|
||||
h4 {
|
||||
margin: 0em;
|
||||
margin: 0;
|
||||
}
|
||||
.news_content {
|
||||
margin-left: 1em;
|
||||
@ -582,14 +541,14 @@ a:not(.button) {
|
||||
|
||||
/* CALLS */
|
||||
.news_call {
|
||||
margin: 0em 0em 1em 1em;
|
||||
margin: 0 0 1em 1em;
|
||||
padding: 0.4em;
|
||||
padding-left: 1em;
|
||||
background: $secondary-neutral-light-color;
|
||||
border: 1px solid grey;
|
||||
box-shadow: $shadow-color 1px 1px 1px;
|
||||
h4 {
|
||||
margin: 0em;
|
||||
margin: 0;
|
||||
}
|
||||
.news_date {
|
||||
font-size: 0.9em;
|
||||
@ -631,12 +590,12 @@ a:not(.button) {
|
||||
width: 19%;
|
||||
float: left;
|
||||
min-width: 15em;
|
||||
margin: 0em;
|
||||
margin: 0;
|
||||
img {
|
||||
max-height: 15em;
|
||||
max-width: 12em;
|
||||
display: block;
|
||||
margin: 0em auto;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
@ -646,7 +605,6 @@ a:not(.button) {
|
||||
padding: 0.5em 1em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 1.2em;
|
||||
border-radius: 2px;
|
||||
float: right;
|
||||
@ -1198,8 +1156,8 @@ u,
|
||||
color: $primary-neutral-dark-color;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0em;
|
||||
padding: 0em;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@ -1393,7 +1351,7 @@ footer {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
div {
|
||||
margin: 0.6em 0em;
|
||||
margin: 0.6em 0;
|
||||
color: $white-color;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
@ -1507,514 +1465,3 @@ a.ui-button:active,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* --------------------------------------pedagogy-----------------------------------*/
|
||||
|
||||
$pedagogy-blue: #1bb9ea;
|
||||
$pedagogy-orange: #ea7900;
|
||||
$pedagogy-hover-blue: #0e97ce;
|
||||
$pedagogy-light-blue: #caf0ff;
|
||||
$pedagogy-white-text: #f0f0f0;
|
||||
|
||||
.pedagogy {
|
||||
#pagination {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.star-not-checked {
|
||||
color: #f7f7f7;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
&.star-checked {
|
||||
color: $pedagogy-orange;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&.grade-without-star {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
&.star-not-checked {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
&.star-checked {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-devices) {
|
||||
&.grade-without-star {
|
||||
display: block;
|
||||
}
|
||||
&.grade-with-star {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#dynamic_view {
|
||||
font-size: 1.1em;
|
||||
|
||||
overflow-wrap: break-word;
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
#search_form {
|
||||
.search-form-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-template-areas:
|
||||
"action-bar action-bar"
|
||||
"search-bar search-bar"
|
||||
"radio-department radio-department"
|
||||
"radio-credit-type radio-semester";
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
grid-area: action-bar;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
grid-area: search-bar;
|
||||
display: grid;
|
||||
grid-template-columns: auto 200px;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas: "search-bar-input search-bar-button";
|
||||
|
||||
@media screen and (max-width: $medium-devices) {
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas: "search-bar-input search-bar-button";
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-devices) {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas: "search-bar-input";
|
||||
|
||||
.search-bar-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar-input {
|
||||
grid-area: search-bar-input;
|
||||
background: $pedagogy-light-blue;
|
||||
}
|
||||
|
||||
.search-bar-button {
|
||||
grid-area: search-bar-button;
|
||||
background: $pedagogy-orange;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-department {
|
||||
grid-area: radio-department;
|
||||
}
|
||||
|
||||
.radio-credit-type {
|
||||
grid-area: radio-credit-type;
|
||||
}
|
||||
|
||||
.radio-semester {
|
||||
grid-area: radio-semester;
|
||||
}
|
||||
|
||||
.radio-guide input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
.radio-guide {
|
||||
margin-top: 10px;
|
||||
color: white;
|
||||
}
|
||||
.radio-guide label {
|
||||
display: inline-block;
|
||||
background-color: $pedagogy-blue;
|
||||
padding: 10px 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.radio-guide input[type="radio"]:checked + label {
|
||||
background-color: $pedagogy-orange;
|
||||
}
|
||||
.radio-guide input[type="checkbox"]:checked + label {
|
||||
background-color: $pedagogy-orange;
|
||||
}
|
||||
.radio-guide label:hover {
|
||||
background-color: $pedagogy-hover-blue;
|
||||
}
|
||||
}
|
||||
|
||||
#uv_detail {
|
||||
color: #062f38;
|
||||
|
||||
.uv-quick-info-container {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 20% 20% 20% auto;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"hours-cm hours-td hours-tp hours-te hours-the"
|
||||
"department credit-type semester . .";
|
||||
}
|
||||
|
||||
.department {
|
||||
grid-area: department;
|
||||
}
|
||||
|
||||
.credit-type {
|
||||
grid-area: credit-type;
|
||||
}
|
||||
|
||||
.semester {
|
||||
grid-area: semester;
|
||||
}
|
||||
|
||||
.hours-cm {
|
||||
grid-area: hours-cm;
|
||||
}
|
||||
|
||||
.hours-td {
|
||||
grid-area: hours-td;
|
||||
}
|
||||
|
||||
.hours-tp {
|
||||
grid-area: hours-tp;
|
||||
}
|
||||
|
||||
.hours-te {
|
||||
grid-area: hours-te;
|
||||
}
|
||||
|
||||
.hours-the {
|
||||
grid-area: hours-the;
|
||||
}
|
||||
|
||||
#leave_comment_not_allowed {
|
||||
p {
|
||||
text-align: center;
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
#leave_comment {
|
||||
.leave-comment-grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: 270px auto;
|
||||
grid-template-rows: 100%;
|
||||
grid-template-areas: "stars comment";
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: 100%;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"stars"
|
||||
"comment";
|
||||
}
|
||||
}
|
||||
|
||||
.ui-accordion-content {
|
||||
background-color: $white-color;
|
||||
border-color: $pedagogy-orange;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.form-stars {
|
||||
grid-area: stars;
|
||||
}
|
||||
|
||||
.form-comment {
|
||||
grid-area: comment;
|
||||
}
|
||||
|
||||
.ui-accordion-header {
|
||||
background-color: $pedagogy-orange;
|
||||
color: $pedagogy-white-text;
|
||||
clip-path: polygon(0 0%, 0 100%, 30% 100%, 33% 0);
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
clip-path: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-accordion-header-icon {
|
||||
color: $pedagogy-white-text;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.input-stars {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.uv-details-container {
|
||||
display: grid;
|
||||
grid-template-columns: 150px 100px auto;
|
||||
grid-template-rows: 156px 1fr;
|
||||
grid-template-areas:
|
||||
"grade grade-stars uv-infos"
|
||||
". . uv-infos";
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: 50% 50%;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"grade grade-stars"
|
||||
"uv-infos uv-infos";
|
||||
}
|
||||
}
|
||||
|
||||
.grade {
|
||||
grid-area: grade;
|
||||
color: $pedagogy-white-text;
|
||||
background-color: $pedagogy-blue;
|
||||
padding-right: 10px;
|
||||
|
||||
> p {
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.grade-stars {
|
||||
grid-area: grade-stars;
|
||||
color: $pedagogy-white-text;
|
||||
background-color: $pedagogy-blue;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.uv-infos {
|
||||
grid-area: uv-infos;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.comment-container {
|
||||
display: grid;
|
||||
grid-template-columns: 300px auto;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-template-areas:
|
||||
"grade-block comment"
|
||||
"grade-block info"
|
||||
"comment-end-bar comment-end-bar";
|
||||
margin-bottom: 30px;
|
||||
margin-top: 10px;
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto auto auto auto;
|
||||
grid-template-areas:
|
||||
"grade-block"
|
||||
"comment"
|
||||
"info"
|
||||
"comment-end-bar";
|
||||
}
|
||||
|
||||
.grade-block {
|
||||
grid-area: grade-block;
|
||||
width: 300px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 150px 150px;
|
||||
grid-template-rows: 156px auto;
|
||||
grid-template-areas:
|
||||
"grade-type grade-stars"
|
||||
"grade-extension grade-extension";
|
||||
grid-gap: 15px;
|
||||
|
||||
clip-path: polygon(0 0, 0 100%, 100% 100%, 100% 30px, 270px 0);
|
||||
align-items: start;
|
||||
|
||||
background-color: $pedagogy-blue;
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: 50% auto;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas: "grade-type grade-stars";
|
||||
width: auto;
|
||||
clip-path: none;
|
||||
align-content: space-evenly;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.grade-extension {
|
||||
grid-area: grade-extension;
|
||||
background-color: $pedagogy-blue;
|
||||
}
|
||||
|
||||
.grade-type {
|
||||
grid-area: grade-type;
|
||||
|
||||
> p {
|
||||
color: $pedagogy-white-text;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.grade-stars {
|
||||
grid-area: grade-stars;
|
||||
}
|
||||
}
|
||||
|
||||
.comment {
|
||||
grid-area: comment;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"anchor"
|
||||
"markdown";
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
border-left: solid;
|
||||
border-right: solid;
|
||||
border-color: $pedagogy-blue;
|
||||
}
|
||||
|
||||
.anchor {
|
||||
grid-area: anchor;
|
||||
text-align: right;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
grid-area: markdown;
|
||||
|
||||
min-height: 139px;
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
padding: 10px;
|
||||
text-align: justify;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
grid-area: info;
|
||||
padding-bottom: 10px;
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
border-left: solid;
|
||||
border-right: solid;
|
||||
border-color: $pedagogy-blue;
|
||||
}
|
||||
|
||||
.status-reported {
|
||||
color: red;
|
||||
float: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-end-bar {
|
||||
grid-area: comment-end-bar;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 33% auto auto;
|
||||
grid-template-rows: 2.5em;
|
||||
grid-template-areas: "author date report";
|
||||
|
||||
background-color: $pedagogy-blue;
|
||||
margin-top: -1px;
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-template-areas:
|
||||
"report"
|
||||
"date"
|
||||
"author";
|
||||
margin-top: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.author {
|
||||
grid-area: author;
|
||||
|
||||
padding-top: 6px;
|
||||
padding-left: 20px;
|
||||
|
||||
background-color: $pedagogy-orange;
|
||||
clip-path: polygon(0 10px, 0 100%, 350px 200%, 300px 10px);
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
clip-path: none;
|
||||
padding: 0;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $pedagogy-white-text;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: $pedagogy-hover-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
grid-area: date;
|
||||
color: $pedagogy-white-text;
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.report {
|
||||
grid-area: report;
|
||||
justify-self: right;
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
|
||||
a {
|
||||
color: $pedagogy-white-text;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: $pedagogy-hover-blue;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
text-align: center;
|
||||
justify-self: inherit;
|
||||
padding-bottom: 7px;
|
||||
background-color: $white-color;
|
||||
|
||||
border-left: solid;
|
||||
border-right: solid;
|
||||
border-color: $pedagogy-blue;
|
||||
|
||||
a {
|
||||
color: $black-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -199,12 +199,6 @@
|
||||
> form {
|
||||
> p {
|
||||
box-sizing: border-box;
|
||||
|
||||
> input {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
> .results_on_deck > div {
|
||||
@ -219,12 +213,15 @@
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
> input {
|
||||
width: 100%;
|
||||
input {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Django moment
|
||||
> div.mini_profile_link {
|
||||
position: relative;
|
||||
|
||||
@ -106,7 +105,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Django moment
|
||||
> a.mini_profile_link {
|
||||
display: none;
|
||||
}
|
||||
|
@ -6,7 +6,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="{{ static('core/img/favicon.ico') }}">
|
||||
<link rel="stylesheet" href="{{ static('core/base.css') }}">
|
||||
<link rel="stylesheet" href="{{ static('core/jquery.datetimepicker.min.css') }}">
|
||||
<link rel="stylesheet" href="{{ static('ajax_select/css/ajax_select.css') }}">
|
||||
<link rel="stylesheet" href="{{ scss('core/style.scss') }}">
|
||||
<link rel="stylesheet" href="{{ scss('core/markdown.scss') }}">
|
||||
@ -295,47 +294,27 @@
|
||||
</code>
|
||||
</footer>
|
||||
{% endif %}
|
||||
<!--
|
||||
{% block tests %}
|
||||
{{ tests }}
|
||||
{% endblock %}
|
||||
-->
|
||||
|
||||
{% block script %}
|
||||
<script src="{{ static('core/js/ui/jquery-ui.min.js') }}"></script>
|
||||
<script src="{{ static('core/js/ui/i18n/datepicker-fr.js') }}"></script>
|
||||
<script src="{{ static('core/js/jquery.datetimepicker.full.min.js') }}"></script>
|
||||
<script src="{{ static('ajax_select/js/ajax_select.js') }}"></script>
|
||||
<script src="{{ url('javascript-catalog') }}"></script>
|
||||
<script>
|
||||
function showMenu() {
|
||||
let navbar = document.getElementById("navbar-content");
|
||||
const current = navbar.style.getPropertyValue("display");
|
||||
navbar.style.setProperty("display", current == "none" ? "block" : "none");
|
||||
navbar.style.setProperty("display", current === "none" ? "block" : "none");
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
$('.select_date').datepicker({
|
||||
changeMonth: true,
|
||||
changeYear: true,
|
||||
dayNamesShort: $.datepicker.regional[ "{{ request.LANGUAGE_CODE }}" ].dayNamesShort,
|
||||
dayNames: $.datepicker.regional[ "{{ request.LANGUAGE_CODE }}" ].dayNames,
|
||||
monthNamesShort: $.datepicker.regional[ "{{ request.LANGUAGE_CODE }}" ].monthNamesShort,
|
||||
monthNames: $.datepicker.regional[ "{{ request.LANGUAGE_CODE }}" ].monthNames,
|
||||
}).datepicker( $.datepicker.regional[ "{{ request.LANGUAGE_CODE }}"] );
|
||||
|
||||
$(document).keydown(function (e) {
|
||||
if ($(e.target).is('input')) { return }
|
||||
if ($(e.target).is('textarea')) { return }
|
||||
if ($(e.target).is('select')) { return }
|
||||
if (e.keyCode == 83) {
|
||||
if (e.keyCode === 83) {
|
||||
$("#search").focus();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
jQuery.datetimepicker.setLocale('{{ request.LANGUAGE_CODE|lower }}');
|
||||
$('.select_datetime').datetimepicker({
|
||||
format: 'Y-m-d H:i:s',
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
@ -1,198 +0,0 @@
|
||||
<div>
|
||||
{# Depends on this package https://github.com/lonaru/easy-markdown-editor #}
|
||||
<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% if widget.value %}{{ widget.value }}{% endif %}</textarea>
|
||||
|
||||
{# The easymde script can be included twice, it's safe in the code #}
|
||||
<script src="{{ statics.js }}"> </script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
const css = "{{ statics.css }}";
|
||||
let lastAPICall;
|
||||
|
||||
// Only import the css once
|
||||
if (!document.head.innerHTML.includes(css)) {
|
||||
document.head.innerHTML += '<link rel="stylesheet" href="' + css + '">';
|
||||
}
|
||||
|
||||
// Pretty markdown input
|
||||
const easymde = new EasyMDE({
|
||||
element: document.getElementById("{{ widget.attrs.id }}"),
|
||||
spellChecker: false,
|
||||
autoDownloadFontAwesome: false,
|
||||
previewRender: function (plainText, preview) {
|
||||
clearTimeout(lastAPICall);
|
||||
lastAPICall = setTimeout(async () => {
|
||||
const res = await fetch("{{ markdown_api_url }}", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ text: plainText }),
|
||||
});
|
||||
preview.innerHTML = await res.text();
|
||||
}, 300);
|
||||
return null;
|
||||
},
|
||||
forceSync: true, // Avoid validation error on generic create view
|
||||
toolbar: [
|
||||
{
|
||||
name: "heading-smaller",
|
||||
action: EasyMDE.toggleHeadingSmaller,
|
||||
className: "fa fa-header",
|
||||
title: "{{ translations.heading_smaller }}"
|
||||
},
|
||||
{
|
||||
name: "italic",
|
||||
action: EasyMDE.toggleItalic,
|
||||
className: "fa fa-italic",
|
||||
title: "{{ translations.italic }}"
|
||||
},
|
||||
{
|
||||
name: "bold",
|
||||
action: EasyMDE.toggleBold,
|
||||
className: "fa fa-bold",
|
||||
title: "{{ translations.bold }}"
|
||||
},
|
||||
{
|
||||
name: "strikethrough",
|
||||
action: EasyMDE.toggleStrikethrough,
|
||||
className: "fa fa-strikethrough",
|
||||
title: "{{ translations.strikethrough }}"
|
||||
},
|
||||
{
|
||||
name: "underline",
|
||||
action: function customFunction(editor){
|
||||
let cm = editor.codemirror;
|
||||
cm.replaceSelection('__' + cm.getSelection() + '__');
|
||||
},
|
||||
className: "fa fa-underline",
|
||||
title: "{{ translations.underline }}"
|
||||
},
|
||||
{
|
||||
name: "superscript",
|
||||
action: function customFunction(editor){
|
||||
let cm = editor.codemirror;
|
||||
cm.replaceSelection('^' + cm.getSelection() + '^');
|
||||
},
|
||||
className: "fa fa-superscript",
|
||||
title: "{{ translations.superscript }}"
|
||||
},
|
||||
{
|
||||
name: "subscript",
|
||||
action: function customFunction(editor){
|
||||
let cm = editor.codemirror;
|
||||
cm.replaceSelection('~' + cm.getSelection() + '~');
|
||||
},
|
||||
className: "fa fa-subscript",
|
||||
title: "{{ translations.subscript }}"
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
action: EasyMDE.toggleCodeBlock,
|
||||
className: "fa fa-code",
|
||||
title: "{{ translations.code }}"
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "quote",
|
||||
action: EasyMDE.toggleBlockquote,
|
||||
className: "fa fa-quote-left",
|
||||
title: "{{ translations.quote }}"
|
||||
},
|
||||
{
|
||||
name: "unordered-list",
|
||||
action: EasyMDE.toggleUnorderedList,
|
||||
className: "fa fa-list-ul",
|
||||
title: "{{ translations.unordered_list }}"
|
||||
},
|
||||
{
|
||||
name: "ordered-list",
|
||||
action: EasyMDE.toggleOrderedList,
|
||||
className: "fa fa-list-ol",
|
||||
title: "{{ translations.ordered_list }}"
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "link",
|
||||
action: EasyMDE.drawLink,
|
||||
className: "fa fa-link",
|
||||
title: "{{ translations.link }}"
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
action: EasyMDE.drawImage,
|
||||
className: "fa fa-picture-o",
|
||||
title: "{{ translations.image }}"
|
||||
},
|
||||
{
|
||||
name: "table",
|
||||
action: EasyMDE.drawTable,
|
||||
className: "fa fa-table",
|
||||
title: "{{ translations.table }}"
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "clean-block",
|
||||
action: EasyMDE.cleanBlock,
|
||||
className: "fa fa-eraser fa-clean-block",
|
||||
title: "{{ translations.clean_block }}"
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "preview",
|
||||
action: EasyMDE.togglePreview,
|
||||
className: "fa fa-eye no-disable",
|
||||
title: "{{ translations.preview }}"
|
||||
},
|
||||
{
|
||||
name: "side-by-side",
|
||||
action: EasyMDE.toggleSideBySide,
|
||||
className: "fa fa-columns no-disable no-mobile",
|
||||
title: "{{ translations.side_by_side }}"
|
||||
},
|
||||
{
|
||||
name: "fullscreen",
|
||||
action: EasyMDE.toggleFullScreen,
|
||||
className: "fa fa-arrows-alt no-disable no-mobile",
|
||||
title: "{{ translations.fullscreen }}"
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "guide",
|
||||
action: "/page/Aide_sur_la_syntaxe",
|
||||
className: "fa fa-question-circle",
|
||||
title: "{{ translations.guide }}"
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
const textarea = document.getElementById('{{ widget.attrs.id }}');
|
||||
const submits = textarea
|
||||
.closest('form')
|
||||
.querySelectorAll('input[type="submit"]');
|
||||
const parentDiv = textarea.parentElement;
|
||||
let submitPressed = false;
|
||||
|
||||
function checkMarkdownInput(e) {
|
||||
// an attribute is null if it does not exist, else a string
|
||||
const required = textarea.getAttribute('required') != null;
|
||||
const length = textarea.value.trim().length;
|
||||
|
||||
if (required && length == 0) {
|
||||
parentDiv.style.boxShadow = 'red 0px 0px 1.5px 1px';
|
||||
} else {
|
||||
parentDiv.style.boxShadow = '';
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmitClick(e) {
|
||||
if (!submitPressed) {
|
||||
easymde.codemirror.on('change', checkMarkdownInput);
|
||||
}
|
||||
submitPressed = true;
|
||||
checkMarkdownInput(e);
|
||||
}
|
||||
|
||||
submits.forEach((submit) => {
|
||||
submit.addEventListener('click', onSubmitClick);
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</div>
|
@ -22,4 +22,3 @@
|
||||
<input type="submit" value="{% trans %}Register{% endtrans %}" />
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
@ -4,6 +4,15 @@
|
||||
<link rel="stylesheet" href="{{ scss('sas/album.scss') }}">
|
||||
{%- endblock -%}
|
||||
|
||||
{% block additional_js %}
|
||||
<script defer src="{{ static('core/js/jszip/jszip.min.js') }}"></script>
|
||||
<script defer src="{{ static('core/js/jszip/jszip-utils.min.js') }}"></script>
|
||||
<script defer type="module">
|
||||
import { showSaveFilePicker } from "{{ static('core/js/native-file-system-adapter/mod.js') }}";
|
||||
window.showSaveFilePicker = showSaveFilePicker; /* Export function to normal javascript */
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% trans user_name=profile.get_display_name() %}{{ user_name }}'s pictures{% endtrans %}
|
||||
{% endblock %}
|
||||
@ -11,19 +20,19 @@
|
||||
{% block content %}
|
||||
<main>
|
||||
{% if can_edit(profile, user) %}
|
||||
<button id="download_all_pictures", onclick=download_pictures()>{% trans %}Download all my pictures{% endtrans %}</button>
|
||||
<button disabled id="download" onclick="download('{{ url('api:pictures') }}?users_identified={{ object.id }}')">{% trans %}Download all my pictures{% endtrans %}</button>
|
||||
{% endif %}
|
||||
{% for a in albums %}
|
||||
<h4>{{ a.name }}</h4>
|
||||
{% for album, pictures in albums|items %}
|
||||
<h4>{{ album }}</h4>
|
||||
<div class="photos">
|
||||
{% for p in pictures[a.id] %}
|
||||
{% if p.can_be_viewed_by(user) %}
|
||||
<a href="{{ url("sas:picture", picture_id=p.id) }}#pict">
|
||||
{% for picture in pictures %}
|
||||
{% if picture.can_be_viewed_by(user) %}
|
||||
<a href="{{ url("sas:picture", picture_id=picture.id) }}#pict">
|
||||
<div
|
||||
class="photo{% if not p.is_moderated %} not_moderated{% endif %}"
|
||||
style="background-image: url('{% if p.file %}{{ p.get_download_thumb_url() }}{% else %}{{ static('core/img/sas.jpg') }}{% endif %}');"
|
||||
class="photo{% if not picture.is_moderated %} not_moderated{% endif %}"
|
||||
style="background-image: url('{% if picture.file %}{{ picture.get_download_thumb_url() }}{% else %}{{ static('core/img/sas.jpg') }}{% endif %}');"
|
||||
>
|
||||
{% if not p.is_moderated %}
|
||||
{% if not picture.is_moderated %}
|
||||
<div class="overlay"> </div>
|
||||
<div class="text">{% trans %}To be moderated{% endtrans %}</div>
|
||||
{% else %}
|
||||
@ -42,70 +51,52 @@
|
||||
</div>
|
||||
<br>
|
||||
{% endfor %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
/* Enable button once everything is loaded and if JSZip is supported */
|
||||
document.getElementById("download").disabled = !JSZip.support.blob;
|
||||
});
|
||||
async function download(url) {
|
||||
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
/**
|
||||
* Download a list of files.
|
||||
* @author speedplane
|
||||
*/
|
||||
function download_files(files) {
|
||||
function download_next(i) {
|
||||
if (i >= files.length) {
|
||||
let zip = new JSZip();
|
||||
let size = 0;
|
||||
let pictures = await (await fetch(url)).json();
|
||||
pictures.forEach(async (picture) => {
|
||||
size += picture.size;
|
||||
zip.file(
|
||||
"IMG_" + picture.date + picture.name.slice(picture.name.lastIndexOf(".")),
|
||||
new Promise(function (resolve, reject) {
|
||||
JSZipUtils.getBinaryContent(picture.full_size_url, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
var a = document.createElement('a');
|
||||
a.href = files[i].download;
|
||||
a.target = '_parent';
|
||||
// Use a.download if available, it prevents plugins from opening.
|
||||
if ('download' in a) {
|
||||
a.download = files[i].filename;
|
||||
}
|
||||
// Add a to the doc for click to work.
|
||||
(document.body || document.documentElement).appendChild(a);
|
||||
if (a.click) {
|
||||
a.click(); // The click method is supported by most browsers.
|
||||
} else {
|
||||
$(a).click(); // Backup using jquery
|
||||
}
|
||||
// Delete the temporary link.
|
||||
a.parentNode.removeChild(a);
|
||||
// Download the next file with a small timeout. The timeout is necessary
|
||||
// for IE, which will otherwise only download the first file.
|
||||
setTimeout(function() {
|
||||
download_next(i + 1);
|
||||
}, 500);
|
||||
}
|
||||
// Initiate the first download.
|
||||
download_next(0);
|
||||
}
|
||||
function download_pictures() {
|
||||
$("#download_all_pictures").prop("disabled", true);
|
||||
const xhr = new XMLHttpRequest();
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "{{ url('api:pictures') }}?users_identified={{ object.id }}",
|
||||
tryCount: 0,
|
||||
xhr: function(){
|
||||
return xhr;
|
||||
},
|
||||
success: function(data){
|
||||
$("#download_all_pictures").prop("disabled", false);
|
||||
to_download = [];
|
||||
data.forEach(picture =>
|
||||
to_download.push({ download: picture["full_size_url"], filename: picture["name"] })
|
||||
resolve(data);
|
||||
})
|
||||
}),
|
||||
{ binary: true }
|
||||
);
|
||||
download_files(to_download);
|
||||
},
|
||||
error: function(data){
|
||||
console.log("Error retrieving data from url: " + data);
|
||||
$("#download_all_pictures").text("{% trans %}Error downloading your pictures{% endtrans %}");
|
||||
}
|
||||
});
|
||||
|
||||
let fileHandle = await window.showSaveFilePicker({
|
||||
_preferPolyfill: false,
|
||||
suggestedName: "{%- trans -%} pictures {%- endtrans -%}.zip",
|
||||
types: {},
|
||||
excludeAcceptAllOption: false,
|
||||
})
|
||||
let writeStream = await fileHandle.createWritable();
|
||||
|
||||
await zip.generateInternalStream({
|
||||
type: "uint8array",
|
||||
streamFiles: true,
|
||||
compression: "DEFLATE",
|
||||
compressionOptions: { level: 9 }
|
||||
})
|
||||
.on("data", (data) => writeStream.write(data))
|
||||
.on("error", (err) => console.error(err))
|
||||
.on("end", () => writeStream.close())
|
||||
.resume();
|
||||
}
|
||||
</script>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
|
198
core/templates/core/widgets/markdown_textarea.jinja
Normal file
198
core/templates/core/widgets/markdown_textarea.jinja
Normal file
@ -0,0 +1,198 @@
|
||||
<div>
|
||||
{# Depends on this package https://github.com/lonaru/easy-markdown-editor #}
|
||||
<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>{% if widget.value %}{{ widget.value }}{% endif %}</textarea>
|
||||
|
||||
{# The easymde script can be included twice, it's safe in the code #}
|
||||
<script src="{{ statics.js }}"> </script>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
const css = "{{ statics.css }}";
|
||||
let lastAPICall;
|
||||
|
||||
// Only import the css once
|
||||
if (!document.head.innerHTML.includes(css)) {
|
||||
document.head.innerHTML += '<link rel="stylesheet" href="' + css + '">';
|
||||
}
|
||||
|
||||
// Pretty markdown input
|
||||
const easymde = new EasyMDE({
|
||||
element: document.getElementById("{{ widget.attrs.id }}"),
|
||||
spellChecker: false,
|
||||
autoDownloadFontAwesome: false,
|
||||
previewRender: function (plainText, preview) {
|
||||
clearTimeout(lastAPICall);
|
||||
lastAPICall = setTimeout(async () => {
|
||||
const res = await fetch("{{ markdown_api_url }}", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ text: plainText }),
|
||||
});
|
||||
preview.innerHTML = await res.text();
|
||||
}, 300);
|
||||
return null;
|
||||
},
|
||||
forceSync: true, // Avoid validation error on generic create view
|
||||
toolbar: [
|
||||
{
|
||||
name: "heading-smaller",
|
||||
action: EasyMDE.toggleHeadingSmaller,
|
||||
className: "fa fa-header",
|
||||
title: "{{ translations.heading_smaller }}"
|
||||
},
|
||||
{
|
||||
name: "italic",
|
||||
action: EasyMDE.toggleItalic,
|
||||
className: "fa fa-italic",
|
||||
title: "{{ translations.italic }}"
|
||||
},
|
||||
{
|
||||
name: "bold",
|
||||
action: EasyMDE.toggleBold,
|
||||
className: "fa fa-bold",
|
||||
title: "{{ translations.bold }}"
|
||||
},
|
||||
{
|
||||
name: "strikethrough",
|
||||
action: EasyMDE.toggleStrikethrough,
|
||||
className: "fa fa-strikethrough",
|
||||
title: "{{ translations.strikethrough }}"
|
||||
},
|
||||
{
|
||||
name: "underline",
|
||||
action: function customFunction(editor){
|
||||
let cm = editor.codemirror;
|
||||
cm.replaceSelection('__' + cm.getSelection() + '__');
|
||||
},
|
||||
className: "fa fa-underline",
|
||||
title: "{{ translations.underline }}"
|
||||
},
|
||||
{
|
||||
name: "superscript",
|
||||
action: function customFunction(editor){
|
||||
let cm = editor.codemirror;
|
||||
cm.replaceSelection('^' + cm.getSelection() + '^');
|
||||
},
|
||||
className: "fa fa-superscript",
|
||||
title: "{{ translations.superscript }}"
|
||||
},
|
||||
{
|
||||
name: "subscript",
|
||||
action: function customFunction(editor){
|
||||
let cm = editor.codemirror;
|
||||
cm.replaceSelection('~' + cm.getSelection() + '~');
|
||||
},
|
||||
className: "fa fa-subscript",
|
||||
title: "{{ translations.subscript }}"
|
||||
},
|
||||
{
|
||||
name: "code",
|
||||
action: EasyMDE.toggleCodeBlock,
|
||||
className: "fa fa-code",
|
||||
title: "{{ translations.code }}"
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "quote",
|
||||
action: EasyMDE.toggleBlockquote,
|
||||
className: "fa fa-quote-left",
|
||||
title: "{{ translations.quote }}"
|
||||
},
|
||||
{
|
||||
name: "unordered-list",
|
||||
action: EasyMDE.toggleUnorderedList,
|
||||
className: "fa fa-list-ul",
|
||||
title: "{{ translations.unordered_list }}"
|
||||
},
|
||||
{
|
||||
name: "ordered-list",
|
||||
action: EasyMDE.toggleOrderedList,
|
||||
className: "fa fa-list-ol",
|
||||
title: "{{ translations.ordered_list }}"
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "link",
|
||||
action: EasyMDE.drawLink,
|
||||
className: "fa fa-link",
|
||||
title: "{{ translations.link }}"
|
||||
},
|
||||
{
|
||||
name: "image",
|
||||
action: EasyMDE.drawImage,
|
||||
className: "fa fa-picture-o",
|
||||
title: "{{ translations.image }}"
|
||||
},
|
||||
{
|
||||
name: "table",
|
||||
action: EasyMDE.drawTable,
|
||||
className: "fa fa-table",
|
||||
title: "{{ translations.table }}"
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "clean-block",
|
||||
action: EasyMDE.cleanBlock,
|
||||
className: "fa fa-eraser fa-clean-block",
|
||||
title: "{{ translations.clean_block }}"
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "preview",
|
||||
action: EasyMDE.togglePreview,
|
||||
className: "fa fa-eye no-disable",
|
||||
title: "{{ translations.preview }}"
|
||||
},
|
||||
{
|
||||
name: "side-by-side",
|
||||
action: EasyMDE.toggleSideBySide,
|
||||
className: "fa fa-columns no-disable no-mobile",
|
||||
title: "{{ translations.side_by_side }}"
|
||||
},
|
||||
{
|
||||
name: "fullscreen",
|
||||
action: EasyMDE.toggleFullScreen,
|
||||
className: "fa fa-arrows-alt no-disable no-mobile",
|
||||
title: "{{ translations.fullscreen }}"
|
||||
},
|
||||
"|",
|
||||
{
|
||||
name: "guide",
|
||||
action: "/page/Aide_sur_la_syntaxe",
|
||||
className: "fa fa-question-circle",
|
||||
title: "{{ translations.guide }}"
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
const textarea = document.getElementById('{{ widget.attrs.id }}');
|
||||
const submits = textarea
|
||||
.closest('form')
|
||||
.querySelectorAll('input[type="submit"]');
|
||||
const parentDiv = textarea.parentElement;
|
||||
let submitPressed = false;
|
||||
|
||||
function checkMarkdownInput(e) {
|
||||
// an attribute is null if it does not exist, else a string
|
||||
const required = textarea.getAttribute('required') != null;
|
||||
const length = textarea.value.trim().length;
|
||||
|
||||
if (required && length == 0) {
|
||||
parentDiv.style.boxShadow = 'red 0px 0px 1.5px 1px';
|
||||
} else {
|
||||
parentDiv.style.boxShadow = '';
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmitClick(e) {
|
||||
if (!submitPressed) {
|
||||
easymde.codemirror.on('change', checkMarkdownInput);
|
||||
}
|
||||
submitPressed = true;
|
||||
checkMarkdownInput(e);
|
||||
}
|
||||
|
||||
submits.forEach((submit) => {
|
||||
submit.addEventListener('click', onSubmitClick);
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</div>
|
33
core/templates/core/widgets/nfc.jinja
Normal file
33
core/templates/core/widgets/nfc.jinja
Normal file
@ -0,0 +1,33 @@
|
||||
<span>
|
||||
<input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}>
|
||||
<!-- NFC icon not available in fontawesome 4.7 -->
|
||||
<button type="button" id="{{ widget.attrs.id }}_button"><i class="fa fa-tag"></i></button>
|
||||
</span>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
let button = document.getElementById("{{ widget.attrs.id }}_button");
|
||||
button.addEventListener("click", async () => {
|
||||
let input = document.getElementById("{{ widget.attrs.id }}");
|
||||
const ndef = new NDEFReader();
|
||||
await ndef.scan();
|
||||
|
||||
ndef.addEventListener("readingerror", () => {
|
||||
alert("{{ translations.unsupported }}")
|
||||
});
|
||||
|
||||
ndef.addEventListener("reading", ({ message, serialNumber }) => {
|
||||
input.value = serialNumber.replaceAll(":", "").toUpperCase();
|
||||
/* Auto submit form */
|
||||
b = document.createElement("button");
|
||||
input.appendChild(b)
|
||||
b.click()
|
||||
b.remove()
|
||||
});
|
||||
|
||||
});
|
||||
/* Disable feature if browser is not supported or if not HTTPS */
|
||||
if (typeof NDEFReader === "undefined") {
|
||||
button.remove();
|
||||
}
|
||||
});
|
||||
</script>
|
@ -23,15 +23,17 @@
|
||||
#
|
||||
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import phonenumbers
|
||||
from django import template
|
||||
from django.template import TemplateSyntaxError
|
||||
from django.template.defaultfilters import stringfilter
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from core.markdown import markdown as md
|
||||
from core.scss.processor import ScssProcessor
|
||||
from core.scss.processor import process_scss_path
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@ -86,5 +88,7 @@ def format_timedelta(value: datetime.timedelta) -> str:
|
||||
@register.simple_tag()
|
||||
def scss(path):
|
||||
"""Return path of the corresponding css file after compilation."""
|
||||
processor = ScssProcessor(path)
|
||||
return processor.get_converted_scss()
|
||||
path = Path(path)
|
||||
if path.suffix != ".scss":
|
||||
raise TemplateSyntaxError("`scss` tag has been called with a non-scss file")
|
||||
return process_scss_path(path)
|
||||
|
@ -14,7 +14,6 @@
|
||||
#
|
||||
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
from smtplib import SMTPException
|
||||
|
||||
import freezegun
|
||||
@ -210,7 +209,7 @@ def test_custom_markdown_syntax(md, html):
|
||||
|
||||
|
||||
def test_full_markdown_syntax():
|
||||
syntax_path = Path(settings.BASE_DIR) / "core" / "fixtures"
|
||||
syntax_path = settings.BASE_DIR / "core" / "fixtures"
|
||||
md = (syntax_path / "SYNTAX.md").read_text()
|
||||
html = (syntax_path / "SYNTAX.html").read_text()
|
||||
result = markdown(md)
|
||||
|
@ -13,7 +13,6 @@
|
||||
#
|
||||
#
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from datetime import date
|
||||
@ -96,10 +95,6 @@ def get_semester_code(d: Optional[date] = None) -> str:
|
||||
return "P" + str(start.year)[-2:]
|
||||
|
||||
|
||||
def file_exist(path):
|
||||
return os.path.exists(path)
|
||||
|
||||
|
||||
def scale_dimension(width, height, long_edge):
|
||||
if width > height:
|
||||
ratio = long_edge * 1.0 / width
|
||||
|
@ -14,7 +14,6 @@
|
||||
#
|
||||
|
||||
# This file contains all the views that concern the page model
|
||||
import os
|
||||
from wsgiref.util import FileWrapper
|
||||
|
||||
from ajax_select import make_ajax_field
|
||||
@ -59,17 +58,17 @@ def send_file(request, file_id, file_class=SithFile, file_attr="file"):
|
||||
):
|
||||
raise PermissionDenied
|
||||
name = f.__getattribute__(file_attr).name
|
||||
filepath = os.path.join(settings.MEDIA_ROOT, name)
|
||||
filepath = settings.MEDIA_ROOT / name
|
||||
|
||||
# check if file exists on disk
|
||||
if not os.path.exists(filepath.encode("utf-8")):
|
||||
raise Http404()
|
||||
if not filepath.exists():
|
||||
raise Http404
|
||||
|
||||
with open(filepath.encode("utf-8"), "rb") as filename:
|
||||
with open(filepath, "rb") as filename:
|
||||
wrapper = FileWrapper(filename)
|
||||
response = HttpResponse(wrapper, content_type=f.mime_type)
|
||||
response["Last-Modified"] = http_date(f.date.timestamp())
|
||||
response["Content-Length"] = os.path.getsize(filepath.encode("utf-8"))
|
||||
response["Content-Length"] = filepath.stat().st_size
|
||||
response["Content-Disposition"] = ('inline; filename="%s"' % f.name).encode(
|
||||
"utf-8"
|
||||
)
|
||||
|
@ -20,7 +20,6 @@
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
import datetime
|
||||
import re
|
||||
from io import BytesIO
|
||||
|
||||
@ -39,14 +38,11 @@ from django.forms import (
|
||||
Textarea,
|
||||
TextInput,
|
||||
)
|
||||
from django.forms.utils import to_current_timezone
|
||||
from django.templatetags.static import static
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from django.utils.translation import gettext
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
from PIL import Image
|
||||
|
||||
from core.models import Gift, Page, SithFile, User
|
||||
@ -56,25 +52,21 @@ from core.utils import resize_image
|
||||
|
||||
|
||||
class SelectDateTime(DateTimeInput):
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
if attrs:
|
||||
attrs["class"] = "select_datetime"
|
||||
else:
|
||||
attrs = {"class": "select_datetime"}
|
||||
return super().render(name, value, attrs, renderer)
|
||||
def __init__(self, attrs=None, format=None): # noqa A002
|
||||
default = {"type": "datetime-local"}
|
||||
attrs = default if attrs is None else default | attrs
|
||||
super().__init__(attrs=attrs, format=format or "%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
class SelectDate(DateInput):
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
if attrs:
|
||||
attrs["class"] = "select_date"
|
||||
else:
|
||||
attrs = {"class": "select_date"}
|
||||
return super().render(name, value, attrs, renderer)
|
||||
def __init__(self, attrs=None, format=None): # noqa A002
|
||||
default = {"type": "date"}
|
||||
attrs = default if attrs is None else default | attrs
|
||||
super().__init__(attrs=attrs, format=format or "%Y-%m-%d")
|
||||
|
||||
|
||||
class MarkdownInput(Textarea):
|
||||
template_name = "core/markdown_textarea.jinja"
|
||||
template_name = "core/widgets/markdown_textarea.jinja"
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
@ -108,6 +100,15 @@ class MarkdownInput(Textarea):
|
||||
return context
|
||||
|
||||
|
||||
class NFCTextInput(TextInput):
|
||||
template_name = "core/widgets/nfc.jinja"
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
context = super().get_context(name, value, attrs)
|
||||
context["translations"] = {"unsupported": _("Unsupported NFC card")}
|
||||
return context
|
||||
|
||||
|
||||
class SelectFile(TextInput):
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
if attrs:
|
||||
@ -235,8 +236,8 @@ class UserProfileForm(forms.ModelForm):
|
||||
"profile_pict": forms.ClearableFileInput,
|
||||
"avatar_pict": forms.ClearableFileInput,
|
||||
"scrub_pict": forms.ClearableFileInput,
|
||||
"phone": PhoneNumberInternationalFallbackWidget,
|
||||
"parent_phone": PhoneNumberInternationalFallbackWidget,
|
||||
"phone": RegionalPhoneNumberWidget,
|
||||
"parent_phone": RegionalPhoneNumberWidget,
|
||||
"quote": forms.Textarea,
|
||||
}
|
||||
labels = {
|
||||
@ -247,12 +248,6 @@ class UserProfileForm(forms.ModelForm):
|
||||
"scrub_pict": _("Scrub: let other know how your scrub looks like!"),
|
||||
}
|
||||
|
||||
def __init__(self, *arg, **kwargs):
|
||||
super().__init__(*arg, **kwargs)
|
||||
|
||||
def full_clean(self):
|
||||
super().full_clean()
|
||||
|
||||
def generate_name(self, field_name, f):
|
||||
field_name = field_name[:-4]
|
||||
return field_name + str(self.instance.id) + "." + f.content_type.split("/")[-1]
|
||||
@ -394,27 +389,3 @@ class GiftForm(forms.ModelForm):
|
||||
id=user_id
|
||||
)
|
||||
self.fields["user"].widget = forms.HiddenInput()
|
||||
|
||||
|
||||
class TzAwareDateTimeField(forms.DateTimeField):
|
||||
def __init__(self, input_formats=None, widget=SelectDateTime, **kwargs):
|
||||
if input_formats is None:
|
||||
input_formats = ["%Y-%m-%d %H:%M:%S"]
|
||||
super().__init__(input_formats=input_formats, widget=widget, **kwargs)
|
||||
|
||||
def prepare_value(self, value):
|
||||
# the db value is a datetime as a string in UTC
|
||||
if isinstance(value, str):
|
||||
# convert it into a naive datetime (no timezone attached)
|
||||
value = parse_datetime(value)
|
||||
# attach it to the UTC timezone (so that to_current_timezone()) if not None
|
||||
# converts it to the local timezone)
|
||||
if value is not None:
|
||||
value = timezone.make_aware(value, datetime.timezone.utc)
|
||||
|
||||
if isinstance(value, datetime.datetime):
|
||||
value = to_current_timezone(value)
|
||||
# otherwise it is formatted according to locale (in french)
|
||||
value = str(value)
|
||||
|
||||
return value
|
||||
|
@ -21,6 +21,7 @@
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
import itertools
|
||||
|
||||
# This file contains all the views that concern the user model
|
||||
from datetime import date, timedelta
|
||||
@ -31,6 +32,7 @@ from django.contrib.auth import login, views
|
||||
from django.contrib.auth.forms import PasswordChangeForm
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import PermissionDenied, ValidationError
|
||||
from django.db.models import F
|
||||
from django.forms import CheckboxSelectMultiple
|
||||
from django.forms.models import modelform_factory
|
||||
from django.http import Http404, HttpResponse
|
||||
@ -311,21 +313,15 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["albums"] = []
|
||||
kwargs["pictures"] = {}
|
||||
picture_qs = (
|
||||
pictures = list(
|
||||
Picture.objects.filter(people__user_id=self.object.id)
|
||||
.order_by("parent__date", "id")
|
||||
.all()
|
||||
.order_by("-parent__date", "-date")
|
||||
.annotate(album=F("parent__name"))
|
||||
)
|
||||
last_album = None
|
||||
for picture in picture_qs:
|
||||
album = picture.parent
|
||||
if album.id != last_album and album not in kwargs["albums"]:
|
||||
kwargs["albums"].append(album)
|
||||
kwargs["pictures"][album.id] = []
|
||||
last_album = album.id
|
||||
kwargs["pictures"][album.id].append(picture)
|
||||
kwargs["albums"] = {
|
||||
album: list(picts)
|
||||
for album, picts in itertools.groupby(pictures, lambda i: i.album)
|
||||
}
|
||||
return kwargs
|
||||
|
||||
|
||||
|
@ -3,7 +3,7 @@ from ajax_select.fields import AutoCompleteSelectField, AutoCompleteSelectMultip
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.views.forms import SelectDate, TzAwareDateTimeField
|
||||
from core.views.forms import NFCTextInput, SelectDate, SelectDateTime
|
||||
from counter.models import (
|
||||
BillingInfo,
|
||||
Counter,
|
||||
@ -37,6 +37,9 @@ class StudentCardForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = StudentCard
|
||||
fields = ["uid"]
|
||||
widgets = {
|
||||
"uid": NFCTextInput,
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
@ -55,7 +58,10 @@ class GetUserForm(forms.Form):
|
||||
"""
|
||||
|
||||
code = forms.CharField(
|
||||
label="Code", max_length=StudentCard.UID_SIZE, required=False
|
||||
label="Code",
|
||||
max_length=StudentCard.UID_SIZE,
|
||||
required=False,
|
||||
widget=NFCTextInput,
|
||||
)
|
||||
id = AutoCompleteSelectField(
|
||||
"users", required=False, label=_("Select user"), help_text=None
|
||||
@ -86,6 +92,14 @@ class GetUserForm(forms.Form):
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class NFCCardForm(forms.Form):
|
||||
student_card_uid = forms.CharField(
|
||||
max_length=StudentCard.UID_SIZE,
|
||||
required=False,
|
||||
widget=NFCTextInput,
|
||||
)
|
||||
|
||||
|
||||
class RefillForm(forms.ModelForm):
|
||||
error_css_class = "error"
|
||||
required_css_class = "required"
|
||||
@ -168,8 +182,12 @@ class ProductEditForm(forms.ModelForm):
|
||||
|
||||
|
||||
class CashSummaryFormBase(forms.Form):
|
||||
begin_date = TzAwareDateTimeField(label=_("Begin date"), required=False)
|
||||
end_date = TzAwareDateTimeField(label=_("End date"), required=False)
|
||||
begin_date = forms.DateTimeField(
|
||||
label=_("Begin date"), widget=SelectDateTime, required=False
|
||||
)
|
||||
end_date = forms.DateTimeField(
|
||||
label=_("End date"), widget=SelectDateTime, required=False
|
||||
)
|
||||
|
||||
|
||||
class EticketForm(forms.ModelForm):
|
||||
|
@ -34,7 +34,7 @@
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="action" value="add_student_card">
|
||||
{% trans %}Add a student card{% endtrans %}
|
||||
<input type="text" name="student_card_uid"/>
|
||||
{{ student_card_input.student_card_uid }}
|
||||
{% if request.session['not_valid_student_card_uid'] %}
|
||||
<p><strong>{% trans %}This is not a valid student card UID{% endtrans %}</strong></p>
|
||||
{% endif %}
|
||||
@ -42,7 +42,7 @@
|
||||
</form>
|
||||
<h6>{% trans %}Registered cards{% endtrans %}</h6>
|
||||
{% if student_cards %}
|
||||
<p>{{ student_cards }}</p>
|
||||
|
||||
<ul>
|
||||
{% for card in student_cards %}
|
||||
<li>{{ card.uid }}</li>
|
||||
|
@ -61,6 +61,7 @@ from counter.forms import (
|
||||
CounterEditForm,
|
||||
EticketForm,
|
||||
GetUserForm,
|
||||
NFCCardForm,
|
||||
ProductEditForm,
|
||||
RefillForm,
|
||||
StudentCardForm,
|
||||
@ -679,6 +680,7 @@ class CounterClick(CounterTabsMixin, CanViewMixin, DetailView):
|
||||
)
|
||||
kwargs["customer"] = self.customer
|
||||
kwargs["student_cards"] = self.customer.student_cards.all()
|
||||
kwargs["student_card_input"] = NFCCardForm()
|
||||
kwargs["basket_total"] = self.sum_basket(self.request)
|
||||
kwargs["refill_form"] = self.refill_form or RefillForm()
|
||||
kwargs["student_card_max_uid_size"] = StudentCard.UID_SIZE
|
||||
|
@ -148,12 +148,14 @@ Ces règles sont automatiquement appliquées quand
|
||||
vous faites tourner Ruff, donc vous n'avez pas à trop
|
||||
vous poser de questions de ce côté-là.
|
||||
|
||||
En ce qui concerne les autres langages utilisés
|
||||
(Jinja, SCSS, Javascript), nous n'avons pas fixé
|
||||
de convention à suivre.
|
||||
Pour SCSS et Javascript, vous pouvez utiliser
|
||||
En ce qui concerne les templates Jinja
|
||||
et les fichiers SCSS, la norme de formatage
|
||||
est celle par défaut de `djHTML`.
|
||||
|
||||
Pour Javascript, vous pouvez utiliser
|
||||
Prettier, avec sa configuration par défaut,
|
||||
qui est plutôt bonne.
|
||||
qui est plutôt bonne,
|
||||
mais nous n'avons pas de norme établie pour le projet.
|
||||
|
||||
### Qualité du code
|
||||
|
||||
|
@ -353,3 +353,16 @@ sans que l'utilisateur ait à s'en soucier.
|
||||
Bien installé, il peut effectuer ce travail
|
||||
à chaque sauvegarde d'un fichier dans son éditeur,
|
||||
ce qui est très agréable pour travailler.
|
||||
|
||||
### DjHTML
|
||||
|
||||
[Site officiel](https://github.com/rtts/djhtml)
|
||||
|
||||
Ruff permet de formater les fichiers Python,
|
||||
mais il ne formatte pas les templates et les feuilles de style.
|
||||
Pour ça, il faut un autre outil, aisément intégrable
|
||||
dans la CI : `djHTML`.
|
||||
|
||||
En utilisant conjointement Ruff et djHTML,
|
||||
on arrive donc à la fois à formater les fichiers
|
||||
Python et les fichiers relatifs au frontend.
|
||||
|
0
eboutic/tests/__init__.py
Normal file
0
eboutic/tests/__init__.py
Normal file
@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Skia < skia AT libskia DOT so >
|
||||
#
|
||||
# Beerware licensed software - 2017
|
||||
#
|
||||
|
||||
import base64
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
||||
from cryptography.hazmat.primitives.hashes import SHA1
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
load_pem_private_key,
|
||||
load_pem_public_key,
|
||||
)
|
||||
|
||||
with open("./private_key.pem", "br") as f:
|
||||
PRIVKEY = f.read()
|
||||
with open("./public_key.pem", "br") as f:
|
||||
PUBKEY = f.read()
|
||||
|
||||
data = "Amount=400&BasketID=4000&Auto=42&Error=00000\n".encode("utf-8")
|
||||
|
||||
# Sign
|
||||
privkey: RSAPrivateKey = load_pem_private_key(PRIVKEY, None)
|
||||
signature = privkey.sign(data, PKCS1v15(), SHA1())
|
||||
b64sig = base64.b64encode(signature)
|
||||
print(b64sig)
|
||||
|
||||
# Verify
|
||||
pubkey = load_pem_public_key(PUBKEY)
|
||||
signature = base64.b64decode(b64sig)
|
||||
try:
|
||||
pubkey.verify(signature, data, PKCS1v15(), SHA1())
|
||||
print("Verify OK")
|
||||
except InvalidSignature as e:
|
||||
print("Verify failed")
|
41
eboutic/tests/test_crypto.py
Executable file
41
eboutic/tests/test_crypto.py
Executable file
@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Skia < skia AT libskia DOT so >
|
||||
#
|
||||
# Beerware licensed software - 2017
|
||||
#
|
||||
|
||||
import base64
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
|
||||
from cryptography.hazmat.primitives.hashes import SHA1
|
||||
from cryptography.hazmat.primitives.serialization import (
|
||||
load_pem_private_key,
|
||||
load_pem_public_key,
|
||||
)
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def test_signature_valid():
|
||||
"""Test that data sent to the bank is correctly signed."""
|
||||
data = "Amount=400&BasketID=4000&Auto=42&Error=00000\n".encode("utf-8")
|
||||
|
||||
# Sign
|
||||
key_dir = Path(settings.BASE_DIR) / "eboutic" / "tests"
|
||||
privkey: RSAPrivateKey = load_pem_private_key(
|
||||
(key_dir / "private_key.pem").read_bytes(), None
|
||||
)
|
||||
pubkey: RSAPublicKey = load_pem_public_key(
|
||||
(key_dir / "public_key.pem").read_bytes()
|
||||
)
|
||||
signature = privkey.sign(data, PKCS1v15(), SHA1())
|
||||
b64sig = base64.b64encode(signature)
|
||||
signature = base64.b64decode(b64sig)
|
||||
try:
|
||||
pubkey.verify(signature, data, PKCS1v15(), SHA1())
|
||||
except InvalidSignature:
|
||||
pytest.fail("Failed to validate signature")
|
@ -12,7 +12,7 @@ from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateVi
|
||||
|
||||
from core.models import User
|
||||
from core.views import CanCreateMixin, CanEditMixin, CanViewMixin
|
||||
from core.views.forms import MarkdownInput, TzAwareDateTimeField
|
||||
from core.views.forms import MarkdownInput, SelectDateTime
|
||||
from election.models import Candidature, Election, ElectionList, Role, Vote
|
||||
|
||||
# Custom form field
|
||||
@ -160,12 +160,18 @@ class ElectionForm(forms.ModelForm):
|
||||
label=_("candidature groups"),
|
||||
)
|
||||
|
||||
start_date = TzAwareDateTimeField(label=_("Start date"), required=True)
|
||||
end_date = TzAwareDateTimeField(label=_("End date"), required=True)
|
||||
start_candidature = TzAwareDateTimeField(
|
||||
label=_("Start candidature"), required=True
|
||||
start_date = forms.DateTimeField(
|
||||
label=_("Start date"), widget=SelectDateTime, required=True
|
||||
)
|
||||
end_date = forms.DateTimeField(
|
||||
label=_("End date"), widget=SelectDateTime, required=True
|
||||
)
|
||||
start_candidature = forms.DateTimeField(
|
||||
label=_("Start candidature"), widget=SelectDateTime, required=True
|
||||
)
|
||||
end_candidature = forms.DateTimeField(
|
||||
label=_("End candidature"), widget=SelectDateTime, required=True
|
||||
)
|
||||
end_candidature = TzAwareDateTimeField(label=_("End candidature"), required=True)
|
||||
|
||||
|
||||
# Display elections
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -30,7 +30,7 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import ListView, View
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.views.generic.edit import FormView
|
||||
from phonenumber_field.widgets import PhoneNumberInternationalFallbackWidget
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
|
||||
from core.models import User
|
||||
from core.views import FormerSubscriberMixin, search_user
|
||||
@ -64,7 +64,7 @@ class SearchForm(forms.ModelForm):
|
||||
]
|
||||
widgets = {
|
||||
"date_of_birth": SelectDate,
|
||||
"phone": PhoneNumberInternationalFallbackWidget,
|
||||
"phone": RegionalPhoneNumberWidget,
|
||||
}
|
||||
|
||||
quick = forms.CharField(label=_("Last/First name or nickname"), max_length=255)
|
||||
|
511
pedagogy/static/pedagogy/css/pedagogy.scss
Normal file
511
pedagogy/static/pedagogy/css/pedagogy.scss
Normal file
@ -0,0 +1,511 @@
|
||||
@import "core/static/core/colors";
|
||||
|
||||
|
||||
$pedagogy-blue: #1bb9ea;
|
||||
$pedagogy-orange: #ea7900;
|
||||
$pedagogy-hover-blue: #0e97ce;
|
||||
$pedagogy-light-blue: #caf0ff;
|
||||
$pedagogy-white-text: #f0f0f0;
|
||||
|
||||
$small-devices: 576px;
|
||||
$medium-devices: 768px;
|
||||
$large-devices: 992px;
|
||||
|
||||
.pedagogy {
|
||||
&.star-not-checked {
|
||||
color: #f7f7f7;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
&.star-checked {
|
||||
color: $pedagogy-orange;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&.grade-without-star {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
&.star-not-checked {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
&.star-checked {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-devices) {
|
||||
&.grade-without-star {
|
||||
display: block;
|
||||
}
|
||||
&.grade-with-star {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
#dynamic_view {
|
||||
font-size: 1.1em;
|
||||
|
||||
overflow-wrap: break-word;
|
||||
|
||||
td {
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
#search_form {
|
||||
.search-form-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-template-areas:
|
||||
"action-bar action-bar"
|
||||
"search-bar search-bar"
|
||||
"radio-department radio-department"
|
||||
"radio-credit-type radio-semester";
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
grid-area: action-bar;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
grid-area: search-bar;
|
||||
display: grid;
|
||||
grid-template-columns: auto 200px;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas: "search-bar-input search-bar-button";
|
||||
|
||||
@media screen and (max-width: $medium-devices) {
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas: "search-bar-input search-bar-button";
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-devices) {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas: "search-bar-input";
|
||||
|
||||
.search-bar-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar-input {
|
||||
grid-area: search-bar-input;
|
||||
background: $pedagogy-light-blue;
|
||||
}
|
||||
|
||||
.search-bar-button {
|
||||
grid-area: search-bar-button;
|
||||
background: $pedagogy-orange;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.radio-department {
|
||||
grid-area: radio-department;
|
||||
}
|
||||
|
||||
.radio-credit-type {
|
||||
grid-area: radio-credit-type;
|
||||
}
|
||||
|
||||
.radio-semester {
|
||||
grid-area: radio-semester;
|
||||
}
|
||||
|
||||
.radio-guide input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
.radio-guide {
|
||||
margin-top: 10px;
|
||||
color: white;
|
||||
}
|
||||
.radio-guide label {
|
||||
display: inline-block;
|
||||
background-color: $pedagogy-blue;
|
||||
padding: 10px 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.radio-guide input[type="radio"]:checked + label {
|
||||
background-color: $pedagogy-orange;
|
||||
}
|
||||
.radio-guide input[type="checkbox"]:checked + label {
|
||||
background-color: $pedagogy-orange;
|
||||
}
|
||||
.radio-guide label:hover {
|
||||
background-color: $pedagogy-hover-blue;
|
||||
}
|
||||
}
|
||||
|
||||
#uv_detail {
|
||||
color: #062f38;
|
||||
|
||||
.uv-quick-info-container {
|
||||
display: grid;
|
||||
grid-template-columns: 20% 20% 20% 20% auto;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"hours-cm hours-td hours-tp hours-te hours-the"
|
||||
"department credit-type semester . .";
|
||||
}
|
||||
|
||||
.department {
|
||||
grid-area: department;
|
||||
}
|
||||
|
||||
.credit-type {
|
||||
grid-area: credit-type;
|
||||
}
|
||||
|
||||
.semester {
|
||||
grid-area: semester;
|
||||
}
|
||||
|
||||
.hours-cm {
|
||||
grid-area: hours-cm;
|
||||
}
|
||||
|
||||
.hours-td {
|
||||
grid-area: hours-td;
|
||||
}
|
||||
|
||||
.hours-tp {
|
||||
grid-area: hours-tp;
|
||||
}
|
||||
|
||||
.hours-te {
|
||||
grid-area: hours-te;
|
||||
}
|
||||
|
||||
.hours-the {
|
||||
grid-area: hours-the;
|
||||
}
|
||||
|
||||
#leave_comment_not_allowed {
|
||||
p {
|
||||
text-align: center;
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
#leave_comment {
|
||||
.leave-comment-grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: 270px auto;
|
||||
grid-template-rows: 100%;
|
||||
grid-template-areas: "stars comment";
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: 100%;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"stars"
|
||||
"comment";
|
||||
}
|
||||
}
|
||||
|
||||
.ui-accordion-content {
|
||||
background-color: $white-color;
|
||||
border-color: $pedagogy-orange;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.form-stars {
|
||||
grid-area: stars;
|
||||
}
|
||||
|
||||
.form-comment {
|
||||
grid-area: comment;
|
||||
}
|
||||
|
||||
.ui-accordion-header {
|
||||
background-color: $pedagogy-orange;
|
||||
color: $pedagogy-white-text;
|
||||
clip-path: polygon(0 0%, 0 100%, 30% 100%, 33% 0);
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
clip-path: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-accordion-header-icon {
|
||||
color: $pedagogy-white-text;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.input-stars {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.uv-details-container {
|
||||
display: grid;
|
||||
grid-template-columns: 150px 100px auto;
|
||||
grid-template-rows: 156px 1fr;
|
||||
grid-template-areas:
|
||||
"grade grade-stars uv-infos"
|
||||
". . uv-infos";
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: 50% 50%;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"grade grade-stars"
|
||||
"uv-infos uv-infos";
|
||||
}
|
||||
}
|
||||
|
||||
.grade {
|
||||
grid-area: grade;
|
||||
color: $pedagogy-white-text;
|
||||
background-color: $pedagogy-blue;
|
||||
padding-right: 10px;
|
||||
|
||||
> p {
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.grade-stars {
|
||||
grid-area: grade-stars;
|
||||
color: $pedagogy-white-text;
|
||||
background-color: $pedagogy-blue;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.uv-infos {
|
||||
grid-area: uv-infos;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.comment-container {
|
||||
display: grid;
|
||||
grid-template-columns: 300px auto;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-template-areas:
|
||||
"grade-block comment"
|
||||
"grade-block info"
|
||||
"comment-end-bar comment-end-bar";
|
||||
margin-bottom: 30px;
|
||||
margin-top: 10px;
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto auto auto auto;
|
||||
grid-template-areas:
|
||||
"grade-block"
|
||||
"comment"
|
||||
"info"
|
||||
"comment-end-bar";
|
||||
}
|
||||
|
||||
.grade-block {
|
||||
grid-area: grade-block;
|
||||
width: 300px;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 150px 150px;
|
||||
grid-template-rows: 156px auto;
|
||||
grid-template-areas:
|
||||
"grade-type grade-stars"
|
||||
"grade-extension grade-extension";
|
||||
grid-gap: 15px;
|
||||
|
||||
clip-path: polygon(0 0, 0 100%, 100% 100%, 100% 30px, 270px 0);
|
||||
align-items: start;
|
||||
|
||||
background-color: $pedagogy-blue;
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: 50% auto;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas: "grade-type grade-stars";
|
||||
width: auto;
|
||||
clip-path: none;
|
||||
align-content: space-evenly;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.grade-extension {
|
||||
grid-area: grade-extension;
|
||||
background-color: $pedagogy-blue;
|
||||
}
|
||||
|
||||
.grade-type {
|
||||
grid-area: grade-type;
|
||||
|
||||
> p {
|
||||
color: $pedagogy-white-text;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.grade-stars {
|
||||
grid-area: grade-stars;
|
||||
}
|
||||
}
|
||||
|
||||
.comment {
|
||||
grid-area: comment;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"anchor"
|
||||
"markdown";
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
border-left: solid;
|
||||
border-right: solid;
|
||||
border-color: $pedagogy-blue;
|
||||
}
|
||||
|
||||
.anchor {
|
||||
grid-area: anchor;
|
||||
text-align: right;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.markdown {
|
||||
grid-area: markdown;
|
||||
|
||||
min-height: 139px;
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
padding: 10px;
|
||||
text-align: justify;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
grid-area: info;
|
||||
padding-bottom: 10px;
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
border-left: solid;
|
||||
border-right: solid;
|
||||
border-color: $pedagogy-blue;
|
||||
}
|
||||
|
||||
.status-reported {
|
||||
color: red;
|
||||
float: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-end-bar {
|
||||
grid-area: comment-end-bar;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 33% auto auto;
|
||||
grid-template-rows: 2.5em;
|
||||
grid-template-areas: "author date report";
|
||||
|
||||
background-color: $pedagogy-blue;
|
||||
margin-top: -1px;
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-template-areas:
|
||||
"report"
|
||||
"date"
|
||||
"author";
|
||||
margin-top: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.author {
|
||||
grid-area: author;
|
||||
|
||||
padding-top: 6px;
|
||||
padding-left: 20px;
|
||||
|
||||
background-color: $pedagogy-orange;
|
||||
clip-path: polygon(0 10px, 0 100%, 350px 200%, 300px 10px);
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
clip-path: none;
|
||||
padding: 0;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $pedagogy-white-text;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: $pedagogy-hover-blue;
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
grid-area: date;
|
||||
color: $pedagogy-white-text;
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.report {
|
||||
grid-area: report;
|
||||
justify-self: right;
|
||||
padding-right: 30px;
|
||||
padding-left: 30px;
|
||||
|
||||
a {
|
||||
color: $pedagogy-white-text;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: $pedagogy-hover-blue;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $large-devices) {
|
||||
text-align: center;
|
||||
justify-self: inherit;
|
||||
padding-bottom: 7px;
|
||||
background-color: $white-color;
|
||||
|
||||
border-left: solid;
|
||||
border-right: solid;
|
||||
border-color: $pedagogy-blue;
|
||||
|
||||
a {
|
||||
color: $black-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
|
||||
{% extends "core/base.jinja" %}
|
||||
|
||||
{% block title %}
|
||||
@ -9,6 +8,11 @@
|
||||
<script src="{{ static('core/js/alpinejs.min.js') }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_css %}
|
||||
<link rel="stylesheet" href="{{ scss('pedagogy/css/pedagogy.scss') }}">
|
||||
<link rel="stylesheet" href="{{ scss('core/pagination.scss') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.6, maximum-scale=2">
|
||||
@ -40,10 +44,11 @@
|
||||
</div>
|
||||
<div class="radio-department">
|
||||
<div class="radio-guide">
|
||||
{% for (display_name, real_name) in [
|
||||
{% set departments = [
|
||||
("EDIM", "EDIM"), ("ENERGIE", "EE"), ("IMSI", "IMSI"),
|
||||
("INFO", "GI"), ("GMC", "MC"), ("HUMA", "HUMA"), ("TC", "TC")
|
||||
] %}
|
||||
{% for (display_name, real_name) in departments %}
|
||||
<input
|
||||
type="checkbox"
|
||||
name="department"
|
||||
@ -97,7 +102,7 @@
|
||||
</thead>
|
||||
<tbody id="dynamic_view_content">
|
||||
<template x-for="uv in uvs.results" :key="uv.id">
|
||||
<tr @click="window.location.href = `/pedagogy/uv/${uv.id}`">
|
||||
<tr @click="window.location.href = `/pedagogy/uv/${uv.id}`" class="clickable">
|
||||
<td><a :href="`/pedagogy/uv/${uv.id}`" x-text="uv.code"></a></td>
|
||||
<td x-text="uv.title"></td>
|
||||
<td x-text="uv.department"></td>
|
||||
@ -112,12 +117,16 @@
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<nav id="pagination" class="hidden" :style="max_page() == 0 && 'display: none;'">
|
||||
<button @click="page--" :disabled="page == 1">{% trans %}Previous{% endtrans %}</button>
|
||||
<nav class="pagination" x-show="max_page() > 1">
|
||||
<button @click="page--" :disabled="page <= 1">
|
||||
<i class="fa fa-caret-left"></i>
|
||||
</button>
|
||||
<template x-for="i in max_page()">
|
||||
<button x-text="i" @click="page = i" :class="i == page && 'active'"></button>
|
||||
<button x-text="i" @click="page = i" :class="(page === i) && 'active'"></button>
|
||||
</template>
|
||||
<button @click="page++" :disabled="page == max_page()">{% trans %}Next{% endtrans %}</button>
|
||||
<button @click="page++" :disabled="page >= max_page()">
|
||||
<i class="fa fa-caret-right"></i>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<script>
|
||||
@ -126,6 +135,7 @@
|
||||
function update_query_string(key, value) {
|
||||
const url = new URL(window.location.href);
|
||||
if (!value) {
|
||||
{# If the value is null, undefined or empty => delete it #}
|
||||
url.searchParams.delete(key)
|
||||
} else if (Array.isArray(value)) {
|
||||
url.searchParams.delete(key)
|
||||
@ -150,8 +160,8 @@
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("uv_search", () => ({
|
||||
uvs: [],
|
||||
page: initialUrlParams.get("page") || page_default,
|
||||
page_size: initialUrlParams.get("page_size") || page_size_default,
|
||||
page: parseInt(initialUrlParams.get("page")) || page_default,
|
||||
page_size: parseInt(initialUrlParams.get("page_size")) || page_size_default,
|
||||
search: initialUrlParams.get("search") || "",
|
||||
department: initialUrlParams.getAll("department"),
|
||||
credit_type: initialUrlParams.getAll("credit_type"),
|
||||
@ -186,7 +196,7 @@
|
||||
},
|
||||
|
||||
max_page() {
|
||||
return Math.round(this.uvs.count / this.page_size);
|
||||
return Math.ceil(this.uvs.count / this.page_size);
|
||||
}
|
||||
}))
|
||||
})
|
||||
|
412
poetry.lock
generated
412
poetry.lock
generated
@ -394,43 +394,38 @@ toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "42.0.8"
|
||||
version = "43.0.0"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"},
|
||||
{file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"},
|
||||
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"},
|
||||
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"},
|
||||
{file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"},
|
||||
{file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"},
|
||||
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"},
|
||||
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"},
|
||||
{file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"},
|
||||
{file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"},
|
||||
{file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"},
|
||||
{file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"},
|
||||
{file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"},
|
||||
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"},
|
||||
{file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"},
|
||||
{file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"},
|
||||
{file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"},
|
||||
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"},
|
||||
{file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"},
|
||||
{file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"},
|
||||
{file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -443,7 +438,7 @@ nox = ["nox"]
|
||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
||||
sdist = ["build"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||
test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
@ -504,14 +499,18 @@ bcrypt = ["bcrypt"]
|
||||
|
||||
[[package]]
|
||||
name = "django-ajax-selects"
|
||||
version = "2.2.1"
|
||||
version = "3.0.2"
|
||||
description = "Edit ForeignKey, ManyToManyField and CharField in Django Admin using jQuery UI AutoComplete."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
files = [
|
||||
{file = "django-ajax-selects-2.2.1.tar.gz", hash = "sha256:996ffb38dff1a621b358613afdf2681dbf261e5976da3c30a75e9b08fd81a887"},
|
||||
{file = "django_ajax_selects-3.0.2-py3-none-any.whl", hash = "sha256:83da065b3fe6bdee5996662734eb18ee70fee510171c179a02f1ce45dcaa2870"},
|
||||
{file = "django_ajax_selects-3.0.2.tar.gz", hash = "sha256:8554659f5c7da50cfe1f0d0e14c6f360d0f2ab2d94b24e3203cc4fe974bd945a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Django = ">=3.2"
|
||||
|
||||
[[package]]
|
||||
name = "django-countries"
|
||||
version = "7.6.1"
|
||||
@ -597,13 +596,13 @@ jinja2 = ">=3"
|
||||
|
||||
[[package]]
|
||||
name = "django-ninja"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
description = "Django Ninja - Fast Django REST framework"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "django_ninja-1.2.1-py3-none-any.whl", hash = "sha256:acb7a0005e84acdb0ae96066c42c7f304f988a078d370e5952382b928bb28a08"},
|
||||
{file = "django_ninja-1.2.1.tar.gz", hash = "sha256:667ff27304039d4692421709ae532fd62b16a4d34a969ef850d5cd22cb46090a"},
|
||||
{file = "django_ninja-1.2.2-py3-none-any.whl", hash = "sha256:ca38d410359439d927fa0f59b265c2badb86200d3e534f6ed98e8ca1f5866c97"},
|
||||
{file = "django_ninja-1.2.2.tar.gz", hash = "sha256:757197b5e238d4ac2256ecf1a7d8f5c1883e1890ad154f2f38ee7d7ed687b9a2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -617,20 +616,20 @@ test = ["django-stubs", "mypy (==1.7.1)", "psycopg2-binary", "pytest", "pytest-a
|
||||
|
||||
[[package]]
|
||||
name = "django-ninja-extra"
|
||||
version = "0.21.1"
|
||||
version = "0.21.2"
|
||||
description = "Django Ninja Extra - Class Based Utility and more for Django Ninja(Fast Django REST framework)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "django_ninja_extra-0.21.1-py3-none-any.whl", hash = "sha256:331cdf9cbeb8a122a8192c35ac1fba373b0736f4d91d75bc2d39fd0e8d8a66ea"},
|
||||
{file = "django_ninja_extra-0.21.1.tar.gz", hash = "sha256:7e0de377c2afd0d4b6655e01901bb8c370c04ffdf5471a17b14e8db0d1002e8e"},
|
||||
{file = "django_ninja_extra-0.21.2-py3-none-any.whl", hash = "sha256:71febb620590f3b1724fecf710db0408cef778d87c0ae6eeb489b11752b4485d"},
|
||||
{file = "django_ninja_extra-0.21.2.tar.gz", hash = "sha256:2bb4bed7edbc49d5cfd3895d9acce65971e5d5bffc7fce0fb753e6c485fedc55"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
asgiref = "*"
|
||||
contextlib2 = "*"
|
||||
Django = ">=2.2"
|
||||
django-ninja = "1.2.1"
|
||||
django-ninja = "1.2.2"
|
||||
injector = ">=0.19.0"
|
||||
|
||||
[[package]]
|
||||
@ -646,13 +645,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "django-phonenumber-field"
|
||||
version = "6.4.0"
|
||||
version = "8.0.0"
|
||||
description = "An international phone number field for django models."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "django-phonenumber-field-6.4.0.tar.gz", hash = "sha256:72a3e7a3e7493bf2a12c07a3bc77ce89813acc16592bf04d0eee3b5a452097ed"},
|
||||
{file = "django_phonenumber_field-6.4.0-py3-none-any.whl", hash = "sha256:a31b4f05ac0ff898661516c84940f83adb5cdcf0ae4b9b1d31bb8ad3ff345b58"},
|
||||
{file = "django_phonenumber_field-8.0.0-py3-none-any.whl", hash = "sha256:196c917b70c01a98e327f482eb8a4a4a55a29891db551f99078585397370b3ba"},
|
||||
{file = "django_phonenumber_field-8.0.0.tar.gz", hash = "sha256:8a560fe1b01b94c9de8cde22bc373b695f023cc6df4baba00264cb079da9f631"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -677,23 +676,36 @@ django = "*"
|
||||
|
||||
[[package]]
|
||||
name = "django-simple-captcha"
|
||||
version = "0.5.20"
|
||||
version = "0.6.0"
|
||||
description = "A very simple, yet powerful, Django captcha application"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "django-simple-captcha-0.5.20.tar.gz", hash = "sha256:20273009a7beb44297e9f6c7a6bd21ada3d2fa93c314d2f6bf5e394ceeb6a297"},
|
||||
{file = "django_simple_captcha-0.5.20-py2.py3-none-any.whl", hash = "sha256:3359cb033c489eae6544a80ad92517db3d35b3b328b3b427393399c3d7f55275"},
|
||||
{file = "django-simple-captcha-0.6.0.tar.gz", hash = "sha256:d188516d326fadd2d5ad076eb89649d55c02cabafe3fdcc2154ac18e9f6d4b97"},
|
||||
{file = "django_simple_captcha-0.6.0-py2.py3-none-any.whl", hash = "sha256:3ae9a7e650cb0cdbcfd4a75aa91fdf25dcc523ef541a7b1f004bd4357798fc03"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Django = ">=3.2"
|
||||
Django = ">=4.2"
|
||||
django-ranged-response = "0.2.0"
|
||||
Pillow = ">=6.2.0"
|
||||
|
||||
[package.extras]
|
||||
test = ["testfixtures"]
|
||||
|
||||
[[package]]
|
||||
name = "djhtml"
|
||||
version = "3.0.6"
|
||||
description = "Django/Jinja template indenter"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "djhtml-3.0.6.tar.gz", hash = "sha256:abfc4d7b4730432ca6a98322fbdf8ae9d6ba254ea57ba3759a10ecb293bc57de"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["nox", "pre-commit"]
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
version = "0.19"
|
||||
@ -733,6 +745,20 @@ files = [
|
||||
[package.extras]
|
||||
tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"]
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "26.0.0"
|
||||
description = "Faker is a Python package that generates fake data for you."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Faker-26.0.0-py3-none-any.whl", hash = "sha256:886ee28219be96949cd21ecc96c4c742ee1680e77f687b095202c8def1a08f06"},
|
||||
{file = "Faker-26.0.0.tar.gz", hash = "sha256:0f60978314973de02c00474c2ae899785a42b2cf4f41b7987e93c132a2b8a4a9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = ">=2.4"
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.15.4"
|
||||
@ -932,16 +958,17 @@ i18n = ["Babel (>=2.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "libsass"
|
||||
version = "0.22.0"
|
||||
version = "0.23.0"
|
||||
description = "Sass for Python: A straightforward binding of libsass for Python."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "libsass-0.22.0-cp36-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f1efc1b612299c88aec9e39d6ca0c266d360daa5b19d9430bdeaffffa86993f9"},
|
||||
{file = "libsass-0.22.0-cp37-abi3-macosx_10_15_x86_64.whl", hash = "sha256:081e256ab3c5f3f09c7b8dea3bf3bf5e64a97c6995fd9eea880639b3f93a9f9a"},
|
||||
{file = "libsass-0.22.0-cp37-abi3-win32.whl", hash = "sha256:89c5ce497fcf3aba1dd1b19aae93b99f68257e5f2026b731b00a872f13324c7f"},
|
||||
{file = "libsass-0.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:65455a2728b696b62100eb5932604aa13a29f4ac9a305d95773c14aaa7200aaf"},
|
||||
{file = "libsass-0.22.0.tar.gz", hash = "sha256:3ab5ad18e47db560f4f0c09e3d28cf3bb1a44711257488ac2adad69f4f7f8425"},
|
||||
{file = "libsass-0.23.0-cp38-abi3-macosx_11_0_x86_64.whl", hash = "sha256:34cae047cbbfc4ffa832a61cbb110f3c95f5471c6170c842d3fed161e40814dc"},
|
||||
{file = "libsass-0.23.0-cp38-abi3-macosx_14_0_arm64.whl", hash = "sha256:ea97d1b45cdc2fc3590cb9d7b60f1d8915d3ce17a98c1f2d4dd47ee0d9c68ce6"},
|
||||
{file = "libsass-0.23.0-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4a218406d605f325d234e4678bd57126a66a88841cb95bee2caeafdc6f138306"},
|
||||
{file = "libsass-0.23.0-cp38-abi3-win32.whl", hash = "sha256:31e86d92a5c7a551df844b72d83fc2b5e50abc6fbbb31e296f7bebd6489ed1b4"},
|
||||
{file = "libsass-0.23.0-cp38-abi3-win_amd64.whl", hash = "sha256:a2ec85d819f353cbe807432d7275d653710d12b08ec7ef61c124a580a8352f3c"},
|
||||
{file = "libsass-0.23.0.tar.gz", hash = "sha256:6f209955ede26684e76912caf329f4ccb57e4a043fd77fe0e7348dd9574f1880"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1146,13 +1173,13 @@ cache = ["platformdirs"]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.5.29"
|
||||
version = "9.5.30"
|
||||
description = "Documentation that simply works"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mkdocs_material-9.5.29-py3-none-any.whl", hash = "sha256:afc1f508e2662ded95f0a35a329e8a5acd73ee88ca07ba73836eb6fcdae5d8b4"},
|
||||
{file = "mkdocs_material-9.5.29.tar.gz", hash = "sha256:3e977598ec15a4ddad5c4dfc9e08edab6023edb51e88f0729bd27be77e3d322a"},
|
||||
{file = "mkdocs_material-9.5.30-py3-none-any.whl", hash = "sha256:fc070689c5250a180e9b9d79d8491ef9a3a7acb240db0728728d6c31eeb131d4"},
|
||||
{file = "mkdocs_material-9.5.30.tar.gz", hash = "sha256:3fd417dd42d679e3ba08b9e2d72cd8b8af142cc4a3969676ad6b00993dd182ec"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1186,13 +1213,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings"
|
||||
version = "0.25.1"
|
||||
version = "0.25.2"
|
||||
description = "Automatic documentation from sources, for MkDocs."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mkdocstrings-0.25.1-py3-none-any.whl", hash = "sha256:da01fcc2670ad61888e8fe5b60afe9fee5781017d67431996832d63e887c2e51"},
|
||||
{file = "mkdocstrings-0.25.1.tar.gz", hash = "sha256:c3a2515f31577f311a9ee58d089e4c51fc6046dbd9e9b4c3de4c3194667fe9bf"},
|
||||
{file = "mkdocstrings-0.25.2-py3-none-any.whl", hash = "sha256:9e2cda5e2e12db8bb98d21e3410f3f27f8faab685a24b03b06ba7daa5b92abfc"},
|
||||
{file = "mkdocstrings-0.25.2.tar.gz", hash = "sha256:5cf57ad7f61e8be3111a2458b4e49c2029c9cb35525393b179f9c916ca8042dc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1212,17 +1239,17 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings-python"
|
||||
version = "1.10.5"
|
||||
version = "1.10.7"
|
||||
description = "A Python handler for mkdocstrings."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mkdocstrings_python-1.10.5-py3-none-any.whl", hash = "sha256:92e3c588ef1b41151f55281d075de7558dd8092e422cb07a65b18ee2b0863ebb"},
|
||||
{file = "mkdocstrings_python-1.10.5.tar.gz", hash = "sha256:acdc2a98cd9d46c7ece508193a16ca03ccabcb67520352b7449f84b57c162bdf"},
|
||||
{file = "mkdocstrings_python-1.10.7-py3-none-any.whl", hash = "sha256:8999acb8e2cb6ae5edb844ce1ed6a5fcc14285f85cfd9df374d9a0f0be8a40b6"},
|
||||
{file = "mkdocstrings_python-1.10.7.tar.gz", hash = "sha256:bfb5e29acfc69c9177d2b11c18d3127d16e553b8da9bb6d184e428d54795600b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
griffe = ">=0.47"
|
||||
griffe = ">=0.48"
|
||||
mkdocstrings = ">=0.25"
|
||||
|
||||
[[package]]
|
||||
@ -1580,13 +1607,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pure-eval"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
description = "Safely evaluate AST nodes without side effects"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"},
|
||||
{file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"},
|
||||
{file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"},
|
||||
{file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -1770,13 +1797,13 @@ extra = ["pygments (>=2.12)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.2.2"
|
||||
version = "8.3.2"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
|
||||
{file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
|
||||
{file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"},
|
||||
{file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1784,7 +1811,7 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=1.5,<2.0"
|
||||
pluggy = ">=1.5,<2"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
@ -1915,90 +1942,90 @@ pyyaml = "*"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2024.5.15"
|
||||
version = "2024.7.24"
|
||||
description = "Alternative regular expression module, to replace re."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-win32.whl", hash = "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3"},
|
||||
{file = "regex-2024.5.15-cp310-cp310-win_amd64.whl", hash = "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-win32.whl", hash = "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa"},
|
||||
{file = "regex-2024.5.15-cp311-cp311-win_amd64.whl", hash = "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-win32.whl", hash = "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe"},
|
||||
{file = "regex-2024.5.15-cp312-cp312-win_amd64.whl", hash = "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-win32.whl", hash = "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9"},
|
||||
{file = "regex-2024.5.15-cp38-cp38-win_amd64.whl", hash = "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-win32.whl", hash = "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694"},
|
||||
{file = "regex-2024.5.15-cp39-cp39-win_amd64.whl", hash = "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388"},
|
||||
{file = "regex-2024.5.15.tar.gz", hash = "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"},
|
||||
{file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"},
|
||||
{file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"},
|
||||
{file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"},
|
||||
{file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"},
|
||||
{file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"},
|
||||
{file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2044,48 +2071,49 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.5.2"
|
||||
version = "0.5.5"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.5.2-py3-none-linux_armv6l.whl", hash = "sha256:7bab8345df60f9368d5f4594bfb8b71157496b44c30ff035d1d01972e764d3be"},
|
||||
{file = "ruff-0.5.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1aa7acad382ada0189dbe76095cf0a36cd0036779607c397ffdea16517f535b1"},
|
||||
{file = "ruff-0.5.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aec618d5a0cdba5592c60c2dee7d9c865180627f1a4a691257dea14ac1aa264d"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b62adc5ce81780ff04077e88bac0986363e4a3260ad3ef11ae9c14aa0e67ef"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc42ebf56ede83cb080a50eba35a06e636775649a1ffd03dc986533f878702a3"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15c6e9f88c67ffa442681365d11df38afb11059fc44238e71a9d9f1fd51de70"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d3de9a5960f72c335ef00763d861fc5005ef0644cb260ba1b5a115a102157251"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe5a968ae933e8f7627a7b2fc8893336ac2be0eb0aace762d3421f6e8f7b7f83"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04f54a9018f75615ae52f36ea1c5515e356e5d5e214b22609ddb546baef7132"},
|
||||
{file = "ruff-0.5.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed02fb52e3741f0738db5f93e10ae0fb5c71eb33a4f2ba87c9a2fa97462a649"},
|
||||
{file = "ruff-0.5.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3cf8fe659f6362530435d97d738eb413e9f090e7e993f88711b0377fbdc99f60"},
|
||||
{file = "ruff-0.5.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:237a37e673e9f3cbfff0d2243e797c4862a44c93d2f52a52021c1a1b0899f846"},
|
||||
{file = "ruff-0.5.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2a2949ce7c1cbd8317432ada80fe32156df825b2fd611688814c8557824ef060"},
|
||||
{file = "ruff-0.5.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:481af57c8e99da92ad168924fd82220266043c8255942a1cb87958b108ac9335"},
|
||||
{file = "ruff-0.5.2-py3-none-win32.whl", hash = "sha256:f1aea290c56d913e363066d83d3fc26848814a1fed3d72144ff9c930e8c7c718"},
|
||||
{file = "ruff-0.5.2-py3-none-win_amd64.whl", hash = "sha256:8532660b72b5d94d2a0a7a27ae7b9b40053662d00357bb2a6864dd7e38819084"},
|
||||
{file = "ruff-0.5.2-py3-none-win_arm64.whl", hash = "sha256:73439805c5cb68f364d826a5c5c4b6c798ded6b7ebaa4011f01ce6c94e4d5583"},
|
||||
{file = "ruff-0.5.2.tar.gz", hash = "sha256:2c0df2d2de685433794a14d8d2e240df619b748fbe3367346baa519d8e6f1ca2"},
|
||||
{file = "ruff-0.5.5-py3-none-linux_armv6l.whl", hash = "sha256:605d589ec35d1da9213a9d4d7e7a9c761d90bba78fc8790d1c5e65026c1b9eaf"},
|
||||
{file = "ruff-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00817603822a3e42b80f7c3298c8269e09f889ee94640cd1fc7f9329788d7bf8"},
|
||||
{file = "ruff-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:187a60f555e9f865a2ff2c6984b9afeffa7158ba6e1eab56cb830404c942b0f3"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe26fc46fa8c6e0ae3f47ddccfbb136253c831c3289bba044befe68f467bfb16"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad25dd9c5faac95c8e9efb13e15803cd8bbf7f4600645a60ffe17c73f60779b"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f70737c157d7edf749bcb952d13854e8f745cec695a01bdc6e29c29c288fc36e"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:cfd7de17cef6ab559e9f5ab859f0d3296393bc78f69030967ca4d87a541b97a0"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a09b43e02f76ac0145f86a08e045e2ea452066f7ba064fd6b0cdccb486f7c3e7"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0b856cb19c60cd40198be5d8d4b556228e3dcd545b4f423d1ad812bfdca5884"},
|
||||
{file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"},
|
||||
{file = "ruff-0.5.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ac9dc814e510436e30d0ba535f435a7f3dc97f895f844f5b3f347ec8c228a523"},
|
||||
{file = "ruff-0.5.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:af9bdf6c389b5add40d89b201425b531e0a5cceb3cfdcc69f04d3d531c6be74f"},
|
||||
{file = "ruff-0.5.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d40a8533ed545390ef8315b8e25c4bb85739b90bd0f3fe1280a29ae364cc55d8"},
|
||||
{file = "ruff-0.5.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cab904683bf9e2ecbbe9ff235bfe056f0eba754d0168ad5407832928d579e7ab"},
|
||||
{file = "ruff-0.5.5-py3-none-win32.whl", hash = "sha256:696f18463b47a94575db635ebb4c178188645636f05e934fdf361b74edf1bb2d"},
|
||||
{file = "ruff-0.5.5-py3-none-win_amd64.whl", hash = "sha256:50f36d77f52d4c9c2f1361ccbfbd09099a1b2ea5d2b2222c586ab08885cf3445"},
|
||||
{file = "ruff-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3191317d967af701f1b73a31ed5788795936e423b7acce82a2b63e26eb3e89d6"},
|
||||
{file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "1.45.0"
|
||||
version = "2.11.0"
|
||||
description = "Python client for Sentry (https://sentry.io)"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "sentry-sdk-1.45.0.tar.gz", hash = "sha256:509aa9678c0512344ca886281766c2e538682f8acfa50fd8d405f8c417ad0625"},
|
||||
{file = "sentry_sdk-1.45.0-py2.py3-none-any.whl", hash = "sha256:1ce29e30240cc289a027011103a8c83885b15ef2f316a60bcc7c5300afa144f1"},
|
||||
{file = "sentry_sdk-2.11.0-py2.py3-none-any.whl", hash = "sha256:d964710e2dbe015d9dc4ff0ad16225d68c3b36936b742a6fe0504565b760a3b7"},
|
||||
{file = "sentry_sdk-2.11.0.tar.gz", hash = "sha256:4ca16e9f5c7c6bc2fb2d5c956219f4926b148e511fffdbbde711dc94f1e0468f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = "*"
|
||||
urllib3 = {version = ">=1.26.11", markers = "python_version >= \"3.6\""}
|
||||
urllib3 = ">=1.26.11"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp (>=3.5)"]
|
||||
anthropic = ["anthropic (>=0.16)"]
|
||||
arq = ["arq (>=0.23)"]
|
||||
asyncpg = ["asyncpg (>=0.23)"]
|
||||
beam = ["apache-beam (>=2.12)"]
|
||||
@ -2098,13 +2126,15 @@ django = ["django (>=1.8)"]
|
||||
falcon = ["falcon (>=1.4)"]
|
||||
fastapi = ["fastapi (>=0.79.0)"]
|
||||
flask = ["blinker (>=1.1)", "flask (>=0.11)", "markupsafe"]
|
||||
grpcio = ["grpcio (>=1.21.1)"]
|
||||
grpcio = ["grpcio (>=1.21.1)", "protobuf (>=3.8.0)"]
|
||||
httpx = ["httpx (>=0.16.0)"]
|
||||
huey = ["huey (>=2)"]
|
||||
huggingface-hub = ["huggingface-hub (>=0.22)"]
|
||||
langchain = ["langchain (>=0.0.210)"]
|
||||
loguru = ["loguru (>=0.5)"]
|
||||
openai = ["openai (>=1.0.0)", "tiktoken (>=0.3.0)"]
|
||||
opentelemetry = ["opentelemetry-distro (>=0.35b0)"]
|
||||
opentelemetry-experimental = ["opentelemetry-distro (>=0.40b0,<1.0)", "opentelemetry-instrumentation-aiohttp-client (>=0.40b0,<1.0)", "opentelemetry-instrumentation-django (>=0.40b0,<1.0)", "opentelemetry-instrumentation-fastapi (>=0.40b0,<1.0)", "opentelemetry-instrumentation-flask (>=0.40b0,<1.0)", "opentelemetry-instrumentation-requests (>=0.40b0,<1.0)", "opentelemetry-instrumentation-sqlite3 (>=0.40b0,<1.0)", "opentelemetry-instrumentation-urllib (>=0.40b0,<1.0)"]
|
||||
opentelemetry-experimental = ["opentelemetry-instrumentation-aio-pika (==0.46b0)", "opentelemetry-instrumentation-aiohttp-client (==0.46b0)", "opentelemetry-instrumentation-aiopg (==0.46b0)", "opentelemetry-instrumentation-asgi (==0.46b0)", "opentelemetry-instrumentation-asyncio (==0.46b0)", "opentelemetry-instrumentation-asyncpg (==0.46b0)", "opentelemetry-instrumentation-aws-lambda (==0.46b0)", "opentelemetry-instrumentation-boto (==0.46b0)", "opentelemetry-instrumentation-boto3sqs (==0.46b0)", "opentelemetry-instrumentation-botocore (==0.46b0)", "opentelemetry-instrumentation-cassandra (==0.46b0)", "opentelemetry-instrumentation-celery (==0.46b0)", "opentelemetry-instrumentation-confluent-kafka (==0.46b0)", "opentelemetry-instrumentation-dbapi (==0.46b0)", "opentelemetry-instrumentation-django (==0.46b0)", "opentelemetry-instrumentation-elasticsearch (==0.46b0)", "opentelemetry-instrumentation-falcon (==0.46b0)", "opentelemetry-instrumentation-fastapi (==0.46b0)", "opentelemetry-instrumentation-flask (==0.46b0)", "opentelemetry-instrumentation-grpc (==0.46b0)", "opentelemetry-instrumentation-httpx (==0.46b0)", "opentelemetry-instrumentation-jinja2 (==0.46b0)", "opentelemetry-instrumentation-kafka-python (==0.46b0)", "opentelemetry-instrumentation-logging (==0.46b0)", "opentelemetry-instrumentation-mysql (==0.46b0)", "opentelemetry-instrumentation-mysqlclient (==0.46b0)", "opentelemetry-instrumentation-pika (==0.46b0)", "opentelemetry-instrumentation-psycopg (==0.46b0)", "opentelemetry-instrumentation-psycopg2 (==0.46b0)", "opentelemetry-instrumentation-pymemcache (==0.46b0)", "opentelemetry-instrumentation-pymongo (==0.46b0)", "opentelemetry-instrumentation-pymysql (==0.46b0)", "opentelemetry-instrumentation-pyramid (==0.46b0)", "opentelemetry-instrumentation-redis (==0.46b0)", "opentelemetry-instrumentation-remoulade (==0.46b0)", "opentelemetry-instrumentation-requests (==0.46b0)", "opentelemetry-instrumentation-sklearn (==0.46b0)", "opentelemetry-instrumentation-sqlalchemy (==0.46b0)", "opentelemetry-instrumentation-sqlite3 (==0.46b0)", "opentelemetry-instrumentation-starlette (==0.46b0)", "opentelemetry-instrumentation-system-metrics (==0.46b0)", "opentelemetry-instrumentation-threading (==0.46b0)", "opentelemetry-instrumentation-tornado (==0.46b0)", "opentelemetry-instrumentation-tortoiseorm (==0.46b0)", "opentelemetry-instrumentation-urllib (==0.46b0)", "opentelemetry-instrumentation-urllib3 (==0.46b0)", "opentelemetry-instrumentation-wsgi (==0.46b0)"]
|
||||
pure-eval = ["asttokens", "executing", "pure-eval"]
|
||||
pymongo = ["pymongo (>=3.1)"]
|
||||
pyspark = ["pyspark (>=2.4.4)"]
|
||||
@ -2114,7 +2144,7 @@ sanic = ["sanic (>=0.8)"]
|
||||
sqlalchemy = ["sqlalchemy (>=1.2)"]
|
||||
starlette = ["starlette (>=0.19.1)"]
|
||||
starlite = ["starlite (>=1.48)"]
|
||||
tornado = ["tornado (>=5)"]
|
||||
tornado = ["tornado (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
@ -2206,13 +2236,13 @@ test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-htmlhelp"
|
||||
version = "2.0.5"
|
||||
version = "2.0.6"
|
||||
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "sphinxcontrib_htmlhelp-2.0.5-py3-none-any.whl", hash = "sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"},
|
||||
{file = "sphinxcontrib_htmlhelp-2.0.5.tar.gz", hash = "sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015"},
|
||||
{file = "sphinxcontrib_htmlhelp-2.0.6-py3-none-any.whl", hash = "sha256:1b9af5a2671a61410a868fce050cab7ca393c218e6205cbc7f590136f207395c"},
|
||||
{file = "sphinxcontrib_htmlhelp-2.0.6.tar.gz", hash = "sha256:c6597da06185f0e3b4dc952777a04200611ef563882e0c244d27a15ee22afa73"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -2236,19 +2266,19 @@ test = ["flake8", "mypy", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-qthelp"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "sphinxcontrib_qthelp-1.0.7-py3-none-any.whl", hash = "sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"},
|
||||
{file = "sphinxcontrib_qthelp-1.0.7.tar.gz", hash = "sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6"},
|
||||
{file = "sphinxcontrib_qthelp-1.0.8-py3-none-any.whl", hash = "sha256:323d6acc4189af76dfe94edd2a27d458902319b60fcca2aeef3b2180c106a75f"},
|
||||
{file = "sphinxcontrib_qthelp-1.0.8.tar.gz", hash = "sha256:db3f8fa10789c7a8e76d173c23364bdf0ebcd9449969a9e6a3dd31b8b7469f03"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
lint = ["docutils-stubs", "flake8", "mypy"]
|
||||
standalone = ["Sphinx (>=5)"]
|
||||
test = ["pytest"]
|
||||
test = ["defusedxml (>=0.7.1)", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "sphinxcontrib-serializinghtml"
|
||||
@ -2472,4 +2502,4 @@ filelock = ">=3.4"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "f8e48947d004d61d63a345d36d7b42777030e1ac3687bb27f97b2c51318fcc8d"
|
||||
content-hash = "5e90eff0d3e11e48c1467165d121f1cff094cf83834b5df517708e72bc82425f"
|
||||
|
@ -22,23 +22,23 @@ license = "GPL-3.0-only"
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
Django = "^4.2.14"
|
||||
django-ninja = "^1.2.0"
|
||||
django-ninja-extra = "^0.21.0"
|
||||
django-ninja = "^1.2.2"
|
||||
django-ninja-extra = "^0.21.2"
|
||||
Pillow = "^10.4.0"
|
||||
mistune = "^3.0.2"
|
||||
django-jinja = "^2.11"
|
||||
cryptography = "^42.0.8"
|
||||
django-phonenumber-field = "^6.3"
|
||||
cryptography = "^43.0.0"
|
||||
django-phonenumber-field = "^8.0.0"
|
||||
phonenumbers = "^8.12"
|
||||
django-ajax-selects = "^2.1.0"
|
||||
django-ajax-selects = "^3.0.2"
|
||||
reportlab = "^4.2"
|
||||
django-haystack = "^3.2.1"
|
||||
xapian-haystack = "^3.0.1"
|
||||
libsass = "^0.22"
|
||||
libsass = "^0.23"
|
||||
django-ordered-model = "^3.7"
|
||||
django-simple-captcha = "^0.5.17"
|
||||
django-simple-captcha = "^0.6.0"
|
||||
python-dateutil = "^2.8.2"
|
||||
sentry-sdk = "^1.21.0"
|
||||
sentry-sdk = "^2.11.0"
|
||||
pygraphviz = "^1.1"
|
||||
Jinja2 = "^3.1"
|
||||
django-countries = "^7.5.1"
|
||||
@ -47,30 +47,32 @@ Sphinx = "^5" # Needed for building xapian
|
||||
tomli = "^2.0.1"
|
||||
django-honeypot = "^1.2.0"
|
||||
|
||||
# deps used in prod, but unnecessary for development
|
||||
[tool.poetry.group.prod.dependencies]
|
||||
# deps used in prod, but unnecessary for development
|
||||
psycopg2-binary = "^2.9"
|
||||
|
||||
[tool.poetry.group.prod]
|
||||
optional = true
|
||||
|
||||
# deps used for development purposes, but unneeded in prod
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
django-debug-toolbar = "^4.0.0"
|
||||
# deps used for development purposes, but unneeded in prod
|
||||
django-debug-toolbar = "^4.4.6"
|
||||
ipython = "^8.26.0"
|
||||
pre-commit = "^3.7.1"
|
||||
ruff = "^0.5.1" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml
|
||||
ruff = "^0.5.5" # Version used in pipeline is controlled by pre-commit hooks in .pre-commit.config.yaml
|
||||
djhtml = "^3.0.6"
|
||||
faker = "^26.0.0"
|
||||
|
||||
# deps used for testing purposes
|
||||
[tool.poetry.group.tests.dependencies]
|
||||
freezegun = "^1.2.2" # used to test time-dependent code
|
||||
pytest = "^8.2.2"
|
||||
# deps used for testing purposes
|
||||
freezegun = "^1.5.1" # used to test time-dependent code
|
||||
pytest = "^8.3.2"
|
||||
pytest-cov = "^5.0.0"
|
||||
pytest-django = "^4.8.0"
|
||||
model-bakery = "^1.18.2"
|
||||
|
||||
# deps used to work on the documentation
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
# deps used to work on the documentation
|
||||
mkdocs = "^1.6.0"
|
||||
mkdocs-material = "^9.5.28"
|
||||
mkdocstrings = "^0.25.1"
|
||||
|
38
sas/api.py
38
sas/api.py
@ -1,17 +1,19 @@
|
||||
from django.conf import settings
|
||||
from ninja import Query
|
||||
from ninja_extra import ControllerBase, api_controller, route
|
||||
from ninja_extra.exceptions import PermissionDenied
|
||||
from ninja_extra.permissions import IsAuthenticated
|
||||
from pydantic import NonNegativeInt
|
||||
|
||||
from core.models import User
|
||||
from sas.models import Picture
|
||||
from sas.models import PeoplePictureRelation, Picture
|
||||
from sas.schemas import PictureFilterSchema, PictureSchema
|
||||
|
||||
|
||||
@api_controller("/sas")
|
||||
class SasController(ControllerBase):
|
||||
@api_controller("/sas/picture")
|
||||
class PicturesController(ControllerBase):
|
||||
@route.get(
|
||||
"/picture",
|
||||
"",
|
||||
response=list[PictureSchema],
|
||||
permissions=[IsAuthenticated],
|
||||
url_name="pictures",
|
||||
@ -22,11 +24,17 @@ class SasController(ControllerBase):
|
||||
A user with an active subscription can see any picture, as long
|
||||
as it has been moderated and not asked for removal.
|
||||
An unsubscribed user can see the pictures he has been identified on
|
||||
(only the moderated ones, too)
|
||||
(only the moderated ones, too).
|
||||
|
||||
Notes:
|
||||
Trying to fetch the pictures of another user with this route
|
||||
while being unsubscribed will just result in an empty response.
|
||||
|
||||
Notes:
|
||||
Unsubscribed users who are identified is not a rare case.
|
||||
They can be UTT students, faluchards from other schools,
|
||||
or even Richard Stallman (that ain't no joke,
|
||||
cf. https://ae.utbm.fr/user/32663/pictures/)
|
||||
"""
|
||||
user: User = self.context.request.user
|
||||
if not user.is_subscribed and filters.users_identified != {user.id}:
|
||||
@ -45,3 +53,23 @@ class SasController(ControllerBase):
|
||||
picture.compressed_url = picture.get_download_compressed_url()
|
||||
picture.thumb_url = picture.get_download_thumb_url()
|
||||
return pictures
|
||||
|
||||
|
||||
@api_controller("/sas/relation", tags="User identification on SAS pictures")
|
||||
class UsersIdentifiedController(ControllerBase):
|
||||
@route.delete("/{relation_id}", permissions=[IsAuthenticated])
|
||||
def delete_relation(self, relation_id: NonNegativeInt):
|
||||
"""Untag a user from a SAS picture.
|
||||
|
||||
Root and SAS admins can delete any picture identification.
|
||||
All other users can delete their own identification.
|
||||
"""
|
||||
relation = self.get_object_or_exception(PeoplePictureRelation, pk=relation_id)
|
||||
user: User = self.context.request.user
|
||||
if (
|
||||
relation.user_id != user.id
|
||||
and not user.is_root
|
||||
and not user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
|
||||
):
|
||||
raise PermissionDenied
|
||||
relation.delete()
|
||||
|
@ -13,7 +13,6 @@
|
||||
#
|
||||
#
|
||||
|
||||
import os
|
||||
from io import BytesIO
|
||||
|
||||
from django.conf import settings
|
||||
@ -46,9 +45,7 @@ class Picture(SithFile):
|
||||
|
||||
@property
|
||||
def is_vertical(self):
|
||||
with open(
|
||||
os.path.join(settings.MEDIA_ROOT, self.file.name).encode("utf-8"), "rb"
|
||||
) as f:
|
||||
with open(settings.MEDIA_ROOT / self.file.name, "rb") as f:
|
||||
im = Image.open(BytesIO(f.read()))
|
||||
(w, h) = im.size
|
||||
return (w / h) < 1
|
||||
@ -112,9 +109,7 @@ class Picture(SithFile):
|
||||
def rotate(self, degree):
|
||||
for attr in ["file", "compressed", "thumbnail"]:
|
||||
name = self.__getattribute__(attr).name
|
||||
with open(
|
||||
os.path.join(settings.MEDIA_ROOT, name).encode("utf-8"), "r+b"
|
||||
) as file:
|
||||
with open(settings.MEDIA_ROOT / name, "r+b") as file:
|
||||
if file:
|
||||
im = Image.open(BytesIO(file.read()))
|
||||
file.seek(0)
|
||||
|
@ -1,10 +1,10 @@
|
||||
from datetime import datetime
|
||||
|
||||
from ninja import FilterSchema, ModelSchema
|
||||
from pydantic import Field
|
||||
from ninja import FilterSchema, ModelSchema, Schema
|
||||
from pydantic import Field, NonNegativeInt
|
||||
|
||||
from core.schemas import SimpleUserSchema
|
||||
from sas.models import Picture
|
||||
from sas.models import PeoplePictureRelation, Picture
|
||||
|
||||
|
||||
class PictureFilterSchema(FilterSchema):
|
||||
@ -23,3 +23,14 @@ class PictureSchema(ModelSchema):
|
||||
full_size_url: str
|
||||
compressed_url: str
|
||||
thumb_url: str
|
||||
|
||||
|
||||
class PictureCreateRelationSchema(Schema):
|
||||
user_id: NonNegativeInt
|
||||
picture_id: NonNegativeInt
|
||||
|
||||
|
||||
class CreatedPictureRelationSchema(ModelSchema):
|
||||
class Meta:
|
||||
model = PeoplePictureRelation
|
||||
fields = ["id", "user", "picture"]
|
||||
|
@ -4,11 +4,19 @@
|
||||
<link rel="stylesheet" href="{{ scss('sas/picture.scss') }}">
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block additional_js -%}
|
||||
<script src="{{ static('core/js/alpinejs.min.js') }}" defer></script>
|
||||
{%- endblock -%}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
|
||||
{% if picture.get_previous() %}
|
||||
<link rel="preload" as="image" href="{{ url("sas:download_compressed", picture_id=picture.get_previous().id) }}">
|
||||
<link
|
||||
rel="preload"
|
||||
as="image"
|
||||
href="{{ url("sas:download_compressed", picture_id=picture.get_previous().id) }}"
|
||||
>
|
||||
{% endif %}
|
||||
{% if picture.get_next() %}
|
||||
<link rel="preload" as="image" href="{{ url("sas:download_compressed", picture_id=picture.get_next().id) }}">
|
||||
@ -36,7 +44,8 @@
|
||||
|
||||
<div class="title">
|
||||
<h3>{{ picture.get_display_name() }}</h3>
|
||||
<h4>{{ picture.parent.children.filter(id__lte=picture.id).count() }} / {{ picture.parent.children.count() }}</h4>
|
||||
<h4>{{ picture.parent.children.filter(id__lte=picture.id).count() }}
|
||||
/ {{ picture.parent.children.count() }}</h4>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
@ -100,7 +109,9 @@
|
||||
<h5>{% trans %}Tools{% endtrans %}</h5>
|
||||
<div>
|
||||
<div>
|
||||
<a class="text" href="{{ picture.get_download_url() }}">{% trans %}HD version{% endtrans %}</a>
|
||||
<a class="text" href="{{ picture.get_download_url() }}">
|
||||
{% trans %}HD version{% endtrans %}
|
||||
</a>
|
||||
<br>
|
||||
<a class="text danger" href="?ask_removal">{% trans %}Ask for removal{% endtrans %}</a>
|
||||
</div>
|
||||
@ -139,20 +150,18 @@
|
||||
{{ form.as_p() }}
|
||||
<input type="submit" value="{% trans %}Go{% endtrans %}" />
|
||||
</form>
|
||||
<ul>
|
||||
{% for r in picture.people.all() %}
|
||||
<ul x-data="user_identification">
|
||||
<template x-for="item in items" :key="item.id">
|
||||
<li>
|
||||
<a class="user" href="{{ r.user.get_absolute_url() }}">
|
||||
{% if r.user.profile_pict %}
|
||||
<div class="profile-pic" style="background-image: url('{{ r.user.profile_pict.get_download_url() }}');"></div>
|
||||
{% endif %}
|
||||
<span>{{ r.user.get_short_name() }}</span>
|
||||
<a class="user" :href="item.user.url">
|
||||
<img class="profile-pic" :src="item.user.picture" alt="image de profil"/>
|
||||
<span x-text="item.user.name"></span>
|
||||
</a>
|
||||
{% if user == r.user or user.can_edit(picture) %}
|
||||
<a class="delete" href="?remove_user={{ r.user.id }}">❌</a>
|
||||
{% endif %}
|
||||
<template x-if="can_be_removed(item)">
|
||||
<a class="delete clickable" @click="remove(item)">❌</a>
|
||||
</template>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -162,6 +171,42 @@
|
||||
{% block script %}
|
||||
{{ super() }}
|
||||
<script>
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("user_identification", () => ({
|
||||
items: [
|
||||
{%- for r in picture.people.select_related("user", "user__profile_pict") -%}
|
||||
{
|
||||
id: {{ r.id }},
|
||||
user: {
|
||||
id: {{ r.user.id }},
|
||||
name: "{{ r.user.get_short_name()|safe }}",
|
||||
url: "{{ r.user.get_absolute_url() }}",
|
||||
{% if r.user.profile_pict %}
|
||||
picture: "{{ r.user.profile_pict.get_download_url() }}",
|
||||
{% else %}
|
||||
picture: "{{ static('core/img/unknown.jpg') }}",
|
||||
{% endif %}
|
||||
},
|
||||
},
|
||||
{%- endfor -%}
|
||||
],
|
||||
can_be_removed(item) {
|
||||
{# If user is root or sas admin, he has the right, at "compile" time.
|
||||
If not, he can delete only its own identification. #}
|
||||
{% if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %}
|
||||
return true;
|
||||
{% else %}
|
||||
return item.user.id === {{ user.id }};
|
||||
{% endif %}
|
||||
},
|
||||
async remove(item) {
|
||||
const res = await fetch(`/api/sas/relation/${item.id}`, {method: "DELETE"});
|
||||
if (res.ok) {
|
||||
this.items = this.items.filter((i) => i.id !== item.id)
|
||||
}
|
||||
},
|
||||
}))
|
||||
});
|
||||
$(() => {
|
||||
$(document).keydown((e) => {
|
||||
switch (e.keyCode) {
|
||||
|
@ -1,10 +1,12 @@
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from model_bakery import baker
|
||||
from model_bakery.recipe import Recipe
|
||||
|
||||
from core.baker_recipes import old_subscriber_user, subscriber_user
|
||||
from core.models import User
|
||||
from core.models import RealGroup, User
|
||||
from sas.models import Album, PeoplePictureRelation, Picture
|
||||
|
||||
|
||||
@ -32,6 +34,8 @@ class TestSas(TestCase):
|
||||
baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_b)
|
||||
baker.make(PeoplePictureRelation, picture=pictures[4], user=cls.user_c)
|
||||
|
||||
|
||||
class TestPictureSearch(TestSas):
|
||||
def test_anonymous_user_forbidden(self):
|
||||
res = self.client.get(reverse("api:pictures"))
|
||||
assert res.status_code == 403
|
||||
@ -101,3 +105,49 @@ class TestSas(TestCase):
|
||||
+ f"?users_identified={self.user_a.id}&users_identified={self.user_b.id}"
|
||||
)
|
||||
assert res.status_code == 403
|
||||
|
||||
|
||||
class TestPictureRelation(TestSas):
|
||||
def test_delete_relation_route_forbidden(self):
|
||||
"""Test that unauthorized users are properly 403ed"""
|
||||
# take a picture where user_a isn't identified
|
||||
relation = PeoplePictureRelation.objects.exclude(user=self.user_a).first()
|
||||
|
||||
res = self.client.delete(f"/api/sas/relation/{relation.id}")
|
||||
assert res.status_code == 403
|
||||
|
||||
for user in baker.make(User), self.user_a:
|
||||
self.client.force_login(user)
|
||||
res = self.client.delete(f"/api/sas/relation/{relation.id}")
|
||||
assert res.status_code == 403
|
||||
|
||||
def test_delete_relation_with_authorized_users(self):
|
||||
"""Test that deletion works as intended when called by an authorized user."""
|
||||
relation: PeoplePictureRelation = self.user_a.pictures.first()
|
||||
sas_admin_group = RealGroup.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)
|
||||
sas_admin = baker.make(User, groups=[sas_admin_group])
|
||||
root = baker.make(User, is_superuser=True)
|
||||
for user in sas_admin, root, self.user_a:
|
||||
with transaction.atomic():
|
||||
self.client.force_login(user)
|
||||
res = self.client.delete(f"/api/sas/relation/{relation.id}")
|
||||
assert res.status_code == 200
|
||||
assert not PeoplePictureRelation.objects.filter(pk=relation.id).exists()
|
||||
transaction.set_rollback(True)
|
||||
public = baker.make(User)
|
||||
relation = public.pictures.create(picture=relation.picture)
|
||||
self.client.force_login(public)
|
||||
res = self.client.delete(f"/api/sas/relation/{relation.id}")
|
||||
assert res.status_code == 200
|
||||
assert not PeoplePictureRelation.objects.filter(pk=relation.id).exists()
|
||||
|
||||
def test_delete_twice(self):
|
||||
"""Test a duplicate call on the delete route."""
|
||||
self.client.force_login(baker.make(User, is_superuser=True))
|
||||
relation = PeoplePictureRelation.objects.first()
|
||||
res = self.client.delete(f"/api/sas/relation/{relation.id}")
|
||||
assert res.status_code == 200
|
||||
relation_count = PeoplePictureRelation.objects.count()
|
||||
res = self.client.delete(f"/api/sas/relation/{relation.id}")
|
||||
assert res.status_code == 404
|
||||
assert PeoplePictureRelation.objects.count() == relation_count
|
||||
|
@ -143,12 +143,6 @@ class PictureView(CanViewMixin, DetailView, FormMixin):
|
||||
self.object.rotate(270)
|
||||
if "rotate_left" in request.GET:
|
||||
self.object.rotate(90)
|
||||
if "remove_user" in request.GET:
|
||||
user = get_object_or_404(User, pk=int(request.GET["remove_user"]))
|
||||
if user.id == request.user.id or request.user.is_in_group(
|
||||
pk=settings.SITH_GROUP_SAS_ADMIN_ID
|
||||
):
|
||||
user.picture.filter(picture=self.object).delete()
|
||||
if "ask_removal" in request.GET.keys():
|
||||
self.object.is_moderated = False
|
||||
self.object.asked_for_removal = True
|
||||
|
@ -34,10 +34,9 @@ https://docs.djangoproject.com/en/1.8/ref/settings/
|
||||
"""
|
||||
|
||||
import binascii
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import sentry_sdk
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -45,7 +44,7 @@ from sentry_sdk.integrations.django import DjangoIntegration
|
||||
|
||||
from .honeypot import custom_honeypot_error
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BASE_DIR = Path(__file__).parent.parent.resolve()
|
||||
|
||||
os.environ["HTTPS"] = "off"
|
||||
|
||||
@ -212,7 +211,7 @@ REST_FRAMEWORK = {}
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
@ -252,19 +251,19 @@ USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)
|
||||
LOCALE_PATHS = [BASE_DIR / "locale"]
|
||||
|
||||
PHONENUMBER_DEFAULT_REGION = "FR"
|
||||
|
||||
# Medias
|
||||
MEDIA_ROOT = "./data/"
|
||||
MEDIA_URL = "/data/"
|
||||
MEDIA_ROOT = BASE_DIR / "data"
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.8/howto/static-files/
|
||||
|
||||
STATIC_URL = "/static/"
|
||||
STATIC_ROOT = "./static/"
|
||||
STATIC_ROOT = BASE_DIR / "static"
|
||||
|
||||
# Static files finders which allow to see static folder in all apps
|
||||
STATICFILES_FINDERS = [
|
||||
@ -288,7 +287,6 @@ HONEYPOT_VALUE = "content"
|
||||
HONEYPOT_RESPONDER = custom_honeypot_error # Make honeypot errors less suspicious
|
||||
HONEYPOT_FIELD_NAME_FORUM = "message2" # Only used on forum
|
||||
|
||||
|
||||
# Email
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
EMAIL_HOST = "localhost"
|
||||
@ -725,10 +723,11 @@ if SENTRY_DSN:
|
||||
environment=SENTRY_ENV,
|
||||
)
|
||||
|
||||
|
||||
SITH_FRONT_DEP_VERSIONS = {
|
||||
"https://github.com/Stuk/jszip-utils": "0.1.0",
|
||||
"https://github.com/Stuk/jszip": "3.10.1",
|
||||
"https://github.com/jimmywarting/native-file-system-adapter": "3.0.1",
|
||||
"https://github.com/chartjs/Chart.js/": "2.6.0",
|
||||
"https://github.com/xdan/datetimepicker/": "2.5.21",
|
||||
"https://github.com/Ionaru/easy-markdown-editor/": "2.18.0",
|
||||
"https://github.com/FortAwesome/Font-Awesome/": "4.7.0",
|
||||
"https://github.com/jquery/jquery/": "3.6.2",
|
||||
@ -736,7 +735,6 @@ SITH_FRONT_DEP_VERSIONS = {
|
||||
"https://github.com/viralpatel/jquery.shorten/": "",
|
||||
"https://github.com/getsentry/sentry-javascript/": "4.0.6",
|
||||
"https://github.com/jhuckaby/webcamjs/": "1.0.0",
|
||||
"https://github.com/vuejs/vue-next": "3.2.18",
|
||||
"https://github.com/alpinejs/alpine": "3.10.5",
|
||||
"https://github.com/mrdoob/three.js/": "r148",
|
||||
"https://github.com/vasturiano/three-spritetext": "1.6.5",
|
||||
|
@ -24,18 +24,18 @@ from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.edit import CreateView, FormView
|
||||
|
||||
from core.models import User
|
||||
from core.views.forms import SelectDate, TzAwareDateTimeField
|
||||
from core.views.forms import SelectDate, SelectDateTime
|
||||
from subscription.models import Subscription
|
||||
|
||||
|
||||
class SelectionDateForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["start_date"] = TzAwareDateTimeField(
|
||||
label=_("Start date"), required=True
|
||||
self.fields["start_date"] = forms.DateTimeField(
|
||||
label=_("Start date"), widget=SelectDateTime, required=True
|
||||
)
|
||||
self.fields["end_date"] = TzAwareDateTimeField(
|
||||
label=_("End date"), required=True
|
||||
self.fields["end_date"] = forms.DateTimeField(
|
||||
label=_("End date"), widget=SelectDateTime, required=True
|
||||
)
|
||||
|
||||
|
||||
@ -56,7 +56,7 @@ class SubscriptionForm(forms.ModelForm):
|
||||
max_length=User._meta.get_field("first_name").max_length
|
||||
)
|
||||
self.fields["email"] = forms.EmailField()
|
||||
self.fields["date_of_birth"] = forms.DateTimeField(widget=SelectDate)
|
||||
self.fields["date_of_birth"] = forms.DateField(widget=SelectDate)
|
||||
|
||||
self.field_order = [
|
||||
"member",
|
||||
|
Loading…
Reference in New Issue
Block a user