Configure ruff and fix issues. (#64)
Reviewed-on: https://git.data.coop/data.coop/membersystem/pulls/64 Co-authored-by: Víðir Valberg Guðmundsson <valberg@orn.li> Co-committed-by: Víðir Valberg Guðmundsson <valberg@orn.li>
This commit is contained in:
parent
21b59467ea
commit
ee537adc05
43 changed files with 535 additions and 450 deletions
|
@ -1,6 +1,5 @@
|
||||||
default_language_version:
|
default_language_version:
|
||||||
python: python3
|
python: python3
|
||||||
exclude: ^.*\b(migrations)\b.*$
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.6.0
|
rev: v4.6.0
|
||||||
|
|
|
@ -88,57 +88,52 @@ target-version = "py312"
|
||||||
extend-exclude = [
|
extend-exclude = [
|
||||||
".git",
|
".git",
|
||||||
"__pycache__",
|
"__pycache__",
|
||||||
"manage.py",
|
|
||||||
"asgi.py",
|
|
||||||
"wsgi.py",
|
|
||||||
]
|
]
|
||||||
line-length = 120
|
lint.select = ["ALL"]
|
||||||
|
lint.ignore = [
|
||||||
[tool.ruff.lint]
|
"G004", # https://docs.astral.sh/ruff/rules/logging-f-string/
|
||||||
select = ["ALL"]
|
"EM101", # https://docs.astral.sh/ruff/rules/raw-string-in-exception/
|
||||||
ignore = [
|
"EM102", # https://docs.astral.sh/ruff/rules/f-string-in-exception/
|
||||||
"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
|
|
||||||
"COM812", # missing-trailing-comma (https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules)
|
"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)
|
"ISC001", # single-line-implicit-string-concatenation (https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules)
|
||||||
"D100", # Missing docstring in public module
|
"ARG001", # https://docs.astral.sh/ruff/rules/unused-function-argument/
|
||||||
"D101", # Missing docstring in public class
|
"ARG002", # https://docs.astral.sh/ruff/rules/unused-method-argument/
|
||||||
"D102", # Missing docstring in public method
|
"ARG004", # https://docs.astral.sh/ruff/rules/unused-static-method-argument/
|
||||||
"D105", # Missing docstring in magic method
|
"S101", # https://docs.astral.sh/ruff/rules/assert/
|
||||||
"D106", # Missing docstring in public nested class
|
"FIX002", # https://docs.astral.sh/ruff/rules/line-contains-todo/ - we rely on TD*
|
||||||
"D107", # Missing docstring in `__init__`
|
"D104", # https://docs.astral.sh/ruff/rules/undocumented-public-package/
|
||||||
"D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`
|
"D105", # https://docs.astral.sh/ruff/rules/undocumented-magic-method/
|
||||||
"D213", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
|
"D106", # https://docs.astral.sh/ruff/rules/undocumented-public-nested-class/
|
||||||
"FIX", # TODO, FIXME, XXX
|
"D107", # https://docs.astral.sh/ruff/rules/undocumented-public-init/
|
||||||
"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
|
|
||||||
]
|
]
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
[tool.ruff.lint.isort]
|
[tool.ruff.lint.isort]
|
||||||
force-single-line = true
|
force-single-line = true
|
||||||
|
|
||||||
|
[tool.ruff.lint.pydocstyle]
|
||||||
|
convention = "pep257"
|
||||||
|
|
||||||
|
[tool.ruff.lint.pylint]
|
||||||
|
max-args = 10
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"tests.py" = [
|
"test*.py" = [
|
||||||
"S101", # Use of assert
|
"S101", # https://docs.astral.sh/ruff/rules/assert/
|
||||||
"SLF001", # Private member access
|
"PLR2004", # https://docs.astral.sh/ruff/rules/magic-value-comparison/
|
||||||
"D100", # Docstrings
|
"PT009", # https://docs.astral.sh/ruff/rules/pytest-unittest-assertion/
|
||||||
"D103", # Docstrings
|
"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/*" = [
|
"factories.py" = [
|
||||||
"ANN001",
|
"PLR0913" # https://docs.astral.sh/ruff/rules/too-many-arguments/
|
||||||
"ANN201",
|
]
|
||||||
"ARG001", # TODO: Unused function argument. These are mostly pytest fixtures. Find a way to allow these in tests. Remove this after.
|
"*/migrations/*" = [
|
||||||
"D103",
|
"RUF001",
|
||||||
"D104",
|
"RUF012",
|
||||||
"S101", # Use of `assert` detected
|
"D" # https://docs.astral.sh/ruff/rules/#pydocstyle-d
|
||||||
"PGH004",
|
|
||||||
"PT004",
|
|
||||||
"RET504",
|
|
||||||
]
|
]
|
||||||
|
|
134
requirements.txt
134
requirements.txt
|
@ -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
|
|
|
@ -30,7 +30,8 @@ class OrderAdminForm(forms.ModelForm):
|
||||||
model = models.Order
|
model = models.Order
|
||||||
exclude = () # noqa: DJ006
|
exclude = () # noqa: DJ006
|
||||||
|
|
||||||
def clean(self): # noqa: ANN201
|
def clean(self) -> None:
|
||||||
|
"""Clean the order."""
|
||||||
cd = super().clean()
|
cd = super().clean()
|
||||||
if not cd["account"] and cd["member"]:
|
if not cd["account"] and cd["member"]:
|
||||||
try:
|
try:
|
||||||
|
@ -55,6 +56,7 @@ class OrderAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.action(description="Send order link to selected unpaid orders")
|
@admin.action(description="Send order link to selected unpaid orders")
|
||||||
def send_order(self, request: HttpRequest, queryset: QuerySet[models.Order]) -> None:
|
def send_order(self, request: HttpRequest, queryset: QuerySet[models.Order]) -> None:
|
||||||
|
"""Send the order to the member."""
|
||||||
for order in queryset:
|
for order in queryset:
|
||||||
if order.is_paid:
|
if order.is_paid:
|
||||||
messages.error(
|
messages.error(
|
||||||
|
@ -81,14 +83,20 @@ class PaymentAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(models.Product)
|
@admin.register(models.Product)
|
||||||
class ProductAdmin(admin.ModelAdmin):
|
class ProductAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for the Product model."""
|
||||||
|
|
||||||
list_display = ("name", "price", "vat")
|
list_display = ("name", "price", "vat")
|
||||||
|
|
||||||
|
|
||||||
class TransactionInline(admin.TabularInline):
|
class TransactionInline(admin.TabularInline):
|
||||||
|
"""Inline admin for the Transaction model."""
|
||||||
|
|
||||||
model = models.Transaction
|
model = models.Transaction
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Account)
|
@admin.register(models.Account)
|
||||||
class AccountAdmin(admin.ModelAdmin):
|
class AccountAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for the Account model."""
|
||||||
|
|
||||||
list_display = ("owner", "balance")
|
list_display = ("owner", "balance")
|
||||||
inlines = (TransactionInline,)
|
inlines = (TransactionInline,)
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
# Generated by Django 5.0.6 on 2024-07-14 22:16
|
# 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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
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 = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='payment',
|
model_name="payment",
|
||||||
name='stripe_charge_id',
|
name="stripe_charge_id",
|
||||||
field=models.CharField(blank=True, default='', max_length=255),
|
field=models.CharField(blank=True, default="", max_length=255),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,77 +2,113 @@
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import djmoney.models.fields
|
import djmoney.models.fields
|
||||||
from django.db import migrations, models
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounting', '0003_alter_payment_stripe_charge_id'),
|
("accounting", "0003_alter_payment_stripe_charge_id"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='PaymentType',
|
name="PaymentType",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
("modified", models.DateTimeField(auto_now=True, verbose_name="modified")),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
("created", models.DateTimeField(auto_now_add=True, verbose_name="created")),
|
||||||
('name', models.CharField(max_length=1024, verbose_name='description')),
|
("name", models.CharField(max_length=1024, verbose_name="description")),
|
||||||
('description', models.TextField(blank=True, max_length=2048)),
|
("description", models.TextField(blank=True, max_length=2048)),
|
||||||
('enabled', models.BooleanField(default=True)),
|
("enabled", models.BooleanField(default=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Product',
|
name="Product",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
("modified", models.DateTimeField(auto_now=True, verbose_name="modified")),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
("created", models.DateTimeField(auto_now_add=True, verbose_name="created")),
|
||||||
('name', models.CharField(max_length=512)),
|
("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)),
|
"price_currency",
|
||||||
('vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)),
|
djmoney.models.fields.CurrencyField(
|
||||||
('vat', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)),
|
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={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='payment',
|
model_name="payment",
|
||||||
name='external_transaction_id',
|
name="external_transaction_id",
|
||||||
field=models.CharField(blank=True, default='', max_length=255),
|
field=models.CharField(blank=True, default="", max_length=255),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='payment',
|
model_name="payment",
|
||||||
name='stripe_charge_id',
|
name="stripe_charge_id",
|
||||||
field=models.CharField(blank=True, default='', max_length=255),
|
field=models.CharField(blank=True, default="", max_length=255),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='payment',
|
model_name="payment",
|
||||||
name='payment_type',
|
name="payment_type",
|
||||||
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to='accounting.paymenttype'),
|
field=models.ForeignKey(
|
||||||
|
default=1, on_delete=django.db.models.deletion.PROTECT, to="accounting.paymenttype"
|
||||||
|
),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='OrderProduct',
|
name="OrderProduct",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
("modified", models.DateTimeField(auto_now=True, verbose_name="modified")),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
("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)),
|
"price_currency",
|
||||||
('vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default=None, editable=False, max_length=3)),
|
djmoney.models.fields.CurrencyField(
|
||||||
('vat', djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16)),
|
choices=[("DKK", "DKK")], default=None, editable=False, max_length=3
|
||||||
('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')),
|
),
|
||||||
|
("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={
|
options={
|
||||||
'abstract': False,
|
"abstract": False,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,40 +1,44 @@
|
||||||
# Generated by Django 5.0.7 on 2024-07-21 14:53
|
# Generated by Django 5.0.7 on 2024-07-21 14:53
|
||||||
|
|
||||||
import django.db.models.deletion
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounting', '0004_paymenttype_product_payment_external_transaction_id_and_more'),
|
("accounting", "0004_paymenttype_product_payment_external_transaction_id_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='order',
|
model_name="order",
|
||||||
name='price',
|
name="price",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='order',
|
model_name="order",
|
||||||
name='price_currency',
|
name="price_currency",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='order',
|
model_name="order",
|
||||||
name='vat',
|
name="vat",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='order',
|
model_name="order",
|
||||||
name='vat_currency',
|
name="vat_currency",
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='orderproduct',
|
model_name="orderproduct",
|
||||||
name='order',
|
name="order",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_products', to='accounting.order'),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, related_name="order_products", to="accounting.order"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='orderproduct',
|
model_name="orderproduct",
|
||||||
name='product',
|
name="product",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='order_products', to='accounting.product'),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, related_name="order_products", to="accounting.product"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
# Generated by Django 5.0.7 on 2024-07-21 15:17
|
# Generated by Django 5.0.7 on 2024-07-21 15:17
|
||||||
|
|
||||||
import django.db.models.deletion
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounting', '0005_remove_order_price_remove_order_price_currency_and_more'),
|
("accounting", "0005_remove_order_price_remove_order_price_currency_and_more"),
|
||||||
('membership', '0006_waitinglistentry_alter_membership_options'),
|
("membership", "0006_waitinglistentry_alter_membership_options"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='account',
|
model_name="account",
|
||||||
name='owner',
|
name="owner",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.member'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="membership.member"),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='order',
|
model_name="order",
|
||||||
name='user',
|
name="user",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.member'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="membership.member"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,42 +1,44 @@
|
||||||
# Generated by Django 5.1b1 on 2024-08-01 10:50
|
# Generated by Django 5.1b1 on 2024-08-01 10:50
|
||||||
|
|
||||||
import django.db.models.deletion
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounting', '0006_alter_account_owner_alter_order_user'),
|
("accounting", "0006_alter_account_owner_alter_order_user"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='orderproduct',
|
name="orderproduct",
|
||||||
options={'verbose_name': 'ordered product', 'verbose_name_plural': 'ordered products'},
|
options={"verbose_name": "ordered product", "verbose_name_plural": "ordered products"},
|
||||||
),
|
),
|
||||||
migrations.RenameField(
|
migrations.RenameField(
|
||||||
model_name='order',
|
model_name="order",
|
||||||
old_name='user',
|
old_name="user",
|
||||||
new_name='member',
|
new_name="member",
|
||||||
),
|
),
|
||||||
migrations.RemoveField(
|
migrations.RemoveField(
|
||||||
model_name='payment',
|
model_name="payment",
|
||||||
name='stripe_charge_id',
|
name="stripe_charge_id",
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='orderproduct',
|
model_name="orderproduct",
|
||||||
name='quantity',
|
name="quantity",
|
||||||
field=models.PositiveSmallIntegerField(default=1),
|
field=models.PositiveSmallIntegerField(default=1),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='orderproduct',
|
model_name="orderproduct",
|
||||||
name='order',
|
name="order",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='accounting.order'),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, related_name="items", to="accounting.order"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='orderproduct',
|
model_name="orderproduct",
|
||||||
name='product',
|
name="product",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='accounting.product'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="accounting.product"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -11,7 +11,7 @@ from . import models
|
||||||
|
|
||||||
# method for updating
|
# method for updating
|
||||||
@receiver(post_save, sender=models.Payment)
|
@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."""
|
"""Check that we receive Payments with the correct amount."""
|
||||||
if instance.amount != instance.order.total_with_vat:
|
if instance.amount != instance.order.total_with_vat:
|
||||||
mail_admins(
|
mail_admins(
|
||||||
|
@ -21,14 +21,14 @@ def check_total_amount(sender: models.Payment, instance: models.Payment, **kwarg
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=models.Payment)
|
@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."""
|
"""Mark an order as paid when payment is received."""
|
||||||
instance.order.is_paid = True
|
instance.order.is_paid = True
|
||||||
instance.order.save()
|
instance.order.save()
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=models.Order)
|
@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."""
|
"""Mark a membership as activated when its order is marked as paid."""
|
||||||
if instance.is_paid:
|
if instance.is_paid:
|
||||||
Membership.objects.filter(order=instance, activated=False, activated_on=None).update(
|
Membership.objects.filter(order=instance, activated=False, activated_on=None).update(
|
||||||
|
|
|
@ -83,7 +83,8 @@ def order_pay(request: HttpRequest, order_id: int) -> HttpResponse:
|
||||||
mail_admins("Error in checkout", str(e))
|
mail_admins("Error in checkout", str(e))
|
||||||
raise
|
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)
|
return redirect(checkout_session.url)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Run administrative tasks."""
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError:
|
except ImportError as exc:
|
||||||
raise ImportError(
|
raise ImportError( # noqa: TRY003
|
||||||
"Couldn't import Django. Are you sure it's installed and "
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
"available on your PYTHONPATH environment variable? Did you "
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
"forget to activate a virtual environment?",
|
"forget to activate a virtual environment?",
|
||||||
)
|
) from exc
|
||||||
execute_from_command_line(sys.argv)
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
|
@ -56,7 +56,7 @@ def decorate_ensure_membership_type_exists(membership_type: MembershipType, labe
|
||||||
"""Generate an admin action for given membership type and label."""
|
"""Generate an admin action for given membership type and label."""
|
||||||
|
|
||||||
@admin.action(description=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 ensure_membership_type_exists(request, queryset, membership_type)
|
||||||
|
|
||||||
return admin_action
|
return admin_action
|
||||||
|
@ -102,6 +102,7 @@ class MemberAdmin(UserAdmin):
|
||||||
|
|
||||||
@admin.display(description="membership")
|
@admin.display(description="membership")
|
||||||
def current_membership(self, instance: Member) -> Membership | None:
|
def current_membership(self, instance: Member) -> Membership | None:
|
||||||
|
"""Get the current membership for the member."""
|
||||||
return instance.memberships.current()
|
return instance.memberships.current()
|
||||||
|
|
||||||
def get_actions(self, request: HttpRequest) -> dict:
|
def get_actions(self, request: HttpRequest) -> dict:
|
||||||
|
@ -122,6 +123,7 @@ class MemberAdmin(UserAdmin):
|
||||||
|
|
||||||
@admin.action(description="Send invite email to selected inactive accounts")
|
@admin.action(description="Send invite email to selected inactive accounts")
|
||||||
def send_invite(self, request: HttpRequest, queryset: QuerySet[Member]) -> None:
|
def send_invite(self, request: HttpRequest, queryset: QuerySet[Member]) -> None:
|
||||||
|
"""Send invite email to the selected inactive accounts."""
|
||||||
for member in queryset:
|
for member in queryset:
|
||||||
if member.is_active:
|
if member.is_active:
|
||||||
messages.error(
|
messages.error(
|
||||||
|
|
|
@ -5,17 +5,23 @@
|
||||||
* Generally, an email consists of templates (for body and subject) and a get_context() method.
|
* 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 import messages
|
||||||
from django.contrib.auth.tokens import default_token_generator
|
from django.contrib.auth.tokens import default_token_generator
|
||||||
from django.contrib.sites.shortcuts import get_current_site
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
from django.core.mail.message import EmailMessage
|
from django.core.mail.message import EmailMessage
|
||||||
from django.http import HttpRequest
|
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
from django.utils import translation
|
from django.utils import translation
|
||||||
from django.utils.translation import gettext_lazy as _
|
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):
|
class BaseEmail(EmailMessage):
|
||||||
|
@ -31,7 +37,7 @@ class BaseEmail(EmailMessage):
|
||||||
template_subject = None
|
template_subject = None
|
||||||
default_subject = "SET SUBJECT HERE"
|
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.context = kwargs.pop("context", {})
|
||||||
self.user = kwargs.pop("user", None)
|
self.user = kwargs.pop("user", None)
|
||||||
if self.user:
|
if self.user:
|
||||||
|
@ -95,16 +101,19 @@ class BaseEmail(EmailMessage):
|
||||||
|
|
||||||
|
|
||||||
class InviteEmail(BaseEmail):
|
class InviteEmail(BaseEmail):
|
||||||
|
"""Email for invitations."""
|
||||||
|
|
||||||
template = "membership/emails/invite.txt"
|
template = "membership/emails/invite.txt"
|
||||||
default_subject = _("Invite to data.coop membership")
|
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
|
self.membership = membership
|
||||||
kwargs["user"] = membership.user
|
kwargs["user"] = membership.user
|
||||||
kwargs["from_email"] = "kasserer@data.coop"
|
kwargs["from_email"] = "kasserer@data.coop"
|
||||||
super().__init__(request, *args, **kwargs)
|
super().__init__(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self) -> dict:
|
def get_context_data(self) -> dict:
|
||||||
|
"""Resolve context for invitation emails."""
|
||||||
c = super().get_context_data()
|
c = super().get_context_data()
|
||||||
c["membership"] = self.membership
|
c["membership"] = self.membership
|
||||||
c["token"] = default_token_generator.make_token(self.membership.user)
|
c["token"] = default_token_generator.make_token(self.membership.user)
|
||||||
|
@ -113,16 +122,19 @@ class InviteEmail(BaseEmail):
|
||||||
|
|
||||||
|
|
||||||
class OrderEmail(BaseEmail):
|
class OrderEmail(BaseEmail):
|
||||||
|
"""Email for orders."""
|
||||||
|
|
||||||
template = "membership/emails/order.txt"
|
template = "membership/emails/order.txt"
|
||||||
default_subject = _("Your data.coop order and payment")
|
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
|
self.order = order
|
||||||
kwargs["user"] = order.member
|
kwargs["user"] = order.member
|
||||||
kwargs["from_email"] = "kasserer@data.coop"
|
kwargs["from_email"] = "kasserer@data.coop"
|
||||||
super().__init__(request, *args, **kwargs)
|
super().__init__(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_context_data(self) -> dict:
|
def get_context_data(self) -> dict:
|
||||||
|
"""Resolve context for order emails."""
|
||||||
c = super().get_context_data()
|
c = super().get_context_data()
|
||||||
c["order"] = self.order
|
c["order"] = self.order
|
||||||
return c
|
return c
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"""Form for the membership app."""
|
||||||
|
|
||||||
from allauth.account.adapter import get_adapter as get_allauth_adapter
|
from allauth.account.adapter import get_adapter as get_allauth_adapter
|
||||||
from allauth.account.forms import SetPasswordForm
|
from allauth.account.forms import SetPasswordForm
|
||||||
from django import forms
|
from django import forms
|
||||||
|
@ -12,7 +14,7 @@ class InviteForm(SetPasswordForm):
|
||||||
widget=forms.TextInput(attrs={"placeholder": _("Username"), "autocomplete": "username"}),
|
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")
|
self.membership = kwargs.pop("membership")
|
||||||
kwargs["user"] = self.membership.user
|
kwargs["user"] = self.membership.user
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
# Generated by Django 5.0.7 on 2024-07-20 20:45
|
# 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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('membership', '0005_member'),
|
("membership", "0005_member"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='WaitingListEntry',
|
name="WaitingListEntry",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
("modified", models.DateTimeField(auto_now=True, verbose_name="modified")),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
("created", models.DateTimeField(auto_now_add=True, verbose_name="created")),
|
||||||
('email', models.EmailField(max_length=254)),
|
("email", models.EmailField(max_length=254)),
|
||||||
('geography', models.CharField(blank=True, default='', verbose_name='geography')),
|
("geography", models.CharField(blank=True, default="", verbose_name="geography")),
|
||||||
('comment', models.TextField(blank=True)),
|
("comment", models.TextField(blank=True)),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'waiting list entry',
|
"verbose_name": "waiting list entry",
|
||||||
'verbose_name_plural': 'waiting list entries',
|
"verbose_name_plural": "waiting list entries",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='membership',
|
name="membership",
|
||||||
options={'verbose_name': 'medlemskab', 'verbose_name_plural': 'medlemskaber'},
|
options={"verbose_name": "medlemskab", "verbose_name_plural": "medlemskaber"},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,61 +2,77 @@
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounting', '0007_alter_orderproduct_options_rename_user_order_member_and_more'),
|
("accounting", "0007_alter_orderproduct_options_rename_user_order_member_and_more"),
|
||||||
('membership', '0006_waitinglistentry_alter_membership_options'),
|
("membership", "0006_waitinglistentry_alter_membership_options"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='membership',
|
model_name="membership",
|
||||||
name='activated',
|
name="activated",
|
||||||
field=models.BooleanField(default=False, help_text='Membership was activated.', verbose_name='activated'),
|
field=models.BooleanField(default=False, help_text="Membership was activated.", verbose_name="activated"),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='membership',
|
model_name="membership",
|
||||||
name='activated_on',
|
name="activated_on",
|
||||||
field=models.DateTimeField(blank=True, null=True),
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='membership',
|
model_name="membership",
|
||||||
name='order',
|
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'),
|
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(
|
migrations.AddField(
|
||||||
model_name='membership',
|
model_name="membership",
|
||||||
name='revoked',
|
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'),
|
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(
|
migrations.AddField(
|
||||||
model_name='membership',
|
model_name="membership",
|
||||||
name='revoked_on',
|
name="revoked_on",
|
||||||
field=models.DateTimeField(blank=True, null=True),
|
field=models.DateTimeField(blank=True, null=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='membership',
|
model_name="membership",
|
||||||
name='revoked_reason',
|
name="revoked_reason",
|
||||||
field=models.TextField(blank=True),
|
field=models.TextField(blank=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='membershiptype',
|
model_name="membershiptype",
|
||||||
name='active',
|
name="active",
|
||||||
field=models.BooleanField(default=True),
|
field=models.BooleanField(default=True),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='membershiptype',
|
model_name="membershiptype",
|
||||||
name='products',
|
name="products",
|
||||||
field=models.ManyToManyField(to='accounting.product'),
|
field=models.ManyToManyField(to="accounting.product"),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='membership',
|
model_name="membership",
|
||||||
name='user',
|
name="user",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to=settings.AUTH_USER_MODEL),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT, related_name="memberships", to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
# Generated by Django 5.1b1 on 2024-08-04 10:26
|
# Generated by Django 5.1b1 on 2024-08-04 10:26
|
||||||
|
|
||||||
import django.db.models.deletion
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('membership', '0007_membership_activated_membership_activated_on_and_more'),
|
("membership", "0007_membership_activated_membership_activated_on_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='membership',
|
model_name="membership",
|
||||||
name='membership_type',
|
name="membership_type",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='membership.membershiptype', verbose_name='membership type'),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name="memberships",
|
||||||
|
to="membership.membershiptype",
|
||||||
|
verbose_name="membership type",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,32 +1,33 @@
|
||||||
# Generated by Django 5.1rc1 on 2024-08-07 22:32
|
# Generated by Django 5.1rc1 on 2024-08-07 22:32
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
from django.db import migrations, models
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
def create_uuid(apps, schema_editor):
|
def create_uuid(apps, schema_editor) -> None: # noqa: ANN001
|
||||||
Membership = apps.get_model('membership', 'Membership')
|
Membership = apps.get_model("membership", "Membership")
|
||||||
for membership in Membership.objects.all():
|
for membership in Membership.objects.all():
|
||||||
membership.referral_code = uuid.uuid4()
|
membership.referral_code = uuid.uuid4()
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('membership', '0008_alter_membership_membership_type'),
|
("membership", "0008_alter_membership_membership_type"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='membership',
|
model_name="membership",
|
||||||
name='referral_code',
|
name="referral_code",
|
||||||
field=models.UUIDField(blank=True, null=True, default=uuid.uuid4, editable=False),
|
field=models.UUIDField(blank=True, null=True, default=uuid.uuid4, editable=False),
|
||||||
),
|
),
|
||||||
migrations.RunPython(create_uuid),
|
migrations.RunPython(create_uuid),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='membership',
|
model_name="membership",
|
||||||
name='referral_code',
|
name="referral_code",
|
||||||
field=models.UUIDField(unique=True, default=uuid.uuid4, editable=False),
|
field=models.UUIDField(unique=True, default=uuid.uuid4, editable=False),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,19 +1,26 @@
|
||||||
# Generated by Django 5.1rc1 on 2024-08-14 08:05
|
# Generated by Django 5.1rc1 on 2024-08-14 08:05
|
||||||
|
|
||||||
import django.db.models.deletion
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('membership', '0009_membership_referral_code'),
|
("membership", "0009_membership_referral_code"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='waitinglistentry',
|
model_name="waitinglistentry",
|
||||||
name='member',
|
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'),
|
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",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,46 +4,60 @@ import django.db.models.deletion
|
||||||
import django_registries.registry
|
import django_registries.registry
|
||||||
import services.registry
|
import services.registry
|
||||||
from django.conf import settings
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('membership', '0010_waitinglistentry_member'),
|
("membership", "0010_waitinglistentry_member"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ServiceAccess',
|
name="ServiceAccess",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
("modified", models.DateTimeField(auto_now=True, verbose_name="modified")),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
("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')),
|
"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={
|
options={
|
||||||
'verbose_name': 'service access',
|
"verbose_name": "service access",
|
||||||
'verbose_name_plural': 'service accesses',
|
"verbose_name_plural": "service accesses",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='membership',
|
name="membership",
|
||||||
options={'verbose_name': 'membership', 'verbose_name_plural': 'memberships'},
|
options={"verbose_name": "membership", "verbose_name_plural": "memberships"},
|
||||||
),
|
),
|
||||||
migrations.RemoveConstraint(
|
migrations.RemoveConstraint(
|
||||||
model_name='subscriptionperiod',
|
model_name="subscriptionperiod",
|
||||||
name='exclude_overlapping_periods',
|
name="exclude_overlapping_periods",
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='serviceaccess',
|
model_name="serviceaccess",
|
||||||
name='user',
|
name="user",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||||
),
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='serviceaccess',
|
model_name="serviceaccess",
|
||||||
constraint=models.UniqueConstraint(fields=('user', 'service'), name='unique_user_service'),
|
constraint=models.UniqueConstraint(fields=("user", "service"), name="unique_user_service"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,19 +6,30 @@ from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('membership', '0011_serviceaccess_alter_membership_options_and_more'),
|
("membership", "0011_serviceaccess_alter_membership_options_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='membership',
|
name="membership",
|
||||||
options={'verbose_name': 'medlemskab', 'verbose_name_plural': 'medlemskaber'},
|
options={"verbose_name": "medlemskab", "verbose_name_plural": "medlemskaber"},
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='serviceaccess',
|
model_name="serviceaccess",
|
||||||
name='service',
|
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'),
|
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",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,13 +4,12 @@ from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('membership', '0012_alter_membership_options_alter_serviceaccess_service'),
|
("membership", "0012_alter_membership_options_alter_serviceaccess_service"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.DeleteModel(
|
migrations.DeleteModel(
|
||||||
name='ServiceAccess',
|
name="ServiceAccess",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,14 +4,13 @@ from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('membership', '0013_delete_serviceaccess'),
|
("membership", "0013_delete_serviceaccess"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='membership',
|
name="membership",
|
||||||
options={'verbose_name': 'membership', 'verbose_name_plural': 'memberships'},
|
options={"verbose_name": "membership", "verbose_name_plural": "memberships"},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -188,7 +188,8 @@ class Membership(DirtyFieldsMixin, CreatedModifiedAbstract):
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.user} - {self.period}"
|
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
|
is_new = not self.pk
|
||||||
# A Membership is considered recently activated when:
|
# A Membership is considered recently activated when:
|
||||||
# It was created w/ activated=True OR
|
# It was created w/ activated=True OR
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
"""Permissions for the membership app."""
|
"""Permissions for the membership app."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission as DjangoPermission
|
from django.contrib.auth.models import Permission as DjangoPermission
|
||||||
|
@ -9,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
PERMISSIONS = []
|
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."""
|
"""Persist all permissions."""
|
||||||
for permission in PERMISSIONS:
|
for permission in PERMISSIONS:
|
||||||
permission.persist_permission()
|
permission.persist_permission()
|
||||||
|
@ -24,7 +26,7 @@ class Permission:
|
||||||
app_label: str
|
app_label: str
|
||||||
model: str
|
model: str
|
||||||
|
|
||||||
def __post_init__(self, *args, **kwargs) -> None:
|
def __post_init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
|
||||||
"""Post init method."""
|
"""Post init method."""
|
||||||
PERMISSIONS.append(self)
|
PERMISSIONS.append(self)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
@ratelimit(group="membership", key="ip", rate="10/d", method="ALL", block=True)
|
||||||
@member_view(
|
@member_view(
|
||||||
paths="invite/<str:referral_code>/<str:token>/",
|
paths="invite/<str:referral_code>/<str:token>/",
|
||||||
|
@ -146,7 +152,7 @@ def invite(request: HttpRequest, referral_code: str, token: str) -> HttpResponse
|
||||||
token_valid = default_token_generator.check_token(membership.user, token)
|
token_valid = default_token_generator.check_token(membership.user, token)
|
||||||
|
|
||||||
if not token_valid:
|
if not token_valid:
|
||||||
raise HttpResponseForbidden("Token not valid - maybe it expired?")
|
raise InvalidTokenError
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = InviteForm(membership=membership, data=request.POST)
|
form = InviteForm(membership=membership, data=request.POST)
|
||||||
|
|
|
@ -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
|
import os
|
||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"""Admin for the services app."""
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from services.models import ServiceAccess
|
from services.models import ServiceAccess
|
||||||
|
@ -6,6 +8,8 @@ from services.models import ServiceRequest
|
||||||
|
|
||||||
@admin.register(ServiceRequest)
|
@admin.register(ServiceRequest)
|
||||||
class ServiceRequestAdmin(admin.ModelAdmin):
|
class ServiceRequestAdmin(admin.ModelAdmin):
|
||||||
|
"""Admin for the ServiceRequest model."""
|
||||||
|
|
||||||
list_display = ("member", "service", "request", "status")
|
list_display = ("member", "service", "request", "status")
|
||||||
list_filter = ("request", "status")
|
list_filter = ("request", "status")
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
"""Config for the services app."""
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class ServicesConfig(AppConfig):
|
class ServicesConfig(AppConfig):
|
||||||
|
"""Config for the services app."""
|
||||||
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
name = "services"
|
name = "services"
|
||||||
|
|
|
@ -1,36 +1,64 @@
|
||||||
# Generated by Django 5.1rc1 on 2024-12-24 17:32
|
# Generated by Django 5.1rc1 on 2024-12-24 17:32
|
||||||
|
|
||||||
import django.db.models.deletion
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('membership', '0012_alter_membership_options_alter_serviceaccess_service'),
|
("membership", "0012_alter_membership_options_alter_serviceaccess_service"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ServiceRequest',
|
name="ServiceRequest",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
("modified", models.DateTimeField(auto_now=True, verbose_name="modified")),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
("created", models.DateTimeField(auto_now_add=True, verbose_name="created")),
|
||||||
('service', models.CharField(choices=[])),
|
("service", models.CharField(choices=[])),
|
||||||
('request', models.CharField(choices=[('CREATION', 'Creation'), ('PASSWORD_RESET', 'Password reset'), ('DELETION', 'Deletion')])),
|
(
|
||||||
('is_auto_created', models.BooleanField(default=False)),
|
"request",
|
||||||
('status', models.CharField(choices=[('NEW', 'New'), ('RESOLVED', 'Resolved')], default='NEW', max_length=24)),
|
models.CharField(
|
||||||
('member_notes', models.TextField(blank=True, help_text='Notes from the member, intended to guide the resolution of the request.')),
|
choices=[
|
||||||
('admin_notes', models.TextField(blank=True, help_text='Readable by member: Notes from the admin / status updates, resolutions etc.')),
|
("CREATION", "Creation"),
|
||||||
('member', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='membership.member')),
|
("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={
|
options={
|
||||||
'verbose_name': 'Service Request',
|
"verbose_name": "Service Request",
|
||||||
'verbose_name_plural': 'Service Requests',
|
"verbose_name_plural": "Service Requests",
|
||||||
'constraints': [models.CheckConstraint(condition=models.Q(('status__in', ['NEW', 'RESOLVED'])), name='services_servicerequest_status_valid')],
|
"constraints": [
|
||||||
|
models.CheckConstraint(
|
||||||
|
condition=models.Q(("status__in", ["NEW", "RESOLVED"])),
|
||||||
|
name="services_servicerequest_status_valid",
|
||||||
|
)
|
||||||
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,51 +2,83 @@
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import django_registries.registry
|
import django_registries.registry
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
import services.registry
|
import services.registry
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('membership', '0013_delete_serviceaccess'),
|
("membership", "0013_delete_serviceaccess"),
|
||||||
('services', '0001_initial'),
|
("services", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='servicerequest',
|
name="servicerequest",
|
||||||
options={'verbose_name': 'service request', 'verbose_name_plural': 'service requests'},
|
options={"verbose_name": "service request", "verbose_name_plural": "service requests"},
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='servicerequest',
|
model_name="servicerequest",
|
||||||
name='member',
|
name="member",
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_requests', to='membership.member'),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, related_name="service_requests", to="membership.member"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='servicerequest',
|
model_name="servicerequest",
|
||||||
name='request',
|
name="request",
|
||||||
field=models.CharField(choices=[('CREATION', 'Creation'), ('PASSWORD_RESET', 'Password reset'), ('DELETION', 'Deletion')], max_length=24),
|
field=models.CharField(
|
||||||
|
choices=[("CREATION", "Creation"), ("PASSWORD_RESET", "Password reset"), ("DELETION", "Deletion")],
|
||||||
|
max_length=24,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='servicerequest',
|
model_name="servicerequest",
|
||||||
name='service',
|
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),
|
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(
|
migrations.CreateModel(
|
||||||
name='ServiceAccess',
|
name="ServiceAccess",
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
|
("modified", models.DateTimeField(auto_now=True, verbose_name="modified")),
|
||||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
|
("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')),
|
"service",
|
||||||
('member', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.member')),
|
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={
|
options={
|
||||||
'verbose_name': 'service access',
|
"verbose_name": "service access",
|
||||||
'verbose_name_plural': 'service accesses',
|
"verbose_name_plural": "service accesses",
|
||||||
'unique_together': {('member', 'service')},
|
"unique_together": {("member", "service")},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
# Generated by Django 5.1.4 on 2025-01-16 07:20
|
# 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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('membership', '0014_alter_membership_options'),
|
("membership", "0014_alter_membership_options"),
|
||||||
('services', '0002_alter_servicerequest_options_and_more'),
|
("services", "0002_alter_servicerequest_options_and_more"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterUniqueTogether(
|
migrations.AlterUniqueTogether(
|
||||||
name='serviceaccess',
|
name="serviceaccess",
|
||||||
unique_together=set(),
|
unique_together=set(),
|
||||||
),
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='serviceaccess',
|
model_name="serviceaccess",
|
||||||
constraint=models.UniqueConstraint(fields=('member', 'service'), name='unique_user_service'),
|
constraint=models.UniqueConstraint(fields=("member", "service"), name="unique_user_service"),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"""Models for the services app."""
|
||||||
|
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from django.contrib.sites.models import Site
|
from django.contrib.sites.models import Site
|
||||||
|
@ -16,6 +18,8 @@ if typing.TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class ServiceRequestStatus(TextChoices):
|
class ServiceRequestStatus(TextChoices):
|
||||||
|
"""Status options for service requests."""
|
||||||
|
|
||||||
NEW = "NEW", _("New")
|
NEW = "NEW", _("New")
|
||||||
RESOLVED = "RESOLVED", _("Resolved")
|
RESOLVED = "RESOLVED", _("Resolved")
|
||||||
|
|
||||||
|
@ -46,7 +50,7 @@ class ServiceAccess(CreatedModifiedAbstract):
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"{self.member} - {self.service}"
|
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."""
|
"""Ensure that existing ServiceRequest objects are automatically resolved."""
|
||||||
is_new = not self.pk
|
is_new = not self.pk
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
@ -59,6 +63,8 @@ class ServiceAccess(CreatedModifiedAbstract):
|
||||||
|
|
||||||
|
|
||||||
class ServiceRequest(CreatedModifiedAbstract):
|
class ServiceRequest(CreatedModifiedAbstract):
|
||||||
|
"""A service request for a member."""
|
||||||
|
|
||||||
member = models.ForeignKey("membership.Member", on_delete=models.CASCADE, related_name="service_requests")
|
member = models.ForeignKey("membership.Member", on_delete=models.CASCADE, related_name="service_requests")
|
||||||
service = ServiceRegistry.choices_field()
|
service = ServiceRegistry.choices_field()
|
||||||
request = models.CharField(max_length=24, choices=ServiceRequests.choices)
|
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
|
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."""
|
"""Create notifications when new service requests are added."""
|
||||||
is_new = not self.pk
|
is_new = not self.pk
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ class ServiceRegistry(Registry):
|
||||||
|
|
||||||
|
|
||||||
class ServiceRequests(TextChoices):
|
class ServiceRequests(TextChoices):
|
||||||
|
"""Service request choices."""
|
||||||
|
|
||||||
CREATION = "CREATION", _("Creation")
|
CREATION = "CREATION", _("Creation")
|
||||||
PASSWORD_RESET = "PASSWORD_RESET", _("Password reset")
|
PASSWORD_RESET = "PASSWORD_RESET", _("Password reset")
|
||||||
DELETION = "DELETION", _("Deletion")
|
DELETION = "DELETION", _("Deletion")
|
||||||
|
@ -39,11 +41,6 @@ class ServiceInterface(Interface):
|
||||||
|
|
||||||
request_types: list[str, str] = DEFAULT_SERVICE_REQUEST_TYPES
|
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]] = []
|
subscribe_fields: tuple[tuple[str, forms.Field]] = []
|
||||||
|
|
||||||
def get_form_class(self) -> type:
|
def get_form_class(self) -> type:
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"""Utils for communicating with matrix."""
|
||||||
|
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
|
@ -63,8 +63,6 @@ class RenderConfig:
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
"""Render a list of objects with a table."""
|
"""Render a list of objects with a table."""
|
||||||
# TODO: List actions
|
|
||||||
|
|
||||||
entity_name = self.entity_name
|
entity_name = self.entity_name
|
||||||
entity_name_plural = self.entity_name_plural
|
entity_name_plural = self.entity_name_plural
|
||||||
objects = self.objects
|
objects = self.objects
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"""Pytest configuration."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
@ -12,11 +14,13 @@ from membership.models import SubscriptionPeriod
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def membership_type():
|
def membership_type():
|
||||||
|
"""Provide a membership type."""
|
||||||
return MembershipType.objects.create(name="Test Membership Type")
|
return MembershipType.objects.create(name="Test Membership Type")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def current_period():
|
def current_period():
|
||||||
|
"""Provide a current subscription period."""
|
||||||
SubscriptionPeriod.objects.create(
|
SubscriptionPeriod.objects.create(
|
||||||
period=DateRange(timezone.now().date() - timedelta(days=182), timezone.now().date() + timedelta(days=183))
|
period=DateRange(timezone.now().date() - timedelta(days=182), timezone.now().date() + timedelta(days=183))
|
||||||
)
|
)
|
||||||
|
@ -25,17 +29,18 @@ def current_period():
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def active_membership(membership_type, current_period):
|
def active_membership(membership_type, current_period):
|
||||||
|
"""Provide an active membership."""
|
||||||
member = Member.objects.create_user("test", "lala@adas.com", "1234")
|
member = Member.objects.create_user("test", "lala@adas.com", "1234")
|
||||||
membership = Membership.objects.create(
|
return Membership.objects.create(
|
||||||
user=member,
|
user=member,
|
||||||
membership_type=membership_type,
|
membership_type=membership_type,
|
||||||
period=current_period,
|
period=current_period,
|
||||||
activated=True,
|
activated=True,
|
||||||
)
|
)
|
||||||
return membership
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@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"):
|
with mock.patch("utils.matrix.httpx.post"):
|
||||||
yield
|
yield
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
from project.settings import * # noqa
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"""Tests for accounting."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from accounting import models
|
from accounting import models
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -5,6 +7,7 @@ from django.contrib.auth.models import User
|
||||||
|
|
||||||
@pytest.mark.django_db()
|
@pytest.mark.django_db()
|
||||||
def test_balance() -> None:
|
def test_balance() -> None:
|
||||||
|
"""Test balance."""
|
||||||
user = User.objects.create_user("test", "lala@adas.com", "1234")
|
user = User.objects.create_user("test", "lala@adas.com", "1234")
|
||||||
account = models.Account.objects.create(owner=user)
|
account = models.Account.objects.create(owner=user)
|
||||||
assert account.balance == 0
|
assert account.balance == 0
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
"""Tests for services."""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from membership.models import Membership
|
from membership.models import Membership
|
||||||
from services.models import ServiceRequest
|
from services.models import ServiceRequest
|
||||||
|
@ -6,6 +8,7 @@ from services.models import ServiceRequestStatus
|
||||||
|
|
||||||
@pytest.mark.django_db()
|
@pytest.mark.django_db()
|
||||||
def test_membership_activation(active_membership: Membership):
|
def test_membership_activation(active_membership: Membership):
|
||||||
|
"""Test membership activation."""
|
||||||
assert ServiceRequest.objects.filter(
|
assert ServiceRequest.objects.filter(
|
||||||
member=active_membership.user,
|
member=active_membership.user,
|
||||||
status=ServiceRequestStatus.NEW,
|
status=ServiceRequestStatus.NEW,
|
||||||
|
|
Loading…
Add table
Reference in a new issue