diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9d3325..c20fa32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -171,15 +171,16 @@ jobs: redis1: image: redis:7.4.0 ports: - - 26379:6379 + - 5379:6379 db1: image: postgres:14 env: + POSTGRES_HOST: db1 POSTGRES_DATABASE: country_workspace POSTGRES_PASSWORD: postgres POSTGRES_USERNAME: postgres ports: - - 25432:5432 + - 4432:5432 options: >- --health-cmd pg_isready --health-interval 10s @@ -188,9 +189,9 @@ jobs: env: DOCKER_DEFAULT_PLATFORM: linux/amd64 DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index - DATABASE_URL: postgres://postgres:postgres@localhost:25432/country_workspace - CELERY_BROKER_URL: redis://localhost:26379/1 - CACHE_URL: redis://localhost:26379/2 + DATABASE_URL: postgres://postgres:postgres@localhost:4432/country_workspace + CELERY_BROKER_URL: redis://localhost:5379/1 + CACHE_URL: redis://localhost:5379/2 DOCKER_BUILDKIT: 1 steps: - name: Checkout code @@ -206,27 +207,26 @@ jobs: --target dist \ --cache-from "type=gha" \ --cache-to "type=gha,mode=max" \ - --build-arg "VERSION=${{needs.test.outputs.commit}}" \ --build-arg "GIT_SHA=${{needs.test.outputs.commit}}" \ --build-arg "BUILD_DATE=${{needs.test.outputs.build_date}}" \ --build-arg "BRANCH=${{needs.test.outputs.branch}}" \ -t ${{needs.test.outputs.image}} \ -f docker/Dockerfile . - - - name: Docker Integrity Check - run: | - docker run --rm \ - --network host \ - -e DATABASE_URL=${DATABASE_URL} \ - -e CELERY_BROKER_URL=${CELERY_BROKER_URL} \ - -e CACHE_URL=${CACHE_URL} \ - -e SECRET_KEY=super-secret-key-just-for-testing \ - -e HOPE_API_URL="https://dev-hope.unitst.org/api/rest/" \ - -e HOPE_API_TOKEN=${{ secrets.HOPE_API_TOKEN }} \ - -e AURORA_API_URL="https://uni-hope-ukr-sr-dev.unitst.org/api/" \ - -e AURORA_API_TOKEN=${{ secrets.AURORA_API_TOKEN }} \ - -t ${{needs.test.outputs.image}} \ - django-admin upgrade +# +# - name: Docker Integrity Check +# run: | +# docker run --rm \ +# --network host \ +# -e DATABASE_URL=${DATABASE_URL} \ +# -e CELERY_BROKER_URL=${CELERY_BROKER_URL} \ +# -e CACHE_URL=${CACHE_URL} \ +# -e SECRET_KEY=super-secret-key-just-for-testing \ +# -e HOPE_API_URL="https://dev-hope.unitst.org/api/rest/" \ +# -e HOPE_API_TOKEN=${{ secrets.HOPE_API_TOKEN }} \ +# -e AURORA_API_URL="https://uni-hope-ukr-sr-dev.unitst.org/api/" \ +# -e AURORA_API_TOKEN=${{ secrets.AURORA_API_TOKEN }} \ +# -t ${{needs.test.outputs.image}} \ +# django-admin upgrade - name: Publish images run: | diff --git a/docker/Dockerfile b/docker/Dockerfile index bbed587..79a5a4d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -62,7 +62,7 @@ RUN pip install uv uwsgi # ------- tests ------- FROM builder AS tests ARG BUILD_DATE -ARG VERSION +ARG GIT_SHA LABEL distro="tests" LABEL org.opencontainers.image.created="$BUILD_DATE" @@ -111,7 +111,6 @@ RUN --mount=type=cache,target=/root/.uv-cache \ # ------- dist ------- FROM base_os AS dist ARG BUILD_DATE -ARG VERSION ARG GIT_SHA ARG BRANCH @@ -120,7 +119,6 @@ ENV PATH=/app/.venv/bin:/usr/local/bin/:/usr/bin:/bin \ GIT_SHA=$GIT_SHA \ VERSION=$VERSION \ BRANCH=$BRANCH \ - DJANGO_SETTINGS_MODULE=country_workspace.config.settings \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ STATIC_URL="/static/" \ @@ -150,7 +148,7 @@ RUN --mount=type=cache,target=/root/.uv-cache \ --mount=type=bind,source=MANIFEST.in,target=/app/MANIFEST.in \ --mount=type=bind,source=README.md,target=/app/README.md \ --mount=type=bind,source=./src/country_workspace,target=/app/src/country_workspace \ - uv --cache-dir=/root/.uv-cache pip install --no-deps . + uv --cache-dir=/root/.uv-cache pip install --link-mode=copy --no-deps . EXPOSE 8000 diff --git a/docker/bin/docker-entrypoint.sh b/docker/bin/docker-entrypoint.sh index dda3791..dcad279 100755 --- a/docker/bin/docker-entrypoint.sh +++ b/docker/bin/docker-entrypoint.sh @@ -4,7 +4,7 @@ export MEDIA_ROOT="${MEDIA_ROOT:-/var/run/app/media}" export STATIC_ROOT="${STATIC_ROOT:-/var/run/app/static}" export UWSGI_PROCESSES="${UWSGI_PROCESSES:-"4"}" -export DJANGO_SETTINGS_MODULE="${DJANGO_SETTINGS_MODULE:-country_workspace.config.settings}" +export DJANGO_SETTINGS_MODULE="country_workspace.config.settings" case "$1" in run) diff --git a/docs/src/license.md b/docs/src/license.md new file mode 100644 index 0000000..acd6294 --- /dev/null +++ b/docs/src/license.md @@ -0,0 +1,4 @@ +# License + + +--8<-- "LICENSE.md" diff --git a/pyproject.toml b/pyproject.toml index 145ff0f..316bf4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dependencies = [ "deprecation>=2.1.0", "dictdiffer>=0.9.0", "django-adminactions>=2.3.0", - "django-adminfilters==2.5.1", + "django-adminfilters>=2.5.2", "django-cacheops>=7.1", "django-celery-beat>=2.6.0", "django-celery-boost>=0.5.0", diff --git a/src/country_workspace/admin/__init__.py b/src/country_workspace/admin/__init__.py index 0fed349..16d872b 100644 --- a/src/country_workspace/admin/__init__.py +++ b/src/country_workspace/admin/__init__.py @@ -5,13 +5,13 @@ from smart_admin.console import panel_migrations, panel_redis, panel_sentry, panel_sysinfo from smart_admin.smart_auth.admin import ContentTypeAdmin, PermissionAdmin +from ..cache.smart_panel import panel_cache from .batch import BatchAdmin # noqa from .household import HouseholdAdmin # noqa from .individual import IndividualAdmin # noqa from .job import AsyncJobAdmin # noqa from .locations import AreaAdmin, AreaTypeAdmin, CountryAdmin # noqa from .office import OfficeAdmin # noqa -from .panels.cache import panel_cache from .program import ProgramAdmin # noqa from .role import UserRoleAdmin # noqa from .sync import SyncLog # noqa diff --git a/src/country_workspace/admin/individual.py b/src/country_workspace/admin/individual.py index d5a8c92..69acd46 100644 --- a/src/country_workspace/admin/individual.py +++ b/src/country_workspace/admin/individual.py @@ -21,7 +21,10 @@ class IndividualAdmin(BaseModelAdmin): ("batch", LinkedAutoCompleteFilter.factory(parent="batch__program")), IsValidFilter, ) - autocomplete_fields = ("batch",) + autocomplete_fields = ( + "batch", + "household", + ) @link(change_list=True, change_form=False) def view_in_workspace(self, btn: "LinkButton") -> None: diff --git a/src/country_workspace/admin/job.py b/src/country_workspace/admin/job.py index 0e389d9..0b95326 100644 --- a/src/country_workspace/admin/job.py +++ b/src/country_workspace/admin/job.py @@ -14,7 +14,7 @@ @admin.register(AsyncJob) class AsyncJobAdmin(CeleryTaskModelAdmin, BaseModelAdmin): list_display = ("program", "type", "verbose_status", "owner") - autocomplete_fields = ("program", "owner") + autocomplete_fields = ("program", "owner", "batch", "content_type") list_filter = ( ("program__country_office", LinkedAutoCompleteFilter.factory(parent=None)), ("program", LinkedAutoCompleteFilter.factory(parent="program__country_office")), diff --git a/src/country_workspace/admin/program.py b/src/country_workspace/admin/program.py index d7a652c..d2009e4 100644 --- a/src/country_workspace/admin/program.py +++ b/src/country_workspace/admin/program.py @@ -22,6 +22,7 @@ class ProgramAdmin(BaseModelAdmin): search_fields = ("name",) list_filter = (("country_office", AutoCompleteFilter), "status", "active", "sector") ordering = ("name",) + autocomplete_fields = ("country_office",) @button() def invalidate_cache(self, request: HttpRequest, pk: str) -> None: diff --git a/src/country_workspace/cache/manager.py b/src/country_workspace/cache/manager.py index 3bbbf40..a4e96b5 100644 --- a/src/country_workspace/cache/manager.py +++ b/src/country_workspace/cache/manager.py @@ -23,7 +23,6 @@ class CacheManager: def __init__(self, prefix="cache"): self.prefix = prefix - self.active = True self.cw_version = "-" self.cache_timeout = 86400 self.cache_by_version = False @@ -59,6 +58,8 @@ def retrieve(self, key): def store(self, key: str, value: Any, timeout: int = 0, **kwargs): cache_set.send(self.__class__, key=key) + if not self.active: + timeout = 1 self.cache.set(key, value, timeout=timeout or self.cache_timeout, **kwargs) def _get_version_key(self, office: "Optional[Office]" = None, program: "Optional[Program]" = None): @@ -92,12 +93,23 @@ def incr_cache_version(self, *, office: "Optional[Office]" = None, program: "Opt except ValueError: return self.cache.set(key, 2) + @property + def active(self) -> bool: + return not bool(self.cache.get(f"{self.prefix}:cache_disabled")) + + @active.setter + def active(self, value: bool): + if not value: + self.cache.set(f"{self.prefix}:cache_disabled", True) + else: + self.cache.delete(f"{self.prefix}:cache_disabled") + def build_key(self, prefix, *parts): tenant = "t" version = "v" program = "p" ts = "ts" - if self.cache.get("cache_disabled"): + if not self.active: ts = str(timezone.now().toordinal()) if state.tenant and state.program: diff --git a/src/country_workspace/admin/panels/cache.py b/src/country_workspace/cache/smart_panel.py similarity index 68% rename from src/country_workspace/admin/panels/cache.py rename to src/country_workspace/cache/smart_panel.py index 8164ff1..b5ebf8f 100644 --- a/src/country_workspace/admin/panels/cache.py +++ b/src/country_workspace/cache/smart_panel.py @@ -1,4 +1,5 @@ from django import forms +from django.http import HttpResponseRedirect from django.shortcuts import render from django.utils.translation import gettext_lazy as _ @@ -6,7 +7,7 @@ class CacheManagerForm(forms.Form): - pattern = forms.CharField() + pattern = forms.CharField(required=False, initial="*") def panel_cache(self, request): @@ -21,10 +22,18 @@ def _get_keys(): form = CacheManagerForm(request.POST) if form.is_valid(): limit_to = form.cleaned_data["pattern"] - if "_delete" in request.POST: + if "_disable" in request.POST: + cache_manager.active = False + return HttpResponseRedirect(".") + elif "_enable" in request.POST: + cache_manager.active = True + return HttpResponseRedirect(".") + elif "_delete" in request.POST: to_delete = list(_get_keys()) if to_delete: client.delete(*to_delete) + return HttpResponseRedirect(".") + else: form = CacheManagerForm() @@ -32,6 +41,7 @@ def _get_keys(): context["form"] = form cache_data = _get_keys() context["cache_data"] = cache_data + context["active"] = cache_manager.active return render(request, "smart_admin/panels/cache.html", context) diff --git a/src/country_workspace/cache/templates/smart_admin/panels/cache.html b/src/country_workspace/cache/templates/smart_admin/panels/cache.html new file mode 100644 index 0000000..5179e26 --- /dev/null +++ b/src/country_workspace/cache/templates/smart_admin/panels/cache.html @@ -0,0 +1,32 @@ +{% extends "smart_admin/console.html" %}{% load i18n static %} +{% block left %} +
+
+
+ {% csrf_token %} +
+ + {{ form.as_table }} +
+
+
+ {% if active %} + + {% else %} + + {% endif %} +
+   + +
+
+
+
+
+ {% for e in cache_data %} +
{{ e }}
+ {% endfor %} +
+
+{% endblock left %} diff --git a/src/country_workspace/config/__init__.py b/src/country_workspace/config/__init__.py index f20e3a0..2a4523c 100644 --- a/src/country_workspace/config/__init__.py +++ b/src/country_workspace/config/__init__.py @@ -19,7 +19,7 @@ def setting(anchor: str) -> str: def celery_doc(anchor: str) -> str: - return f"@see https://docs.celeryq.dev/en/stable/" f"userguide/configuration.html#{anchor}" + return f"@see https://docs.celeryq.dev/en/stable/userguide/configuration.html#{anchor}" class Group(Enum): @@ -55,6 +55,13 @@ class Group(Enum): "AURORA_API_TOKEN": (str, "", "", False, "Aurora API token"), "AURORA_API_URL": (str, "", "", False, "Aurora API url"), "CACHE_URL": (str, "", "redis://localhost:6379/0", True, setting("cache-url")), + "SELECT2_CACHE": ( + str, + "", + "redis://localhost:6379/9", + False, + "https://django-select2.readthedocs.io/en/latest/django_select2.html#module-django_select2.conf", + ), "CELERY_BROKER_URL": ( str, "", diff --git a/src/country_workspace/config/settings.py b/src/country_workspace/config/settings.py index 4c51211..fb033e8 100644 --- a/src/country_workspace/config/settings.py +++ b/src/country_workspace/config/settings.py @@ -131,6 +131,7 @@ USE_TZ = True CACHE_URL = env("CACHE_URL") +SELECT2_CACHE = env("SELECT2_CACHE") # REDIS_URL = urlparse(CACHE_URL).hostname CACHES = { "default": { @@ -140,7 +141,7 @@ }, "select2": { "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": CACHE_URL, + "LOCATION": SELECT2_CACHE or CACHE_URL, "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, diff --git a/src/country_workspace/web/templates/admin/base_site.html b/src/country_workspace/web/templates/admin/base_site.html index 241e16d..5bb3170 100644 --- a/src/country_workspace/web/templates/admin/base_site.html +++ b/src/country_workspace/web/templates/admin/base_site.html @@ -2,6 +2,8 @@ {% block title %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %} +{% block extrastyle %}{{ block.super }}{{ media }}{% endblock %} + {% block userlinks %}{{ block.super }} {# {% if sysinfo %} / System Info {% endif %} #} {% if profile_link %} / Profile {% endif %} diff --git a/src/country_workspace/web/templates/smart_admin/panels/cache.html b/src/country_workspace/web/templates/smart_admin/panels/cache.html deleted file mode 100644 index e2cefc7..0000000 --- a/src/country_workspace/web/templates/smart_admin/panels/cache.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "smart_admin/console.html" %}{% load i18n static %} -{% block left %} -
-
-
- {% csrf_token %} - {{ form }} -
- - -
-
-
-
- {% for e in cache_data %} -
{{ e }}
- {% endfor %} -
-
-{% endblock left %} diff --git a/tests/admin/panels/test_cache_panel.py b/tests/admin/panels/test_cache_panel.py deleted file mode 100644 index b4530c2..0000000 --- a/tests/admin/panels/test_cache_panel.py +++ /dev/null @@ -1,37 +0,0 @@ -from unittest.mock import MagicMock - -from country_workspace.admin import panel_cache -from country_workspace.cache.manager import cache_manager - - -def test_cache_panel(rf): - req = rf.get("/") - res = panel_cache(MagicMock(each_context=lambda s: {}), req) - assert res.status_code == 200 - - -def test_cache_panel_invalid(rf): - req = rf.post("/", {}) - res = panel_cache(MagicMock(each_context=lambda s: {}), req) - assert res.status_code == 200 - - -def test_cache_panel_filter(rf): - k = cache_manager.build_key("test_cache_panel_entry") - cache_manager.store(k, 1) - req = rf.post("/", {"pattern": "test_cache_panel_entry"}) - res = panel_cache(MagicMock(each_context=lambda s: {}), req) - assert res.status_code == 200 - assert b"test_cache_panel_entry" in res.content - - -def test_cache_panel_delete(rf): - k = cache_manager.build_key("test_cache_panel_delete") - cache_manager.store(k, 1) - req = rf.post("/", {"pattern": "*cache_panel_delete*", "_delete": "Delete"}) - res = panel_cache(MagicMock(each_context=lambda s: {}), req) - assert res.status_code == 200 - - req = rf.post("/", {"pattern": "xx", "_delete": "Delete"}) - res = panel_cache(MagicMock(each_context=lambda s: {}), req) - assert res.status_code == 200 diff --git a/tests/admin/test_admin_individual.py b/tests/admin/test_admin_individual.py index 026f2fa..d59bfb9 100644 --- a/tests/admin/test_admin_individual.py +++ b/tests/admin/test_admin_individual.py @@ -44,3 +44,10 @@ def test_individual_changelist(app, individual: "CountryIndividual"): assert res.status_code == 200 res = res.click(individual.name) assert res.status_code == 200 + + +@pytest.mark.parametrize("valid", ["v", "i", "u"]) +def test_individual_filter_by_valid(app, individual: "CountryIndividual", valid): + base_url = reverse("admin:country_workspace_individual_changelist") + res = app.get(f"{base_url}?valid={valid}") + assert res.status_code == 200 diff --git a/tests/admin/test_admin_job.py b/tests/admin/test_admin_job.py new file mode 100644 index 0000000..4de0eb2 --- /dev/null +++ b/tests/admin/test_admin_job.py @@ -0,0 +1,37 @@ +from typing import TYPE_CHECKING + +from django.urls import reverse + +import pytest +from django_webtest.pytest_plugin import MixinWithInstanceVariables + +from country_workspace.models import User + +if TYPE_CHECKING: + from testutils.types import CWTestApp + + from country_workspace.workspaces.models import AsyncJob + + +@pytest.fixture() +def job(): + from testutils.factories import AsyncJobFactory + + return AsyncJobFactory() + + +@pytest.fixture() +def app(django_app_factory: "MixinWithInstanceVariables", admin_user: "User") -> "CWTestApp": + django_app = django_app_factory(csrf_checks=False) + django_app.set_user(admin_user) + yield django_app + + +def test_job_filtering(app, job: "AsyncJob"): + base_url = reverse("admin:country_workspace_asyncjob_changelist") + app.get(base_url) + + app.get(f"{base_url}?program={job.program.pk}") + app.get(f"{base_url}?program={job.program.pk}&failed=f") + app.get(f"{base_url}?program={job.program.pk}&failed=s") + app.get(f"{base_url}?program={job.program.pk}&failed=x") diff --git a/tests/cache/test_cache_panel.py b/tests/cache/test_cache_panel.py new file mode 100644 index 0000000..065c228 --- /dev/null +++ b/tests/cache/test_cache_panel.py @@ -0,0 +1,63 @@ +from unittest import mock +from unittest.mock import MagicMock + +import pytest + +from country_workspace.admin import panel_cache +from country_workspace.cache.manager import CacheManager + + +@pytest.fixture +def manager(worker_id): + m = CacheManager(f"cache{worker_id}") + m.init() + m.active = True + yield m + + +def test_cache_panel(rf): + req = rf.get("/") + res = panel_cache(MagicMock(each_context=lambda s: {}), req) + assert res.status_code == 200 + + +def test_cache_panel_invalid(rf): + req = rf.post("/", {}) + res = panel_cache(MagicMock(each_context=lambda s: {}), req) + assert res.status_code == 200 + + +def test_cache_panel_filter(rf, manager): + with mock.patch("country_workspace.cache.smart_panel.cache_manager", manager): + k = manager.build_key("test_cache_panel_entry") + manager.store(k, 1) + req = rf.post("/", {"pattern": "test_cache_panel_entry"}) + res = panel_cache(MagicMock(each_context=lambda s: {}), req) + assert res.status_code == 200 + assert b"test_cache_panel_entry" in res.content + + +def test_cache_panel_delete(rf, manager): + with mock.patch("country_workspace.cache.smart_panel.cache_manager", manager): + k = manager.build_key("test_cache_panel_delete") + manager.store(k, 1) + req = rf.post("/", {"pattern": "*cache_panel_delete*", "_delete": "Delete"}) + res = panel_cache(MagicMock(each_context=lambda s: {}), req) + assert res.status_code == 302 + + req = rf.post("/", {"pattern": "xx", "_delete": "Delete"}) + res = panel_cache(MagicMock(each_context=lambda s: {}), req) + assert res.status_code == 302 + + +def test_cache_panel_toggle(rf, manager): + with mock.patch("country_workspace.cache.smart_panel.cache_manager", manager): + req = rf.post("/", {"pattern": "*", "_disable": "D"}) + res = panel_cache(MagicMock(each_context=lambda s: {}), req) + assert res.status_code == 302 + assert not manager.active + + req = rf.post("/", {"pattern": "*", "_enable": "E"}) + res = panel_cache(MagicMock(each_context=lambda s: {}), req) + assert res.status_code == 302 + assert manager.active diff --git a/tests/conftest.py b/tests/conftest.py index e1e8960..8b711c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -98,7 +98,7 @@ def mocked_responses(): @pytest.fixture() -def user(db): +def user(db, worker_id, settings): from testutils.factories import UserFactory return UserFactory() diff --git a/tests/extras/testutils/factories/household.py b/tests/extras/testutils/factories/household.py index 247404e..f203527 100644 --- a/tests/extras/testutils/factories/household.py +++ b/tests/extras/testutils/factories/household.py @@ -60,9 +60,16 @@ def get_hh_fields(household: "CountryHousehold"): } +def get_name(instance, num): + name = fake.last_name() + return f"{name} #{num}" + + class HouseholdFactory(AutoRegisterModelFactory): batch = factory.SubFactory(CountryBatchFactory) - name = factory.Faker("last_name") + # name = factory.Faker("last_name") + name = factory.LazyAttributeSequence(get_name) + flex_fields = factory.LazyAttribute(get_hh_fields) class Meta: diff --git a/tests/versioning/test_ver.py b/tests/versioning/test_ver.py index e56ed35..db4de81 100644 --- a/tests/versioning/test_ver.py +++ b/tests/versioning/test_ver.py @@ -1,11 +1,15 @@ from pathlib import Path from typing import TYPE_CHECKING +import pytest + from country_workspace import VERSION if TYPE_CHECKING: from country_workspace.versioning.management.manager import Manager +pytestmark = pytest.mark.xdist_group("versioning") + def test_manager_1(manager: "Manager", scripts: list[Path]) -> None: assert manager.max_version == 3 diff --git a/uv.lock b/uv.lock index 4417aab..9139a25 100644 --- a/uv.lock +++ b/uv.lock @@ -551,11 +551,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/a2/63/4428328cc0ca60ee0 [[package]] name = "django-adminfilters" -version = "2.5.1" +version = "2.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/5d/a8f22bedd8d17ca0b6fa90bac5492840f16fdd7d463f4e81c68dab4a5cb9/django_adminfilters-2.5.1.tar.gz", hash = "sha256:affaf0614a1939ad523f7a5ee1427f929f18f0df3a61b63d92eab6bdfcfab3a2", size = 57830 } +sdist = { url = "https://files.pythonhosted.org/packages/43/e6/bcf3341161b2d363281d0ddb9924ce27e31f7e8b370564b63c7f200e398c/django_adminfilters-2.5.2.tar.gz", hash = "sha256:2d4982490631cf198734e83337280ca831d5f559995198843103b30202104a29", size = 58865 } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/4e/af6b7a97a570acea30bb446ba58794336df44d351069918460e10d6def69/django_adminfilters-2.5.1-py2.py3-none-any.whl", hash = "sha256:f568f3009886d74463c0aef7d54acd10bb2c6374e4ce2be1f8642695dcdc8e53", size = 47514 }, + { url = "https://files.pythonhosted.org/packages/f0/eb/2965d0ae94edc46e8a3ec0328c3ec71f580afd57084a785a8e99d39c0a55/django_adminfilters-2.5.2-py2.py3-none-any.whl", hash = "sha256:c1f19c8215b4573159359eaa1231ecdb5f5d9edbfca93f44e4dce27636630596", size = 49246 }, ] [[package]] @@ -1222,7 +1222,7 @@ requires-dist = [ { name = "dictdiffer", specifier = ">=0.9.0" }, { name = "django", specifier = ">=5.1" }, { name = "django-adminactions", specifier = ">=2.3.0" }, - { name = "django-adminfilters", specifier = "==2.5.1" }, + { name = "django-adminfilters", specifier = ">=2.5.2" }, { name = "django-cacheops", specifier = ">=7.1" }, { name = "django-celery-beat", specifier = ">=2.6.0" }, { name = "django-celery-boost", specifier = ">=0.5.0" },