From 930c6a305926d9fe4377d6d2914cd5cd6e13735f Mon Sep 17 00:00:00 2001 From: Nikolai R Kristiansen Date: Fri, 30 May 2025 13:04:13 +0200 Subject: [PATCH] chore: ruff fixes and formatting --- .gitignore | 1 + .pre-commit-config.yaml | 12 +- fabfile.py | 38 +++--- mailadmin/admin.py | 7 +- mailadmin/api.py | 61 +++++---- mailadmin/backends.py | 14 +-- mailadmin/management/__init__.py | 0 mailadmin/management/commands/__init__.py | 0 .../management/commands/import_groups.py | 28 ----- mailadmin/migrations/0001_initial.py | 43 ++++--- .../migrations/0002_orgunit_inside_id.py | 12 +- .../migrations/0003_auto_20141105_2153.py | 34 +++-- .../migrations/0004_auto_20150125_1401.py | 38 +++--- .../migrations/0005_auto_20150125_1940.py | 12 +- .../migrations/0006_auto_20160110_1613.py | 17 ++- mailadmin/models.py | 17 ++- mailadmin/permissions.py | 18 +-- mailadmin/serializers.py | 30 +++-- mailadmin/test_all.py | 16 +++ mailadmin/tests.py | 15 --- mailadmin/urls.py | 30 +++-- mailadmin/views.py | 46 +++---- mailadminsite/settings.py | 119 +++++++++--------- mailadminsite/urls.py | 12 +- mailadminsite/wsgi.py | 2 + pyproject.toml | 33 +++++ uv.lock | 102 +++++++++++++++ 27 files changed, 444 insertions(+), 313 deletions(-) delete mode 100644 mailadmin/management/__init__.py delete mode 100644 mailadmin/management/commands/__init__.py delete mode 100644 mailadmin/management/commands/import_groups.py create mode 100644 mailadmin/test_all.py delete mode 100644 mailadmin/tests.py diff --git a/.gitignore b/.gitignore index 42ce1ea..934bb72 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules/ .sass-cache/ staticfiles/ mailadmin/static/mailadmin/dist/ +__pycache__/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e2b0a86..be10ef3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,12 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - # - repo: https://github.com/astral-sh/ruff-pre-commit - # rev: 'v0.11.7' - # hooks: - # - id: ruff - # args: [--fix, --exit-non-zero-on-fix] - # - id: ruff-format + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: 'v0.11.7' + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format - repo: https://github.com/adamchainz/django-upgrade rev: "1.24.0" hooks: diff --git a/fabfile.py b/fabfile.py index 261dc66..3a90db0 100644 --- a/fabfile.py +++ b/fabfile.py @@ -7,27 +7,33 @@ from invoke import task @task() def deploy(c): """Make sure proxy_user is set to your neuf username.""" - project_path = '/var/www/neuf.no/mailadmin' - proxy_user = os.getenv('DEPLOY_USER', os.getenv('USER')) + project_path = "/var/www/neuf.no/mailadmin" + proxy_user = os.getenv("DEPLOY_USER", os.getenv("USER")) - c = Connection(host='gitdeploy@dreamcast.neuf.no', gateway=Connection('login.neuf.no', user=proxy_user)) + c = Connection( + host="gitdeploy@dreamcast.neuf.no", + gateway=Connection("login.neuf.no", user=proxy_user), + ) - with c.cd(project_path), c.prefix('source {}/venv/bin/activate'.format(project_path)): - c.run('git pull') # Get source - c.run('pip install -U pip') - c.run('pip install -r requirements.txt') # install deps in virtualenv - with c.cd('mailadmin/static/mailadmin'): # install and compile frontend deps - c.run('npm i') - c.run('npm run build') - c.run('python manage.py collectstatic --noinput -i node_modules') # Collect static - c.run('python manage.py migrate') # Run DB migrations + with ( + c.cd(project_path), + c.prefix(f"source {project_path}/venv/bin/activate"), + ): + c.run("git pull") # Get source + c.run("pip install -U pip") + c.run("pip install -r requirements.txt") # install deps in virtualenv + with c.cd("mailadmin/static/mailadmin"): # install and compile frontend deps + c.run("npm i") + c.run("npm run build") + c.run("python manage.py collectstatic --noinput -i node_modules") # Collect static + c.run("python manage.py migrate") # Run DB migrations # Reload gunicorn - c.sudo('/usr/bin/supervisorctl pid lister.neuf.no | xargs kill -HUP', shell=False) + c.sudo("/usr/bin/supervisorctl pid lister.neuf.no | xargs kill -HUP", shell=False) @task def build(c): - with c.cd('mailadmin/static/mailadmin'): - c.run('npm i') - c.run('npm run build') + with c.cd("mailadmin/static/mailadmin"): + c.run("npm i") + c.run("npm run build") diff --git a/mailadmin/admin.py b/mailadmin/admin.py index ffd0329..1ca46d5 100644 --- a/mailadmin/admin.py +++ b/mailadmin/admin.py @@ -10,9 +10,10 @@ class PrefixInline(admin.TabularInline): @admin.register(OrgUnit) class OrgUnitAdmin(admin.ModelAdmin): - list_display = ('name', 'is_active') - list_filter = ('is_active',) - filter_horizontal = ('admin_groups', 'member_groups') + list_display = ("name", "is_active") + list_filter = ("is_active",) + filter_horizontal = ("admin_groups", "member_groups") inlines = [PrefixInline] + admin.site.register(Prefix) diff --git a/mailadmin/api.py b/mailadmin/api.py index a54147a..050ef76 100644 --- a/mailadmin/api.py +++ b/mailadmin/api.py @@ -1,38 +1,44 @@ +import logging + import requests from django.conf import settings -import logging -from requests.exceptions import ConnectionError from requests.auth import HTTPBasicAuth -from rest_framework import exceptions -from rest_framework import status +from requests.exceptions import ConnectionError +from rest_framework import exceptions, status + logger = logging.getLogger(__name__) class DPDAPIException(exceptions.APIException): status_code = 503 - detail = 'Unknown error.' + detail = "Unknown error." def __init__(self, status_code, detail): self.status_code = status_code self.detail = detail -class DjangoPostfixDovecotAPI(object): +class DjangoPostfixDovecotAPI: """ django-postfix-dovecot-api https://git.neuf.no/edb/django-postfix-dovecot-api#tab-readme """ - def __init__(self, base_url=settings.DPD_API_URL, username=settings.DPD_API_USERNAME, - password=settings.DPD_API_PASSWORD): + + def __init__( + self, + base_url=settings.DPD_API_URL, + username=settings.DPD_API_USERNAME, + password=settings.DPD_API_PASSWORD, + ): self.base_url = base_url self.username = username self.password = password def _request(self, method, path, params=None, json=None): - headers = {'content-type': 'application/json'} - url = "{}{}".format(self.base_url, path) + headers = {"content-type": "application/json"} + url = f"{self.base_url}{path}" try: r = requests.request( @@ -41,20 +47,21 @@ class DjangoPostfixDovecotAPI(object): params=params, auth=HTTPBasicAuth(self.username, self.password), json=json, - headers=headers) + headers=headers, + ) except ConnectionError as e: - msg = 'mxapi at {} unavailable {}'.format(url, str(e)) + msg = f"mxapi at {url} unavailable {e!s}" logger.warning(msg) - raise DPDAPIException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail={'error': msg}) + raise DPDAPIException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail={"error": msg}) try: r.raise_for_status() except requests.exceptions.RequestException as e: - logger.warning('mxapi errored {}'.format(e.response)) + logger.warning(f"mxapi errored {e.response}") try: raise DPDAPIException(status_code=e.response.status_code, detail=e.response.json()) except ValueError: - raise DPDAPIException(status_code=e.response.status_code, detail='mxapi fatal error') + raise DPDAPIException(status_code=e.response.status_code, detail="mxapi fatal error") try: ret = r.json() @@ -64,33 +71,25 @@ class DjangoPostfixDovecotAPI(object): return ret def create_aliases(self, aliases): - return self._request('POST', '/aliases/create_bulk/', json=aliases) + return self._request("POST", "/aliases/create_bulk/", json=aliases) def delete_aliases(self, aliases): - return self._request('DELETE', '/aliases/delete_bulk/', json=aliases) + return self._request("DELETE", "/aliases/delete_bulk/", json=aliases) def list_aliases_regex(self, regex): - """ Limit aliases by regular expression and domain name + """Limit aliases by regular expression and domain name :param regex: Regular expression filtering out aliases """ params = { - 'domain__name': settings.NEUF_EMAIL_DOMAIN_NAME, - 'source__iregex': regex + "domain__name": settings.NEUF_EMAIL_DOMAIN_NAME, + "source__iregex": regex, } - return self._request( - 'GET', - '/aliases/', - params=params - ) + return self._request("GET", "/aliases/", params=params) def list_domains(self, name=None): params = {} if name is not None: - params['name'] = name + params["name"] = name - return self._request( - 'GET', - '/domains/', - params=params - ) + return self._request("GET", "/domains/", params=params) diff --git a/mailadmin/backends.py b/mailadmin/backends.py index 0600b98..71cfcb4 100644 --- a/mailadmin/backends.py +++ b/mailadmin/backends.py @@ -17,14 +17,14 @@ class LDAPEmailBackend(LDAPBackend): the user's DN and ldap_user.attrs contains all of their LDAP attributes. """ model = self.get_user_model() - username_field = getattr(model, 'USERNAME_FIELD', 'username') + username_field = getattr(model, "USERNAME_FIELD", "username") kwargs = { - username_field + '__iexact': ldap_user.attrs['uid'][0], - 'defaults': { - username_field: ldap_user.attrs['uid'][0].lower(), - 'email': email - } + username_field + "__iexact": ldap_user.attrs["uid"][0], + "defaults": { + username_field: ldap_user.attrs["uid"][0].lower(), + "email": email, + }, } - return model.objects.get_or_create(**kwargs) \ No newline at end of file + return model.objects.get_or_create(**kwargs) diff --git a/mailadmin/management/__init__.py b/mailadmin/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/mailadmin/management/commands/__init__.py b/mailadmin/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/mailadmin/management/commands/import_groups.py b/mailadmin/management/commands/import_groups.py deleted file mode 100644 index 0b04591..0000000 --- a/mailadmin/management/commands/import_groups.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.core.management.base import BaseCommand, CommandError -from django.contrib.auth.models import Group - -import requests - -from mailadmin.models import OrgUnit - - -class Command(BaseCommand): - help = 'Adds groups and OrgUnits from inside.studentersamfundet.no.' - - def handle(self, *args, **options): - r = requests.get('https://inside.studentersamfundet.no/api/groups.php') - res = r.json() - if 'results' not in res: - self.stderr.write('Error: No groups, bailing...') - - groups = res['results'] - for g in groups: - if g['posix_group'] != "": - obj, created = Group.objects.get_or_create(name=g['posix_group']) - ou_obj, ou_created = OrgUnit.objects.get_or_create(name=g['division_name'], inside_id=g['division_id']) - if 'styret' in g['posix_group']: - ou_obj.admin_groups.add(obj) - else: - ou_obj.member_groups.add(obj) - - self.stdout.write('Done.') diff --git a/mailadmin/migrations/0001_initial.py b/mailadmin/migrations/0001_initial.py index 40f283a..e929649 100644 --- a/mailadmin/migrations/0001_initial.py +++ b/mailadmin/migrations/0001_initial.py @@ -1,30 +1,39 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('auth', '0001_initial'), + ("auth", "0001_initial"), ] operations = [ migrations.CreateModel( - name='OrgUnit', + name="OrgUnit", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=500)), - ('prefix', models.CharField(max_length=256, null=True, blank=True)), - ('posix_group', models.CharField(max_length=256, null=True, blank=True)), - ('is_active', models.BooleanField(default=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('update', models.DateTimeField(auto_now=True)), - ('groups', models.ManyToManyField(to='auth.Group', null=True, blank=True)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=500)), + ("prefix", models.CharField(max_length=256, null=True, blank=True)), + ( + "posix_group", + models.CharField(max_length=256, null=True, blank=True), + ), + ("is_active", models.BooleanField(default=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("update", models.DateTimeField(auto_now=True)), + ( + "groups", + models.ManyToManyField(to="auth.Group", null=True, blank=True), + ), ], - options={ - }, + options={}, bases=(models.Model,), ), ] diff --git a/mailadmin/migrations/0002_orgunit_inside_id.py b/mailadmin/migrations/0002_orgunit_inside_id.py index e0b93ae..284691e 100644 --- a/mailadmin/migrations/0002_orgunit_inside_id.py +++ b/mailadmin/migrations/0002_orgunit_inside_id.py @@ -1,19 +1,15 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('mailadmin', '0001_initial'), + ("mailadmin", "0001_initial"), ] operations = [ migrations.AddField( - model_name='orgunit', - name='inside_id', + model_name="orgunit", + name="inside_id", field=models.IntegerField(null=True, blank=True), preserve_default=True, ), diff --git a/mailadmin/migrations/0003_auto_20141105_2153.py b/mailadmin/migrations/0003_auto_20141105_2153.py index 92781f5..2738de6 100644 --- a/mailadmin/migrations/0003_auto_20141105_2153.py +++ b/mailadmin/migrations/0003_auto_20141105_2153.py @@ -1,39 +1,35 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('auth', '0001_initial'), - ('mailadmin', '0002_orgunit_inside_id'), + ("auth", "0001_initial"), + ("mailadmin", "0002_orgunit_inside_id"), ] operations = [ migrations.AlterModelOptions( - name='orgunit', - options={'ordering': ['name']}, + name="orgunit", + options={"ordering": ["name"]}, ), migrations.RemoveField( - model_name='orgunit', - name='groups', + model_name="orgunit", + name="groups", ), migrations.RemoveField( - model_name='orgunit', - name='posix_group', + model_name="orgunit", + name="posix_group", ), migrations.AddField( - model_name='orgunit', - name='admin_groups', - field=models.ManyToManyField(related_name='admin_orgunits', null=True, to='auth.Group', blank=True), + model_name="orgunit", + name="admin_groups", + field=models.ManyToManyField(related_name="admin_orgunits", null=True, to="auth.Group", blank=True), preserve_default=True, ), migrations.AddField( - model_name='orgunit', - name='member_groups', - field=models.ManyToManyField(related_name='member_orgunits', null=True, to='auth.Group', blank=True), + model_name="orgunit", + name="member_groups", + field=models.ManyToManyField(related_name="member_orgunits", null=True, to="auth.Group", blank=True), preserve_default=True, ), ] diff --git a/mailadmin/migrations/0004_auto_20150125_1401.py b/mailadmin/migrations/0004_auto_20150125_1401.py index 2eb2685..29028c8 100644 --- a/mailadmin/migrations/0004_auto_20150125_1401.py +++ b/mailadmin/migrations/0004_auto_20150125_1401.py @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations import django.db.models.deletion +from django.db import migrations, models def normalize_prefixes(apps, schema_editor): @@ -19,26 +16,39 @@ def noop(*args, **kwargs): class Migration(migrations.Migration): - dependencies = [ - ('mailadmin', '0003_auto_20141105_2153'), + ("mailadmin", "0003_auto_20141105_2153"), ] operations = [ migrations.CreateModel( - name='Prefix', + name="Prefix", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.CharField(max_length=500, null=True, blank=True)), - ('orgunit', models.ForeignKey(related_name='prefixes', to='mailadmin.OrgUnit', on_delete=django.db.models.deletion.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.CharField(max_length=500, null=True, blank=True)), + ( + "orgunit", + models.ForeignKey( + related_name="prefixes", + to="mailadmin.OrgUnit", + on_delete=django.db.models.deletion.CASCADE, + ), + ), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.RunPython(normalize_prefixes, noop), migrations.RemoveField( - model_name='orgunit', - name='prefix', + model_name="orgunit", + name="prefix", ), ] diff --git a/mailadmin/migrations/0005_auto_20150125_1940.py b/mailadmin/migrations/0005_auto_20150125_1940.py index 29b344f..058aa95 100644 --- a/mailadmin/migrations/0005_auto_20150125_1940.py +++ b/mailadmin/migrations/0005_auto_20150125_1940.py @@ -1,18 +1,14 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations +from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('mailadmin', '0004_auto_20150125_1401'), + ("mailadmin", "0004_auto_20150125_1401"), ] operations = [ migrations.AlterModelOptions( - name='prefix', - options={'verbose_name_plural': 'prefixes'}, + name="prefix", + options={"verbose_name_plural": "prefixes"}, ), ] diff --git a/mailadmin/migrations/0006_auto_20160110_1613.py b/mailadmin/migrations/0006_auto_20160110_1613.py index c348fd1..a134cc2 100644 --- a/mailadmin/migrations/0006_auto_20160110_1613.py +++ b/mailadmin/migrations/0006_auto_20160110_1613.py @@ -1,25 +1,22 @@ -# -*- coding: utf-8 -*- # Generated by Django 1.9.1 on 2016-01-10 15:13 -from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('mailadmin', '0005_auto_20150125_1940'), + ("mailadmin", "0005_auto_20150125_1940"), ] operations = [ migrations.AlterField( - model_name='orgunit', - name='admin_groups', - field=models.ManyToManyField(blank=True, related_name='admin_orgunits', to='auth.Group'), + model_name="orgunit", + name="admin_groups", + field=models.ManyToManyField(blank=True, related_name="admin_orgunits", to="auth.Group"), ), migrations.AlterField( - model_name='orgunit', - name='member_groups', - field=models.ManyToManyField(blank=True, related_name='member_orgunits', to='auth.Group'), + model_name="orgunit", + name="member_groups", + field=models.ManyToManyField(blank=True, related_name="member_orgunits", to="auth.Group"), ), ] diff --git a/mailadmin/models.py b/mailadmin/models.py index 7533db4..7eb4ab9 100644 --- a/mailadmin/models.py +++ b/mailadmin/models.py @@ -8,31 +8,30 @@ class OrgUnit(models.Model): name = models.CharField(max_length=500) is_active = models.BooleanField(default=True) - admin_groups = models.ManyToManyField(Group, blank=True, related_name='admin_orgunits') - member_groups = models.ManyToManyField(Group, blank=True, related_name='member_orgunits') + admin_groups = models.ManyToManyField(Group, blank=True, related_name="admin_orgunits") + member_groups = models.ManyToManyField(Group, blank=True, related_name="member_orgunits") created = models.DateTimeField(auto_now_add=True) update = models.DateTimeField(auto_now=True) inside_id = models.IntegerField(null=True, blank=True) class Meta: - ordering = ['name'] + ordering = ["name"] class PrefixQueryset(models.QuerySet): - def as_regex(self): - prefixes = self.values_list('name', flat=True) - return '|'.join(['^{0}-|^{0}@'.format(p) for p in prefixes]) + prefixes = self.values_list("name", flat=True) + return "|".join([f"^{p}-|^{p}@" for p in prefixes]) class Prefix(models.Model): def __str__(self): - return self.name or '' + return self.name or "" name = models.CharField(max_length=500, blank=True, null=True) - orgunit = models.ForeignKey('mailadmin.OrgUnit', models.CASCADE, related_name='prefixes') + orgunit = models.ForeignKey("mailadmin.OrgUnit", models.CASCADE, related_name="prefixes") objects = PrefixQueryset.as_manager() class Meta: - verbose_name_plural = 'prefixes' + verbose_name_plural = "prefixes" diff --git a/mailadmin/permissions.py b/mailadmin/permissions.py index f810afd..71e1a8a 100644 --- a/mailadmin/permissions.py +++ b/mailadmin/permissions.py @@ -1,17 +1,21 @@ -from rest_framework import permissions import re +from rest_framework import permissions + from mailadmin.serializers import AliasSerializer class SourcePrefixOwner(permissions.BasePermission): - """ Ref: http://www.django-rest-framework.org/api-guide/permissions """ + """Ref: http://www.django-rest-framework.org/api-guide/permissions""" def has_permission(self, request, view): - assert hasattr(view, 'user_prefixes') + assert hasattr(view, "user_prefixes") # Read permissions (GET, HEAD or OPTIONS) are allowed for all authenticated request - if request.method in permissions.SAFE_METHODS or request.method not in ['DELETE', 'POST']: + if request.method in permissions.SAFE_METHODS or request.method not in [ + "DELETE", + "POST", + ]: return True if request.user.is_superuser: @@ -29,8 +33,4 @@ class SourcePrefixOwner(permissions.BasePermission): # Prepare regular expression regexp = my_prefixes.as_regex() # All source adresses should match the prefix regexp - for alias in serializer.initial_data: - if re.search(regexp, alias.get('source', '')) is None: - return False - - return True + return all(re.search(regexp, alias.get("source", "")) is not None for alias in serializer.initial_data) diff --git a/mailadmin/serializers.py b/mailadmin/serializers.py index 167eb3d..4d3cbe2 100644 --- a/mailadmin/serializers.py +++ b/mailadmin/serializers.py @@ -1,15 +1,15 @@ from django.conf import settings -from django.contrib.auth.models import User, Group +from django.contrib.auth.models import Group, User from rest_framework import serializers +from rest_framework.relations import SlugRelatedField from mailadmin.models import OrgUnit -from rest_framework.relations import SlugRelatedField class GroupSerializer(serializers.ModelSerializer): class Meta: model = Group - fields = ['id', 'name'] + fields = ["id", "name"] class UserSerializer(serializers.ModelSerializer): @@ -17,21 +17,27 @@ class UserSerializer(serializers.ModelSerializer): class Meta: model = User - exclude = ['password', 'user_permissions'] + exclude = ["password", "user_permissions"] class OrgUnitSerializer(serializers.ModelSerializer): admin_groups = GroupSerializer(many=True) member_groups = GroupSerializer(many=True) - prefixes = SlugRelatedField( - many=True, - read_only=True, - slug_field='name') + prefixes = SlugRelatedField(many=True, read_only=True, slug_field="name") class Meta: model = OrgUnit - fields = ['id', 'admin_groups', 'member_groups', 'prefixes', 'inside_id', 'name', 'is_active', - 'created', 'update'] + fields = [ + "id", + "admin_groups", + "member_groups", + "prefixes", + "inside_id", + "name", + "is_active", + "created", + "update", + ] class AliasSerializer(serializers.Serializer): @@ -42,8 +48,8 @@ class AliasSerializer(serializers.Serializer): def validate_source(self, value): _domain_name = settings.NEUF_EMAIL_DOMAIN_NAME - domain_part = value.split('@')[1] + domain_part = value.split("@")[1] if domain_part != _domain_name: - raise serializers.ValidationError("source '{0}' is not in domain {1}".format(value, _domain_name)) + raise serializers.ValidationError(f"source '{value}' is not in domain {_domain_name}") return value diff --git a/mailadmin/test_all.py b/mailadmin/test_all.py new file mode 100644 index 0000000..a4d5ded --- /dev/null +++ b/mailadmin/test_all.py @@ -0,0 +1,16 @@ +from http import HTTPStatus + +import pytest +from django.contrib.auth.models import User +from django.urls import reverse + + +@pytest.fixture +def user(db): + return User.objects.create(username="admin") + + +def test_open_lists(user, client): + client.force_login(user=user) + response = client.get(reverse("lists")) + assert response.status_code == HTTPStatus.OK diff --git a/mailadmin/tests.py b/mailadmin/tests.py deleted file mode 100644 index f353eed..0000000 --- a/mailadmin/tests.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.contrib.auth.models import User -from django.test import TestCase -# import unittest -from django.urls import reverse - - -class FrontendTest(TestCase): - - def setUp(self): - self.user = User.objects.create(username='admin') - self.client.force_login(user=self.user) - - def test_open_lists(self): - response = self.client.get(reverse('lists')) - self.assertEqual(response.status_code, 200) diff --git a/mailadmin/urls.py b/mailadmin/urls.py index 4fecdc9..5860f3f 100644 --- a/mailadmin/urls.py +++ b/mailadmin/urls.py @@ -1,23 +1,31 @@ from django.urls import path from rest_framework.routers import DefaultRouter -from mailadmin.views import MyUserViewSet, MyOrgUnitViewSet, OrgUnitViewSet, AliasesView, get_domains, index, lists, \ - AliasesDeleteView +from mailadmin.views import ( + AliasesDeleteView, + AliasesView, + MyOrgUnitViewSet, + MyUserViewSet, + OrgUnitViewSet, + get_domains, + index, + lists, +) urlpatterns = [ - path('', index, name='index'), - path('lists/', lists, name='lists'), + path("", index, name="index"), + path("lists/", lists, name="lists"), ] # API router = DefaultRouter() -router.register(r'api/me', MyUserViewSet, basename='me') -router.register(r'api/orgunits', MyOrgUnitViewSet, basename='ous') -router.register(r'api/orgunits/all', OrgUnitViewSet, basename='allous') +router.register(r"api/me", MyUserViewSet, basename="me") +router.register(r"api/orgunits", MyOrgUnitViewSet, basename="ous") +router.register(r"api/orgunits/all", OrgUnitViewSet, basename="allous") urlpatterns += router.urls urlpatterns += [ - path('api/aliases/', AliasesView.as_view(), name='aliases'), - path('api/aliases/delete/', AliasesDeleteView.as_view(), name='aliases-delete'), - path('api/domains/', get_domains, name='email-domains'), -] \ No newline at end of file + path("api/aliases/", AliasesView.as_view(), name="aliases"), + path("api/aliases/delete/", AliasesDeleteView.as_view(), name="aliases-delete"), + path("api/domains/", get_domains, name="email-domains"), +] diff --git a/mailadmin/views.py b/mailadmin/views.py index b53f7cf..5f68b51 100644 --- a/mailadmin/views.py +++ b/mailadmin/views.py @@ -1,21 +1,21 @@ +import logging + from django.conf import settings -from django.contrib.auth import login, logout as auth_logout +from django.contrib.auth import login +from django.contrib.auth import logout as auth_logout from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.models import User -from django.shortcuts import render, redirect -import logging -from rest_framework import viewsets, views +from django.shortcuts import redirect, render +from rest_framework import status, views, viewsets from rest_framework.decorators import api_view from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from rest_framework import status from mailadmin.api import DjangoPostfixDovecotAPI from mailadmin.models import OrgUnit, Prefix -from mailadmin.serializers import UserSerializer, OrgUnitSerializer, AliasSerializer from mailadmin.permissions import SourcePrefixOwner - +from mailadmin.serializers import AliasSerializer, OrgUnitSerializer, UserSerializer logger = logging.getLogger(__name__) @@ -26,14 +26,13 @@ def index(request): if form.is_valid(): # Log the user in. login(request, form.get_user()) - return redirect('lists') + return redirect("lists") + elif request.user.is_authenticated: + return redirect("lists") else: - if request.user.is_authenticated: - return redirect('lists') - else: - form = AuthenticationForm(request) + form = AuthenticationForm(request) - return render(request, "index.html", {'form': form}) + return render(request, "index.html", {"form": form}) @login_required @@ -43,19 +42,22 @@ def lists(request): def logout(request): auth_logout(request) - return redirect('index') + return redirect("index") class BaseAliasesView(views.APIView): _api = DjangoPostfixDovecotAPI() - permission_classes = (IsAuthenticated, SourcePrefixOwner,) + permission_classes = ( + IsAuthenticated, + SourcePrefixOwner, + ) def _user_groups(self, request): - group_filter = request.query_params.get('groups', None) + group_filter = request.query_params.get("groups", None) if group_filter is None: user_groups = request.user.groups.all() else: - group_filter = group_filter.split(',') if ',' in group_filter else [group_filter] + group_filter = group_filter.split(",") if "," in group_filter else [group_filter] user_groups = request.user.groups.filter(pk__in=group_filter) return user_groups @@ -71,14 +73,15 @@ class BaseAliasesView(views.APIView): class AliasesView(BaseAliasesView): - """ List and create aliases from django-postfix-dovecot-api """ + """List and create aliases from django-postfix-dovecot-api""" + def get(self, request): user_prefixes = self.user_prefixes(request) # Prepare regular expression regex = user_prefixes.as_regex() if len(user_prefixes) == 0: - return Response({'result': 'No results'}) # Need at least one + return Response({"result": "No results"}) # Need at least one return Response(self._api.list_aliases_regex(regex)) @@ -91,7 +94,8 @@ class AliasesView(BaseAliasesView): class AliasesDeleteView(BaseAliasesView): - """ Delete aliases (using POST) from django-postfix-dovecot-api """ + """Delete aliases (using POST) from django-postfix-dovecot-api""" + def post(self, request): aliases = AliasSerializer(data=request.data, many=True) if not aliases.is_valid(): @@ -128,7 +132,7 @@ class OrgUnitViewSet(viewsets.ReadOnlyModelViewSet): permission_classes = [IsAuthenticated] -@api_view(['GET']) +@api_view(["GET"]) def get_domains(request): api = DjangoPostfixDovecotAPI() domains = api.list_domains(name=settings.NEUF_EMAIL_DOMAIN_NAME) diff --git a/mailadminsite/settings.py b/mailadminsite/settings.py index f90e5ac..f570e84 100644 --- a/mailadminsite/settings.py +++ b/mailadminsite/settings.py @@ -1,4 +1,6 @@ +import contextlib from pathlib import Path + import ldap from django_auth_ldap.config import LDAPSearch, PosixGroupType @@ -9,7 +11,7 @@ BASE_DIR = Path(__file__).parent.parent # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'g658807^w2!kzsdb_wznxo$5xpjb2x(sfoub%l_ap77@@_zt&*' +SECRET_KEY = "g658807^w2!kzsdb_wznxo$5xpjb2x(sfoub%l_ap77@@_zt&*" # noqa: S105 # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -18,56 +20,55 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - - 'rest_framework', - 'bootstrap4', - 'mailadmin', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", + "bootstrap4", + "mailadmin", ) MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", ] -ROOT_URLCONF = 'mailadminsite.urls' +ROOT_URLCONF = "mailadminsite.urls" -WSGI_APPLICATION = 'mailadminsite.wsgi.application' +WSGI_APPLICATION = "mailadminsite.wsgi.application" # Database # https://docs.djangoproject.com/en/1.7/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR.joinpath('db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR.joinpath("db.sqlite3"), } } TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.debug', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', - 'django.template.context_processors.request', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", + "django.template.context_processors.request", ], }, }, @@ -75,35 +76,35 @@ TEMPLATES = [ # Internationalization # https://docs.djangoproject.com/en/1.7/topics/i18n/ -LANGUAGE_CODE = 'nb' -TIME_ZONE = 'Europe/Oslo' +LANGUAGE_CODE = "nb" +TIME_ZONE = "Europe/Oslo" USE_I18N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ -STATIC_URL = '/static/' -STATIC_ROOT = BASE_DIR.joinpath('staticfiles') +STATIC_URL = "/static/" +STATIC_ROOT = BASE_DIR.joinpath("staticfiles") -LOGIN_REDIRECT_URL = '/lists/' +LOGIN_REDIRECT_URL = "/lists/" # Django Postfix Dovecot -DPD_API_URL = 'http://localhost:8080/api' +DPD_API_URL = "http://localhost:8080/api" DPD_API_USERNAME = "" DPD_API_PASSWORD = "" -NEUF_EMAIL_DOMAIN_NAME = 'stry.no' +NEUF_EMAIL_DOMAIN_NAME = "stry.no" AUTHENTICATION_BACKENDS = ( - 'mailadmin.backends.LDAPEmailBackend', - 'mailadmin.backends.LDAPUsernameBackend', - 'django.contrib.auth.backends.ModelBackend', + "mailadmin.backends.LDAPEmailBackend", + "mailadmin.backends.LDAPUsernameBackend", + "django.contrib.auth.backends.ModelBackend", ) # LDAP server URI and BIND_DN, same as db-settings -AUTH_LDAP_U_SERVER_URI = 'ldap://localhost/' -AUTH_LDAP_U_BIND_DN = 'cn=admin,dc=neuf,dc=no' -AUTH_LDAP_U_BIND_PASSWORD = 'toor' +AUTH_LDAP_U_SERVER_URI = "ldap://localhost/" +AUTH_LDAP_U_BIND_DN = "cn=admin,dc=neuf,dc=no" +AUTH_LDAP_U_BIND_PASSWORD = "toor" # noqa: S105 # Basic user auth AUTH_LDAP_U_USER_SEARCH = LDAPSearch("ou=People,dc=neuf,dc=no", ldap.SCOPE_ONELEVEL, "(uid=%(user)s)") @@ -116,7 +117,7 @@ AUTH_LDAP_U_MIRROR_GROUPS = True AUTH_LDAP_U_USER_FLAGS_BY_GROUP = { "is_active": "cn=dns-aktiv,ou=Groups,dc=neuf,dc=no", "is_staff": "cn=edb,ou=Groups,dc=neuf,dc=no", - "is_superuser": "cn=edbadmin,ou=Groups,dc=neuf,dc=no" + "is_superuser": "cn=edbadmin,ou=Groups,dc=neuf,dc=no", } # User attribute mappings AUTH_LDAP_U_USER_ATTR_MAP = { @@ -125,9 +126,7 @@ AUTH_LDAP_U_USER_ATTR_MAP = { "email": "mail", } # User profile attribute mappings -AUTH_LDAP_U_PROFILE_ATTR_MAP = { - "home_directory": "homeDirectory" -} +AUTH_LDAP_U_PROFILE_ATTR_MAP = {"home_directory": "homeDirectory"} # Allways update the django user object on authentication. AUTH_LDAP_U_ALWAYS_UPDATE_USER = True @@ -146,16 +145,10 @@ AUTH_LDAP_E_USER_ATTR_MAP = AUTH_LDAP_U_USER_ATTR_MAP AUTH_LDAP_E_PROFILE_ATTR_MAP = AUTH_LDAP_U_PROFILE_ATTR_MAP AUTH_LDAP_E_ALWAYS_UPDATE_USER = AUTH_LDAP_U_ALWAYS_UPDATE_USER -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework.authentication.SessionAuthentication', - ) -} +REST_FRAMEWORK = {"DEFAULT_AUTHENTICATION_CLASSES": ("rest_framework.authentication.SessionAuthentication",)} -DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" # Local settings -try: - from .local_settings import * -except ImportError: - pass +with contextlib.suppress(ImportError): + from .local_settings import * # noqa: F403 diff --git a/mailadminsite/urls.py b/mailadminsite/urls.py index 78a0d72..ecc707b 100644 --- a/mailadminsite/urls.py +++ b/mailadminsite/urls.py @@ -1,18 +1,18 @@ -from django.urls import include, path, re_path from django.contrib import admin from django.contrib.auth.views import LoginView from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.urls import include, path, re_path from rest_framework import urls as rest_framework_urls from mailadmin import urls as mailadmin_urls from mailadmin.views import logout urlpatterns = [ - path('', include(mailadmin_urls)), - re_path(r'^admin/', admin.site.urls), - path('api-auth/', include(rest_framework_urls, namespace='rest_framework')), - path('accounts/login/', LoginView.as_view(), name='login'), - path('accounts/logout/', logout, name='logout') + path("", include(mailadmin_urls)), + re_path(r"^admin/", admin.site.urls), + path("api-auth/", include(rest_framework_urls, namespace="rest_framework")), + path("accounts/login/", LoginView.as_view(), name="login"), + path("accounts/logout/", logout, name="logout"), ] urlpatterns += staticfiles_urlpatterns() diff --git a/mailadminsite/wsgi.py b/mailadminsite/wsgi.py index 4cfa052..5aef036 100644 --- a/mailadminsite/wsgi.py +++ b/mailadminsite/wsgi.py @@ -8,7 +8,9 @@ https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ """ import os + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mailadminsite.settings") from django.core.wsgi import get_wsgi_application + application = get_wsgi_application() diff --git a/pyproject.toml b/pyproject.toml index 87954d1..e4b3d7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,3 +17,36 @@ dependencies = [ "requests==2.*", "sentry-sdk==1.*", ] + +[dependency-groups] +dev = [ + "pytest>=8.3.5", + "pytest-django>=4.11.1", + "ruff>=0.11.12", +] + +[tool.ruff] +line-length = 120 +target-version = "py310" + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "D", # pydocstyle: Allow undocumented code + "ANN", # flake8-annotations: Allow untyped code + "TD", "FIX", # Allow TODOs and FIXMEs + "FBT002", # Allow boolean args + "G004", # Allow logging using f-string + "EM101", "EM102", # Allow string in exceptions (prioritice code readability vs exception time readability) + "TRY003", # Allow long exception messages + "E501", # Allow long lines + "COM812", "ISC001", # Avoid conflicting with ruff formatter + "RUF012" # FIXME: Fixy fix +] + +[tool.ruff.lint.per-file-ignores] +# Testing code can deviate a bit more +"**/tests.py" = ["S101", "S106", "PLR2004"] + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "mailadminsite.settings" diff --git a/uv.lock b/uv.lock index dab71c3..50ec6cd 100644 --- a/uv.lock +++ b/uv.lock @@ -127,6 +127,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + [[package]] name = "cryptography" version = "45.0.3" @@ -295,6 +304,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + [[package]] name = "invoke" version = "2.2.0" @@ -323,6 +341,13 @@ dependencies = [ { name = "sentry-sdk" }, ] +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-django" }, + { name = "ruff" }, +] + [package.metadata] requires-dist = [ { name = "django", specifier = "==4.2.*" }, @@ -339,6 +364,13 @@ requires-dist = [ { name = "sentry-sdk", specifier = "==1.*" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-django", specifier = ">=4.11.1" }, + { name = "ruff", specifier = ">=0.11.12" }, +] + [[package]] name = "markdown" version = "3.8" @@ -357,6 +389,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/01/e80141f1cd0459e4c9a5dd309dee135bbae41d6c6c121252fdd853001a8a/mysqlclient-2.2.7-cp313-cp313-win_amd64.whl", hash = "sha256:201a6faa301011dd07bca6b651fe5aaa546d7c9a5426835a06c3172e1056a3c5", size = 208000, upload-time = "2025-01-10T11:56:32.293Z" }, ] +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + [[package]] name = "paramiko" version = "3.5.1" @@ -371,6 +412,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/f8/c7bd0ef12954a81a1d3cea60a13946bd9a49a0036a5927770c461eade7ae/paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61", size = 227298, upload-time = "2025-02-04T02:37:57.672Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "pyasn1" version = "0.6.1" @@ -421,6 +471,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/22/d3db169895faaf3e2eda892f005f433a62db2decbcfbc2f61e6517adfa87/PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93", size = 212141, upload-time = "2022-01-07T22:06:01.861Z" }, ] +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest-django" +version = "4.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202, upload-time = "2025-04-03T18:56:09.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" }, +] + [[package]] name = "python-ldap" version = "3.4.4" @@ -446,6 +523,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, ] +[[package]] +name = "ruff" +version = "0.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" }, + { url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" }, + { url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" }, + { url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" }, + { url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" }, + { url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" }, + { url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" }, + { url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" }, + { url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" }, +] + [[package]] name = "sentry-sdk" version = "1.45.1"