mirror of
https://github.com/ae-utbm/sith.git
synced 2026-06-21 07:22:40 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b71130ca2 | |||
| 455b81cff6 | |||
| 9ceb51a54e | |||
| 790a1e15b1 | |||
| b2ffcd3a37 | |||
| ca37996d6a | |||
| 173311c1d5 | |||
| 2995823d6e |
@@ -1,5 +1,4 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from aemark import markdown
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -8,6 +7,7 @@ from pytest_django.asserts import assertHTMLEqual, assertRedirects
|
|||||||
|
|
||||||
from club.models import Club, ClubRole, Membership
|
from club.models import Club, ClubRole, Membership
|
||||||
from core.baker_recipes import subscriber_user
|
from core.baker_recipes import subscriber_user
|
||||||
|
from core.markdown import markdown
|
||||||
from core.models import PageRev, User
|
from core.models import PageRev, User
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,13 +1,13 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from aemark import markdown
|
|
||||||
from ninja import FilterLookup, FilterSchema, ModelSchema
|
from ninja import FilterLookup, FilterSchema, ModelSchema
|
||||||
from ninja_extra import service_resolver
|
from ninja_extra import service_resolver
|
||||||
from ninja_extra.context import RouteContext
|
from ninja_extra.context import RouteContext
|
||||||
|
|
||||||
from club.schemas import ClubProfileSchema
|
from club.schemas import ClubProfileSchema
|
||||||
from com.models import News, NewsDate
|
from com.models import News, NewsDate
|
||||||
|
from core.markdown import markdown
|
||||||
|
|
||||||
|
|
||||||
class NewsDateFilterSchema(FilterSchema):
|
class NewsDateFilterSchema(FilterSchema):
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from datetime import timedelta
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from aemark import markdown
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
@@ -14,6 +13,7 @@ from pytest_django.asserts import assertNumQueries
|
|||||||
|
|
||||||
from com.ics_calendar import IcsCalendar
|
from com.ics_calendar import IcsCalendar
|
||||||
from com.models import News, NewsDate
|
from com.models import News, NewsDate
|
||||||
|
from core.markdown import markdown
|
||||||
from core.models import User
|
from core.models import User
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+27
-26
@@ -2,9 +2,12 @@
|
|||||||
<h1>Markdown-AE Documentation</h1>
|
<h1>Markdown-AE Documentation</h1>
|
||||||
<p>Le Markdown le plus standard se trouve documenté ici:
|
<p>Le Markdown le plus standard se trouve documenté ici:
|
||||||
<a href="https://www.markdownguide.org/basic-syntax">https://www.markdownguide.org/basic-syntax</a>.<br />
|
<a href="https://www.markdownguide.org/basic-syntax">https://www.markdownguide.org/basic-syntax</a>.<br />
|
||||||
Si cette page n’est pas exhaustive vis à vis de la syntaxe du site AE,
|
Si cette page n'est pas exhaustive vis à vis de la syntaxe du site AE,
|
||||||
elle a au moins le mérite de bien documenter le Markdown original.</p>
|
elle a au moins le mérite de bien documenter le Markdown original.</p>
|
||||||
<p>Le réel parseur du site AE est une version tunée de <a href="https://github.com/kivikakk/comrak">comrak</a>.</p>
|
<p>Le réel parseur du site AE est une version tunée de <a href="https://github.com/lepture/mistune">mistune</a>.<br />
|
||||||
|
Les plus aventureux pourront aller lire ses <a href="https://github.com/lepture/mistune/blob/master/tests/fixtures">tests</a>
|
||||||
|
afin d'en connaître la syntaxe le plus finement possible.<br />
|
||||||
|
En pratique, cette page devrait déjà résumer une bonne partie.</p>
|
||||||
<h2>Basique</h2>
|
<h2>Basique</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Mettre le texte en <strong>gras</strong> : <code>**texte**</code></li>
|
<li>Mettre le texte en <strong>gras</strong> : <code>**texte**</code></li>
|
||||||
@@ -12,8 +15,8 @@ elle a au moins le mérite de bien documenter le Markdown original.</p>
|
|||||||
<li><u>Souligner</u> le texte : <code>__texte__</code></li>
|
<li><u>Souligner</u> le texte : <code>__texte__</code></li>
|
||||||
<li><del>Barrer du texte</del> : <code>~~texte~~</code></li>
|
<li><del>Barrer du texte</del> : <code>~~texte~~</code></li>
|
||||||
<li>On peut bien sûr tout <del><em><strong><u>combiner</u></strong></em></del> : <code>~~***__texte__***~~</code></li>
|
<li>On peut bien sûr tout <del><em><strong><u>combiner</u></strong></em></del> : <code>~~***__texte__***~~</code></li>
|
||||||
<li>Mettre du texte<sup>en exposant</sup> : <code><sup>texte</sup></code></li>
|
<li>Mettre du texte^en exposant^ : <code><sup>texte</sup></code></li>
|
||||||
<li>Mettre du texte<sub>en indice</sub> : <code><sub>texte</sub></code></li>
|
<li>Mettre du texte~en indice~ : <code><sub>texte</sub></code></li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Liens</h2>
|
<h2>Liens</h2>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -25,10 +28,10 @@ elle a au moins le mérite de bien documenter le Markdown original.</p>
|
|||||||
</ul>
|
</ul>
|
||||||
<p><a href="http://www.site.com">nom du lien</a></p>
|
<p><a href="http://www.site.com">nom du lien</a></p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Les liens peuvent être internes au site de l’AE, on peut dès lors éviter d’entrer
|
<li>Les liens peuvent être internes au site de l'AE, on peut dès lors éviter d'entrer
|
||||||
l’adresse complète d’une page : <code>[nom du lien](page://nomDeLaPage)</code></li>
|
l'adresse complète d'une page : <code>[nom du lien](page://nomDeLaPage)</code></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p><a href="/page/nomDeLaPage">nom du lien</a></p>
|
<p><a href="/page/nomDeLaPage/">nom du lien</a></p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>On peut également utiliser une image pour les liens :
|
<li>On peut également utiliser une image pour les liens :
|
||||||
<code>[nom du lien](options)</code></li>
|
<code>[nom du lien](options)</code></li>
|
||||||
@@ -109,7 +112,7 @@ etc...
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p>L’alignement dans les cellules est géré comme suit, avec les ‘:’ sur la ligne en dessous du titre:</p>
|
<p>L'alignement dans les cellules est géré comme suit, avec les ':' sur la ligne en dessous du titre:</p>
|
||||||
<pre><code>| Titre | Titre2 | Titre3 |
|
<pre><code>| Titre | Titre2 | Titre3 |
|
||||||
|:-------|:------:|-------:|
|
|:-------|:------:|-------:|
|
||||||
| gauche | centre | droite |
|
| gauche | centre | droite |
|
||||||
@@ -117,16 +120,16 @@ etc...
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th align="left">Titre</th>
|
<th style="text-align:left">Titre</th>
|
||||||
<th align="center">Titre2</th>
|
<th style="text-align:center">Titre2</th>
|
||||||
<th align="right">Titre3</th>
|
<th style="text-align:right">Titre3</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="left">gauche</td>
|
<td style="text-align:left">gauche</td>
|
||||||
<td align="center">centre</td>
|
<td style="text-align:center">centre</td>
|
||||||
<td align="right">droite</td>
|
<td style="text-align:right">droite</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -138,11 +141,11 @@ etc...
|
|||||||

|

|
||||||

|

|
||||||
</code></pre>
|
</code></pre>
|
||||||
<p><img src="/static/core/img/logo.png" style="width:50%" alt="image à 50%" title="Image à 50%" /><br />
|
<p><img src="/static/core/img/logo.png" alt="image à 50%" title="Image à 50%" style="width:50%;" /><br />
|
||||||
Image à 50% de la largeur de la page.</p>
|
Image à 50% de la largeur de la page.</p>
|
||||||
<p><img src="/static/core/img/logo.png" style="width:350px" alt="image de 350 pixels de large" title="Image de 350 pixels" /><br />
|
<p><img src="/static/core/img/logo.png" alt="image de 350 pixels de large" title="Image de 350 pixels" style="width:350px;" /><br />
|
||||||
Image de 350 pixels de large.</p>
|
Image de 350 pixels de large.</p>
|
||||||
<p><img src="/static/core/img/logo.png" style="width:350px;height:100px" alt="image de 350x100 pixels" title="Image de 350x100 pixels" /><br />
|
<p><img src="/static/core/img/logo.png" alt="image de 350x100 pixels" title="Image de 350x100 pixels" style="width:350px;height:100px;" /><br />
|
||||||
Image de 350x100 pixels.</p>
|
Image de 350x100 pixels.</p>
|
||||||
<p>(devrait pouvoir détecter si vidéo ou non)</p>
|
<p>(devrait pouvoir détecter si vidéo ou non)</p>
|
||||||
<h2>Blocs de citations</h2>
|
<h2>Blocs de citations</h2>
|
||||||
@@ -156,9 +159,9 @@ Image de 350x100 pixels.</p>
|
|||||||
un bloc de
|
un bloc de
|
||||||
citation</p>
|
citation</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
<p>Il est possible d’intégrer de la syntaxe Markdown-AE dans un tel bloc.</p>
|
<p>Il est possible d'intégrer de la syntaxe Markdown-AE dans un tel bloc.</p>
|
||||||
<h2>Note de bas de page</h2>
|
<h2>Note de bas de page</h2>
|
||||||
<p>On les crée comme ça<sup class="footnote-ref"><a href="#fn-key" id="fnref-key" data-footnote-ref>1</a></sup>:</p>
|
<p>On les crée comme ça<sup class="footnote-ref" id="fnref-1"><a href="#fn-1">1</a></sup>:</p>
|
||||||
<pre><code>Je fais une note[^clef].
|
<pre><code>Je fais une note[^clef].
|
||||||
|
|
||||||
[^clef]: je note ensuite où je veux le contenu de ma clef qui apparaîtra quand même en bas
|
[^clef]: je note ensuite où je veux le contenu de ma clef qui apparaîtra quand même en bas
|
||||||
@@ -172,15 +175,13 @@ citation</p>
|
|||||||
</code></pre>
|
</code></pre>
|
||||||
<h2>Échapper des caractères</h2>
|
<h2>Échapper des caractères</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Il est possible d’ignorer un caractère spécial en l’échappant à l’aide d’un \</li>
|
<li>Il est possible d'ignorer un caractère spécial en l'échappant à l'aide d'un \</li>
|
||||||
<li>L’échappement de blocs de codes complet se fera à l’aide de balises <nosyntax></nosyntax></li>
|
<li>L'échappement de blocs de codes complet se fera à l'aide de balises <nosyntax></nosyntax></li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Autres (hérité de l’ancien wiki)</h2>
|
<h2>Autres (hérité de l'ancien wiki)</h2>
|
||||||
<p>Une ligne peut être créée avec une ligne contenant 4 tirets (<code>----</code>).</p>
|
<p>Une ligne peut être créée avec une ligne contenant 4 tirets (<code>----</code>).</p>
|
||||||
<section class="footnotes" data-footnotes>
|
<section class="footnotes">
|
||||||
<ol>
|
<ol>
|
||||||
<li id="fn-key">
|
<li id="fn-1"><p>ceci est le contenu de ma clef<a href="#fnref-1" class="footnote">↩</a></p></li>
|
||||||
<p>ceci est le contenu de ma clef <a href="#fnref-key" class="footnote-backref" data-footnote-backref data-footnote-backref-idx="1" aria-label="Back to reference 1">↩</a></p>
|
|
||||||
</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ https://www.markdownguide.org/basic-syntax.
|
|||||||
Si cette page n'est pas exhaustive vis à vis de la syntaxe du site AE,
|
Si cette page n'est pas exhaustive vis à vis de la syntaxe du site AE,
|
||||||
elle a au moins le mérite de bien documenter le Markdown original.
|
elle a au moins le mérite de bien documenter le Markdown original.
|
||||||
|
|
||||||
Le réel parseur du site AE est une version tunée de [comrak](https://github.com/kivikakk/comrak).
|
Le réel parseur du site AE est une version tunée de [mistune](https://github.com/lepture/mistune).
|
||||||
|
Les plus aventureux pourront aller lire ses [tests](https://github.com/lepture/mistune/blob/master/tests/fixtures)
|
||||||
|
afin d'en connaître la syntaxe le plus finement possible.
|
||||||
|
En pratique, cette page devrait déjà résumer une bonne partie.
|
||||||
|
|
||||||
## Basique
|
## Basique
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,11 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
from aemark import markdown
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from core.markdown import markdown
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Output the fully rendered SYNTAX.md file"
|
help = "Output the fully rendered SYNTAX.md file"
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#
|
||||||
|
# Copyright 2023 © AE UTBM
|
||||||
|
# ae@utbm.fr / ae.info@utbm.fr
|
||||||
|
#
|
||||||
|
# This file is part of the website of the UTBM Student Association (AE UTBM),
|
||||||
|
# https://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# You can find the source code of the website at https://github.com/ae-utbm/sith
|
||||||
|
#
|
||||||
|
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
|
||||||
|
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
|
||||||
|
# OR WITHIN THE LOCAL FILE "LICENSE"
|
||||||
|
#
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import mistune
|
||||||
|
from django.urls import reverse
|
||||||
|
from mistune import HTMLRenderer, Markdown
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from mistune import InlineParser, InlineState
|
||||||
|
|
||||||
|
# match __text__, without linebreak in the text, nor backslash prepending an underscore
|
||||||
|
# Examples :
|
||||||
|
# - "__text__" : OK
|
||||||
|
# - "__te xt__" : OK
|
||||||
|
# - "__te_xt__" : nope (underscore in the middle)
|
||||||
|
# - "__te\_xt__" : Ok (the middle underscore is escaped)
|
||||||
|
# - "__te\nxt__" : nope (there is a linebreak in the text)
|
||||||
|
# - "\__text__" : nope (one of the underscores have a backslash prepended)
|
||||||
|
# - "\\__text__" : Ok (the backslash is ignored, because there is another backslash before)
|
||||||
|
UNDERLINED_RE = (
|
||||||
|
r"(?<!\\)(?:\\{2})*" # ignore if there is an odd number of backslashes before
|
||||||
|
r"_{2}" # two underscores
|
||||||
|
r"(?P<underlined>([^\\_]|\\.)+)" # the actual text
|
||||||
|
r"_{2}" # closing underscores
|
||||||
|
)
|
||||||
|
|
||||||
|
SITH_LINK_RE = (
|
||||||
|
r"\[(?P<page_name>[\w\s]+)\]" # [nom du lien]
|
||||||
|
r"\(page:\/\/" # (page://
|
||||||
|
r"(?P<page_slug>[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9])" # actual page name
|
||||||
|
r"\)" # )
|
||||||
|
)
|
||||||
|
|
||||||
|
CUSTOM_DIMENSIONS_IMAGE_RE = (
|
||||||
|
r"\[(?P<img_name>[\w\s]+)\]" # [nom du lien]
|
||||||
|
r"\(img:\/\/" # (img://
|
||||||
|
r"(?P<img_slug>[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9])" # actual page name
|
||||||
|
r"\)" # )
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_underline(_inline: InlineParser, m: re.Match, state: InlineState):
|
||||||
|
state.append_token({"type": "underline", "raw": m.group("underlined")})
|
||||||
|
return m.end()
|
||||||
|
|
||||||
|
|
||||||
|
def underline(md_instance: Markdown):
|
||||||
|
md_instance.inline.register(
|
||||||
|
"underline",
|
||||||
|
UNDERLINED_RE,
|
||||||
|
parse_underline,
|
||||||
|
before="emphasis",
|
||||||
|
)
|
||||||
|
md_instance.renderer.register("underline", lambda _, text: f"<u>{text}</u>")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_sith_link(_inline: InlineParser, m: re.Match, state: InlineState):
|
||||||
|
page_name = m.group("page_name")
|
||||||
|
page_slug = m.group("page_slug")
|
||||||
|
state.append_token(
|
||||||
|
{
|
||||||
|
"type": "link",
|
||||||
|
"children": [{"type": "text", "raw": page_name}],
|
||||||
|
"attrs": {"url": reverse("core:page", kwargs={"page_name": page_slug})},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return m.end()
|
||||||
|
|
||||||
|
|
||||||
|
def sith_link(md_instance: Markdown):
|
||||||
|
md_instance.inline.register(
|
||||||
|
"sith_link",
|
||||||
|
SITH_LINK_RE,
|
||||||
|
parse_sith_link,
|
||||||
|
before="emphasis",
|
||||||
|
)
|
||||||
|
# no custom renderer here.
|
||||||
|
# we just add another parsing rule, but render it as if it was
|
||||||
|
# a regular markdown link
|
||||||
|
|
||||||
|
|
||||||
|
class SithRenderer(HTMLRenderer):
|
||||||
|
def image(self, text: str, url: str, title=None) -> str:
|
||||||
|
if "?" not in url:
|
||||||
|
return super().image(text, url, title)
|
||||||
|
|
||||||
|
new_url, params = url.rsplit("?", maxsplit=1)
|
||||||
|
m = re.match(r"^(?P<width>\d+(%|px)?)(x(?P<height>\d+(%|px)?))?$", params)
|
||||||
|
if not m:
|
||||||
|
return super().image(text, url, title)
|
||||||
|
|
||||||
|
width, height = m.group("width"), m.group("height")
|
||||||
|
if not width.endswith(("%", "px")):
|
||||||
|
width += "px"
|
||||||
|
style = f"width:{width};"
|
||||||
|
if height is not None:
|
||||||
|
if not height.endswith(("%", "px")):
|
||||||
|
height += "px"
|
||||||
|
style += f"height:{height};"
|
||||||
|
return super().image(text, new_url, title).replace("/>", f'style="{style}" />')
|
||||||
|
|
||||||
|
|
||||||
|
markdown = mistune.create_markdown(
|
||||||
|
renderer=SithRenderer(escape=True),
|
||||||
|
plugins=[
|
||||||
|
underline,
|
||||||
|
sith_link,
|
||||||
|
"strikethrough",
|
||||||
|
"footnotes",
|
||||||
|
"table",
|
||||||
|
"spoiler",
|
||||||
|
"subscript",
|
||||||
|
"superscript",
|
||||||
|
"url",
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -25,13 +25,14 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import phonenumbers
|
import phonenumbers
|
||||||
from aemark import markdown as md
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.forms import BoundField
|
from django.forms import BoundField
|
||||||
from django.template.defaultfilters import stringfilter
|
from django.template.defaultfilters import stringfilter
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ngettext
|
from django.utils.translation import ngettext
|
||||||
|
|
||||||
|
from core.markdown import markdown as md
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+12
-9
@@ -18,7 +18,6 @@ from smtplib import SMTPException
|
|||||||
|
|
||||||
import freezegun
|
import freezegun
|
||||||
import pytest
|
import pytest
|
||||||
from aemark import markdown
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
@@ -35,6 +34,7 @@ from pytest_django.asserts import assertInHTML, assertRedirects
|
|||||||
from antispam.models import ToxicDomain
|
from antispam.models import ToxicDomain
|
||||||
from club.models import Club
|
from club.models import Club
|
||||||
from core.baker_recipes import subscriber_user
|
from core.baker_recipes import subscriber_user
|
||||||
|
from core.markdown import markdown
|
||||||
from core.models import AnonymousUser, Group, Page, User, validate_promo
|
from core.models import AnonymousUser, Group, Page, User, validate_promo
|
||||||
from core.utils import get_last_promo, get_semester_code, get_start_of_semester
|
from core.utils import get_last_promo, get_semester_code, get_start_of_semester
|
||||||
from core.views import AllowFragment
|
from core.views import AllowFragment
|
||||||
@@ -200,28 +200,31 @@ class TestUserLogin:
|
|||||||
[
|
[
|
||||||
(
|
(
|
||||||
"[nom du lien](page://nomDeLaPage)",
|
"[nom du lien](page://nomDeLaPage)",
|
||||||
'<a href="/page/nomDeLaPage">nom du lien</a>',
|
'<a href="/page/nomDeLaPage/">nom du lien</a>',
|
||||||
),
|
),
|
||||||
("__texte__", "<u>texte</u>"),
|
("__texte__", "<u>texte</u>"),
|
||||||
("~~***__texte__***~~", "<del><em><strong><u>texte</u></strong></em></del>"),
|
("~~***__texte__***~~", "<del><em><strong><u>texte</u></strong></em></del>"),
|
||||||
(
|
(
|
||||||
'',
|
'',
|
||||||
'<img src="/img.png" style="width:50%" alt="tst_alt" title="tst_title" />',
|
'<img src="/img.png" alt="tst_alt" title="tst_title" style="width:50%;" />',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"[texte](page://tst-page)",
|
||||||
|
'<a href="/page/tst-page/">texte</a>',
|
||||||
),
|
),
|
||||||
("[texte](page://tst-page)", '<a href="/page/tst-page">texte</a>'),
|
|
||||||
(
|
(
|
||||||
"",
|
"",
|
||||||
'<img src="/img.png" style="width:50px;height:450px" alt="" />',
|
'<img src="/img.png" alt="" style="width:50px;height:450px;" />',
|
||||||
),
|
),
|
||||||
("", '<img src="/img.png" alt="" />'),
|
("", '<img src="/img.png" alt="" />'),
|
||||||
(
|
(
|
||||||
"",
|
"",
|
||||||
'<img src="/img.png" style="width:50%;height:120%" alt="" />',
|
'<img src="/img.png" alt="" style="width:50%;height:120%;" />',
|
||||||
),
|
),
|
||||||
("", '<img src="/img.png" style="width:50px" alt="" />'),
|
("", '<img src="/img.png" alt="" style="width:50px;" />'),
|
||||||
(
|
(
|
||||||
"",
|
"",
|
||||||
'<img src="/img.png" style="width:50px;height:120%" alt="" />',
|
'<img src="/img.png" alt="" style="width:50px;height:120%;" />',
|
||||||
),
|
),
|
||||||
# when the image dimension has a wrong format, don't touch the url
|
# when the image dimension has a wrong format, don't touch the url
|
||||||
("", '<img src="/img.png?50pxxxxxxxx" alt="" />'),
|
("", '<img src="/img.png?50pxxxxxxxx" alt="" />'),
|
||||||
@@ -347,7 +350,7 @@ http://git.an
|
|||||||
<p><a href="http://git.an">http://git.an</a></p>
|
<p><a href="http://git.an">http://git.an</a></p>
|
||||||
<h1>Swag</h1>
|
<h1>Swag</h1>
|
||||||
<p><guy>Bibou</guy></p>
|
<p><guy>Bibou</guy></p>
|
||||||
<script>alert('Guy');</script>
|
<p><script>alert('Guy');</script></p>
|
||||||
"""
|
"""
|
||||||
assertInHTML(expected, response.text)
|
assertInHTML(expected, response.text)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from datetime import timedelta
|
|||||||
|
|
||||||
import freezegun
|
import freezegun
|
||||||
import pytest
|
import pytest
|
||||||
from aemark import markdown
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
@@ -14,6 +13,7 @@ from pytest_django.asserts import assertHTMLEqual, assertRedirects
|
|||||||
|
|
||||||
from club.models import Club, Membership
|
from club.models import Club, Membership
|
||||||
from core.baker_recipes import board_user, subscriber_user
|
from core.baker_recipes import board_user, subscriber_user
|
||||||
|
from core.markdown import markdown
|
||||||
from core.models import AnonymousUser, Page, PageRev, User
|
from core.models import AnonymousUser, Page, PageRev, User
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -200,11 +200,7 @@ class TestFilterInactive(TestCase):
|
|||||||
]
|
]
|
||||||
sale_recipe.make(customer=cls.users[3].customer, date=time_active)
|
sale_recipe.make(customer=cls.users[3].customer, date=time_active)
|
||||||
baker.make(
|
baker.make(
|
||||||
Refilling,
|
Refilling, customer=cls.users[4].customer, date=time_active, counter=counter
|
||||||
customer=cls.users[4].customer,
|
|
||||||
date=time_active,
|
|
||||||
counter=counter,
|
|
||||||
amount=1,
|
|
||||||
)
|
)
|
||||||
sale_recipe.make(customer=cls.users[5].customer, date=time_inactive)
|
sale_recipe.make(customer=cls.users[5].customer, date=time_inactive)
|
||||||
|
|
||||||
@@ -459,9 +455,7 @@ def test_user_preferences(client: Client):
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_user_stats(client: Client):
|
def test_user_stats(client: Client):
|
||||||
user = subscriber_user.make()
|
user = subscriber_user.make()
|
||||||
baker.make(
|
baker.make(Refilling, customer=user.customer, amount=99999)
|
||||||
Refilling, customer=user.customer, amount=settings.SITH_ACCOUNT_MAX_MONEY
|
|
||||||
)
|
|
||||||
bars = [b[0] for b in settings.SITH_COUNTER_BARS]
|
bars = [b[0] for b in settings.SITH_COUNTER_BARS]
|
||||||
baker.make(
|
baker.make(
|
||||||
Permanency,
|
Permanency,
|
||||||
|
|||||||
+4
-50
@@ -1,68 +1,22 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import checks
|
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.functional import cached_property
|
|
||||||
|
|
||||||
|
|
||||||
class CurrencyField(models.DecimalField):
|
class CurrencyField(models.DecimalField):
|
||||||
"""Custom database field used for currency."""
|
"""Custom database field used for currency."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs):
|
||||||
self, verbose_name=None, name=None, min_value=None, max_value=None, **kwargs
|
kwargs["max_digits"] = 12
|
||||||
):
|
kwargs["decimal_places"] = 2
|
||||||
kwargs.update({"max_digits": 12, "decimal_places": 2})
|
super().__init__(*args, **kwargs)
|
||||||
self.min_value = min_value
|
|
||||||
self.max_value = max_value
|
|
||||||
super().__init__(verbose_name, name, **kwargs)
|
|
||||||
|
|
||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
return super().to_python(value).quantize(Decimal("0.01"))
|
return super().to_python(value).quantize(Decimal("0.01"))
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def validators(self):
|
|
||||||
res = []
|
|
||||||
if self.max_value:
|
|
||||||
res.append(MaxValueValidator(self.max_value))
|
|
||||||
if self.min_value:
|
|
||||||
res.append(MinValueValidator(self.min_value))
|
|
||||||
return [*super().validators, *res]
|
|
||||||
|
|
||||||
def check(self, **kwargs): # pragma: no cover
|
|
||||||
# this is executed during runserver, but won't run in prod
|
|
||||||
errors = super().check(**kwargs)
|
|
||||||
for name, val in ("min_value", self.min_value), ("max_value", self.max_value):
|
|
||||||
if not val:
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
float(val)
|
|
||||||
except ValueError:
|
|
||||||
errors.append(
|
|
||||||
checks.Error(
|
|
||||||
f"CurrencyField.{name} must be a valid float",
|
|
||||||
obj=self,
|
|
||||||
id="sith.E001",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return errors
|
|
||||||
|
|
||||||
def formfield(self, **kwargs):
|
|
||||||
return super().formfield(
|
|
||||||
**{"min_value": self.min_value, "max_value": self.max_value, **kwargs}
|
|
||||||
)
|
|
||||||
|
|
||||||
def deconstruct(self):
|
|
||||||
name, path, args, kwargs = super().deconstruct()
|
|
||||||
if self.min_value is not None:
|
|
||||||
kwargs["min_value"] = self.min_value
|
|
||||||
if self.max_value is not None:
|
|
||||||
kwargs["max_value"] = self.max_value
|
|
||||||
return name, path, args, kwargs
|
|
||||||
|
|
||||||
|
|
||||||
if settings.TESTING:
|
if settings.TESTING:
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
|
|||||||
+27
-59
@@ -3,16 +3,13 @@ import math
|
|||||||
import uuid
|
import uuid
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import date, datetime, timezone
|
from datetime import date, datetime, timezone
|
||||||
from typing import ClassVar
|
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Exists, OuterRef, Q
|
from django.db.models import Exists, OuterRef, Q
|
||||||
from django.forms import BaseModelFormSet
|
from django.forms import BaseModelFormSet
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_celery_beat.models import ClockedSchedule
|
from django_celery_beat.models import ClockedSchedule
|
||||||
@@ -42,7 +39,6 @@ from counter.models import (
|
|||||||
Customer,
|
Customer,
|
||||||
Eticket,
|
Eticket,
|
||||||
InvoiceCall,
|
InvoiceCall,
|
||||||
Permanency,
|
|
||||||
Price,
|
Price,
|
||||||
Product,
|
Product,
|
||||||
ProductFormula,
|
ProductFormula,
|
||||||
@@ -155,13 +151,12 @@ class CounterLoginForm(LoginForm):
|
|||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
message=_("You are not a barman of this counter."), code="not_barman"
|
message=_("You are not a barman of this counter."), code="not_barman"
|
||||||
)
|
)
|
||||||
if Permanency.objects.filter(end=None, user=user).exists():
|
|
||||||
if user in self.request.barmen:
|
if user in self.request.barmen:
|
||||||
message = _("You are already logged in this counter.")
|
message = (
|
||||||
elif user in self.counter.barmen_list:
|
_("You are already logged in this counter.")
|
||||||
message = _("You are already logged in another counter.")
|
if user in self.counter.barmen_list
|
||||||
else:
|
else _("You are already logged in another counter.")
|
||||||
message = _("You are already logged on another device")
|
)
|
||||||
raise ValidationError(message=message, code="already_logged_in")
|
raise ValidationError(message=message, code="already_logged_in")
|
||||||
|
|
||||||
|
|
||||||
@@ -173,19 +168,18 @@ class RefillForm(forms.ModelForm):
|
|||||||
|
|
||||||
error_css_class = "error"
|
error_css_class = "error"
|
||||||
required_css_class = "required"
|
required_css_class = "required"
|
||||||
|
amount = forms.FloatField(
|
||||||
|
min_value=0, widget=forms.NumberInput(attrs={"class": "focus"})
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Refilling
|
model = Refilling
|
||||||
fields = ["amount", "payment_method"]
|
fields = ["amount", "payment_method"]
|
||||||
widgets = {"payment_method": forms.RadioSelect}
|
widgets = {"payment_method": forms.RadioSelect}
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, *args, **kwargs):
|
||||||
self, *args, counter: Counter, operator: User, customer: Customer, **kwargs
|
|
||||||
):
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
max_value = settings.SITH_ACCOUNT_MAX_MONEY - customer.amount
|
|
||||||
# server-side max_value validation is done by Refilling.clean
|
|
||||||
self.fields["amount"].widget.attrs["max"] = max_value
|
|
||||||
self.fields["payment_method"].choices = (
|
self.fields["payment_method"].choices = (
|
||||||
method
|
method
|
||||||
for method in self.fields["payment_method"].choices
|
for method in self.fields["payment_method"].choices
|
||||||
@@ -193,9 +187,6 @@ class RefillForm(forms.ModelForm):
|
|||||||
)
|
)
|
||||||
if self.fields["payment_method"].initial not in self.allowed_refilling_methods:
|
if self.fields["payment_method"].initial not in self.allowed_refilling_methods:
|
||||||
self.fields["payment_method"].initial = self.allowed_refilling_methods[0]
|
self.fields["payment_method"].initial = self.allowed_refilling_methods[0]
|
||||||
self.instance.counter = counter
|
|
||||||
self.instance.operator = operator
|
|
||||||
self.instance.customer = customer
|
|
||||||
|
|
||||||
|
|
||||||
class CounterEditForm(forms.ModelForm):
|
class CounterEditForm(forms.ModelForm):
|
||||||
@@ -569,7 +560,16 @@ class BasketItemForm(forms.Form):
|
|||||||
quantity = forms.IntegerField(min_value=1, required=True)
|
quantity = forms.IntegerField(min_value=1, required=True)
|
||||||
price_id = forms.IntegerField(min_value=0, required=True)
|
price_id = forms.IntegerField(min_value=0, required=True)
|
||||||
|
|
||||||
def __init__(self, allowed_prices: dict[int, Price], *args, **kwargs):
|
def __init__(
|
||||||
|
self,
|
||||||
|
customer: Customer,
|
||||||
|
counter: Counter,
|
||||||
|
allowed_prices: dict[int, Price],
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
self.customer = customer # Used by formset
|
||||||
|
self.counter = counter # Used by formset
|
||||||
self.allowed_prices = allowed_prices
|
self.allowed_prices = allowed_prices
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@@ -604,15 +604,6 @@ class BasketItemForm(forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class BaseBasketForm(forms.BaseFormSet):
|
class BaseBasketForm(forms.BaseFormSet):
|
||||||
# Minimum amount of money there must be on the account after the transaction
|
|
||||||
# If None, the min balance check is skipped
|
|
||||||
min_result_balance: ClassVar[int | None] = 0
|
|
||||||
|
|
||||||
def __init__(self, *args, customer: Customer, counter: Counter, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.customer = customer
|
|
||||||
self.counter = counter
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
self.forms = [form for form in self.forms if form.cleaned_data != {}]
|
self.forms = [form for form in self.forms if form.cleaned_data != {}]
|
||||||
|
|
||||||
@@ -621,8 +612,8 @@ class BaseBasketForm(forms.BaseFormSet):
|
|||||||
|
|
||||||
self._check_forms_have_errors()
|
self._check_forms_have_errors()
|
||||||
self._check_product_are_unique()
|
self._check_product_are_unique()
|
||||||
self._check_recorded_products()
|
self._check_recorded_products(self[0].customer)
|
||||||
self._check_account_balance()
|
self._check_enough_money(self[0].counter, self[0].customer)
|
||||||
|
|
||||||
def _check_forms_have_errors(self):
|
def _check_forms_have_errors(self):
|
||||||
if any(len(form.errors) > 0 for form in self):
|
if any(len(form.errors) > 0 for form in self):
|
||||||
@@ -633,35 +624,12 @@ class BaseBasketForm(forms.BaseFormSet):
|
|||||||
if len(price_ids) != len(self.forms):
|
if len(price_ids) != len(self.forms):
|
||||||
raise forms.ValidationError(_("Duplicated product entries."))
|
raise forms.ValidationError(_("Duplicated product entries."))
|
||||||
|
|
||||||
@cached_property
|
def _check_enough_money(self, counter: Counter, customer: Customer):
|
||||||
def total_price(self):
|
self.total_price = sum([data["total_price"] for data in self.cleaned_data])
|
||||||
refill = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
if self.total_price > customer.amount:
|
||||||
total_other = sum(
|
|
||||||
form.cleaned_data["total_price"]
|
|
||||||
for form in self.forms
|
|
||||||
if form.price.product.product_type_id != refill
|
|
||||||
)
|
|
||||||
total_refill = sum(
|
|
||||||
form.cleaned_data["total_price"]
|
|
||||||
for form in self.forms
|
|
||||||
if form.price.product.product_type_id == refill
|
|
||||||
)
|
|
||||||
return total_other - total_refill
|
|
||||||
|
|
||||||
def _check_account_balance(self):
|
|
||||||
result_balance = self.customer.amount - self.total_price
|
|
||||||
if (
|
|
||||||
self.min_result_balance is not None
|
|
||||||
and self.min_result_balance > result_balance
|
|
||||||
):
|
|
||||||
raise forms.ValidationError(_("Not enough money"))
|
raise forms.ValidationError(_("Not enough money"))
|
||||||
if result_balance > settings.SITH_ACCOUNT_MAX_MONEY:
|
|
||||||
raise ValidationError(
|
|
||||||
_("There cannot be more than %(money)d€ on an AE account")
|
|
||||||
% {"money": settings.SITH_ACCOUNT_MAX_MONEY}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _check_recorded_products(self):
|
def _check_recorded_products(self, customer: Customer):
|
||||||
"""Check for, among other things, ecocups and pitchers"""
|
"""Check for, among other things, ecocups and pitchers"""
|
||||||
items = defaultdict(int)
|
items = defaultdict(int)
|
||||||
for form in self.forms:
|
for form in self.forms:
|
||||||
@@ -670,7 +638,7 @@ class BaseBasketForm(forms.BaseFormSet):
|
|||||||
returnables = list(
|
returnables = list(
|
||||||
ReturnableProduct.objects.filter(
|
ReturnableProduct.objects.filter(
|
||||||
Q(product_id__in=ids) | Q(returned_product_id__in=ids)
|
Q(product_id__in=ids) | Q(returned_product_id__in=ids)
|
||||||
).annotate_balance_for(self.customer)
|
).annotate_balance_for(customer)
|
||||||
)
|
)
|
||||||
limit_reached = []
|
limit_reached = []
|
||||||
for returnable in returnables:
|
for returnable in returnables:
|
||||||
|
|||||||
+19
-21
@@ -1,7 +1,8 @@
|
|||||||
from typing import TYPE_CHECKING, Callable
|
from typing import TYPE_CHECKING, Callable
|
||||||
|
|
||||||
|
from django.db.models import Exists, OuterRef
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject, empty
|
||||||
|
|
||||||
from core.models import User
|
from core.models import User
|
||||||
from counter.models import Permanency
|
from counter.models import Permanency
|
||||||
@@ -10,31 +11,20 @@ if TYPE_CHECKING:
|
|||||||
from django.contrib.sessions.backends.base import SessionBase
|
from django.contrib.sessions.backends.base import SessionBase
|
||||||
|
|
||||||
|
|
||||||
SESSION_PERMANENCES_KEY = "permanence_ids"
|
SESSION_BARMEN_KEY = "barmen_ids"
|
||||||
|
|
||||||
|
|
||||||
def get_cached_barmen(request: HttpRequest) -> set[User]:
|
def get_cached_barmen(request: HttpRequest) -> set[User]:
|
||||||
if not hasattr(request, "_cached_barmen"):
|
if not hasattr(request, "_cached_barmen"):
|
||||||
session: SessionBase = request.session
|
session: SessionBase = request.session
|
||||||
|
barmen_ids = session.get(SESSION_BARMEN_KEY, [])
|
||||||
if session_ids := session.get(SESSION_PERMANENCES_KEY, None):
|
if barmen_ids:
|
||||||
# Get ongoing permanences which id is in session.
|
request._cached_barmen = set(
|
||||||
# Note : we store permanence ids rather than user id to be sure
|
User.objects.filter(
|
||||||
# not to wrongfully mark someone as logged here,
|
Exists(Permanency.objects.filter(user=OuterRef("pk"), end=None)),
|
||||||
# even if it logged out then logged in elsewhere.
|
id__in=barmen_ids,
|
||||||
permanences = (
|
)
|
||||||
Permanency.objects.filter(end=None, id__in=session_ids)
|
|
||||||
.order_by("id")
|
|
||||||
.select_related("user")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# if the list of permanences occurring on this device has changed
|
|
||||||
# since the last page load, change the ids stored in session
|
|
||||||
real_ids = [p.id for p in permanences]
|
|
||||||
if real_ids != session_ids:
|
|
||||||
session[SESSION_PERMANENCES_KEY] = real_ids
|
|
||||||
|
|
||||||
request._cached_barmen = {p.user for p in permanences}
|
|
||||||
else:
|
else:
|
||||||
request._cached_barmen = set()
|
request._cached_barmen = set()
|
||||||
|
|
||||||
@@ -63,4 +53,12 @@ class BarmenMiddleware:
|
|||||||
def __call__(self, request: HttpRequest):
|
def __call__(self, request: HttpRequest):
|
||||||
request.barmen = SimpleLazyObject(lambda: get_cached_barmen(request))
|
request.barmen = SimpleLazyObject(lambda: get_cached_barmen(request))
|
||||||
|
|
||||||
return self.get_response(request)
|
response = self.get_response(request)
|
||||||
|
|
||||||
|
if request.barmen._wrapped is not empty and {
|
||||||
|
b.id for b in request.barmen
|
||||||
|
} != set(request.session.get(SESSION_BARMEN_KEY, [])):
|
||||||
|
# update the session data only if `session.barmen`
|
||||||
|
# has been accessed and modified.
|
||||||
|
request.session[SESSION_BARMEN_KEY] = [b.id for b in request.barmen]
|
||||||
|
return response
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
# Generated by Django 5.2.15 on 2026-06-07 12:08
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
import counter.fields
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [("counter", "0041_alter_billinginfo_country_and_more")]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="customer",
|
|
||||||
name="amount",
|
|
||||||
field=counter.fields.CurrencyField(
|
|
||||||
decimal_places=2,
|
|
||||||
default=0,
|
|
||||||
max_digits=12,
|
|
||||||
max_value=250,
|
|
||||||
verbose_name="amount",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name="refilling",
|
|
||||||
name="amount",
|
|
||||||
field=counter.fields.CurrencyField(
|
|
||||||
decimal_places=2, max_digits=12, min_value=0.01, verbose_name="amount"
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
+8
-20
@@ -28,7 +28,7 @@ from dict2xml import dict2xml
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.validators import MinLengthValidator
|
from django.core.validators import MinLengthValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Exists, F, Max, OuterRef, Q, QuerySet, Subquery, Sum, Value
|
from django.db.models import Exists, F, OuterRef, Q, QuerySet, Subquery, Sum, Value
|
||||||
from django.db.models.functions import Coalesce, Concat, Length
|
from django.db.models.functions import Coalesce, Concat, Length
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@@ -99,9 +99,7 @@ class Customer(models.Model):
|
|||||||
|
|
||||||
user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE)
|
user = models.OneToOneField(User, primary_key=True, on_delete=models.CASCADE)
|
||||||
account_id = models.CharField(_("account id"), max_length=10, unique=True)
|
account_id = models.CharField(_("account id"), max_length=10, unique=True)
|
||||||
amount: CurrencyField = CurrencyField(
|
amount = CurrencyField(_("amount"), default=0)
|
||||||
_("amount"), max_value=settings.SITH_ACCOUNT_MAX_MONEY, default=0
|
|
||||||
)
|
|
||||||
|
|
||||||
objects = CustomerQuerySet.as_manager()
|
objects = CustomerQuerySet.as_manager()
|
||||||
|
|
||||||
@@ -158,15 +156,13 @@ class Customer(models.Model):
|
|||||||
unique_fields=["customer", "returnable"],
|
unique_fields=["customer", "returnable"],
|
||||||
)
|
)
|
||||||
|
|
||||||
@cached_property
|
@property
|
||||||
def can_buy(self) -> bool:
|
def can_buy(self) -> bool:
|
||||||
"""Check if whether this customer has the right to purchase any item."""
|
"""Check if whether this customer has the right to purchase any item."""
|
||||||
subscription_end = self.user.subscriptions.aggregate(
|
subscription = self.user.subscriptions.order_by("subscription_end").last()
|
||||||
res=Max("subscription_end")
|
if subscription is None:
|
||||||
).get("res")
|
|
||||||
if subscription_end is None:
|
|
||||||
return False
|
return False
|
||||||
return (date.today() - subscription_end) < timedelta(days=90)
|
return (date.today() - subscription.subscription_end) < timedelta(days=90)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_or_create(cls, user: User) -> tuple[Customer, bool]:
|
def get_or_create(cls, user: User) -> tuple[Customer, bool]:
|
||||||
@@ -827,7 +823,7 @@ class Refilling(models.Model):
|
|||||||
counter = models.ForeignKey(
|
counter = models.ForeignKey(
|
||||||
Counter, related_name="refillings", blank=False, on_delete=models.CASCADE
|
Counter, related_name="refillings", blank=False, on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
amount: CurrencyField = CurrencyField(_("amount"), min_value=0.01)
|
amount = CurrencyField(_("amount"))
|
||||||
operator = models.ForeignKey(
|
operator = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
related_name="refillings_as_operator",
|
related_name="refillings_as_operator",
|
||||||
@@ -881,14 +877,6 @@ class Refilling(models.Model):
|
|||||||
return False
|
return False
|
||||||
return user.is_owner(self.counter) and self.payment_method != "CARD"
|
return user.is_owner(self.counter) and self.payment_method != "CARD"
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super().clean()
|
|
||||||
if (self.amount + self.customer.amount) > settings.SITH_ACCOUNT_MAX_MONEY:
|
|
||||||
raise ValidationError(
|
|
||||||
_("There cannot be more than %(money)d€ on an AE account")
|
|
||||||
% {"money": settings.SITH_ACCOUNT_MAX_MONEY}
|
|
||||||
)
|
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
self.customer.amount -= self.amount
|
self.customer.amount -= self.amount
|
||||||
self.customer.save()
|
self.customer.save()
|
||||||
@@ -1117,7 +1105,7 @@ class Permanency(models.Model):
|
|||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
)
|
)
|
||||||
start = models.DateTimeField(_("start date"))
|
start = models.DateTimeField(_("start date"))
|
||||||
end = models.DateTimeField(_("end date"), null=True, blank=True, db_index=True)
|
end = models.DateTimeField(_("end date"), null=True, db_index=True)
|
||||||
activity = models.DateTimeField(_("last activity date"), auto_now=True)
|
activity = models.DateTimeField(_("last activity date"), auto_now=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ const productParsingRegex = /^(\d+x)?(.*)/i;
|
|||||||
const codeParsingRegex = / \((\w+)\)$/;
|
const codeParsingRegex = / \((\w+)\)$/;
|
||||||
|
|
||||||
function parseProduct(query: string): [number, string] {
|
function parseProduct(query: string): [number, string] {
|
||||||
const parsed = productParsingRegex.exec(query) as RegExpExecArray;
|
const parsed = productParsingRegex.exec(query);
|
||||||
return [Number.parseInt(parsed[1] || "1", 10), parsed[2]];
|
return [Number.parseInt(parsed[1] || "1", 10), parsed[2]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { BasketItem } from "#counter:counter/basket";
|
|||||||
import type {
|
import type {
|
||||||
CounterConfig,
|
CounterConfig,
|
||||||
CounterItem,
|
CounterItem,
|
||||||
|
ErrorMessage,
|
||||||
ProductFormula,
|
ProductFormula,
|
||||||
} from "#counter:counter/types";
|
} from "#counter:counter/types";
|
||||||
import type { CounterProductSelect } from "./components/counter-product-select-index";
|
import type { CounterProductSelect } from "./components/counter-product-select-index";
|
||||||
@@ -23,7 +24,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.codeField = this.$refs.codeField as CounterProductSelect;
|
this.codeField = this.$refs.codeField;
|
||||||
this.codeField.widget.hook("after", "onOptionSelect", () => {
|
this.codeField.widget.hook("after", "onOptionSelect", () => {
|
||||||
this.handleCode();
|
this.handleCode();
|
||||||
});
|
});
|
||||||
@@ -33,14 +34,14 @@ document.addEventListener("alpine:init", () => {
|
|||||||
// of a formset so we dynamically apply it here
|
// of a formset so we dynamically apply it here
|
||||||
this.$refs.basketManagementForm
|
this.$refs.basketManagementForm
|
||||||
.querySelector("#id_form-TOTAL_FORMS")
|
.querySelector("#id_form-TOTAL_FORMS")
|
||||||
?.setAttribute(":value", "getBasketSize()");
|
.setAttribute(":value", "getBasketSize()");
|
||||||
},
|
},
|
||||||
|
|
||||||
removeFromBasket(id: string) {
|
removeFromBasket(id: string) {
|
||||||
delete this.basket[id];
|
delete this.basket[id];
|
||||||
},
|
},
|
||||||
|
|
||||||
addToBasket(id: string, quantity: number) {
|
addToBasket(id: string, quantity: number): ErrorMessage {
|
||||||
const item: BasketItem =
|
const item: BasketItem =
|
||||||
this.basket[id] || new BasketItem(config.products[id], 0);
|
this.basket[id] || new BasketItem(config.products[id], 0);
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
|
|
||||||
if (item.quantity <= 0) {
|
if (item.quantity <= 0) {
|
||||||
delete this.basket[id];
|
delete this.basket[id];
|
||||||
return;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
this.basket[id] = item;
|
this.basket[id] = item;
|
||||||
@@ -71,7 +72,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
const products = new Set(
|
const products = new Set(
|
||||||
Object.values(this.basket).map((item: BasketItem) => item.product.productId),
|
Object.values(this.basket).map((item: BasketItem) => item.product.productId),
|
||||||
);
|
);
|
||||||
const formula = config.formulas.find((f: ProductFormula) => {
|
const formula: ProductFormula = config.formulas.find((f: ProductFormula) => {
|
||||||
return f.products.every((p: number) => products.has(p));
|
return f.products.every((p: number) => products.has(p));
|
||||||
});
|
});
|
||||||
if (formula === undefined) {
|
if (formula === undefined) {
|
||||||
@@ -79,13 +80,9 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}
|
}
|
||||||
// Now that the formula is found, remove the items composing it from the basket
|
// Now that the formula is found, remove the items composing it from the basket
|
||||||
for (const product of formula.products) {
|
for (const product of formula.products) {
|
||||||
const item = Object.entries(this.basket).find(
|
const key = Object.entries(this.basket).find(
|
||||||
([_, i]: [string, BasketItem]) => i.product.productId === product,
|
([_, i]: [string, BasketItem]) => i.product.productId === product,
|
||||||
);
|
)[0];
|
||||||
if (item === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const key = item[0];
|
|
||||||
this.basket[key].quantity -= 1;
|
this.basket[key].quantity -= 1;
|
||||||
if (this.basket[key].quantity <= 0) {
|
if (this.basket[key].quantity <= 0) {
|
||||||
this.removeFromBasket(key);
|
this.removeFromBasket(key);
|
||||||
@@ -95,7 +92,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
const result = Object.values(config.products)
|
const result = Object.values(config.products)
|
||||||
.filter((item: CounterItem) => item.productId === formula.result)
|
.filter((item: CounterItem) => item.productId === formula.result)
|
||||||
.reduce((acc, curr) => (acc.price.amount < curr.price.amount ? acc : curr));
|
.reduce((acc, curr) => (acc.price.amount < curr.price.amount ? acc : curr));
|
||||||
this.addToBasket(result.price.id.toString(), 1);
|
this.addToBasket(result.price.id, 1);
|
||||||
this.alertMessage.display(
|
this.alertMessage.display(
|
||||||
interpolate(
|
interpolate(
|
||||||
gettext("Formula %(formula)s applied"),
|
gettext("Formula %(formula)s applied"),
|
||||||
@@ -122,18 +119,14 @@ document.addEventListener("alpine:init", () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onRefillingSuccess(event: CustomEvent) {
|
onRefillingSuccess(event: CustomEvent) {
|
||||||
if (
|
if (event.type !== "htmx:after-request" || event.detail.failed) {
|
||||||
event.type !== "htmx:after-swap" ||
|
|
||||||
event.detail.failed ||
|
|
||||||
event.detail.elt.querySelector(".errorlist")
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.customerBalance += Number.parseFloat(
|
this.customerBalance += Number.parseFloat(
|
||||||
(event.detail.target.querySelector("#id_amount") as HTMLInputElement).value,
|
(event.detail.target.querySelector("#id_amount") as HTMLInputElement).value,
|
||||||
);
|
);
|
||||||
document.getElementById("selling-accordion")?.setAttribute("open", "");
|
document.getElementById("selling-accordion").setAttribute("open", "");
|
||||||
this.codeField?.widget.focus();
|
this.codeField.widget.focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
finish() {
|
finish() {
|
||||||
@@ -143,7 +136,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(this.$refs.basketForm as HTMLFormElement).submit();
|
this.$refs.basketForm.submit();
|
||||||
},
|
},
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
@@ -151,8 +144,6 @@ document.addEventListener("alpine:init", () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleCode() {
|
handleCode() {
|
||||||
if (!this.codeField) throw Error("Unexpected null codeField.");
|
|
||||||
|
|
||||||
const [quantity, code] = this.codeField.getSelectedProduct() as [number, string];
|
const [quantity, code] = this.codeField.getSelectedProduct() as [number, string];
|
||||||
|
|
||||||
if (this.codeField.getOperationCodes().includes(code.toUpperCase())) {
|
if (this.codeField.getOperationCodes().includes(code.toUpperCase())) {
|
||||||
|
|||||||
@@ -176,17 +176,13 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
<details
|
<details class="accordion" name="selling">
|
||||||
class="accordion"
|
|
||||||
name="selling"
|
|
||||||
@toggle="if ($event.newState === 'open') $el.querySelector('input[type=number]')?.focus()"
|
|
||||||
>
|
|
||||||
<summary>{% trans %}Refilling{% endtrans %}</summary>
|
<summary>{% trans %}Refilling{% endtrans %}</summary>
|
||||||
{% if object.type == "BAR" %}
|
{% if object.type == "BAR" %}
|
||||||
{% if refilling_fragment %}
|
{% if refilling_fragment %}
|
||||||
<div
|
<div
|
||||||
class="accordion-content"
|
class="accordion-content"
|
||||||
@htmx:after-swap="onRefillingSuccess"
|
@htmx:after-request="onRefillingSuccess"
|
||||||
>
|
>
|
||||||
{{ refilling_fragment }}
|
{{ refilling_fragment }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -144,8 +144,6 @@ class TestRefilling(TestFullClickBase):
|
|||||||
assert self.updated_amount(self.customer) == 0
|
assert self.updated_amount(self.customer) == 0
|
||||||
|
|
||||||
def test_refilling_no_refer_fail(self):
|
def test_refilling_no_refer_fail(self):
|
||||||
"""Check that the refill fails is the HTTP_REFERER header is missing"""
|
|
||||||
|
|
||||||
def refill():
|
def refill():
|
||||||
return self.client.post(
|
return self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
@@ -159,13 +157,13 @@ class TestRefilling(TestFullClickBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.client.force_login(self.club_admin)
|
self.client.force_login(self.club_admin)
|
||||||
assert refill().status_code == 403
|
assert refill()
|
||||||
|
|
||||||
self.client.force_login(self.root)
|
self.client.force_login(self.root)
|
||||||
assert refill().status_code == 403
|
assert refill()
|
||||||
|
|
||||||
self.client.force_login(self.subscriber)
|
self.client.force_login(self.subscriber)
|
||||||
assert refill().status_code == 403
|
assert refill()
|
||||||
|
|
||||||
assert self.updated_amount(self.customer) == 0
|
assert self.updated_amount(self.customer) == 0
|
||||||
|
|
||||||
@@ -201,17 +199,6 @@ class TestRefilling(TestFullClickBase):
|
|||||||
== 404
|
== 404
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_refilling_above_limit_fails(self):
|
|
||||||
"""Test that it's forbidden to refill a customer above the limit."""
|
|
||||||
self.login_in_bar()
|
|
||||||
limit = settings.SITH_ACCOUNT_MAX_MONEY
|
|
||||||
# create a refilling to check that current balance is taken into account
|
|
||||||
baker.make(Refilling, customer=self.customer.customer, amount=limit // 2)
|
|
||||||
response = self.refill_user(self.customer, self.counter, (limit // 2) + 1)
|
|
||||||
assert response.status_code == 200 # no redirect = failure
|
|
||||||
self.customer.customer.refresh_from_db()
|
|
||||||
assert self.updated_amount(self.customer) == limit // 2
|
|
||||||
|
|
||||||
def test_refilling_counter_success(self):
|
def test_refilling_counter_success(self):
|
||||||
self.login_in_bar()
|
self.login_in_bar()
|
||||||
|
|
||||||
@@ -535,19 +522,6 @@ class TestCounterClick(TestFullClickBase):
|
|||||||
|
|
||||||
assert self.updated_amount(self.customer) == Decimal(10)
|
assert self.updated_amount(self.customer) == Decimal(10)
|
||||||
|
|
||||||
def test_unrecord_above_limit_fails(self):
|
|
||||||
"""Test that it's forbidden to give back a recorded product
|
|
||||||
if it puts the account balance above the limit.
|
|
||||||
"""
|
|
||||||
self.login_in_bar()
|
|
||||||
limit = settings.SITH_ACCOUNT_MAX_MONEY
|
|
||||||
# put the account balance just at the limit
|
|
||||||
baker.make(Refilling, customer=self.customer.customer, amount=limit)
|
|
||||||
response = self.submit_basket(self.customer, [BasketItem(self.dcons.id, 1)])
|
|
||||||
assert response.status_code == 200 # no redirect = failure
|
|
||||||
self.customer.customer.refresh_from_db()
|
|
||||||
assert self.updated_amount(self.customer) == limit
|
|
||||||
|
|
||||||
def test_annotate_has_barman_queryset(self):
|
def test_annotate_has_barman_queryset(self):
|
||||||
"""Test if the custom queryset method `annotate_has_barman` works as intended."""
|
"""Test if the custom queryset method `annotate_has_barman` works as intended."""
|
||||||
counters = Counter.objects.annotate_has_barman(self.barmen)
|
counters = Counter.objects.annotate_has_barman(self.barmen)
|
||||||
@@ -786,10 +760,10 @@ class TestBarmanConnection(TestCase):
|
|||||||
assert last_perm.counter == self.counter
|
assert last_perm.counter == self.counter
|
||||||
assert last_perm.user == self.barman
|
assert last_perm.user == self.barman
|
||||||
assert last_perm.end is None
|
assert last_perm.end is None
|
||||||
|
assert self.barman in response.wsgi_request.barmen
|
||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
self.detail_url, {"username": self.barman.username, "password": "plop"}
|
self.detail_url, {"username": self.barman.username, "password": "plop"}
|
||||||
)
|
)
|
||||||
assert self.barman in response.wsgi_request.barmen
|
|
||||||
assert response.context_data.get("barmen") == [self.barman]
|
assert response.context_data.get("barmen") == [self.barman]
|
||||||
soup = BeautifulSoup(response.text, "lxml")
|
soup = BeautifulSoup(response.text, "lxml")
|
||||||
assert soup.find("form", id="select-user-form") is not None
|
assert soup.find("form", id="select-user-form") is not None
|
||||||
@@ -830,41 +804,6 @@ class TestBarmanConnection(TestCase):
|
|||||||
)
|
)
|
||||||
self.assert_counter_login_fails(self.barman)
|
self.assert_counter_login_fails(self.barman)
|
||||||
|
|
||||||
def test_barman_already_logged_in_another_device(self):
|
|
||||||
"""Test when the barman is already logged in the current counter on another device."""
|
|
||||||
other_client = Client()
|
|
||||||
other_client.post(
|
|
||||||
self.login_url, {"username": self.barman.username, "password": "plop"}
|
|
||||||
)
|
|
||||||
self.assert_counter_login_fails(self.barman)
|
|
||||||
|
|
||||||
def test_barman_login_elsewhere(self):
|
|
||||||
"""Test when the barman log himself out then log in on another device."""
|
|
||||||
self.client.post(
|
|
||||||
self.login_url, {"username": self.barman.username, "password": "plop"}
|
|
||||||
)
|
|
||||||
other_client = Client()
|
|
||||||
other_client.post(
|
|
||||||
reverse("counter:logout", kwargs={"counter_id": self.counter.id}),
|
|
||||||
data={"user_id": self.barman.id},
|
|
||||||
)
|
|
||||||
response = other_client.post(
|
|
||||||
self.login_url, {"username": self.barman.username, "password": "plop"}
|
|
||||||
)
|
|
||||||
assert response.status_code == 200
|
|
||||||
assert response.headers["HX-Redirect"] == self.detail_url
|
|
||||||
# the barmen should now be logged in `other_client`...
|
|
||||||
response = other_client.get(
|
|
||||||
self.detail_url, {"username": self.barman.username, "password": "plop"}
|
|
||||||
)
|
|
||||||
assert self.barman in response.wsgi_request.barmen
|
|
||||||
|
|
||||||
# ... but not in `self.client`
|
|
||||||
response = self.client.get(
|
|
||||||
self.detail_url, {"username": self.barman.username, "password": "plop"}
|
|
||||||
)
|
|
||||||
assert self.barman not in response.wsgi_request.barmen
|
|
||||||
|
|
||||||
def test_barman_already_logged_elsewhere(self):
|
def test_barman_already_logged_elsewhere(self):
|
||||||
"""Test when the barman is already logged in another counter."""
|
"""Test when the barman is already logged in another counter."""
|
||||||
other_counter = baker.make(Counter, type="BAR")
|
other_counter = baker.make(Counter, type="BAR")
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ from core.models import User
|
|||||||
from counter.baker_recipes import product_recipe, refill_recipe, sale_recipe
|
from counter.baker_recipes import product_recipe, refill_recipe, sale_recipe
|
||||||
from counter.models import (
|
from counter.models import (
|
||||||
Counter,
|
Counter,
|
||||||
CounterSellers,
|
|
||||||
Customer,
|
Customer,
|
||||||
Refilling,
|
Refilling,
|
||||||
ReturnableProduct,
|
ReturnableProduct,
|
||||||
@@ -39,7 +38,7 @@ class TestStudentCard(TestCase):
|
|||||||
cls.subscriber = subscriber_user.make()
|
cls.subscriber = subscriber_user.make()
|
||||||
|
|
||||||
cls.counter = baker.make(Counter, type="BAR")
|
cls.counter = baker.make(Counter, type="BAR")
|
||||||
CounterSellers.objects.create(counter=cls.counter, user=cls.barmen)
|
cls.counter.sellers.add(cls.barmen)
|
||||||
|
|
||||||
cls.club_counter = baker.make(Counter)
|
cls.club_counter = baker.make(Counter)
|
||||||
role = baker.make(ClubRole, club=cls.club_counter.club, is_board=True)
|
role = baker.make(ClubRole, club=cls.club_counter.club, is_board=True)
|
||||||
|
|||||||
+17
-22
@@ -24,7 +24,7 @@ from django.shortcuts import get_object_or_404, redirect, resolve_url
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.safestring import SafeString
|
from django.utils.safestring import SafeString
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import CreateView, FormView
|
from django.views.generic import FormView
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from ninja.main import HttpRequest
|
from ninja.main import HttpRequest
|
||||||
|
|
||||||
@@ -32,14 +32,7 @@ from core.auth.mixins import CanViewMixin
|
|||||||
from core.models import User
|
from core.models import User
|
||||||
from core.views.mixins import FragmentMixin, UseFragmentsMixin
|
from core.views.mixins import FragmentMixin, UseFragmentsMixin
|
||||||
from counter.forms import BasketForm, RefillForm
|
from counter.forms import BasketForm, RefillForm
|
||||||
from counter.models import (
|
from counter.models import Counter, Customer, ProductFormula, ReturnableProduct, Selling
|
||||||
Counter,
|
|
||||||
Customer,
|
|
||||||
ProductFormula,
|
|
||||||
Refilling,
|
|
||||||
ReturnableProduct,
|
|
||||||
Selling,
|
|
||||||
)
|
|
||||||
from counter.utils import is_logged_in_counter
|
from counter.utils import is_logged_in_counter
|
||||||
from counter.views.mixins import CounterTabsMixin
|
from counter.views.mixins import CounterTabsMixin
|
||||||
from counter.views.student_card import StudentCardFormFragment
|
from counter.views.student_card import StudentCardFormFragment
|
||||||
@@ -73,13 +66,13 @@ class CounterClick(
|
|||||||
current_tab = "counter"
|
current_tab = "counter"
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
return super().get_form_kwargs() | {
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs["form_kwargs"] = {
|
||||||
"customer": self.customer,
|
"customer": self.customer,
|
||||||
"counter": self.object,
|
"counter": self.object,
|
||||||
"form_kwargs": {
|
"allowed_prices": {price.id: price for price in self.prices},
|
||||||
"allowed_prices": {price.id: price for price in self.prices}
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
return kwargs
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.customer = get_object_or_404(Customer, user_id=self.kwargs["user_id"])
|
self.customer = get_object_or_404(Customer, user_id=self.kwargs["user_id"])
|
||||||
@@ -226,10 +219,9 @@ class CounterClick(
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class RefillingCreateView(FragmentMixin, CreateView):
|
class RefillingCreateView(FragmentMixin, FormView):
|
||||||
"""This is a fragment only view which integrates with counter_click.jinja"""
|
"""This is a fragment only view which integrates with counter_click.jinja"""
|
||||||
|
|
||||||
model = Refilling
|
|
||||||
form_class = RefillForm
|
form_class = RefillForm
|
||||||
template_name = "counter/fragments/create_refill.jinja"
|
template_name = "counter/fragments/create_refill.jinja"
|
||||||
|
|
||||||
@@ -250,20 +242,23 @@ class RefillingCreateView(FragmentMixin, CreateView):
|
|||||||
):
|
):
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
|
self.operator = get_operator(request, self.counter, self.customer)
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def render_fragment(self, request, **kwargs) -> SafeString:
|
def render_fragment(self, request, **kwargs) -> SafeString:
|
||||||
self.customer = kwargs.pop("customer")
|
self.customer = kwargs.pop("customer")
|
||||||
self.counter = kwargs.pop("counter")
|
self.counter = kwargs.pop("counter")
|
||||||
self.object = None
|
|
||||||
return super().render_fragment(request, **kwargs)
|
return super().render_fragment(request, **kwargs)
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def form_valid(self, form):
|
||||||
return super().get_form_kwargs() | {
|
res = super().form_valid(form)
|
||||||
"counter": self.counter,
|
form.clean()
|
||||||
"operator": get_operator(self.request, self.counter, self.customer),
|
form.instance.counter = self.counter
|
||||||
"customer": self.customer,
|
form.instance.operator = self.operator
|
||||||
}
|
form.instance.customer = self.customer
|
||||||
|
form.instance.save()
|
||||||
|
return res
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ from django.views.generic.edit import FormView
|
|||||||
from core.auth.mixins import CanViewMixin
|
from core.auth.mixins import CanViewMixin
|
||||||
from core.views import FragmentMixin, UseFragmentsMixin
|
from core.views import FragmentMixin, UseFragmentsMixin
|
||||||
from counter.forms import CounterLoginForm, GetUserForm
|
from counter.forms import CounterLoginForm, GetUserForm
|
||||||
from counter.middleware import SESSION_PERMANENCES_KEY
|
|
||||||
from counter.models import Counter, Permanency
|
from counter.models import Counter, Permanency
|
||||||
from counter.utils import is_logged_in_counter
|
from counter.utils import is_logged_in_counter
|
||||||
from counter.views.mixins import CounterTabsMixin
|
from counter.views.mixins import CounterTabsMixin
|
||||||
@@ -59,8 +58,8 @@ class CounterLoginFragment(FragmentMixin, SingleObjectMixin, FormView):
|
|||||||
|
|
||||||
def form_valid(self, form: CounterLoginForm):
|
def form_valid(self, form: CounterLoginForm):
|
||||||
user = form.get_user()
|
user = form.get_user()
|
||||||
perm = self.object.permanencies.create(user=user, start=timezone.now())
|
self.object.permanencies.create(user=user, start=timezone.now())
|
||||||
self.request.session.setdefault(SESSION_PERMANENCES_KEY, []).append(perm.id)
|
self.request.barmen.add(user)
|
||||||
self.success_url = reverse(
|
self.success_url = reverse(
|
||||||
"counter:details", kwargs={"counter_id": self.object.id}
|
"counter:details", kwargs={"counter_id": self.object.id}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ interface BasketItem {
|
|||||||
name: string;
|
name: string;
|
||||||
quantity: number;
|
quantity: number;
|
||||||
unitPrice: number;
|
unitPrice: number;
|
||||||
isRefill: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const BASKET_CACHE_KEY = "basket";
|
const BASKET_CACHE_KEY = "basket";
|
||||||
const BASKET_CACHE_VERSION = 2;
|
const BASKET_CACHE_VERSION = 1;
|
||||||
|
|
||||||
document.addEventListener("alpine:init", () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
Alpine.data("basket", (validPrices: number[], lastPurchaseTime?: number) => ({
|
Alpine.data("basket", (validPrices: number[], lastPurchaseTime?: number) => ({
|
||||||
@@ -22,7 +21,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
});
|
});
|
||||||
document
|
document
|
||||||
.getElementById("id_form-TOTAL_FORMS")
|
.getElementById("id_form-TOTAL_FORMS")
|
||||||
?.setAttribute(":value", "basket.length");
|
.setAttribute(":value", "basket.length");
|
||||||
},
|
},
|
||||||
|
|
||||||
loadBasket(): BasketItem[] {
|
loadBasket(): BasketItem[] {
|
||||||
@@ -33,8 +32,8 @@ document.addEventListener("alpine:init", () => {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
lastPurchaseTime &&
|
lastPurchaseTime !== null &&
|
||||||
localStorage.basketTimestamp &&
|
localStorage.basketTimestamp !== undefined &&
|
||||||
new Date(lastPurchaseTime) >=
|
new Date(lastPurchaseTime) >=
|
||||||
new Date(Number.parseInt(localStorage.basketTimestamp, 10))
|
new Date(Number.parseInt(localStorage.basketTimestamp, 10))
|
||||||
) {
|
) {
|
||||||
@@ -65,19 +64,6 @@ document.addEventListener("alpine:init", () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the total of money that would be added to the AE account on basket purchase.
|
|
||||||
*/
|
|
||||||
getTotalAdded() {
|
|
||||||
return this.basket
|
|
||||||
.filter((item) => item.isRefill || item.unitPrice < 0)
|
|
||||||
.reduce(
|
|
||||||
(acc: number, item: BasketItem) =>
|
|
||||||
acc + Math.abs(item.quantity * item.unitPrice),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add 1 to the quantity of an item in the basket
|
* Add 1 to the quantity of an item in the basket
|
||||||
* @param {BasketItem} item
|
* @param {BasketItem} item
|
||||||
@@ -100,7 +86,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
|
|
||||||
if (this.basket[index].quantity === 0) {
|
if (this.basket[index].quantity === 0) {
|
||||||
this.basket = this.basket.filter(
|
this.basket = this.basket.filter(
|
||||||
(e: BasketItem) => e.priceId !== this.basket[index].priceId,
|
(e: BasketItem) => e.priceId !== this.basket[index].id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -117,16 +103,14 @@ document.addEventListener("alpine:init", () => {
|
|||||||
* @param id The id of the product to add
|
* @param id The id of the product to add
|
||||||
* @param name The name of the product
|
* @param name The name of the product
|
||||||
* @param price The unit price of the product
|
* @param price The unit price of the product
|
||||||
* @param isRefill true if the product is a refill bond
|
|
||||||
* @returns The created item
|
* @returns The created item
|
||||||
*/
|
*/
|
||||||
createItem(id: number, name: string, price: number, isRefill: boolean): BasketItem {
|
createItem(id: number, name: string, price: number): BasketItem {
|
||||||
const newItem = {
|
const newItem = {
|
||||||
priceId: id,
|
priceId: id,
|
||||||
name,
|
name,
|
||||||
quantity: 0,
|
quantity: 0,
|
||||||
unitPrice: price,
|
unitPrice: price,
|
||||||
isRefill,
|
|
||||||
} as BasketItem;
|
} as BasketItem;
|
||||||
|
|
||||||
this.basket.push(newItem);
|
this.basket.push(newItem);
|
||||||
@@ -141,17 +125,16 @@ document.addEventListener("alpine:init", () => {
|
|||||||
* @param id The id of the product to add
|
* @param id The id of the product to add
|
||||||
* @param name The name of the product
|
* @param name The name of the product
|
||||||
* @param price The unit price of the product
|
* @param price The unit price of the product
|
||||||
* @param isRefill true if the product is a refill bond
|
|
||||||
*/
|
*/
|
||||||
addFromCatalog(id: number, name: string, price: number, isRefill: boolean) {
|
addFromCatalog(id: number, name: string, price: number) {
|
||||||
const item = this.basket.find((e: BasketItem) => e.priceId === id);
|
let item = this.basket.find((e: BasketItem) => e.priceId === id);
|
||||||
|
|
||||||
// if the item is not in the basket, we create it
|
// if the item is not in the basket, we create it
|
||||||
// else we add + 1 to it
|
// else we add + 1 to it
|
||||||
if (item) {
|
if (item) {
|
||||||
this.add(item);
|
this.add(item);
|
||||||
} else {
|
} else {
|
||||||
this.createItem(id, name, price, isRefill);
|
item = this.createItem(id, name, price);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -58,17 +58,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<template x-if="(getTotalAdded() + {{ customer_amount }}) > {{ settings.SITH_ACCOUNT_MAX_MONEY }}">
|
|
||||||
<div class="alert alert-red">
|
|
||||||
<div class="alert-main">
|
|
||||||
{% trans trimmed limit=settings.SITH_ACCOUNT_MAX_MONEY %}
|
|
||||||
You cannot purchase the current basket,
|
|
||||||
because it would put your AE account balance
|
|
||||||
above the {{ limit }}€ limit
|
|
||||||
{% endtrans %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<ul class="item-list">
|
<ul class="item-list">
|
||||||
{# Starting money #}
|
{# Starting money #}
|
||||||
<li>
|
<li>
|
||||||
@@ -120,12 +109,9 @@
|
|||||||
<i class="fa fa-trash"></i>
|
<i class="fa fa-trash"></i>
|
||||||
{% trans %}Clear{% endtrans %}
|
{% trans %}Clear{% endtrans %}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="btn btn-blue">
|
||||||
class="btn btn-blue"
|
|
||||||
:disabled="(getTotalAdded() + {{ customer_amount }}) > {{ settings.SITH_ACCOUNT_MAX_MONEY }}"
|
|
||||||
>
|
|
||||||
<i class="fa fa-check"></i>
|
<i class="fa fa-check"></i>
|
||||||
{% trans %}Validate{% endtrans %}
|
<input type="submit" value="{% trans %}Validate{% endtrans %}"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -213,12 +199,7 @@
|
|||||||
id="{{ price.id }}"
|
id="{{ price.id }}"
|
||||||
class="card clickable shadow"
|
class="card clickable shadow"
|
||||||
:class="{selected: basket.some((i) => i.priceId === {{ price.id }})}"
|
:class="{selected: basket.some((i) => i.priceId === {{ price.id }})}"
|
||||||
@click='addFromCatalog(
|
@click='addFromCatalog({{ price.id }}, {{ price.full_label|tojson }}, {{ price.amount }})'
|
||||||
{{ price.id }},
|
|
||||||
{{ price.full_label|tojson }},
|
|
||||||
{{ price.amount }},
|
|
||||||
{{ (price.product.product_type_id == settings.SITH_COUNTER_PRODUCTTYPE_REFILLING)|lower }}
|
|
||||||
)'
|
|
||||||
{% if price.sold_out %}disabled{% endif %}
|
{% if price.sold_out %}disabled{% endif %}
|
||||||
>
|
>
|
||||||
{% if price.product.icon %}
|
{% if price.product.icon %}
|
||||||
|
|||||||
@@ -278,27 +278,6 @@ class TestEboutic(TestCase):
|
|||||||
)
|
)
|
||||||
assert Basket.objects.count() == 2
|
assert Basket.objects.count() == 2
|
||||||
|
|
||||||
def test_refill_limit(self):
|
|
||||||
"""Test that an eboutic basket cannot refill an account above the limit."""
|
|
||||||
self.client.force_login(self.subscriber)
|
|
||||||
product = product_recipe.make(
|
|
||||||
product_type_id=settings.SITH_COUNTER_PRODUCTTYPE_REFILLING,
|
|
||||||
counters=[self.eboutic],
|
|
||||||
)
|
|
||||||
price = price_recipe.make(
|
|
||||||
product=product,
|
|
||||||
groups=[self.group_cotiz],
|
|
||||||
amount=settings.SITH_ACCOUNT_MAX_MONEY // 10,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.submit_basket([BasketItem(price.id, 10)])
|
|
||||||
assert Basket.objects.count() == 1
|
|
||||||
assertRedirects(response, reverse("eboutic:checkout", kwargs={"basket_id": 1}))
|
|
||||||
|
|
||||||
response = self.submit_basket([BasketItem(price.id, 11)])
|
|
||||||
assert Basket.objects.count() == 1
|
|
||||||
assert response.status_code == 200 # no redirect = form validation failed
|
|
||||||
|
|
||||||
def test_create_basket(self):
|
def test_create_basket(self):
|
||||||
self.client.force_login(self.new_customer)
|
self.client.force_login(self.new_customer)
|
||||||
assertRedirects(
|
assertRedirects(
|
||||||
|
|||||||
+6
-4
@@ -66,7 +66,9 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class BaseEbouticBasketForm(BaseBasketForm):
|
class BaseEbouticBasketForm(BaseBasketForm):
|
||||||
min_result_balance = None # user can pay by card, so no minimum enforced
|
def _check_enough_money(self, *args, **kwargs):
|
||||||
|
# Disable money check
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
EbouticBasketForm = forms.formset_factory(
|
EbouticBasketForm = forms.formset_factory(
|
||||||
@@ -86,15 +88,15 @@ class EbouticMainView(LoginRequiredMixin, FormView):
|
|||||||
form_class = EbouticBasketForm
|
form_class = EbouticBasketForm
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
return super().get_form_kwargs() | {
|
kwargs = super().get_form_kwargs()
|
||||||
|
kwargs["form_kwargs"] = {
|
||||||
"customer": self.customer,
|
"customer": self.customer,
|
||||||
"counter": get_eboutic(),
|
"counter": get_eboutic(),
|
||||||
"form_kwargs": {
|
|
||||||
"allowed_prices": {
|
"allowed_prices": {
|
||||||
price.id: price for price in self.prices if not price.sold_out
|
price.id: price for price in self.prices if not price.sold_out
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
return kwargs
|
||||||
|
|
||||||
def form_valid(self, formset):
|
def form_valid(self, formset):
|
||||||
if len(formset) == 0:
|
if len(formset) == 0:
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-06-10 20:18+0200\n"
|
"POT-Creation-Date: 2026-06-05 13:39+0200\n"
|
||||||
"PO-Revision-Date: 2016-07-18\n"
|
"PO-Revision-Date: 2016-07-18\n"
|
||||||
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
"Last-Translator: Maréchal <thomas.girod@utbm.fr\n"
|
||||||
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
"Language-Team: AE info <ae.info@utbm.fr>\n"
|
||||||
@@ -3217,10 +3217,6 @@ msgstr "Vous êtes déjà connecté à ce comptoir."
|
|||||||
msgid "You are already logged in another counter."
|
msgid "You are already logged in another counter."
|
||||||
msgstr "Vous êtes déjà connecté à un autre comptoir."
|
msgstr "Vous êtes déjà connecté à un autre comptoir."
|
||||||
|
|
||||||
#: counter/forms.py
|
|
||||||
msgid "You are already logged on another device"
|
|
||||||
msgstr "Vous êtes déjà connecté sur un autre appareil"
|
|
||||||
|
|
||||||
#: counter/forms.py
|
#: counter/forms.py
|
||||||
msgid "Regular barmen"
|
msgid "Regular barmen"
|
||||||
msgstr "Barmen réguliers"
|
msgstr "Barmen réguliers"
|
||||||
@@ -3310,11 +3306,6 @@ msgstr "Saisie de produit dupliquée"
|
|||||||
msgid "Not enough money"
|
msgid "Not enough money"
|
||||||
msgstr "Solde insuffisant"
|
msgstr "Solde insuffisant"
|
||||||
|
|
||||||
#: counter/forms.py counter/models.py
|
|
||||||
#, python-format
|
|
||||||
msgid "There cannot be more than %(money)d€ on an AE account"
|
|
||||||
msgstr "Il ne peut pas y avoir plus de %(money)d€ sur un compte AE"
|
|
||||||
|
|
||||||
#: counter/forms.py
|
#: counter/forms.py
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid ""
|
msgid ""
|
||||||
@@ -4437,15 +4428,6 @@ msgstr "Payer avec un compte AE"
|
|||||||
msgid "The online shop of the association."
|
msgid "The online shop of the association."
|
||||||
msgstr "La boutique en ligne de l'association."
|
msgstr "La boutique en ligne de l'association."
|
||||||
|
|
||||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"You cannot purchase the current basket, because it would put your AE account "
|
|
||||||
"balance above the %(limit)s€ limit"
|
|
||||||
msgstr ""
|
|
||||||
"Vous ne pouvez pas finaliser le panier actuel, parce que le solde de votre "
|
|
||||||
"compte AE passerait au-dessus de la limite de %(limit)s€."
|
|
||||||
|
|
||||||
#: eboutic/templates/eboutic/eboutic_main.jinja
|
#: eboutic/templates/eboutic/eboutic_main.jinja
|
||||||
msgid "Clear"
|
msgid "Clear"
|
||||||
msgstr "Vider"
|
msgstr "Vider"
|
||||||
|
|||||||
+2
-2
@@ -23,9 +23,9 @@ dependencies = [
|
|||||||
"django-ninja>=1.6.2,<2.0.0",
|
"django-ninja>=1.6.2,<2.0.0",
|
||||||
"django-ninja-extra>=0.31.4",
|
"django-ninja-extra>=0.31.4",
|
||||||
"Pillow>=12.2.0,<13.0.0",
|
"Pillow>=12.2.0,<13.0.0",
|
||||||
"aemark>=0.1.1",
|
"mistune>=3.2.1,<4.0.0",
|
||||||
"django-jinja<3.0.0,>=2.11.0",
|
"django-jinja<3.0.0,>=2.11.0",
|
||||||
"cryptography>=48.0.0,<49.0.0",
|
"cryptography>=48.0.1,<49.0.0",
|
||||||
"django-phonenumber-field>=8.4.0,<9.0.0",
|
"django-phonenumber-field>=8.4.0,<9.0.0",
|
||||||
"phonenumbers>=9.0.32,<10.0.0",
|
"phonenumbers>=9.0.32,<10.0.0",
|
||||||
"reportlab>=4.5.1,<5.0.0",
|
"reportlab>=4.5.1,<5.0.0",
|
||||||
|
|||||||
@@ -503,12 +503,6 @@ SITH_ACCOUNT_INACTIVITY_DELTA = relativedelta(years=2)
|
|||||||
SITH_ACCOUNT_DUMP_DELTA = timedelta(days=30)
|
SITH_ACCOUNT_DUMP_DELTA = timedelta(days=30)
|
||||||
"""timedelta between the warning mail and the actual account dump"""
|
"""timedelta between the warning mail and the actual account dump"""
|
||||||
|
|
||||||
SITH_ACCOUNT_MAX_MONEY = 250 # €
|
|
||||||
"""Maximum amount of money a sith account can hold.
|
|
||||||
|
|
||||||
This amount is defined by the AE's Terms and Conditions of Sale.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Defines which product type is the refilling type,
|
# Defines which product type is the refilling type,
|
||||||
# and thus increases the account amount
|
# and thus increases the account amount
|
||||||
SITH_COUNTER_PRODUCTTYPE_REFILLING = env.int(
|
SITH_COUNTER_PRODUCTTYPE_REFILLING = env.int(
|
||||||
|
|||||||
@@ -6,71 +6,6 @@ resolution-markers = [
|
|||||||
"python_full_version < '3.13'",
|
"python_full_version < '3.13'",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "aemark"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/11/97/35c298a8557d58f10402f40cc04d8bccba8dd2ab5c89bbbb41ce82f9ae29/aemark-0.1.1.tar.gz", hash = "sha256:01c19374553a1fc4ec5b5b0609a28dea8e724395c4383e2c41e292ee9bf1b9b2", size = 32558, upload-time = "2026-06-19T10:10:17.016Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/0c/b35426c7d1d52b6089d8d4fa8b50e1ca965475a42f000e4855468ea1889f/aemark-0.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d79173c2c5311eea1f5043c24bfb7368ece6df1d041a3cbd51bbc54a3ca8f630", size = 467866, upload-time = "2026-06-19T10:09:09.99Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/b3/8f85ca830a2d3aae2f37383b3eab8d382e438f8c1f764517eaf10d1fd026/aemark-0.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3d3facf829907cadabf2154a86c562fa05b8528f418f933d4517f8b6192bab0f", size = 455182, upload-time = "2026-06-19T10:09:11.172Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/f2/9ff2fe99851e7c7c30da7d7791f3a21868f046b5f2bd326dd52e80d65aca/aemark-0.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbb356e5c208a8f0f4b725117a8075c6fc54911fc62fc7f974934402faa0bab7", size = 544038, upload-time = "2026-06-19T10:09:12.308Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/09/354c635e6f1bcc525ed8923f5f0ed7b7a95c12a5fa211ed4d827c16de41d/aemark-0.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76615cd0fa97dd020aac3748c8fb9dda585d7eea0e151361f872628a1e70ee5a", size = 494693, upload-time = "2026-06-19T10:09:13.422Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/fb/cba45091950f393d909aa4df8df903e795c7b9ba64c208c03aa949b41f4f/aemark-0.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d79a1246166babf5266abd45c39cc584f30cef2a0b6f4cb38384ee63f559c62", size = 597809, upload-time = "2026-06-19T10:09:14.617Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/06/9e403d55750f0c8797379a02bfc39931ccbb97622105b50e84cd6d9e863e/aemark-0.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:802234cbfd60ccd985ce4d6320217f3280319579590a6dd22383d1481130ad00", size = 654480, upload-time = "2026-06-19T10:09:16.078Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/97/9feb3169899d444578f39d1caeab3d92b7e8d629d6a24d44bbda929bc4d0/aemark-0.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b957e390ecd3d9ab22be61bf95214d33673fb2ed1a09617e1b443a8bba1a8cbc", size = 516859, upload-time = "2026-06-19T10:09:17.335Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/b4/f842b159b46ce4fd4271afaa9107517d5f69574c8ff057f9bd65f0b5805b/aemark-0.1.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6947de45713b228190a7bddb52895a5c0b402ff00ff18acb967de7dceded1fa2", size = 518199, upload-time = "2026-06-19T10:09:18.327Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/4f/1e1c1d68a4dbe9aea6b13e7e33bbf3ddd229a86a1fc75b7c2c71d48eb06a/aemark-0.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b633b0a171716424a84b44841c2f43f799da3fe04e3bd2ac356e587f333d98d", size = 720953, upload-time = "2026-06-19T10:09:19.527Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/e8/9b20d96bff485a0e5dabc610d1b7a47373d161b53f82d260b27ca2104b63/aemark-0.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:abd49b2cf5af61cf2b8c2a6e45a87df055b003a45fd10a94a6022857af15a115", size = 771507, upload-time = "2026-06-19T10:09:20.742Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/8d/dd924c4bf790670a198b64b8d99e0d8b3f8a3462d96b241a759edb2bdfa5/aemark-0.1.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0d48f411e544724cb4c5b5932369785dc1c97d32e9f04cf664607f1e67a33874", size = 737232, upload-time = "2026-06-19T10:09:21.797Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/3a/587f3b177ff1af11d91f5c5ba01acfe9ad4875ab7c72d73b4c2a3b090afe/aemark-0.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:66727dc7da3da45ae1c0405d9069d0edd6e5c48fce696ea5e327dce46fbcdb3a", size = 769368, upload-time = "2026-06-19T10:09:23.026Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/cb/9ac41a31e16c66f33bf9366004baeabeab04b0d8f9e2e6867447d6946c74/aemark-0.1.1-cp312-cp312-win32.whl", hash = "sha256:bc4f9e31f97454b70cb0b5b7de25252a59a0e92faef886eda74eb1f65f8c973d", size = 382527, upload-time = "2026-06-19T10:09:24.273Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/01/bc/c629780bc72973cc1cc3a4ca91720daa8edf37bbc2a2c29ceeea797375ca/aemark-0.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:332b51d6ff927a182d16d4da40930d860abe4a99d99d3219816a5442f7ebecf7", size = 394288, upload-time = "2026-06-19T10:09:25.479Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/81/9b/6f6238f0d3c09e1472294b24c3938b97db27ff0952c642b7d3ef12ea4f9d/aemark-0.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:28919d11340c54e541fe31fbadd297686c670054bb93e02c5ecd0a5225ce8f8f", size = 382863, upload-time = "2026-06-19T10:09:26.457Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/1f/bebf99bd5eda377347923448115007389952f29e15895208527af00921ef/aemark-0.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:93a69b7e3be6b00ffcd38be8970c9fe6d99a57844da43fef5205fa834d99fe66", size = 468060, upload-time = "2026-06-19T10:09:27.404Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/c8/24b15087d37128bd7bacada063560d269f6c86ebec58df61ec95b27d78b4/aemark-0.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bb9bf8095b4965a1a8566ca5ea2795fc96f035e36bfd9e472bd2ed3718f01dc3", size = 455308, upload-time = "2026-06-19T10:09:28.466Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/33/b1b3e8e2f667f6d6e38112bdf65f32fa19f9d7b8544feb735aba124c1a10/aemark-0.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d4d1ab6c2bd01a1ec9018c60a2c35ddc1a3d3e37dbba459cb92087f302eb4f7", size = 544214, upload-time = "2026-06-19T10:09:29.64Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/e7/abcfc081d5562d004bc953b8986f25cf500df625ce9874223659bc36b11e/aemark-0.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d296b232d52e795a436b1af211d700903eb6d562567a03d0c79db88188b9936", size = 494722, upload-time = "2026-06-19T10:09:30.974Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/66/0d00cff512be2e06df7f25e62830644846738db2057253f59affbc572f67/aemark-0.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaa6d0b261396b615d45b4ea0b6419625d113cae2d5d0a8e52c08f70494eb943", size = 598196, upload-time = "2026-06-19T10:09:32.044Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/ae/feacddee57fc9c57bfd9d4ec86f6c6626af3cb4a07220eb527404fc8dcda/aemark-0.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4bee927e236005b414dcc274d26be867f02a8de96dcd462a0d9c6944c367fbf0", size = 654718, upload-time = "2026-06-19T10:09:33.073Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/da/00b2bc275c4916a148581dad67914a114554a292964b30ce7c5091251acb/aemark-0.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d39be7d7a5658cebde6e89b3723f82946937589af344e200afa9778e3060e1d", size = 516971, upload-time = "2026-06-19T10:09:34.143Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/7b/c21ae7ee4d3b3f6992f4767953e58db1bd0485fcf7f43fdba47f723d3ebd/aemark-0.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:109315067b14a20e83fde0a5178098731b4f5cd86cd7905f827458946fad1246", size = 518272, upload-time = "2026-06-19T10:09:35.187Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/f8/977f20abc0955b927e60c2b457f3879e3bcde7502054c9b063436e4092b5/aemark-0.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:512eda328fc5494e76af82b59f270214cc50f171c3e08800e80067e9b345038c", size = 721198, upload-time = "2026-06-19T10:09:36.227Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/9e/7ee511429869911a8633d6d172cad26030579c43723c2e610453af1ef06d/aemark-0.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2a644c5630de531fc75a5d63b056a95aedbc6ef4e7883cc29327ac951d535faf", size = 771590, upload-time = "2026-06-19T10:09:37.661Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/90/8b76cfff288bf7aed4173298e453f2add446c8d18abefa3f3abc72924e7f/aemark-0.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c4122ccfba4d5d3a3cb42e6697051543a26ba7128ab7cd4eb6ed3fe5131d8a22", size = 737238, upload-time = "2026-06-19T10:09:38.861Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/d9/3c06cccc62e2aee921e635a00d595f8550b2c0270472779116a6a9be55ff/aemark-0.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1d5f5e22603f97ba2b643426527cb3c19d79463b0bb64fbe03ac2d9e57dc993a", size = 769243, upload-time = "2026-06-19T10:09:40.046Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/2e/8579bf4f4abb1a54195f5f619d4d7d5cd34fe66c7b6ee587d4b5b7c24725/aemark-0.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:e37b06a2cfdab748f280dfc229e37c4c3a82c98b4f49d0d02893a0785c1a8796", size = 394367, upload-time = "2026-06-19T10:09:41.257Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/08/0678635f86109fffdb847f899b4409e6b99fbf967472efdeff3c57e1674c/aemark-0.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:2993588ec27439cc5adc7917851f9d04ee3c95283f1290fe78379d90fbc25247", size = 383114, upload-time = "2026-06-19T10:09:42.474Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/94/b34c102620515afef1a088ae9a82d4abe332ac3075be375d0be70edc95d5/aemark-0.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d0e69cd9da806d2b94ec016544d97a325fcb22477b60c9bf1236521b6978bea1", size = 468275, upload-time = "2026-06-19T10:09:43.686Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/45/c879bb7b354a4e47b8c0b6b20ac95606ccf75512e81c6327487c7a7c2d7a/aemark-0.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20b902d032885f765ce762fa25dbd59fee9f8aa7b827ecc9f16c58496e01e723", size = 455415, upload-time = "2026-06-19T10:09:44.885Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/12/b7bc6d238c0a1c22095d6aa960de2a4c53c73ceea725cca0a007ac75c291/aemark-0.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304667c3a0baa3f7b1cb1170e48cd35001c9aee177af2bf80680f614dd471aeb", size = 543093, upload-time = "2026-06-19T10:09:46.223Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/fa/09cdf04f9859141a149661d709f4d35bc330a98926513f0002c400dff2da/aemark-0.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd161530d9b80640a55fbcfe16289c6e8cb615ae1046ade11e112d3bfa182036", size = 495041, upload-time = "2026-06-19T10:09:47.252Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/74/7ecbd5a4f5120cbb90ea6e2837a36bfc3cf943a6e17472d36ca91b33c518/aemark-0.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea4b3c171bf1d9feaef1ff12321bf94f9e3eb7db38be8f66b408f1d0959f8b2", size = 597685, upload-time = "2026-06-19T10:09:48.348Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/29/b4/171ceb88b503fff6740e46c42d1d17f7db861b7e1e56901f74e11a02fb11/aemark-0.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd8503d1bc5199e1484311b6d65ee380fd22aeb8ec367651c0e39c4d9f27c87f", size = 654372, upload-time = "2026-06-19T10:09:49.428Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/2b/ceed2a269503af5912932a233218c184ba464ec0d4b1a389eccdd760e760/aemark-0.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3aba427b898c2430bacfe4ded62bef90bb5353b452a1e9d0add6179ca851df4", size = 515791, upload-time = "2026-06-19T10:09:50.546Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/38/12f9b5107e9afcab320a84d9d46300cad5bfaf2e90a6a3d9cea248adbcdb/aemark-0.1.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d568d8e99ac0730ab5030cf34987eb22e7b9d50e44964233c161e756f527e6c", size = 519423, upload-time = "2026-06-19T10:09:51.634Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/2d/f8d4fae48934362be4ebcb79b6682d991e823ee8e4de6ef25532307c0d9f/aemark-0.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34530bd2b4e59f6a5e0c4fd715ab34496b0bb5d00b4288b3ee93db43c88a810f", size = 720464, upload-time = "2026-06-19T10:09:52.849Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/2d/db1dc2c2024ac6be28f312aeefb5d1b15b85b2ec329f9cac4bf334b1f358/aemark-0.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:faa4b36ad06ebd5f52f49b96ae440330e4220c5343dc69e7b1b92ff30c612089", size = 772108, upload-time = "2026-06-19T10:09:54.157Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/87/3f2e6b63e3f3b5df70a63bc1d43e213b7b02937447ab6fc94c4ff730a817/aemark-0.1.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fffc0c89abde3dbdf506131e1b1416e499e4543031b558d82cc2c0cd0de450ac", size = 737662, upload-time = "2026-06-19T10:09:55.545Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/17/0e033216483a8ae5d0dde9ae8c0f78b464b62aea70aec9ee5a171aef5e68/aemark-0.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d54c91b7fc7df65553069ea8db6ec8c70c5685735130b177de95b6f436bca81", size = 768312, upload-time = "2026-06-19T10:09:56.803Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/0b/82fe9178afe8aea4c172a8563694afe3519ea29e04d9ca4ccdf3fabdbc17/aemark-0.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:78c706e546fe8899e7a507820b9d63f8938a64158bbd0d8d344f86daaf15a4d8", size = 391756, upload-time = "2026-06-19T10:09:57.916Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/21/ca/9eabfb99fddb3a9266336674120fd8df1ae58d5651f584cabd86a806cba6/aemark-0.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:35399ce8ce8e15e8d2d8a8d007c9b8dfc2a92a923493429e8657b046bcafb366", size = 381522, upload-time = "2026-06-19T10:09:59.082Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/be/dbfad8d80a2f037b769166453b73606e8d6aa672de076717a50fad6a8f22/aemark-0.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:581fd7dbf57e37e006f23d45e7e401f32c83907a4bb6dee845cab0f586cf9e79", size = 542373, upload-time = "2026-06-19T10:10:00.197Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/0c/cffd4911980af1391057fd1e12473fc915c3b51c5c543253e6eb325755a3/aemark-0.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a804d3ffae6bb6d4673e67536f96df4cf0cd48795ffa75b6d9bbaa469d11382d", size = 494187, upload-time = "2026-06-19T10:10:01.321Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/54/620599105646977be2d3766964a24aa45a35dc5aff4838f6edb3a948aea7/aemark-0.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:67dcf05f6f9b0ffb73d3f7baeb4ada87d6b5dc26ec56aca3527abfa7916f05df", size = 596882, upload-time = "2026-06-19T10:10:02.384Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/d8/d0a192801ef68eac915e9157006f4f53a10ed7c0b0b46ac9e372218df9b1/aemark-0.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27a61f80c10ee12ca33c4c3e07304f4b84cdb8245ea274b558a970ceb6767817", size = 652553, upload-time = "2026-06-19T10:10:03.668Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/fa/de4969f930ed199d3e395fbff1821a50928ca47567b161b9f21c5eaabcfb/aemark-0.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba20a42edeb94c99c693e54d80c4c4e68f33e53c3e68422499bf3dca6429254a", size = 514859, upload-time = "2026-06-19T10:10:04.8Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/ad/e8db07f9d0a1a3c314e654ab852a1f95074f0aa2c8922a48fa8dc54ce453/aemark-0.1.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:201208eb9feb93ed1e7ec1217395ed5292ab80b5b903fbfe71cedd636bb87eea", size = 517850, upload-time = "2026-06-19T10:10:06.047Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/72/58/8273730c1e1b3bc622bce10d23dc5e1a6309e48de47c13514a4b5fd3d67b/aemark-0.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:813b76ae8668415a58cdf7c78eff2fe0fa1a26725257adf123d715d6ff4a92b1", size = 719901, upload-time = "2026-06-19T10:10:07.423Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/1f/2d6f5192fd52ced126af5851c7919b574cce36827195a2eb7c6fad38a54d/aemark-0.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:14fec671ca08a4ef55305bfaf267a1671629735f2b281658adf8622280f641e2", size = 770928, upload-time = "2026-06-19T10:10:08.731Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/2f/c0fb323da8d12b18d020949aa15636c97b7ac2cd71ab05b51ad0fbc70af1/aemark-0.1.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:24d4c41d58afbda86332f6c1c6cb6ceccfb087428aaa834ad8c95c55920c017c", size = 737035, upload-time = "2026-06-19T10:10:10.016Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/2f/d17b3f892753a6c61f30ff97ba23d32d07bfa758feeddfac9d256d86246e/aemark-0.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3d4a21593fbf6ce2eb44ed746fc9d98ee2a1c21b5e7d2956c9ffed0a51fa8dca", size = 767584, upload-time = "2026-06-19T10:10:11.21Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/20/7dd827e7f6548d40737ebe90440f50c0229ea4c4a34f6704573d990877cb/aemark-0.1.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8ef394c8c73956adcd978bf0193cd31fc77bed3f35711c6b222fd2baa870737", size = 515792, upload-time = "2026-06-19T10:10:12.478Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/49/c93aef2c5e514ecdb4025668ed6fb2e3ae1d274d00e379e2cf8840f8b04c/aemark-0.1.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64cc5b3ec2d5b2f95fe9fc2b2b4d50fae55f2aa0c100f809692cd62265ea6007", size = 519595, upload-time = "2026-06-19T10:10:13.517Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/42/24/a368d11633b6c9a6f74fb14231d638c94ab5704cee83f8cbf10290ff4822/aemark-0.1.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e4911414cc18d46f3409cb4e138254844a168ae38f86541ebf30d3478df3ceb", size = 514888, upload-time = "2026-06-19T10:10:14.547Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/71/a1112c305a70e59669679a60930104a5a67732264626b906dae8609d89b9/aemark-0.1.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d39cd7d7772b3c122d346a768542e85116795a1a1886bcf47d0074d636d9eb0", size = 518017, upload-time = "2026-06-19T10:10:15.941Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alabaster"
|
name = "alabaster"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
@@ -507,55 +442,55 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cryptography"
|
name = "cryptography"
|
||||||
version = "48.0.0"
|
version = "48.0.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/12/45/870e7f4bef50e5f53b9f51d4428aee5290eedf58ba443f16b1ebb7ab8e66/cryptography-48.0.1.tar.gz", hash = "sha256:266f4ee051abb2f725b74ef8072b521ce1feacf685a3364fa6a6b45548db791a", size = 832989, upload-time = "2026-06-09T22:32:31.8Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" },
|
{ url = "https://files.pythonhosted.org/packages/1b/bc/ee4137cbbe105652c0ee4252792b78fc8e7afa4b8e61d9d5dc05a7f45731/cryptography-48.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3e4a1a3232eef2e6c732827d5722db29a0cc8b27af2a4d865b094cf954be9ca1", size = 8008324, upload-time = "2026-06-09T22:31:00.702Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" },
|
{ url = "https://files.pythonhosted.org/packages/d5/85/6379d42181bfc713094f081360fc5784d6c816b599d45e7f082502d173ce/cryptography-48.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32143b24adb918f078134e1e230f1eb8cc04886b92c28b5f0041aaf3e5699225", size = 4696243, upload-time = "2026-06-09T22:32:33.446Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" },
|
{ url = "https://files.pythonhosted.org/packages/9c/87/c85d147b53323c7eb4d850920c8901377323c2a0ff8d79c262d4fee89aa2/cryptography-48.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0d27a5696721ef7a672b8c810f6aded391058e0b9486e63e6d93baf765da691", size = 4713235, upload-time = "2026-06-09T22:31:40.141Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" },
|
{ url = "https://files.pythonhosted.org/packages/79/58/67cbf8cf1ee7c54b439ca07bbecf8362c07afc11a3724fea70f745784add/cryptography-48.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb86ce1af36fe65041b6db9a8bb064ee621a7e5fded0f80d475ec243477cd242", size = 4702323, upload-time = "2026-06-09T22:31:42.191Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/c6/24266ac10c47f6cd2a865f4446062b466da1d1f10b27189eac00e61bf0c9/cryptography-48.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b024e784ad6c077ee0147b35ea9cbfc1e34e1fd4c1dcca214c2794d73a12df08", size = 5300085, upload-time = "2026-06-09T22:31:58.703Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/bb/cc4b78784f97efc8c5874c2a9743708d172be6663024b34a0467885ae0c8/cryptography-48.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3752f2dbc8f07a30aad2932c986cea495b03bb554887828225da104f732852b6", size = 4746137, upload-time = "2026-06-09T22:31:31.01Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" },
|
{ url = "https://files.pythonhosted.org/packages/1f/52/0c44de3f5267f8fbe8e835138017522a333436166e406f0db9b9e6e3033f/cryptography-48.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:bd81490cd5801d755cf97bb68ac191f14b708470b1c7cf4580f669b9c9264cd8", size = 4333867, upload-time = "2026-06-09T22:32:28.096Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" },
|
{ url = "https://files.pythonhosted.org/packages/9a/2e/772d7adbfa931537bc401640b7cac9976bff689bda187833e5d63b428e49/cryptography-48.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:66fd0771e7b9c6dcd44cf1120690d2338d16d72795cf40cae2786a39eba65429", size = 4701805, upload-time = "2026-06-09T22:31:38.284Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" },
|
{ url = "https://files.pythonhosted.org/packages/f8/a3/b06844f303873493c963caf581c04df31c7035e0c1b0f02c4814d319ec80/cryptography-48.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:3fd2ca57062b241c856670b073487d2e86c4637937ca5601e48f97bf8e11fc8f", size = 5258461, upload-time = "2026-06-09T22:31:04.187Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" },
|
{ url = "https://files.pythonhosted.org/packages/9f/13/8b765e2e12b07c74941caadb9d1c8fdc006c4dfbf2b8f2d610519758954d/cryptography-48.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:0ee6ea481db1ab889cba043ec1eda17bb9c1ea79db6722f779c3667f9f70322f", size = 4745488, upload-time = "2026-06-09T22:32:30.07Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" },
|
{ url = "https://files.pythonhosted.org/packages/2e/aa/48972bce55049b32a94f4907eda4d75fa385aad8a39506cc2fc72196ecf0/cryptography-48.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f2ceef93cb096aa3c4cc4b5c94ca6131f9196d28c64d6111533402a9b2054d41", size = 4830256, upload-time = "2026-06-09T22:31:43.868Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" },
|
{ url = "https://files.pythonhosted.org/packages/47/a2/e5079a032fb85cf6005046ca92bbd78b0c82dad2b5751ab8c311659da06f/cryptography-48.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bd3f92d76217892b15df84ca256c2c113d386fdda7a7d8691aeeced976507c6", size = 4979117, upload-time = "2026-06-09T22:31:05.845Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/a0/8f50cae9c74e718ed769d63ed5c74bd0ea830c9550a74629cebd1b9c7bc7/cryptography-48.0.1-cp311-abi3-win32.whl", hash = "sha256:b9a32b876490d66c8bcc9963ef220199569748434ab01a9d6aaeabf88e7f5158", size = 3304154, upload-time = "2026-06-09T22:32:16.845Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" },
|
{ url = "https://files.pythonhosted.org/packages/c5/69/0572c77dbace6fef72f33755bd52ea399c71367250d366237f8691826b9e/cryptography-48.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:39489bfca54c7a1f6b297efcd8bc608ab92d16c4ca631b0cad4da46724588b24", size = 3817138, upload-time = "2026-06-09T22:32:00.388Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" },
|
{ url = "https://files.pythonhosted.org/packages/42/06/3e768b4c3bc78201583fa35a0e18f640dd782ff41afba88f8545481a8874/cryptography-48.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:f817adc181390bd54f2f700107a7419040fb7c1bdf2fc26f36551a06a68c3345", size = 7989830, upload-time = "2026-06-09T22:31:07.8Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/13/6476736484b94041110c8340a3eb63962fea4975baea8cb4a512adb44d4d/cryptography-48.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d5d30989c6917b478b5817902e85fddaea2261efa8648383d965381ccb9e1ac4", size = 4689201, upload-time = "2026-06-09T22:31:09.745Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" },
|
{ url = "https://files.pythonhosted.org/packages/79/62/65a87f34d2a431546e2509b85d55e8c90df86d668f6731da64d538512ac2/cryptography-48.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:df637c05205ea7c1d7fbcbe54bbfea648a52951155f997af13d895d0ecc96991", size = 4702822, upload-time = "2026-06-09T22:32:24.409Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/59/810b5204b0a9b10f4b6bc06bd551a8b609803cd931806bc3b71884b225e5/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:869c3b8a53bfe27147832df48b32adadf558249d50e76cb3769d40e986b13265", size = 4694875, upload-time = "2026-06-09T22:32:08.737Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/dc/d8ca05ffea724eec6d232ea6f18e74c269eb6bdfdcc9bfba689790d1325f/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:e361afba8918070d376df76f408a4f67fec0ee9cff81a99e48fe9a233ef59e17", size = 5290385, upload-time = "2026-06-09T22:31:15.212Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" },
|
{ url = "https://files.pythonhosted.org/packages/03/8c/3be6cb4da181f5bb6c19cf560c2359d60644a6b5fc5b57854e528f47b296/cryptography-48.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d069066deead00ac7f090be101be875a06855908f7ec004c27b8fefb4acfb411", size = 4737082, upload-time = "2026-06-09T22:32:22.66Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" },
|
{ url = "https://files.pythonhosted.org/packages/aa/f6/d5f60a5a1434dbfd949e227fd0065d194c7e6b6ac526b17f5c06152b8231/cryptography-48.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:09f73a725d582cef64b91281a322cd798d14a33b2b6f2b7ad9531dc336d84c02", size = 4325328, upload-time = "2026-06-09T22:32:10.777Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" },
|
{ url = "https://files.pythonhosted.org/packages/17/b7/ba75dd947a14b6ad907b01ae8f6b5b348cdd1b48142f0063dee9e20c1d9d/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:15254441469dd6bf027039453288e2072124f8b6603563f5d759e1c9b69273fa", size = 4694530, upload-time = "2026-06-09T22:31:53.105Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" },
|
{ url = "https://files.pythonhosted.org/packages/62/29/50d6b9e8aff12d8b67afaeb3569335e32dc83a5723e3bbded24fdac9f809/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:8ace4507d1e6533c125f4fac754f8bb8b6a74c08e92179dabd7e16571a3efbf3", size = 5245046, upload-time = "2026-06-09T22:31:25.774Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" },
|
{ url = "https://files.pythonhosted.org/packages/9f/04/618f4115cfc0add0838c82507aa18a346089428da8653ad38b3ff36f5cb3/cryptography-48.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b4e391975f038e66432328639620a4aff2d307513b004f1ca06d6225bced815c", size = 4736660, upload-time = "2026-06-09T22:32:12.676Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/9c/06e062462a0de28a3b3911322eded4c16deb9f441b1b7575d3dc59488ab5/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42fcd8e26fe555d9b3577a135f5091fefa0aa4e99129c23fb56787a1bd4ada72", size = 4822229, upload-time = "2026-06-09T22:31:17.062Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" },
|
{ url = "https://files.pythonhosted.org/packages/f4/be/0561971eaaee4b8a0e7d5113c536921063ab91aaf23278ac374eaf881e11/cryptography-48.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c1400da5e32a43253392277eac7490a60e497d810a63dd5608d71bbd7af507c9", size = 4966364, upload-time = "2026-06-09T22:31:32.842Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" },
|
{ url = "https://files.pythonhosted.org/packages/a4/27/728c77876f12b000820b69ae490f3c4083775e79e07827e9e60be07ad209/cryptography-48.0.1-cp314-cp314t-win32.whl", hash = "sha256:0df56b056bc17c1b7d6821dfa65216e62bd232d8ab05eb3db44e71d235651471", size = 3278498, upload-time = "2026-06-09T22:31:29.154Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" },
|
{ url = "https://files.pythonhosted.org/packages/06/e3/79a612c6d7b1e6ee0edd43633d53035bec2cfb78c82b76f7864f39e36f34/cryptography-48.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:9de21387aa95e2a895823d0745b430bed4f33503ba9ab5e0b5311f33e37d66d2", size = 3798790, upload-time = "2026-06-09T22:31:56.697Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" },
|
{ url = "https://files.pythonhosted.org/packages/ca/6c/00fa2a95997164c8b2072ce327c23d4ab20809ccc323ea5fab91e53a4bba/cryptography-48.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:4fdc69f8e4316bcf0c8c8ec1f26f285d12e8142d88d96c876a59a03be3f6ae67", size = 7987408, upload-time = "2026-06-09T22:32:20.777Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/d9/45f309a7e4e5f3f8f121d6d3be9e94024a7726ec598d6e08ae04edb2f04d/cryptography-48.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48fe40804d4caa2288f24e70ca8c64c42dd826da0ad7e4f1b41b2128d679e6c8", size = 4690196, upload-time = "2026-06-09T22:31:54.74Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/9f/a1bc8bcc798811b8527eb374bbccf30a3f3e806829d967118222bf1125eb/cryptography-48.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86be3b1b0b6bf09482fb50a979c508d2950ed95f5621ec77f4e385962006b83a", size = 4696782, upload-time = "2026-06-09T22:31:45.615Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" },
|
{ url = "https://files.pythonhosted.org/packages/66/c2/81a4fb4e4373c500bb526bc337ac5719dd31dd15b970b84a238168c6aa08/cryptography-48.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4ab0a343c807bbcd90c971cd1ecf072937cd01847a9e002bef88fb47ac6be577", size = 4696618, upload-time = "2026-06-09T22:31:11.564Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" },
|
{ url = "https://files.pythonhosted.org/packages/e5/0b/aa68b221dde92d09cb29a024ede17550ee21e77a404e59fc093c82bb51e1/cryptography-48.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9621de99d2da096006b629979efd8ae7eb2d8b822488d0c89ee4000c306c59b1", size = 5289970, upload-time = "2026-06-09T22:31:20.368Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" },
|
{ url = "https://files.pythonhosted.org/packages/78/13/fba657f958d2af66ea959a4ba01212632089249d34af1ae48054136344d7/cryptography-48.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:88c852a0ae366e262e5a1744b685e6a433dc8788dd2a277e418bf4904203609d", size = 4731873, upload-time = "2026-06-09T22:31:22.253Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" },
|
{ url = "https://files.pythonhosted.org/packages/4c/4c/9a964756d24a26b3e34dfcb16f961b89838786e6700b635b0d1e3adff4b6/cryptography-48.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:43c5835e2cb98c8733d86f57d6fc879b613f5c3478607281c3e36daffc6dd8a6", size = 4330804, upload-time = "2026-06-09T22:31:36.56Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" },
|
{ url = "https://files.pythonhosted.org/packages/4b/0f/a10f3a6eb12950a10e3a874070283aa2dd5875b2bfd15fad8a3e17b3f13e/cryptography-48.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:fe0180af5bf9236518a087e35bf2d9a347d5f5f51e63c579d683ddff424e3d46", size = 4696217, upload-time = "2026-06-09T22:31:13.351Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" },
|
{ url = "https://files.pythonhosted.org/packages/f3/6f/5cd12f951165ea73ef85266775d97e4c763b2474ccfd816dd69d3a18d6f8/cryptography-48.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:b7a2d1a937a738a881737cec135a38bb61470589b17515b9f73f571d0ae10401", size = 5245252, upload-time = "2026-06-09T22:32:02.193Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" },
|
{ url = "https://files.pythonhosted.org/packages/68/ab/8aaa12e4516ec4464033ab79b6f3b592bd5a92102467c4ace8a0d970203f/cryptography-48.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b74ca3b8e5ecdd833bf6a002ca41b4793bb27fb8f1c06ffaf2643c9e9140e31b", size = 4731388, upload-time = "2026-06-09T22:32:04.019Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" },
|
{ url = "https://files.pythonhosted.org/packages/1b/24/50027ea4dca85ec1f40688f3c24fb32ccacd520583c9592c3cc95628e6fb/cryptography-48.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c37f2461406063b417837f5f3daab668652acd82423efcd7f0a9f04be972de1", size = 4824186, upload-time = "2026-06-09T22:32:18.707Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
|
{ url = "https://files.pythonhosted.org/packages/52/41/04cb5eb17085ade6f50cc611fb657df6a0f5885350de8764ece89c050197/cryptography-48.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:86fe77abb1bd87afb251d4d02ada7ecf53a32cee9b67d976abb2e45a13297475", size = 4964539, upload-time = "2026-06-09T22:31:18.793Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
|
{ url = "https://files.pythonhosted.org/packages/36/bf/ed70785c496e89d7e73b7cda2d21f2447fd6d4e821714b8d04ff217fed92/cryptography-48.0.1-cp39-abi3-win32.whl", hash = "sha256:6b2c0c3e6ccf3ade7750f836ef3ee36eea250cc467d45c256895573ac08cc6f1", size = 3282307, upload-time = "2026-06-09T22:30:53.162Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/ff/371ea7d252656ee1eb6d83eeeef3d1d0c6baf1d6497687d081ea03814670/cryptography-48.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:9a49ca6c81417f6a5edb50375a60cccdd70fa0a91a5211829dbea74eba94d2ac", size = 3793408, upload-time = "2026-06-09T22:32:15.191Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1334,6 +1269,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" },
|
{ url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mistune"
|
||||||
|
version = "3.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ca/84/620cc3f7e3adf6f5067e10f4dbae71295d8f9e16d5d3f9ef97c40f2f592c/mistune-3.2.1.tar.gz", hash = "sha256:7c8e5501d38bac1582e067e46c8343f17d57ea1aaa735823f3aba1fd59c88a28", size = 98003, upload-time = "2026-05-03T14:33:22.312Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/7f/a946aa4f8752b37102b41e64dca18a1976ac705c3a0d1dfe74d820a02552/mistune-3.2.1-py3-none-any.whl", hash = "sha256:78cdb0ba5e938053ccf63651b352508d2efa9411dc8810bfb05f2dc5140c0048", size = 53749, upload-time = "2026-05-03T14:33:20.551Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mkdocs"
|
name = "mkdocs"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
@@ -2133,7 +2077,6 @@ name = "sith"
|
|||||||
version = "3"
|
version = "3"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aemark" },
|
|
||||||
{ name = "celery", extra = ["redis"] },
|
{ name = "celery", extra = ["redis"] },
|
||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
{ name = "dict2xml" },
|
{ name = "dict2xml" },
|
||||||
@@ -2155,6 +2098,7 @@ dependencies = [
|
|||||||
{ name = "ical", version = "13.2.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" },
|
{ name = "ical", version = "13.2.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" },
|
||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
{ name = "libsass" },
|
{ name = "libsass" },
|
||||||
|
{ name = "mistune" },
|
||||||
{ name = "phonenumbers" },
|
{ name = "phonenumbers" },
|
||||||
{ name = "pillow" },
|
{ name = "pillow" },
|
||||||
{ name = "psutil" },
|
{ name = "psutil" },
|
||||||
@@ -2201,9 +2145,8 @@ tests = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "aemark", specifier = ">=0.1.1" },
|
|
||||||
{ name = "celery", extras = ["redis"], specifier = ">=5.6.3,<8" },
|
{ name = "celery", extras = ["redis"], specifier = ">=5.6.3,<8" },
|
||||||
{ name = "cryptography", specifier = ">=48.0.0,<49.0.0" },
|
{ name = "cryptography", specifier = ">=48.0.1,<49.0.0" },
|
||||||
{ name = "dict2xml", specifier = ">=1.7.8,<2.0.0" },
|
{ name = "dict2xml", specifier = ">=1.7.8,<2.0.0" },
|
||||||
{ name = "django", specifier = ">=5.2.15,<6.0.0" },
|
{ name = "django", specifier = ">=5.2.15,<6.0.0" },
|
||||||
{ name = "django-celery-beat", specifier = ">=2.9.0" },
|
{ name = "django-celery-beat", specifier = ">=2.9.0" },
|
||||||
@@ -2222,6 +2165,7 @@ requires-dist = [
|
|||||||
{ name = "ical", specifier = ">=12.0.0,<14.0.0" },
|
{ name = "ical", specifier = ">=12.0.0,<14.0.0" },
|
||||||
{ name = "jinja2", specifier = ">=3.1.6,<4.0.0" },
|
{ name = "jinja2", specifier = ">=3.1.6,<4.0.0" },
|
||||||
{ name = "libsass", specifier = ">=0.23.0,<1.0.0" },
|
{ name = "libsass", specifier = ">=0.23.0,<1.0.0" },
|
||||||
|
{ name = "mistune", specifier = ">=3.2.1,<4.0.0" },
|
||||||
{ name = "phonenumbers", specifier = ">=9.0.32,<10.0.0" },
|
{ name = "phonenumbers", specifier = ">=9.0.32,<10.0.0" },
|
||||||
{ name = "pillow", specifier = ">=12.2.0,<13.0.0" },
|
{ name = "pillow", specifier = ">=12.2.0,<13.0.0" },
|
||||||
{ name = "psutil", specifier = ">=7.2.2,<8.0.0" },
|
{ name = "psutil", specifier = ">=7.2.2,<8.0.0" },
|
||||||
|
|||||||
Reference in New Issue
Block a user