From ee537adc051471e88b454bcea19262fe0187e656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Sun, 26 Jan 2025 12:44:10 +0000 Subject: [PATCH] Configure ruff and fix issues. (#64) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://git.data.coop/data.coop/membersystem/pulls/64 Co-authored-by: Víðir Valberg Guðmundsson Co-committed-by: Víðir Valberg Guðmundsson --- .pre-commit-config.yaml | 1 - pyproject.toml | 81 +++++------ requirements.txt | 134 ------------------ src/accounting/admin.py | 10 +- .../0003_alter_payment_stripe_charge_id.py | 12 +- ...ayment_external_transaction_id_and_more.py | 118 +++++++++------ ...ce_remove_order_price_currency_and_more.py | 38 ++--- ...06_alter_account_owner_alter_order_user.py | 20 +-- ...tions_rename_user_order_member_and_more.py | 38 ++--- src/accounting/signals.py | 6 +- src/accounting/views.py | 3 +- src/manage.py | 16 ++- src/membership/admin.py | 4 +- src/membership/emails.py | 24 +++- src/membership/forms.py | 4 +- ...itinglistentry_alter_membership_options.py | 28 ++-- ...ivated_membership_activated_on_and_more.py | 70 +++++---- .../0008_alter_membership_membership_type.py | 17 ++- .../0009_membership_referral_code.py | 19 +-- .../0010_waitinglistentry_member.py | 19 ++- ...ccess_alter_membership_options_and_more.py | 52 ++++--- ...hip_options_alter_serviceaccess_service.py | 25 +++- .../migrations/0013_delete_serviceaccess.py | 5 +- .../0014_alter_membership_options.py | 7 +- src/membership/models.py | 3 +- src/membership/permissions.py | 6 +- src/membership/views.py | 8 +- src/project/asgi.py | 8 ++ src/project/static/css/dark-style.css | 2 +- src/project/static/css/style.css | 2 +- src/services/admin.py | 4 + src/services/apps.py | 4 + src/services/migrations/0001_initial.py | 62 +++++--- ...2_alter_servicerequest_options_and_more.py | 82 +++++++---- ..._serviceaccess_unique_together_and_more.py | 14 +- src/services/models.py | 10 +- src/services/registry.py | 7 +- src/utils/matrix.py | 2 + src/utils/view_utils.py | 2 - tests/conftest.py | 11 +- tests/settings.py | 1 - tests/test_accounting.py | 3 + tests/test_services.py | 3 + 43 files changed, 535 insertions(+), 450 deletions(-) delete mode 100644 requirements.txt delete mode 100644 tests/settings.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7388e5d..234404f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,5 @@ default_language_version: python: python3 -exclude: ^.*\b(migrations)\b.*$ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 diff --git a/pyproject.toml b/pyproject.toml index 795cea8..48dab62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,57 +88,52 @@ target-version = "py312" extend-exclude = [ ".git", "__pycache__", - "manage.py", - "asgi.py", - "wsgi.py", ] -line-length = 120 - -[tool.ruff.lint] -select = ["ALL"] -ignore = [ - "G004", # Logging statement uses f-string - "ANN101", # Missing type annotation for `self` in method - "ANN102", # Missing type annotation for `cls` in classmethod - "EM101", # Exception must not use a string literal, assign to variable first - "EM102", # Exception must not use a f-string literal, assign to variable first +lint.select = ["ALL"] +lint.ignore = [ + "G004", # https://docs.astral.sh/ruff/rules/logging-f-string/ + "EM101", # https://docs.astral.sh/ruff/rules/raw-string-in-exception/ + "EM102", # https://docs.astral.sh/ruff/rules/f-string-in-exception/ "COM812", # missing-trailing-comma (https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules) "ISC001", # single-line-implicit-string-concatenation (https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules) - "D100", # Missing docstring in public module - "D101", # Missing docstring in public class - "D102", # Missing docstring in public method - "D105", # Missing docstring in magic method - "D106", # Missing docstring in public nested class - "D107", # Missing docstring in `__init__` - "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class` - "D213", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`. - "FIX", # TODO, FIXME, XXX - "TD", # TODO, FIXME, XXX - "ANN002", # Missing type annotation for `*args` - "ANN003", # Missing type annotation for `**kwargs` - "FBT001", # Misbehaves: Boolean-typed positional argument in function definition - "FBT002", # Misbehaves: Boolean-typed positional argument in function definition - "TRY003", # Avoid specifying long messages outside the exception class + "ARG001", # https://docs.astral.sh/ruff/rules/unused-function-argument/ + "ARG002", # https://docs.astral.sh/ruff/rules/unused-method-argument/ + "ARG004", # https://docs.astral.sh/ruff/rules/unused-static-method-argument/ + "S101", # https://docs.astral.sh/ruff/rules/assert/ + "FIX002", # https://docs.astral.sh/ruff/rules/line-contains-todo/ - we rely on TD* + "D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/ + "D105", # https://docs.astral.sh/ruff/rules/undocumented-magic-method/ + "D106", # https://docs.astral.sh/ruff/rules/undocumented-public-nested-class/ + "D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/ ] +line-length = 120 [tool.ruff.lint.isort] force-single-line = true +[tool.ruff.lint.pydocstyle] +convention = "pep257" + +[tool.ruff.lint.pylint] +max-args = 10 + [tool.ruff.lint.per-file-ignores] -"tests.py" = [ - "S101", # Use of assert - "SLF001", # Private member access - "D100", # Docstrings - "D103", # Docstrings +"test*.py" = [ + "S101", # https://docs.astral.sh/ruff/rules/assert/ + "PLR2004", # https://docs.astral.sh/ruff/rules/magic-value-comparison/ + "PT009", # https://docs.astral.sh/ruff/rules/pytest-unittest-assertion/ + "S106", # https://docs.astral.sh/ruff/rules/hardcoded-password-func-arg/ + "PLR0912", # https://docs.astral.sh/ruff/rules/too-many-branches/ + "C901", # https://docs.astral.sh/ruff/rules/complex-structure/ + "SLF001", # https://docs.astral.sh/ruff/rules/private-member-access/ + "ANN001", # https://docs.astral.sh/ruff/rules/missing-type-function-argument/ + "ANN201", # https://docs.astral.sh/ruff/rules/missing-return-type-undocumented-public-function/ ] -"tests/*" = [ - "ANN001", - "ANN201", - "ARG001", # TODO: Unused function argument. These are mostly pytest fixtures. Find a way to allow these in tests. Remove this after. - "D103", - "D104", - "S101", # Use of `assert` detected - "PGH004", - "PT004", - "RET504", +"factories.py" = [ + "PLR0913" # https://docs.astral.sh/ruff/rules/too-many-arguments/ +] +"*/migrations/*" = [ + "RUF001", + "RUF012", + "D" # https://docs.astral.sh/ruff/rules/#pydocstyle-d ] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index c34f66c..0000000 --- a/requirements.txt +++ /dev/null @@ -1,134 +0,0 @@ -# -# This file is autogenerated by hatch-pip-compile with Python 3.12 -# -# - django-allauth~=0.63 -# - django-money~=3.5 -# - django-oauth-toolkit~=2.4 -# - django-ratelimit~=4.1 -# - django-registries==0.0.3 -# - django-stubs-ext~=5.0 -# - django-view-decorator==0.0.4 -# - django-zen-queries~=2.1 -# - django~=5.1 -# - environs[django]<12,>=11 -# - httpx -# - psycopg[binary]~=3.2 -# - stripe~=10.5 -# - uvicorn~=0.30 -# - whitenoise~=6.7 -# - -anyio==4.7.0 - # via httpx -asgiref==3.8.1 - # via django -babel==2.15.0 - # via py-moneyed -certifi==2024.7.4 - # via - # httpcore - # httpx - # requests -cffi==1.16.0 - # via cryptography -charset-normalizer==3.3.2 - # via requests -click==8.1.7 - # via uvicorn -cryptography==43.0.0 - # via jwcrypto -dj-database-url==2.2.0 - # via environs -dj-email-url==1.0.6 - # via environs -django==5.1.4 - # via - # hatch.envs.default - # dj-database-url - # django-allauth - # django-money - # django-oauth-toolkit - # django-registries - # django-stubs-ext - # django-view-decorator - # django-zen-queries -django-allauth==0.63.6 - # via hatch.envs.default -django-cache-url==3.4.5 - # via environs -django-money==3.5.3 - # via hatch.envs.default -django-oauth-toolkit==2.4.0 - # via hatch.envs.default -django-ratelimit==4.1.0 - # via hatch.envs.default -django-registries==0.0.3 - # via hatch.envs.default -django-stubs-ext==5.0.4 - # via hatch.envs.default -django-view-decorator==0.0.4 - # via hatch.envs.default -django-zen-queries==2.1.0 - # via hatch.envs.default -environs==11.0.0 - # via hatch.envs.default -h11==0.14.0 - # via - # httpcore - # uvicorn -httpcore==1.0.7 - # via httpx -httpx==0.28.1 - # via hatch.envs.default -idna==3.7 - # via - # anyio - # httpx - # requests -jwcrypto==1.5.6 - # via django-oauth-toolkit -marshmallow==3.21.3 - # via environs -oauthlib==3.2.2 - # via django-oauth-toolkit -packaging==24.1 - # via marshmallow -psycopg==3.2.1 - # via hatch.envs.default -psycopg-binary==3.2.1 - # via psycopg -py-moneyed==3.0 - # via django-money -pycparser==2.22 - # via cffi -python-dotenv==1.0.1 - # via environs -pytz==2024.1 - # via django-oauth-toolkit -requests==2.32.3 - # via - # django-oauth-toolkit - # stripe -setuptools==72.1.0 - # via django-money -sniffio==1.3.1 - # via anyio -sqlparse==0.5.1 - # via django -stripe==10.6.0 - # via hatch.envs.default -typing-extensions==4.12.2 - # via - # anyio - # dj-database-url - # django-stubs-ext - # jwcrypto - # psycopg - # py-moneyed - # stripe -urllib3==2.2.2 - # via requests -uvicorn==0.30.5 - # via hatch.envs.default -whitenoise==6.7.0 - # via hatch.envs.default diff --git a/src/accounting/admin.py b/src/accounting/admin.py index 1b09077..38b1410 100644 --- a/src/accounting/admin.py +++ b/src/accounting/admin.py @@ -30,7 +30,8 @@ class OrderAdminForm(forms.ModelForm): model = models.Order exclude = () # noqa: DJ006 - def clean(self): # noqa: ANN201 + def clean(self) -> None: + """Clean the order.""" cd = super().clean() if not cd["account"] and cd["member"]: try: @@ -55,6 +56,7 @@ class OrderAdmin(admin.ModelAdmin): @admin.action(description="Send order link to selected unpaid orders") def send_order(self, request: HttpRequest, queryset: QuerySet[models.Order]) -> None: + """Send the order to the member.""" for order in queryset: if order.is_paid: messages.error( @@ -81,14 +83,20 @@ class PaymentAdmin(admin.ModelAdmin): @admin.register(models.Product) class ProductAdmin(admin.ModelAdmin): + """Admin for the Product model.""" + list_display = ("name", "price", "vat") class TransactionInline(admin.TabularInline): + """Inline admin for the Transaction model.""" + model = models.Transaction @admin.register(models.Account) class AccountAdmin(admin.ModelAdmin): + """Admin for the Account model.""" + list_display = ("owner", "balance") inlines = (TransactionInline,) diff --git a/src/accounting/migrations/0003_alter_payment_stripe_charge_id.py b/src/accounting/migrations/0003_alter_payment_stripe_charge_id.py index e86a7da..d2aa1b4 100644 --- a/src/accounting/migrations/0003_alter_payment_stripe_charge_id.py +++ b/src/accounting/migrations/0003_alter_payment_stripe_charge_id.py @@ -1,19 +1,19 @@ # Generated by Django 5.0.6 on 2024-07-14 22:16 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('accounting', '0002_alter_order_price_currency_alter_order_vat_currency_and_more'), + ("accounting", "0002_alter_order_price_currency_alter_order_vat_currency_and_more"), ] operations = [ migrations.AlterField( - model_name='payment', - name='stripe_charge_id', - field=models.CharField(blank=True, default='', max_length=255), + model_name="payment", + name="stripe_charge_id", + field=models.CharField(blank=True, default="", max_length=255), preserve_default=False, ), ] diff --git a/src/accounting/migrations/0004_paymenttype_product_payment_external_transaction_id_and_more.py b/src/accounting/migrations/0004_paymenttype_product_payment_external_transaction_id_and_more.py index aa9a1be..43fbecc 100644 --- a/src/accounting/migrations/0004_paymenttype_product_payment_external_transaction_id_and_more.py +++ b/src/accounting/migrations/0004_paymenttype_product_payment_external_transaction_id_and_more.py @@ -2,77 +2,113 @@ import django.db.models.deletion import djmoney.models.fields -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('accounting', '0003_alter_payment_stripe_charge_id'), + ("accounting", "0003_alter_payment_stripe_charge_id"), ] operations = [ migrations.CreateModel( - name='PaymentType', + name="PaymentType", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), - ('name', models.CharField(max_length=1024, verbose_name='description')), - ('description', models.TextField(blank=True, max_length=2048)), - ('enabled', models.BooleanField(default=True)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("modified", models.DateTimeField(auto_now=True, verbose_name="modified")), + ("created", models.DateTimeField(auto_now_add=True, verbose_name="created")), + ("name", models.CharField(max_length=1024, verbose_name="description")), + ("description", models.TextField(blank=True, max_length=2048)), + ("enabled", models.BooleanField(default=True)), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.CreateModel( - name='Product', + name="Product", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), - ('name', models.CharField(max_length=512)), - ('price_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)), - ('price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)), - ('vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)), - ('vat', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("modified", models.DateTimeField(auto_now=True, verbose_name="modified")), + ("created", models.DateTimeField(auto_now_add=True, verbose_name="created")), + ("name", models.CharField(max_length=512)), + ( + "price_currency", + djmoney.models.fields.CurrencyField( + choices=[("DKK", "DKK")], default=None, editable=False, max_length=3 + ), + ), + ("price", djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)), + ( + "vat_currency", + djmoney.models.fields.CurrencyField( + choices=[("DKK", "DKK")], default=None, editable=False, max_length=3 + ), + ), + ("vat", djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)), ], options={ - 'abstract': False, + "abstract": False, }, ), migrations.AddField( - model_name='payment', - name='external_transaction_id', - field=models.CharField(blank=True, default='', max_length=255), + model_name="payment", + name="external_transaction_id", + field=models.CharField(blank=True, default="", max_length=255), ), migrations.AlterField( - model_name='payment', - name='stripe_charge_id', - field=models.CharField(blank=True, default='', max_length=255), + model_name="payment", + name="stripe_charge_id", + field=models.CharField(blank=True, default="", max_length=255), ), migrations.AddField( - model_name='payment', - name='payment_type', - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to='accounting.paymenttype'), + model_name="payment", + name="payment_type", + field=models.ForeignKey( + default=1, on_delete=django.db.models.deletion.PROTECT, to="accounting.paymenttype" + ), preserve_default=False, ), migrations.CreateModel( - name='OrderProduct', + name="OrderProduct", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), - ('price_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)), - ('price', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)), - ('vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)), - ('vat', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)), - ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ordered_products', to='accounting.order')), - ('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ordered_products', to='accounting.product')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("modified", models.DateTimeField(auto_now=True, verbose_name="modified")), + ("created", models.DateTimeField(auto_now_add=True, verbose_name="created")), + ( + "price_currency", + djmoney.models.fields.CurrencyField( + choices=[("DKK", "DKK")], default=None, editable=False, max_length=3 + ), + ), + ("price", djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)), + ( + "vat_currency", + djmoney.models.fields.CurrencyField( + choices=[("DKK", "DKK")], default=None, editable=False, max_length=3 + ), + ), + ("vat", djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)), + ( + "order", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="ordered_products", + to="accounting.order", + ), + ), + ( + "product", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="ordered_products", + to="accounting.product", + ), + ), ], options={ - 'abstract': False, + "abstract": False, }, ), ] diff --git a/src/accounting/migrations/0005_remove_order_price_remove_order_price_currency_and_more.py b/src/accounting/migrations/0005_remove_order_price_remove_order_price_currency_and_more.py index 6252335..f3a2232 100644 --- a/src/accounting/migrations/0005_remove_order_price_remove_order_price_currency_and_more.py +++ b/src/accounting/migrations/0005_remove_order_price_remove_order_price_currency_and_more.py @@ -1,40 +1,44 @@ # Generated by Django 5.0.7 on 2024-07-21 14:53 import django.db.models.deletion -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('accounting', '0004_paymenttype_product_payment_external_transaction_id_and_more'), + ("accounting", "0004_paymenttype_product_payment_external_transaction_id_and_more"), ] operations = [ migrations.RemoveField( - model_name='order', - name='price', + model_name="order", + name="price", ), migrations.RemoveField( - model_name='order', - name='price_currency', + model_name="order", + name="price_currency", ), migrations.RemoveField( - model_name='order', - name='vat', + model_name="order", + name="vat", ), migrations.RemoveField( - model_name='order', - name='vat_currency', + model_name="order", + name="vat_currency", ), migrations.AlterField( - model_name='orderproduct', - name='order', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_products', to='accounting.order'), + model_name="orderproduct", + name="order", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="order_products", to="accounting.order" + ), ), migrations.AlterField( - model_name='orderproduct', - name='product', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='order_products', to='accounting.product'), + model_name="orderproduct", + name="product", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, related_name="order_products", to="accounting.product" + ), ), ] diff --git a/src/accounting/migrations/0006_alter_account_owner_alter_order_user.py b/src/accounting/migrations/0006_alter_account_owner_alter_order_user.py index cdaeaff..3fe4b79 100644 --- a/src/accounting/migrations/0006_alter_account_owner_alter_order_user.py +++ b/src/accounting/migrations/0006_alter_account_owner_alter_order_user.py @@ -1,25 +1,25 @@ # Generated by Django 5.0.7 on 2024-07-21 15:17 import django.db.models.deletion -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('accounting', '0005_remove_order_price_remove_order_price_currency_and_more'), - ('membership', '0006_waitinglistentry_alter_membership_options'), + ("accounting", "0005_remove_order_price_remove_order_price_currency_and_more"), + ("membership", "0006_waitinglistentry_alter_membership_options"), ] operations = [ migrations.AlterField( - model_name='account', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.member'), + model_name="account", + name="owner", + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="membership.member"), ), migrations.AlterField( - model_name='order', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.member'), + model_name="order", + name="user", + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="membership.member"), ), ] diff --git a/src/accounting/migrations/0007_alter_orderproduct_options_rename_user_order_member_and_more.py b/src/accounting/migrations/0007_alter_orderproduct_options_rename_user_order_member_and_more.py index c60aba3..6bb340b 100644 --- a/src/accounting/migrations/0007_alter_orderproduct_options_rename_user_order_member_and_more.py +++ b/src/accounting/migrations/0007_alter_orderproduct_options_rename_user_order_member_and_more.py @@ -1,42 +1,44 @@ # Generated by Django 5.1b1 on 2024-08-01 10:50 import django.db.models.deletion -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('accounting', '0006_alter_account_owner_alter_order_user'), + ("accounting", "0006_alter_account_owner_alter_order_user"), ] operations = [ migrations.AlterModelOptions( - name='orderproduct', - options={'verbose_name': 'ordered product', 'verbose_name_plural': 'ordered products'}, + name="orderproduct", + options={"verbose_name": "ordered product", "verbose_name_plural": "ordered products"}, ), migrations.RenameField( - model_name='order', - old_name='user', - new_name='member', + model_name="order", + old_name="user", + new_name="member", ), migrations.RemoveField( - model_name='payment', - name='stripe_charge_id', + model_name="payment", + name="stripe_charge_id", ), migrations.AddField( - model_name='orderproduct', - name='quantity', + model_name="orderproduct", + name="quantity", field=models.PositiveSmallIntegerField(default=1), ), migrations.AlterField( - model_name='orderproduct', - name='order', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='accounting.order'), + model_name="orderproduct", + name="order", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="items", to="accounting.order" + ), ), migrations.AlterField( - model_name='orderproduct', - name='product', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='accounting.product'), + model_name="orderproduct", + name="product", + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="accounting.product"), ), ] diff --git a/src/accounting/signals.py b/src/accounting/signals.py index 0746206..7bb61b3 100644 --- a/src/accounting/signals.py +++ b/src/accounting/signals.py @@ -11,7 +11,7 @@ from . import models # method for updating @receiver(post_save, sender=models.Payment) -def check_total_amount(sender: models.Payment, instance: models.Payment, **kwargs: dict) -> None: # noqa: ARG001 +def check_total_amount(sender: models.Payment, instance: models.Payment, **kwargs: dict) -> None: """Check that we receive Payments with the correct amount.""" if instance.amount != instance.order.total_with_vat: mail_admins( @@ -21,14 +21,14 @@ def check_total_amount(sender: models.Payment, instance: models.Payment, **kwarg @receiver(post_save, sender=models.Payment) -def mark_order_paid(sender: models.Payment, instance: models.Payment, **kwargs: dict) -> None: # noqa: ARG001 +def mark_order_paid(sender: models.Payment, instance: models.Payment, **kwargs: dict) -> None: """Mark an order as paid when payment is received.""" instance.order.is_paid = True instance.order.save() @receiver(post_save, sender=models.Order) -def activate_membership(sender: models.Order, instance: models.Order, **kwargs: dict) -> None: # noqa: ARG001 +def activate_membership(sender: models.Order, instance: models.Order, **kwargs: dict) -> None: """Mark a membership as activated when its order is marked as paid.""" if instance.is_paid: Membership.objects.filter(order=instance, activated=False, activated_on=None).update( diff --git a/src/accounting/views.py b/src/accounting/views.py index 7740a16..e6a56e8 100644 --- a/src/accounting/views.py +++ b/src/accounting/views.py @@ -83,7 +83,8 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse: mail_admins("Error in checkout", str(e)) raise - # TODO: Redirect with status=303 + # TODO(benjaoming): Redirect with status=303 + # https://git.data.coop/data.coop/membersystem/issues/63 return redirect(checkout_session.url) diff --git a/src/manage.py b/src/manage.py index 6299152..046f96b 100755 --- a/src/manage.py +++ b/src/manage.py @@ -1,15 +1,23 @@ #!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" + import os import sys -if __name__ == "__main__": + +def main() -> None: + """Run administrative tasks.""" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings") try: from django.core.management import execute_from_command_line - except ImportError: - raise ImportError( + except ImportError as exc: + raise ImportError( # noqa: TRY003 "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?", - ) + ) from exc execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/src/membership/admin.py b/src/membership/admin.py index 69e2c22..7cea5a6 100644 --- a/src/membership/admin.py +++ b/src/membership/admin.py @@ -56,7 +56,7 @@ def decorate_ensure_membership_type_exists(membership_type: MembershipType, labe """Generate an admin action for given membership type and label.""" @admin.action(description=label) - def admin_action(modeladmin: ModelAdmin, request: HttpRequest, queryset: QuerySet) -> HttpResponse: # noqa: ARG001 + def admin_action(modeladmin: ModelAdmin, request: HttpRequest, queryset: QuerySet) -> HttpResponse: return ensure_membership_type_exists(request, queryset, membership_type) return admin_action @@ -102,6 +102,7 @@ class MemberAdmin(UserAdmin): @admin.display(description="membership") def current_membership(self, instance: Member) -> Membership | None: + """Get the current membership for the member.""" return instance.memberships.current() def get_actions(self, request: HttpRequest) -> dict: @@ -122,6 +123,7 @@ class MemberAdmin(UserAdmin): @admin.action(description="Send invite email to selected inactive accounts") def send_invite(self, request: HttpRequest, queryset: QuerySet[Member]) -> None: + """Send invite email to the selected inactive accounts.""" for member in queryset: if member.is_active: messages.error( diff --git a/src/membership/emails.py b/src/membership/emails.py index b7e84c5..cf1cf78 100644 --- a/src/membership/emails.py +++ b/src/membership/emails.py @@ -5,17 +5,23 @@ * Generally, an email consists of templates (for body and subject) and a get_context() method. """ -from accounting.models import Order +from __future__ import annotations + +from typing import TYPE_CHECKING + from django.contrib import messages from django.contrib.auth.tokens import default_token_generator from django.contrib.sites.shortcuts import get_current_site from django.core.mail.message import EmailMessage -from django.http import HttpRequest from django.template import loader from django.utils import translation from django.utils.translation import gettext_lazy as _ -from .models import Membership +if TYPE_CHECKING: + from accounting.models import Order + from django.http import HttpRequest + + from .models import Membership class BaseEmail(EmailMessage): @@ -31,7 +37,7 @@ class BaseEmail(EmailMessage): template_subject = None default_subject = "SET SUBJECT HERE" - def __init__(self, request: HttpRequest, *args, **kwargs) -> None: + def __init__(self, request: HttpRequest, *args, **kwargs) -> None: # noqa: ANN002, ANN003 self.context = kwargs.pop("context", {}) self.user = kwargs.pop("user", None) if self.user: @@ -95,16 +101,19 @@ class BaseEmail(EmailMessage): class InviteEmail(BaseEmail): + """Email for invitations.""" + template = "membership/emails/invite.txt" default_subject = _("Invite to data.coop membership") - def __init__(self, membership: Membership, request: HttpRequest, *args, **kwargs) -> None: + def __init__(self, membership: Membership, request: HttpRequest, *args, **kwargs) -> None: # noqa: ANN002, ANN003 self.membership = membership kwargs["user"] = membership.user kwargs["from_email"] = "kasserer@data.coop" super().__init__(request, *args, **kwargs) def get_context_data(self) -> dict: + """Resolve context for invitation emails.""" c = super().get_context_data() c["membership"] = self.membership c["token"] = default_token_generator.make_token(self.membership.user) @@ -113,16 +122,19 @@ class InviteEmail(BaseEmail): class OrderEmail(BaseEmail): + """Email for orders.""" + template = "membership/emails/order.txt" default_subject = _("Your data.coop order and payment") - def __init__(self, order: Order, request: HttpRequest, *args, **kwargs) -> None: + def __init__(self, order: Order, request: HttpRequest, *args, **kwargs) -> None: # noqa: ANN002, ANN003 self.order = order kwargs["user"] = order.member kwargs["from_email"] = "kasserer@data.coop" super().__init__(request, *args, **kwargs) def get_context_data(self) -> dict: + """Resolve context for order emails.""" c = super().get_context_data() c["order"] = self.order return c diff --git a/src/membership/forms.py b/src/membership/forms.py index 71afaaa..9df7b9e 100644 --- a/src/membership/forms.py +++ b/src/membership/forms.py @@ -1,3 +1,5 @@ +"""Form for the membership app.""" + from allauth.account.adapter import get_adapter as get_allauth_adapter from allauth.account.forms import SetPasswordForm from django import forms @@ -12,7 +14,7 @@ class InviteForm(SetPasswordForm): widget=forms.TextInput(attrs={"placeholder": _("Username"), "autocomplete": "username"}), ) - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003 self.membership = kwargs.pop("membership") kwargs["user"] = self.membership.user super().__init__(*args, **kwargs) diff --git a/src/membership/migrations/0006_waitinglistentry_alter_membership_options.py b/src/membership/migrations/0006_waitinglistentry_alter_membership_options.py index 9c2d923..40dfabd 100644 --- a/src/membership/migrations/0006_waitinglistentry_alter_membership_options.py +++ b/src/membership/migrations/0006_waitinglistentry_alter_membership_options.py @@ -1,32 +1,32 @@ # Generated by Django 5.0.7 on 2024-07-20 20:45 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('membership', '0005_member'), + ("membership", "0005_member"), ] operations = [ migrations.CreateModel( - name='WaitingListEntry', + name="WaitingListEntry", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), - ('email', models.EmailField(max_length=254)), - ('geography', models.CharField(blank=True, default='', verbose_name='geography')), - ('comment', models.TextField(blank=True)), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("modified", models.DateTimeField(auto_now=True, verbose_name="modified")), + ("created", models.DateTimeField(auto_now_add=True, verbose_name="created")), + ("email", models.EmailField(max_length=254)), + ("geography", models.CharField(blank=True, default="", verbose_name="geography")), + ("comment", models.TextField(blank=True)), ], options={ - 'verbose_name': 'waiting list entry', - 'verbose_name_plural': 'waiting list entries', + "verbose_name": "waiting list entry", + "verbose_name_plural": "waiting list entries", }, ), migrations.AlterModelOptions( - name='membership', - options={'verbose_name': 'medlemskab', 'verbose_name_plural': 'medlemskaber'}, + name="membership", + options={"verbose_name": "medlemskab", "verbose_name_plural": "medlemskaber"}, ), ] diff --git a/src/membership/migrations/0007_membership_activated_membership_activated_on_and_more.py b/src/membership/migrations/0007_membership_activated_membership_activated_on_and_more.py index cedb798..383f499 100644 --- a/src/membership/migrations/0007_membership_activated_membership_activated_on_and_more.py +++ b/src/membership/migrations/0007_membership_activated_membership_activated_on_and_more.py @@ -2,61 +2,77 @@ import django.db.models.deletion from django.conf import settings -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('accounting', '0007_alter_orderproduct_options_rename_user_order_member_and_more'), - ('membership', '0006_waitinglistentry_alter_membership_options'), + ("accounting", "0007_alter_orderproduct_options_rename_user_order_member_and_more"), + ("membership", "0006_waitinglistentry_alter_membership_options"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.AddField( - model_name='membership', - name='activated', - field=models.BooleanField(default=False, help_text='Membership was activated.', verbose_name='activated'), + model_name="membership", + name="activated", + field=models.BooleanField(default=False, help_text="Membership was activated.", verbose_name="activated"), ), migrations.AddField( - model_name='membership', - name='activated_on', + model_name="membership", + name="activated_on", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='membership', - name='order', - field=models.ForeignKey(blank=True, help_text='The order filled in for paying this membership.', null=True, on_delete=django.db.models.deletion.PROTECT, to='accounting.order', verbose_name='order'), + model_name="membership", + name="order", + field=models.ForeignKey( + blank=True, + help_text="The order filled in for paying this membership.", + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="accounting.order", + verbose_name="order", + ), ), migrations.AddField( - model_name='membership', - name='revoked', - field=models.BooleanField(default=False, help_text='Membership has explicitly been revoked. Revoking a membership is not associated with regular expiration of the membership period.', verbose_name='revoked'), + model_name="membership", + name="revoked", + field=models.BooleanField( + default=False, + help_text=( + "Membership has explicitly been revoked. Revoking a membership is not associated with regular " + "expiration of the membership period." + ), + verbose_name="revoked", + ), ), migrations.AddField( - model_name='membership', - name='revoked_on', + model_name="membership", + name="revoked_on", field=models.DateTimeField(blank=True, null=True), ), migrations.AddField( - model_name='membership', - name='revoked_reason', + model_name="membership", + name="revoked_reason", field=models.TextField(blank=True), ), migrations.AddField( - model_name='membershiptype', - name='active', + model_name="membershiptype", + name="active", field=models.BooleanField(default=True), ), migrations.AddField( - model_name='membershiptype', - name='products', - field=models.ManyToManyField(to='accounting.product'), + model_name="membershiptype", + name="products", + field=models.ManyToManyField(to="accounting.product"), ), migrations.AlterField( - model_name='membership', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to=settings.AUTH_USER_MODEL), + model_name="membership", + name="user", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, related_name="memberships", to=settings.AUTH_USER_MODEL + ), ), ] diff --git a/src/membership/migrations/0008_alter_membership_membership_type.py b/src/membership/migrations/0008_alter_membership_membership_type.py index 1814aae..eb38676 100644 --- a/src/membership/migrations/0008_alter_membership_membership_type.py +++ b/src/membership/migrations/0008_alter_membership_membership_type.py @@ -1,19 +1,24 @@ # Generated by Django 5.1b1 on 2024-08-04 10:26 import django.db.models.deletion -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('membership', '0007_membership_activated_membership_activated_on_and_more'), + ("membership", "0007_membership_activated_membership_activated_on_and_more"), ] operations = [ migrations.AlterField( - model_name='membership', - name='membership_type', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='membership.membershiptype', verbose_name='membership type'), + model_name="membership", + name="membership_type", + field=models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="memberships", + to="membership.membershiptype", + verbose_name="membership type", + ), ), ] diff --git a/src/membership/migrations/0009_membership_referral_code.py b/src/membership/migrations/0009_membership_referral_code.py index 1c1cf64..a39cbf0 100644 --- a/src/membership/migrations/0009_membership_referral_code.py +++ b/src/membership/migrations/0009_membership_referral_code.py @@ -1,32 +1,33 @@ # Generated by Django 5.1rc1 on 2024-08-07 22:32 import uuid -from django.db import migrations, models + +from django.db import migrations +from django.db import models -def create_uuid(apps, schema_editor): - Membership = apps.get_model('membership', 'Membership') +def create_uuid(apps, schema_editor) -> None: # noqa: ANN001 + Membership = apps.get_model("membership", "Membership") for membership in Membership.objects.all(): membership.referral_code = uuid.uuid4() membership.save() class Migration(migrations.Migration): - dependencies = [ - ('membership', '0008_alter_membership_membership_type'), + ("membership", "0008_alter_membership_membership_type"), ] operations = [ migrations.AddField( - model_name='membership', - name='referral_code', + model_name="membership", + name="referral_code", field=models.UUIDField(blank=True, null=True, default=uuid.uuid4, editable=False), ), migrations.RunPython(create_uuid), migrations.AlterField( - model_name='membership', - name='referral_code', + model_name="membership", + name="referral_code", field=models.UUIDField(unique=True, default=uuid.uuid4, editable=False), ), ] diff --git a/src/membership/migrations/0010_waitinglistentry_member.py b/src/membership/migrations/0010_waitinglistentry_member.py index 3bc2e08..3060fdd 100644 --- a/src/membership/migrations/0010_waitinglistentry_member.py +++ b/src/membership/migrations/0010_waitinglistentry_member.py @@ -1,19 +1,26 @@ # Generated by Django 5.1rc1 on 2024-08-14 08:05 import django.db.models.deletion -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('membership', '0009_membership_referral_code'), + ("membership", "0009_membership_referral_code"), ] operations = [ migrations.AddField( - model_name='waitinglistentry', - name='member', - field=models.ForeignKey(help_text='Once a member account is generated (use the admin action), this field will be marked.', null=True, blank=True, on_delete=django.db.models.deletion.CASCADE, to='membership.member', verbose_name='has member'), + model_name="waitinglistentry", + name="member", + field=models.ForeignKey( + help_text="Once a member account is generated (use the admin action), this field will be marked.", + null=True, + blank=True, + on_delete=django.db.models.deletion.CASCADE, + to="membership.member", + verbose_name="has member", + ), ), ] diff --git a/src/membership/migrations/0011_serviceaccess_alter_membership_options_and_more.py b/src/membership/migrations/0011_serviceaccess_alter_membership_options_and_more.py index 0a8879d..fcbb70a 100644 --- a/src/membership/migrations/0011_serviceaccess_alter_membership_options_and_more.py +++ b/src/membership/migrations/0011_serviceaccess_alter_membership_options_and_more.py @@ -4,46 +4,60 @@ import django.db.models.deletion import django_registries.registry import services.registry from django.conf import settings -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('membership', '0010_waitinglistentry_member'), + ("membership", "0010_waitinglistentry_member"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='ServiceAccess', + name="ServiceAccess", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), - ('service', django_registries.registry.ChoicesField(choices=[('hedgedoc', 'hedgedoc'), ('mail', 'mail'), ('mastodon', 'mastodon'), ('matrix', 'matrix'), ('nextcloud', 'nextcloud'), ('rallly', 'rallly')], registry=services.registry.ServiceRegistry, verbose_name='service')), - ('subscription_data', models.JSONField(blank=True, null=True, verbose_name='subscription data')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("modified", models.DateTimeField(auto_now=True, verbose_name="modified")), + ("created", models.DateTimeField(auto_now_add=True, verbose_name="created")), + ( + "service", + django_registries.registry.ChoicesField( + choices=[ + ("hedgedoc", "hedgedoc"), + ("mail", "mail"), + ("mastodon", "mastodon"), + ("matrix", "matrix"), + ("nextcloud", "nextcloud"), + ("rallly", "rallly"), + ], + registry=services.registry.ServiceRegistry, + verbose_name="service", + ), + ), + ("subscription_data", models.JSONField(blank=True, null=True, verbose_name="subscription data")), ], options={ - 'verbose_name': 'service access', - 'verbose_name_plural': 'service accesses', + "verbose_name": "service access", + "verbose_name_plural": "service accesses", }, ), migrations.AlterModelOptions( - name='membership', - options={'verbose_name': 'membership', 'verbose_name_plural': 'memberships'}, + name="membership", + options={"verbose_name": "membership", "verbose_name_plural": "memberships"}, ), migrations.RemoveConstraint( - model_name='subscriptionperiod', - name='exclude_overlapping_periods', + model_name="subscriptionperiod", + name="exclude_overlapping_periods", ), migrations.AddField( - model_name='serviceaccess', - name='user', + model_name="serviceaccess", + name="user", field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), ), migrations.AddConstraint( - model_name='serviceaccess', - constraint=models.UniqueConstraint(fields=('user', 'service'), name='unique_user_service'), + model_name="serviceaccess", + constraint=models.UniqueConstraint(fields=("user", "service"), name="unique_user_service"), ), ] diff --git a/src/membership/migrations/0012_alter_membership_options_alter_serviceaccess_service.py b/src/membership/migrations/0012_alter_membership_options_alter_serviceaccess_service.py index bfee538..e837966 100644 --- a/src/membership/migrations/0012_alter_membership_options_alter_serviceaccess_service.py +++ b/src/membership/migrations/0012_alter_membership_options_alter_serviceaccess_service.py @@ -6,19 +6,30 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('membership', '0011_serviceaccess_alter_membership_options_and_more'), + ("membership", "0011_serviceaccess_alter_membership_options_and_more"), ] operations = [ migrations.AlterModelOptions( - name='membership', - options={'verbose_name': 'medlemskab', 'verbose_name_plural': 'medlemskaber'}, + name="membership", + options={"verbose_name": "medlemskab", "verbose_name_plural": "medlemskaber"}, ), migrations.AlterField( - model_name='serviceaccess', - name='service', - field=django_registries.registry.ChoicesField(choices=[('forgejo', 'forgejo'), ('hedgedoc', 'hedgedoc'), ('mail', 'mail'), ('mastodon', 'mastodon'), ('matrix', 'matrix'), ('nextcloud', 'nextcloud'), ('rallly', 'rallly')], registry=services.registry.ServiceRegistry, verbose_name='service'), + model_name="serviceaccess", + name="service", + field=django_registries.registry.ChoicesField( + choices=[ + ("forgejo", "forgejo"), + ("hedgedoc", "hedgedoc"), + ("mail", "mail"), + ("mastodon", "mastodon"), + ("matrix", "matrix"), + ("nextcloud", "nextcloud"), + ("rallly", "rallly"), + ], + registry=services.registry.ServiceRegistry, + verbose_name="service", + ), ), ] diff --git a/src/membership/migrations/0013_delete_serviceaccess.py b/src/membership/migrations/0013_delete_serviceaccess.py index e05c56b..5b4924e 100644 --- a/src/membership/migrations/0013_delete_serviceaccess.py +++ b/src/membership/migrations/0013_delete_serviceaccess.py @@ -4,13 +4,12 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('membership', '0012_alter_membership_options_alter_serviceaccess_service'), + ("membership", "0012_alter_membership_options_alter_serviceaccess_service"), ] operations = [ migrations.DeleteModel( - name='ServiceAccess', + name="ServiceAccess", ), ] diff --git a/src/membership/migrations/0014_alter_membership_options.py b/src/membership/migrations/0014_alter_membership_options.py index 0746318..9faac07 100644 --- a/src/membership/migrations/0014_alter_membership_options.py +++ b/src/membership/migrations/0014_alter_membership_options.py @@ -4,14 +4,13 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('membership', '0013_delete_serviceaccess'), + ("membership", "0013_delete_serviceaccess"), ] operations = [ migrations.AlterModelOptions( - name='membership', - options={'verbose_name': 'membership', 'verbose_name_plural': 'memberships'}, + name="membership", + options={"verbose_name": "membership", "verbose_name_plural": "memberships"}, ), ] diff --git a/src/membership/models.py b/src/membership/models.py index 2f1552a..5180c1d 100644 --- a/src/membership/models.py +++ b/src/membership/models.py @@ -188,7 +188,8 @@ class Membership(DirtyFieldsMixin, CreatedModifiedAbstract): def __str__(self) -> str: return f"{self.user} - {self.period}" - def save(self, *args, **kwargs) -> None: + def save(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003 + """Override the standard save method.""" is_new = not self.pk # A Membership is considered recently activated when: # It was created w/ activated=True OR diff --git a/src/membership/permissions.py b/src/membership/permissions.py index 8b79c49..6364cb2 100644 --- a/src/membership/permissions.py +++ b/src/membership/permissions.py @@ -1,5 +1,7 @@ """Permissions for the membership app.""" +from __future__ import annotations + from dataclasses import dataclass from django.contrib.auth.models import Permission as DjangoPermission @@ -9,7 +11,7 @@ from django.utils.translation import gettext_lazy as _ PERMISSIONS = [] -def persist_permissions(*args, **kwargs) -> None: # type: ignore[no-untyped-def] # noqa: ARG001 +def persist_permissions(*args, **kwargs) -> None: # type: ignore[no-untyped-def] # noqa: ANN002, ANN003 """Persist all permissions.""" for permission in PERMISSIONS: permission.persist_permission() @@ -24,7 +26,7 @@ class Permission: app_label: str model: str - def __post_init__(self, *args, **kwargs) -> None: + def __post_init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003 """Post init method.""" PERMISSIONS.append(self) diff --git a/src/membership/views.py b/src/membership/views.py index c82cf5b..b750432 100644 --- a/src/membership/views.py +++ b/src/membership/views.py @@ -123,6 +123,12 @@ def members_admin_detail(request: HttpRequest, member_id: int) -> HttpResponse: ) +class InvalidTokenError(HttpResponseForbidden): + """Exception raised when an invalid token is encountered.""" + + content = "Token not valid - maybe it expired?" + + @ratelimit(group="membership", key="ip", rate="10/d", method="ALL", block=True) @member_view( paths="invite///", @@ -146,7 +152,7 @@ def invite(request: HttpRequest, referral_code: str, token: str) -> HttpResponse token_valid = default_token_generator.check_token(membership.user, token) if not token_valid: - raise HttpResponseForbidden("Token not valid - maybe it expired?") + raise InvalidTokenError if request.method == "POST": form = InviteForm(membership=membership, data=request.POST) diff --git a/src/project/asgi.py b/src/project/asgi.py index 924a849..20d1184 100644 --- a/src/project/asgi.py +++ b/src/project/asgi.py @@ -1,3 +1,11 @@ +"""ASGI config. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + import os from django.core.asgi import get_asgi_application diff --git a/src/project/static/css/dark-style.css b/src/project/static/css/dark-style.css index da95fbd..5d9198a 100644 --- a/src/project/static/css/dark-style.css +++ b/src/project/static/css/dark-style.css @@ -69,4 +69,4 @@ html.dark input[type="email"] { html.dark div.services>div, html.dark div.infobox { background: var(--dark); -} \ No newline at end of file +} diff --git a/src/project/static/css/style.css b/src/project/static/css/style.css index 769f07e..661b6f6 100644 --- a/src/project/static/css/style.css +++ b/src/project/static/css/style.css @@ -577,4 +577,4 @@ span.time_remaining { .pagination .page-item.disabled .page-link { cursor: default; -} \ No newline at end of file +} diff --git a/src/services/admin.py b/src/services/admin.py index e114fae..c24642c 100644 --- a/src/services/admin.py +++ b/src/services/admin.py @@ -1,3 +1,5 @@ +"""Admin for the services app.""" + from django.contrib import admin from services.models import ServiceAccess @@ -6,6 +8,8 @@ from services.models import ServiceRequest @admin.register(ServiceRequest) class ServiceRequestAdmin(admin.ModelAdmin): + """Admin for the ServiceRequest model.""" + list_display = ("member", "service", "request", "status") list_filter = ("request", "status") diff --git a/src/services/apps.py b/src/services/apps.py index 11e745d..8386f87 100644 --- a/src/services/apps.py +++ b/src/services/apps.py @@ -1,6 +1,10 @@ +"""Config for the services app.""" + from django.apps import AppConfig class ServicesConfig(AppConfig): + """Config for the services app.""" + default_auto_field = "django.db.models.BigAutoField" name = "services" diff --git a/src/services/migrations/0001_initial.py b/src/services/migrations/0001_initial.py index 96a8861..ad02977 100644 --- a/src/services/migrations/0001_initial.py +++ b/src/services/migrations/0001_initial.py @@ -1,36 +1,64 @@ # Generated by Django 5.1rc1 on 2024-12-24 17:32 import django.db.models.deletion -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - initial = True dependencies = [ - ('membership', '0012_alter_membership_options_alter_serviceaccess_service'), + ("membership", "0012_alter_membership_options_alter_serviceaccess_service"), ] operations = [ migrations.CreateModel( - name='ServiceRequest', + name="ServiceRequest", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), - ('service', models.CharField(choices=[])), - ('request', models.CharField(choices=[('CREATION', 'Creation'), ('PASSWORD_RESET', 'Password reset'), ('DELETION', 'Deletion')])), - ('is_auto_created', models.BooleanField(default=False)), - ('status', models.CharField(choices=[('NEW', 'New'), ('RESOLVED', 'Resolved')], default='NEW', max_length=24)), - ('member_notes', models.TextField(blank=True, help_text='Notes from the member, intended to guide the resolution of the request.')), - ('admin_notes', models.TextField(blank=True, help_text='Readable by member: Notes from the admin / status updates, resolutions etc.')), - ('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='membership.member')), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("modified", models.DateTimeField(auto_now=True, verbose_name="modified")), + ("created", models.DateTimeField(auto_now_add=True, verbose_name="created")), + ("service", models.CharField(choices=[])), + ( + "request", + models.CharField( + choices=[ + ("CREATION", "Creation"), + ("PASSWORD_RESET", "Password reset"), + ("DELETION", "Deletion"), + ] + ), + ), + ("is_auto_created", models.BooleanField(default=False)), + ( + "status", + models.CharField(choices=[("NEW", "New"), ("RESOLVED", "Resolved")], default="NEW", max_length=24), + ), + ( + "member_notes", + models.TextField( + blank=True, help_text="Notes from the member, intended to guide the resolution of the request." + ), + ), + ( + "admin_notes", + models.TextField( + blank=True, + help_text="Readable by member: Notes from the admin / status updates, resolutions etc.", + ), + ), + ("member", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="membership.member")), ], options={ - 'verbose_name': 'Service Request', - 'verbose_name_plural': 'Service Requests', - 'constraints': [models.CheckConstraint(condition=models.Q(('status__in', ['NEW', 'RESOLVED'])), name='services_servicerequest_status_valid')], + "verbose_name": "Service Request", + "verbose_name_plural": "Service Requests", + "constraints": [ + models.CheckConstraint( + condition=models.Q(("status__in", ["NEW", "RESOLVED"])), + name="services_servicerequest_status_valid", + ) + ], }, ), ] diff --git a/src/services/migrations/0002_alter_servicerequest_options_and_more.py b/src/services/migrations/0002_alter_servicerequest_options_and_more.py index 3a0de96..35ce125 100644 --- a/src/services/migrations/0002_alter_servicerequest_options_and_more.py +++ b/src/services/migrations/0002_alter_servicerequest_options_and_more.py @@ -2,51 +2,83 @@ import django.db.models.deletion import django_registries.registry +from django.db import migrations +from django.db import models + import services.registry -from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('membership', '0013_delete_serviceaccess'), - ('services', '0001_initial'), + ("membership", "0013_delete_serviceaccess"), + ("services", "0001_initial"), ] operations = [ migrations.AlterModelOptions( - name='servicerequest', - options={'verbose_name': 'service request', 'verbose_name_plural': 'service requests'}, + name="servicerequest", + options={"verbose_name": "service request", "verbose_name_plural": "service requests"}, ), migrations.AlterField( - model_name='servicerequest', - name='member', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_requests', to='membership.member'), + model_name="servicerequest", + name="member", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="service_requests", to="membership.member" + ), ), migrations.AlterField( - model_name='servicerequest', - name='request', - field=models.CharField(choices=[('CREATION', 'Creation'), ('PASSWORD_RESET', 'Password reset'), ('DELETION', 'Deletion')], max_length=24), + model_name="servicerequest", + name="request", + field=models.CharField( + choices=[("CREATION", "Creation"), ("PASSWORD_RESET", "Password reset"), ("DELETION", "Deletion")], + max_length=24, + ), ), migrations.AlterField( - model_name='servicerequest', - name='service', - field=django_registries.registry.ChoicesField(choices=[('forgejo', 'forgejo'), ('hedgedoc', 'hedgedoc'), ('mail', 'mail'), ('mastodon', 'mastodon'), ('matrix', 'matrix'), ('nextcloud', 'nextcloud'), ('rallly', 'rallly')], registry=services.registry.ServiceRegistry), + model_name="servicerequest", + name="service", + field=django_registries.registry.ChoicesField( + choices=[ + ("forgejo", "forgejo"), + ("hedgedoc", "hedgedoc"), + ("mail", "mail"), + ("mastodon", "mastodon"), + ("matrix", "matrix"), + ("nextcloud", "nextcloud"), + ("rallly", "rallly"), + ], + registry=services.registry.ServiceRegistry, + ), ), migrations.CreateModel( - name='ServiceAccess', + name="ServiceAccess", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')), - ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), - ('service', django_registries.registry.ChoicesField(choices=[('forgejo', 'forgejo'), ('hedgedoc', 'hedgedoc'), ('mail', 'mail'), ('mastodon', 'mastodon'), ('matrix', 'matrix'), ('nextcloud', 'nextcloud'), ('rallly', 'rallly')], registry=services.registry.ServiceRegistry, verbose_name='service')), - ('subscription_data', models.JSONField(blank=True, null=True, verbose_name='subscription data')), - ('member', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.member')), + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("modified", models.DateTimeField(auto_now=True, verbose_name="modified")), + ("created", models.DateTimeField(auto_now_add=True, verbose_name="created")), + ( + "service", + django_registries.registry.ChoicesField( + choices=[ + ("forgejo", "forgejo"), + ("hedgedoc", "hedgedoc"), + ("mail", "mail"), + ("mastodon", "mastodon"), + ("matrix", "matrix"), + ("nextcloud", "nextcloud"), + ("rallly", "rallly"), + ], + registry=services.registry.ServiceRegistry, + verbose_name="service", + ), + ), + ("subscription_data", models.JSONField(blank=True, null=True, verbose_name="subscription data")), + ("member", models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="membership.member")), ], options={ - 'verbose_name': 'service access', - 'verbose_name_plural': 'service accesses', - 'unique_together': {('member', 'service')}, + "verbose_name": "service access", + "verbose_name_plural": "service accesses", + "unique_together": {("member", "service")}, }, ), ] diff --git a/src/services/migrations/0003_alter_serviceaccess_unique_together_and_more.py b/src/services/migrations/0003_alter_serviceaccess_unique_together_and_more.py index 22a49ec..68907b4 100644 --- a/src/services/migrations/0003_alter_serviceaccess_unique_together_and_more.py +++ b/src/services/migrations/0003_alter_serviceaccess_unique_together_and_more.py @@ -1,22 +1,22 @@ # Generated by Django 5.1.4 on 2025-01-16 07:20 -from django.db import migrations, models +from django.db import migrations +from django.db import models class Migration(migrations.Migration): - dependencies = [ - ('membership', '0014_alter_membership_options'), - ('services', '0002_alter_servicerequest_options_and_more'), + ("membership", "0014_alter_membership_options"), + ("services", "0002_alter_servicerequest_options_and_more"), ] operations = [ migrations.AlterUniqueTogether( - name='serviceaccess', + name="serviceaccess", unique_together=set(), ), migrations.AddConstraint( - model_name='serviceaccess', - constraint=models.UniqueConstraint(fields=('member', 'service'), name='unique_user_service'), + model_name="serviceaccess", + constraint=models.UniqueConstraint(fields=("member", "service"), name="unique_user_service"), ), ] diff --git a/src/services/models.py b/src/services/models.py index b1a61c5..f6ebe6a 100644 --- a/src/services/models.py +++ b/src/services/models.py @@ -1,3 +1,5 @@ +"""Models for the services app.""" + import typing from django.contrib.sites.models import Site @@ -16,6 +18,8 @@ if typing.TYPE_CHECKING: class ServiceRequestStatus(TextChoices): + """Status options for service requests.""" + NEW = "NEW", _("New") RESOLVED = "RESOLVED", _("Resolved") @@ -46,7 +50,7 @@ class ServiceAccess(CreatedModifiedAbstract): def __str__(self) -> str: return f"{self.member} - {self.service}" - def save(self, *args, **kwargs) -> None: + def save(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003 """Ensure that existing ServiceRequest objects are automatically resolved.""" is_new = not self.pk super().save(*args, **kwargs) @@ -59,6 +63,8 @@ class ServiceAccess(CreatedModifiedAbstract): class ServiceRequest(CreatedModifiedAbstract): + """A service request for a member.""" + member = models.ForeignKey("membership.Member", on_delete=models.CASCADE, related_name="service_requests") service = ServiceRegistry.choices_field() request = models.CharField(max_length=24, choices=ServiceRequests.choices) @@ -97,7 +103,7 @@ class ServiceRequest(CreatedModifiedAbstract): member=membership.user, service=service.slug, request=ServiceRequests.CREATION ) - def save(self, *args, **kwargs) -> None: + def save(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003 """Create notifications when new service requests are added.""" is_new = not self.pk diff --git a/src/services/registry.py b/src/services/registry.py index b0bbce4..5e25eb2 100644 --- a/src/services/registry.py +++ b/src/services/registry.py @@ -14,6 +14,8 @@ class ServiceRegistry(Registry): class ServiceRequests(TextChoices): + """Service request choices.""" + CREATION = "CREATION", _("Creation") PASSWORD_RESET = "PASSWORD_RESET", _("Password reset") DELETION = "DELETION", _("Deletion") @@ -39,11 +41,6 @@ class ServiceInterface(Interface): request_types: list[str, str] = DEFAULT_SERVICE_REQUEST_TYPES - # TODO: add a way to add a something which defines the required fields for a service - # - maybe a list of tuples with the field name and the type of the field - # this could be used to generate a form for the service, and also to validate - # the data saved in a JSONField on the ServiceAccess model - subscribe_fields: tuple[tuple[str, forms.Field]] = [] def get_form_class(self) -> type: diff --git a/src/utils/matrix.py b/src/utils/matrix.py index 166f8bb..c1d1204 100644 --- a/src/utils/matrix.py +++ b/src/utils/matrix.py @@ -1,3 +1,5 @@ +"""Utils for communicating with matrix.""" + from urllib.parse import urljoin import httpx diff --git a/src/utils/view_utils.py b/src/utils/view_utils.py index bb04192..6cd82f0 100644 --- a/src/utils/view_utils.py +++ b/src/utils/view_utils.py @@ -63,8 +63,6 @@ class RenderConfig: request: HttpRequest, ) -> HttpResponse: """Render a list of objects with a table.""" - # TODO: List actions - entity_name = self.entity_name entity_name_plural = self.entity_name_plural objects = self.objects diff --git a/tests/conftest.py b/tests/conftest.py index b90a5ef..7119831 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +"""Pytest configuration.""" + from datetime import timedelta from unittest import mock @@ -12,11 +14,13 @@ from membership.models import SubscriptionPeriod @pytest.fixture() def membership_type(): + """Provide a membership type.""" return MembershipType.objects.create(name="Test Membership Type") @pytest.fixture() def current_period(): + """Provide a current subscription period.""" SubscriptionPeriod.objects.create( period=DateRange(timezone.now().date() - timedelta(days=182), timezone.now().date() + timedelta(days=183)) ) @@ -25,17 +29,18 @@ def current_period(): @pytest.fixture() def active_membership(membership_type, current_period): + """Provide an active membership.""" member = Member.objects.create_user("test", "lala@adas.com", "1234") - membership = Membership.objects.create( + return Membership.objects.create( user=member, membership_type=membership_type, period=current_period, activated=True, ) - return membership @pytest.fixture(autouse=True) -def mock_matrix_notify(): +def _mock_matrix_notify() -> None: + """Mock the matrix notify post.""" with mock.patch("utils.matrix.httpx.post"): yield diff --git a/tests/settings.py b/tests/settings.py deleted file mode 100644 index c68aa21..0000000 --- a/tests/settings.py +++ /dev/null @@ -1 +0,0 @@ -from project.settings import * # noqa diff --git a/tests/test_accounting.py b/tests/test_accounting.py index f814ca6..f458b4e 100644 --- a/tests/test_accounting.py +++ b/tests/test_accounting.py @@ -1,3 +1,5 @@ +"""Tests for accounting.""" + import pytest from accounting import models from django.contrib.auth.models import User @@ -5,6 +7,7 @@ from django.contrib.auth.models import User @pytest.mark.django_db() def test_balance() -> None: + """Test balance.""" user = User.objects.create_user("test", "lala@adas.com", "1234") account = models.Account.objects.create(owner=user) assert account.balance == 0 diff --git a/tests/test_services.py b/tests/test_services.py index 4c76c2b..f9a43b6 100644 --- a/tests/test_services.py +++ b/tests/test_services.py @@ -1,3 +1,5 @@ +"""Tests for services.""" + import pytest from membership.models import Membership from services.models import ServiceRequest @@ -6,6 +8,7 @@ from services.models import ServiceRequestStatus @pytest.mark.django_db() def test_membership_activation(active_membership: Membership): + """Test membership activation.""" assert ServiceRequest.objects.filter( member=active_membership.user, status=ServiceRequestStatus.NEW,