diff --git a/accounting/tests.py b/accounting/tests.py index 1140acc7..30d1586b 100644 --- a/accounting/tests.py +++ b/accounting/tests.py @@ -32,7 +32,7 @@ class TestRefoundAccount(TestCase): @classmethod def setUpTestData(cls): cls.skia = User.objects.get(username="skia") - # reffil skia's account + # refill skia's account cls.skia.customer.amount = 800 cls.skia.customer.save() cls.refound_account_url = reverse("accounting:refound_account") diff --git a/accounting/views.py b/accounting/views.py index 12128a04..5f8640fd 100644 --- a/accounting/views.py +++ b/accounting/views.py @@ -17,7 +17,7 @@ import collections from django import forms from django.conf import settings -from django.contrib.auth.mixins import PermissionRequiredMixin +from django.contrib.auth.mixins import PermissionRequiredMixin, UserPassesTestMixin from django.core.exceptions import PermissionDenied, ValidationError from django.db import transaction from django.db.models import Sum @@ -846,27 +846,16 @@ class CloseCustomerAccountForm(forms.Form): ) -class RefoundAccountView(FormView): +class RefoundAccountView(UserPassesTestMixin, FormView): """Create a selling with the same amount than the current user money.""" template_name = "accounting/refound_account.jinja" form_class = CloseCustomerAccountForm - def permission(self, user): - if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID): - return True - else: - raise PermissionDenied - - def dispatch(self, request, *arg, **kwargs): - res = super().dispatch(request, *arg, **kwargs) - if self.permission(request.user): - return res - - def post(self, request, *arg, **kwargs): - self.operator = request.user - if self.permission(request.user): - return super().post(self, request, *arg, **kwargs) + def test_func(self): + return self.request.user.is_root or self.request.user.is_in_group( + pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID + ) def form_valid(self, form): self.customer = form.cleaned_data["user"] @@ -887,7 +876,7 @@ class RefoundAccountView(FormView): label=_("Refound account"), unit_price=uprice, quantity=1, - seller=self.operator, + seller=self.request.user, customer=self.customer.customer, club=refound_club, counter=refound_club_counter, diff --git a/com/schemas.py b/com/schemas.py index 93ee5315..3933daa1 100644 --- a/com/schemas.py +++ b/com/schemas.py @@ -2,7 +2,7 @@ from datetime import datetime from ninja import FilterSchema, ModelSchema from ninja_extra import service_resolver -from ninja_extra.controllers import RouteContext +from ninja_extra.context import RouteContext from pydantic import Field from club.schemas import ClubProfileSchema diff --git a/core/auth/mixins.py b/core/auth/mixins.py index 974e9bd1..54bdc481 100644 --- a/core/auth/mixins.py +++ b/core/auth/mixins.py @@ -169,10 +169,9 @@ class CanCreateMixin(View): super().__init__(*args, **kwargs) def dispatch(self, request, *arg, **kwargs): - res = super().dispatch(request, *arg, **kwargs) if not request.user.is_authenticated: raise PermissionDenied - return res + return super().dispatch(request, *arg, **kwargs) def form_valid(self, form): obj = form.instance diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index 5abcb203..492f971b 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -919,6 +919,7 @@ Welcome to the wiki page! "view_album", "view_peoplepicturerelation", "add_peoplepicturerelation", + "add_page", ] ) ) diff --git a/core/static/bundled/utils/api.ts b/core/static/bundled/utils/api.ts index 225337b0..95a86e1c 100644 --- a/core/static/bundled/utils/api.ts +++ b/core/static/bundled/utils/api.ts @@ -30,7 +30,7 @@ export const paginated = async ( endpoint: PaginatedEndpoint, options?: PaginatedRequest, ): Promise => { - const maxPerPage = 199; + const maxPerPage = 200; const queryParams = options ?? ({} as PaginatedRequest); queryParams.query = queryParams.query ?? {}; queryParams.query.page_size = maxPerPage; diff --git a/core/static/core/header.scss b/core/static/core/header.scss index 7cac3a3e..53b6887f 100644 --- a/core/static/core/header.scss +++ b/core/static/core/header.scss @@ -251,21 +251,31 @@ $hovered-red-text-color: #ff4d4d; justify-content: flex-start; } - >a { + a, button { + font-size: 100%; + margin: 0; text-align: right; color: $text-color; &:hover { color: $hovered-text-color; } + } - &:last-child { - color: $red-text-color; + form#logout-form { + margin: 0; + display: inline; + } + #logout-form button { + color: $red-text-color; - &:hover { - color: $hovered-red-text-color; - } + &:hover { + color: $hovered-red-text-color; } + background: none; + border: none; + cursor: pointer; + padding: 0; } } } diff --git a/core/templates/core/base.jinja b/core/templates/core/base.jinja index 6ee285b2..41b13398 100644 --- a/core/templates/core/base.jinja +++ b/core/templates/core/base.jinja @@ -84,18 +84,18 @@
- {% block tabs %} + {%- block tabs -%} {% include "core/base/tabs.jinja" %} - {% endblock %} + {%- endblock -%} - {% block errors%} + {%- block errors -%} {% if error %} {{ error }} {% endif %} - {% endblock %} + {%- endblock -%} - {% block content %} - {% endblock %} + {%- block content -%} + {%- endblock -%}
diff --git a/core/templates/core/base/header.jinja b/core/templates/core/base/header.jinja index 99cd4c4f..4454aedb 100644 --- a/core/templates/core/base/header.jinja +++ b/core/templates/core/base/header.jinja @@ -59,7 +59,10 @@ {{ page.get_display_name() }} - {% endif %} -{% endmacro %} + {%- endif -%} +{%- endmacro -%} {% block content %} {{ print_page_name(page) }} -
{% if page %} diff --git a/core/tests/test_core.py b/core/tests/test_core.py index e6b37e5c..930e8819 100644 --- a/core/tests/test_core.py +++ b/core/tests/test_core.py @@ -18,7 +18,9 @@ from smtplib import SMTPException import freezegun import pytest +from bs4 import BeautifulSoup from django.contrib.auth.hashers import make_password +from django.contrib.auth.models import Permission from django.core import mail from django.core.cache import cache from django.core.mail import EmailMessage @@ -223,17 +225,19 @@ def test_full_markdown_syntax(): class TestPageHandling(TestCase): @classmethod def setUpTestData(cls): - cls.root = User.objects.get(username="root") - cls.root_group = Group.objects.get(name="Root") + cls.group = baker.make( + Group, permissions=[Permission.objects.get(codename="add_page")] + ) + cls.user = baker.make(User, groups=[cls.group]) def setUp(self): - self.client.force_login(self.root) + self.client.force_login(self.user) def test_create_page_ok(self): """Should create a page correctly.""" response = self.client.post( reverse("core:page_new"), - {"parent": "", "name": "guy", "owner_group": self.root_group.id}, + {"parent": "", "name": "guy", "owner_group": self.group.id}, ) self.assertRedirects( response, reverse("core:page", kwargs={"page_name": "guy"}) @@ -249,32 +253,38 @@ class TestPageHandling(TestCase): def test_create_child_page_ok(self): """Should create a page correctly.""" - # remove all other pages to make sure there is no side effect - Page.objects.all().delete() - self.client.post( - reverse("core:page_new"), - {"parent": "", "name": "guy", "owner_group": str(self.root_group.id)}, + parent = baker.prepare(Page) + parent.save(force_lock=True) + response = self.client.get( + reverse("core:page_new") + f"?page={parent._full_name}/new" ) - page = Page.objects.first() - self.client.post( + + assert response.status_code == 200 + # The name and parent inputs should be already filled + soup = BeautifulSoup(response.content.decode(), "lxml") + assert soup.find("input", {"name": "name"})["value"] == "new" + select = soup.find("autocomplete-select", {"name": "parent"}) + assert select.find("option", {"selected": True})["value"] == str(parent.id) + + response = self.client.post( reverse("core:page_new"), { - "parent": str(page.id), - "name": "bibou", - "owner_group": str(self.root_group.id), + "parent": str(parent.id), + "name": "new", + "owner_group": str(self.group.id), }, ) - response = self.client.get( - reverse("core:page", kwargs={"page_name": "guy/bibou"}) - ) + new_url = reverse("core:page", kwargs={"page_name": f"{parent._full_name}/new"}) + assertRedirects(response, new_url, fetch_redirect_response=False) + response = self.client.get(new_url) assert response.status_code == 200 - assert '' in str(response.content) + assert f'' in response.content.decode() def test_access_child_page_ok(self): """Should display a page correctly.""" - parent = Page(name="guy", owner_group=self.root_group) + parent = Page(name="guy", owner_group=self.group) parent.save(force_lock=True) - page = Page(name="bibou", owner_group=self.root_group, parent=parent) + page = Page(name="bibou", owner_group=self.group, parent=parent) page.save(force_lock=True) response = self.client.get( reverse("core:page", kwargs={"page_name": "guy/bibou"}) @@ -293,7 +303,8 @@ class TestPageHandling(TestCase): def test_create_page_markdown_safe(self): """Should format the markdown and escape html correctly.""" self.client.post( - reverse("core:page_new"), {"parent": "", "name": "guy", "owner_group": "1"} + reverse("core:page_new"), + {"parent": "", "name": "guy", "owner_group": self.group.id}, ) self.client.post( reverse("core:page_edit", kwargs={"page_name": "guy"}), diff --git a/core/tests/test_user.py b/core/tests/test_user.py index 9b2209b3..133f26a5 100644 --- a/core/tests/test_user.py +++ b/core/tests/test_user.py @@ -2,6 +2,7 @@ from datetime import timedelta import pytest from django.conf import settings +from django.contrib import auth from django.core.management import call_command from django.test import Client, TestCase from django.urls import reverse @@ -219,3 +220,12 @@ def test_user_update_groups(client: Client): manageable_groups[1], *hidden_groups[:3], } + + +@pytest.mark.django_db +def test_logout(client: Client): + user = baker.make(User) + client.force_login(user) + res = client.post(reverse("core:logout")) + assertRedirects(res, reverse("core:login")) + assert auth.get_user(client).is_anonymous diff --git a/core/views/files.py b/core/views/files.py index 8070033d..714b505d 100644 --- a/core/views/files.py +++ b/core/views/files.py @@ -403,6 +403,7 @@ class FileModerationView(AllowFragment, ListView): model = SithFile template_name = "core/file_moderation.jinja" queryset = SithFile.objects.filter(is_moderated=False, is_in_sas=False) + ordering = "id" paginate_by = 100 def dispatch(self, request: HttpRequest, *args, **kwargs): diff --git a/core/views/page.py b/core/views/page.py index f4b04f9c..23898217 100644 --- a/core/views/page.py +++ b/core/views/page.py @@ -12,6 +12,7 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # +from django.contrib.auth.mixins import PermissionRequiredMixin # This file contains all the views that concern the page model from django.forms.models import modelform_factory @@ -22,7 +23,6 @@ from django.views.generic import DetailView, ListView from django.views.generic.edit import CreateView, DeleteView, UpdateView from core.auth.mixins import ( - CanCreateMixin, CanEditMixin, CanEditPropMixin, CanViewMixin, @@ -115,20 +115,22 @@ class PageRevView(CanViewMixin, DetailView): return context -class PageCreateView(CanCreateMixin, CreateView): +class PageCreateView(PermissionRequiredMixin, CreateView): model = Page form_class = PageForm template_name = "core/page_prop.jinja" + permission_required = "core.add_page" def get_initial(self): - init = {} - if "page" in self.request.GET: - page_name = self.request.GET["page"] - parent_name = "/".join(page_name.split("/")[:-1]) - parent = Page.get_page_by_full_name(parent_name) + init = super().get_initial() + if "page" not in self.request.GET: + return init + page_name = self.request.GET["page"].rsplit("/", maxsplit=1) + if len(page_name) == 2: + parent = Page.get_page_by_full_name(page_name[0]) if parent is not None: init["parent"] = parent.id - init["name"] = page_name.split("/")[-1] + init["name"] = page_name[-1] return init def get_context_data(self, **kwargs): diff --git a/pyproject.toml b/pyproject.toml index 05246070..78c1b9c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,6 +74,8 @@ tests = [ "pytest-cov<7.0.0,>=6.0.0", "pytest-django<5.0.0,>=4.10.0", "model-bakery<2.0.0,>=1.20.4", + "beautifulsoup4>=4.13.3,<5", + "lxml>=5.3.1,<6", ] docs = [ "mkdocs<2.0.0,>=1.6.1", diff --git a/subscription/forms.py b/subscription/forms.py index ed9978d4..babc613c 100644 --- a/subscription/forms.py +++ b/subscription/forms.py @@ -94,6 +94,13 @@ class SubscriptionNewUserForm(SubscriptionForm): return email def clean(self) -> dict[str, Any]: + """Initialize the [User][core.models.User] linked to this subscription. + + Warning: + The `User` is initialized, but not saved. + Don't use it for operations that expect + a persisted object. + """ member = User( first_name=self.cleaned_data.get("first_name"), last_name=self.cleaned_data.get("last_name"), diff --git a/subscription/models.py b/subscription/models.py index 4d68d979..38d6c0f5 100644 --- a/subscription/models.py +++ b/subscription/models.py @@ -92,9 +92,13 @@ class Subscription(models.Model): self.member.make_home() def get_absolute_url(self): - return reverse("core:user_edit", kwargs={"user_id": self.member.pk}) + return reverse("core:user_edit", kwargs={"user_id": self.member_id}) def clean(self): + if self.member._state.adding: + # if the user is being created, then it makes no sense + # to check if the user is already subscribed + return today = localdate() threshold = timedelta(weeks=settings.SITH_SUBSCRIPTION_END) # a user may subscribe if : diff --git a/uv.lock b/uv.lock index a3311096..61b782df 100644 --- a/uv.lock +++ b/uv.lock @@ -60,6 +60,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336 }, ] +[[package]] +name = "beautifulsoup4" +version = "4.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 }, +] + [[package]] name = "bracex" version = "2.5.post1" @@ -755,6 +768,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/20/caf3c7cf2432d85263119798c45221ddf67bdd7dae8f626d14ff8db04040/libsass-0.23.0-cp38-abi3-win_amd64.whl", hash = "sha256:a2ec85d819f353cbe807432d7275d653710d12b08ec7ef61c124a580a8352f3c", size = 872914 }, ] +[[package]] +name = "lxml" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/f6/c15ca8e5646e937c148e147244817672cf920b56ac0bf2cc1512ae674be8/lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8", size = 3678591 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/f4/5121aa9ee8e09b8b8a28cf3709552efe3d206ca51a20d6fa471b60bb3447/lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c", size = 8191889 }, + { url = "https://files.pythonhosted.org/packages/0a/ca/8e9aa01edddc74878f4aea85aa9ab64372f46aa804d1c36dda861bf9eabf/lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe", size = 4450685 }, + { url = "https://files.pythonhosted.org/packages/b2/b3/ea40a5c98619fbd7e9349df7007994506d396b97620ced34e4e5053d3734/lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9", size = 5051722 }, + { url = "https://files.pythonhosted.org/packages/3a/5e/375418be35f8a695cadfe7e7412f16520e62e24952ed93c64c9554755464/lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a", size = 4786661 }, + { url = "https://files.pythonhosted.org/packages/79/7c/d258eaaa9560f6664f9b426a5165103015bee6512d8931e17342278bad0a/lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0", size = 5311766 }, + { url = "https://files.pythonhosted.org/packages/03/bc/a041415be4135a1b3fdf017a5d873244cc16689456166fbdec4b27fba153/lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7", size = 4836014 }, + { url = "https://files.pythonhosted.org/packages/32/88/047f24967d5e3fc97848ea2c207eeef0f16239cdc47368c8b95a8dc93a33/lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae", size = 4961064 }, + { url = "https://files.pythonhosted.org/packages/3d/b5/ecf5a20937ecd21af02c5374020f4e3a3538e10a32379a7553fca3d77094/lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519", size = 4778341 }, + { url = "https://files.pythonhosted.org/packages/a4/05/56c359e07275911ed5f35ab1d63c8cd3360d395fb91e43927a2ae90b0322/lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322", size = 5345450 }, + { url = "https://files.pythonhosted.org/packages/b7/f4/f95e3ae12e9f32fbcde00f9affa6b0df07f495117f62dbb796a9a31c84d6/lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468", size = 4908336 }, + { url = "https://files.pythonhosted.org/packages/c5/f8/309546aec092434166a6e11c7dcecb5c2d0a787c18c072d61e18da9eba57/lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367", size = 4986049 }, + { url = "https://files.pythonhosted.org/packages/71/1c/b951817cb5058ca7c332d012dfe8bc59dabd0f0a8911ddd7b7ea8e41cfbd/lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd", size = 4860351 }, + { url = "https://files.pythonhosted.org/packages/31/23/45feba8dae1d35fcca1e51b051f59dc4223cbd23e071a31e25f3f73938a8/lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c", size = 5421580 }, + { url = "https://files.pythonhosted.org/packages/61/69/be245d7b2dbef81c542af59c97fcd641fbf45accf2dc1c325bae7d0d014c/lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f", size = 5285778 }, + { url = "https://files.pythonhosted.org/packages/69/06/128af2ed04bac99b8f83becfb74c480f1aa18407b5c329fad457e08a1bf4/lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645", size = 5054455 }, + { url = "https://files.pythonhosted.org/packages/8a/2d/f03a21cf6cc75cdd083563e509c7b6b159d761115c4142abb5481094ed8c/lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5", size = 3486315 }, + { url = "https://files.pythonhosted.org/packages/2b/9c/8abe21585d20ef70ad9cec7562da4332b764ed69ec29b7389d23dfabcea0/lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf", size = 3816925 }, + { url = "https://files.pythonhosted.org/packages/94/1c/724931daa1ace168e0237b929e44062545bf1551974102a5762c349c668d/lxml-5.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e", size = 8171881 }, + { url = "https://files.pythonhosted.org/packages/67/0c/857b8fb6010c4246e66abeebb8639eaabba60a6d9b7c606554ecc5cbf1ee/lxml-5.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd", size = 4440394 }, + { url = "https://files.pythonhosted.org/packages/61/72/c9e81de6a000f9682ccdd13503db26e973b24c68ac45a7029173237e3eed/lxml-5.3.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7", size = 5037860 }, + { url = "https://files.pythonhosted.org/packages/24/26/942048c4b14835711b583b48cd7209bd2b5f0b6939ceed2381a494138b14/lxml-5.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414", size = 4782513 }, + { url = "https://files.pythonhosted.org/packages/e2/65/27792339caf00f610cc5be32b940ba1e3009b7054feb0c4527cebac228d4/lxml-5.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e", size = 5305227 }, + { url = "https://files.pythonhosted.org/packages/18/e1/25f7aa434a4d0d8e8420580af05ea49c3e12db6d297cf5435ac0a054df56/lxml-5.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1", size = 4829846 }, + { url = "https://files.pythonhosted.org/packages/fe/ed/faf235e0792547d24f61ee1448159325448a7e4f2ab706503049d8e5df19/lxml-5.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5", size = 4949495 }, + { url = "https://files.pythonhosted.org/packages/e5/e1/8f572ad9ed6039ba30f26dd4c2c58fb90f79362d2ee35ca3820284767672/lxml-5.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423", size = 4773415 }, + { url = "https://files.pythonhosted.org/packages/a3/75/6b57166b9d1983dac8f28f354e38bff8d6bcab013a241989c4d54c72701b/lxml-5.3.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20", size = 5337710 }, + { url = "https://files.pythonhosted.org/packages/cc/71/4aa56e2daa83bbcc66ca27b5155be2f900d996f5d0c51078eaaac8df9547/lxml-5.3.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8", size = 4897362 }, + { url = "https://files.pythonhosted.org/packages/65/10/3fa2da152cd9b49332fd23356ed7643c9b74cad636ddd5b2400a9730d12b/lxml-5.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9", size = 4977795 }, + { url = "https://files.pythonhosted.org/packages/de/d2/e1da0f7b20827e7b0ce934963cb6334c1b02cf1bb4aecd218c4496880cb3/lxml-5.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c", size = 4858104 }, + { url = "https://files.pythonhosted.org/packages/a5/35/063420e1b33d3308f5aa7fcbdd19ef6c036f741c9a7a4bd5dc8032486b27/lxml-5.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b", size = 5416531 }, + { url = "https://files.pythonhosted.org/packages/c3/83/93a6457d291d1e37adfb54df23498101a4701834258c840381dd2f6a030e/lxml-5.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5", size = 5273040 }, + { url = "https://files.pythonhosted.org/packages/39/25/ad4ac8fac488505a2702656550e63c2a8db3a4fd63db82a20dad5689cecb/lxml-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252", size = 5050951 }, + { url = "https://files.pythonhosted.org/packages/82/74/f7d223c704c87e44b3d27b5e0dde173a2fcf2e89c0524c8015c2b3554876/lxml-5.3.1-cp313-cp313-win32.whl", hash = "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78", size = 3485357 }, + { url = "https://files.pythonhosted.org/packages/80/83/8c54533b3576f4391eebea88454738978669a6cad0d8e23266224007939d/lxml-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332", size = 3814484 }, +] + [[package]] name = "markdown" version = "3.7" @@ -1560,7 +1615,9 @@ prod = [ { name = "psycopg", extra = ["c"] }, ] tests = [ + { name = "beautifulsoup4" }, { name = "freezegun" }, + { name = "lxml" }, { name = "model-bakery" }, { name = "pytest" }, { name = "pytest-cov" }, @@ -1620,7 +1677,9 @@ docs = [ ] prod = [{ name = "psycopg", extras = ["c"], specifier = ">=3.2.3,<4.0.0" }] tests = [ + { name = "beautifulsoup4", specifier = ">=4.13.3,<5" }, { name = "freezegun", specifier = ">=1.5.1,<2.0.0" }, + { name = "lxml", specifier = ">=5.3.1,<6" }, { name = "model-bakery", specifier = ">=1.20.4,<2.0.0" }, { name = "pytest", specifier = ">=8.3.5,<9.0.0" }, { name = "pytest-cov", specifier = ">=6.0.0,<7.0.0" }, @@ -1645,6 +1704,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, ] +[[package]] +name = "soupsieve" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, +] + [[package]] name = "sphinx" version = "5.3.0"