forked from data.coop/membersystem
Compare commits
No commits in common. "dockerfile-tweaks" and "master" have entirely different histories.
dockerfile
...
master
|
@ -1,10 +0,0 @@
|
|||
*
|
||||
.*
|
||||
*/.*
|
||||
|
||||
!src/
|
||||
!requirements.txt
|
||||
!requirements/
|
||||
!entrypoint.sh
|
||||
!pyproject.toml
|
||||
!README.md
|
25
.drone.yml
25
.drone.yml
|
@ -1,25 +0,0 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
steps:
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
environment:
|
||||
BUILD: "${DRONE_COMMIT_SHA}"
|
||||
settings:
|
||||
repo: docker.data.coop/membersystem
|
||||
registry: docker.data.coop
|
||||
username:
|
||||
from_secret: DOCKER_USERNAME
|
||||
password:
|
||||
from_secret: DOCKER_PASSWORD
|
||||
build_args_from_env:
|
||||
- BUILD
|
||||
tags:
|
||||
- "${DRONE_BUILD_NUMBER}"
|
||||
- "latest"
|
||||
when:
|
||||
branch:
|
||||
- main
|
|
@ -1,8 +0,0 @@
|
|||
SECRET_KEY=something-very-random
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_PORT=5432
|
||||
DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
|
||||
# Use something along the the following if you are not using docker
|
||||
# DATABASE_URL=postgres://postgres:postgres@localhost:5432/datacoop_membersystem
|
||||
DEBUG=True
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -2,9 +2,5 @@ __pycache__/
|
|||
*.pyc
|
||||
*.sw*
|
||||
db.sqlite3
|
||||
project/settings/local.py
|
||||
.pytest_cache
|
||||
.idea/
|
||||
*.mo
|
||||
.env
|
||||
venv/
|
||||
.venv/
|
||||
|
|
|
@ -1,37 +1,14 @@
|
|||
default_language_version:
|
||||
python: python3
|
||||
exclude: ^.*\b(migrations)\b.*$
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
- id: check-ast
|
||||
- id: check-merge-conflict
|
||||
- id: check-case-conflict
|
||||
- id: detect-private-key
|
||||
- id: check-added-large-files
|
||||
- id: check-json
|
||||
- id: check-symlinks
|
||||
- id: check-toml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: 'v0.5.2'
|
||||
- id: flake8
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: debug-statements
|
||||
- id: end-of-file-fixer
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v1.0.1
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
- --fix
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.16.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
- --py311-plus
|
||||
exclude: migrations/
|
||||
- repo: https://github.com/adamchainz/django-upgrade
|
||||
rev: 1.19.0
|
||||
hooks:
|
||||
- id: django-upgrade
|
||||
args:
|
||||
- --target-version=5.0
|
||||
- id: reorder-python-imports
|
||||
|
|
45
Dockerfile
45
Dockerfile
|
@ -1,45 +0,0 @@
|
|||
FROM python:3.12-slim-bullseye
|
||||
|
||||
# PYTHONFAULTHANDLER: Propagate tracebacks from all threads.
|
||||
# PYTHONUNBUFFERED: Write terminal output straight to docker (to not confuse Docker Compose).
|
||||
# PYTHONDONTWRITEBYTECODE: Dont write *pyc files at all, making it possible for a 100% read-only container.
|
||||
# PIP_NO_CACHE_DIR: Disable PIP cache, we don't need pip's cache after building the image.
|
||||
# PIP_DISABLE_PIP_VERSION_CHECK: Build the image with the available pip, do not check for updates (faster!)
|
||||
# PIP_DEFAULT_TIMEOUT: Allow for longer timeouts.
|
||||
ENV PYTHONFAULTHANDLER=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
PIP_NO_CACHE_DIR=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||
PIP_DEFAULT_TIMEOUT=100
|
||||
ARG BUILD
|
||||
ENV BUILD ${BUILD}
|
||||
ARG REQUIREMENTS_FILE=requirements.txt
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN groupadd -g 1000 www && useradd -u 1000 -ms /bin/bash -g www www
|
||||
COPY --chown=www:www . .
|
||||
RUN mkdir /app/src/static && \
|
||||
chown www:www /app/src/static && \
|
||||
apt-get update && \
|
||||
apt-get install -y \
|
||||
binutils \
|
||||
libpq-dev \
|
||||
build-essential \
|
||||
netcat-openbsd \
|
||||
libcairo2 \
|
||||
libpango-1.0-0 \
|
||||
libpangocairo-1.0-0 \
|
||||
libgdk-pixbuf2.0-0 \
|
||||
libffi-dev \
|
||||
shared-mime-info \
|
||||
gettext && \
|
||||
pip install --no-cache-dir -r $REQUIREMENTS_FILE && \
|
||||
django-admin compilemessages
|
||||
|
||||
ENTRYPOINT ["./entrypoint.sh"]
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["uvicorn", "project.asgi:application", "--host", "0.0.0.0", "--port", "8000", "--workers", "3", "--lifespan", "off", "--app-dir", "/app/src"]
|
32
Makefile
32
Makefile
|
@ -1,23 +1,15 @@
|
|||
DOCKER_COMPOSE = COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose
|
||||
DOCKER_RUN = ${DOCKER_COMPOSE} run -u `id -u`
|
||||
DOCKER_BUILD = DOCKER_BUILDKIT=1 docker build
|
||||
MANAGE_EXEC = python /app/src/manage.py
|
||||
MANAGE_COMMAND = ${DOCKER_RUN} app ${MANAGE_EXEC}
|
||||
# These are just some make targets, expressing how things
|
||||
# are supposed to be run, but feel free to change them!
|
||||
|
||||
run:
|
||||
${DOCKER_COMPOSE} up
|
||||
dev-setup:
|
||||
pip install -r requirements_dev.txt --upgrade
|
||||
pip install -r requirements.txt --upgrade
|
||||
pre-commit install
|
||||
python manage.py migrate
|
||||
python manage.py createsuperuser
|
||||
|
||||
makemigrations:
|
||||
${MANAGE_COMMAND} makemigrations ${ARGS}
|
||||
lint:
|
||||
pre-commit run --all
|
||||
|
||||
migrate:
|
||||
${MANAGE_COMMAND} migrate ${ARGS}
|
||||
|
||||
createsuperuser:
|
||||
${MANAGE_COMMAND} createsuperuser
|
||||
|
||||
shell:
|
||||
${MANAGE_COMMAND} shell
|
||||
|
||||
manage_command:
|
||||
${MANAGE_COMMAND} ${ARGS}
|
||||
test:
|
||||
pytest
|
||||
|
|
74
README.md
74
README.md
|
@ -1,74 +0,0 @@
|
|||
# data.coop member system
|
||||
|
||||
## Development setup
|
||||
|
||||
There are two ways to setup the development environment.
|
||||
|
||||
- Using the Docker Compose setup provided in this repository.
|
||||
- Using [hatch](https://hatch.pypa.io/) in your host OS.
|
||||
|
||||
|
||||
### Using Docker Compose
|
||||
|
||||
Working with the Docker Compose setup is made easy with the `Makefile` provided in the repository.
|
||||
|
||||
#### Requirements
|
||||
|
||||
- Docker
|
||||
- docker compose plugin
|
||||
|
||||
#### Setup
|
||||
|
||||
1. Setup .env file
|
||||
|
||||
An example .env file is provided in the repository. You can copy it to .env file using the following command:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
The default values in the .env file are suitable for the docker-compose setup.
|
||||
|
||||
2. Migrate
|
||||
|
||||
```bash
|
||||
make migrate
|
||||
```
|
||||
|
||||
3. Run the development server
|
||||
|
||||
```bash
|
||||
make run
|
||||
```
|
||||
|
||||
### Using hatch
|
||||
|
||||
#### Requirements
|
||||
|
||||
- Python 3.12 or higher
|
||||
- [hatch](https://hatch.pypa.io/) (Recommended way to install is using `pipx install hatch`)
|
||||
- A running PostgreSQL server
|
||||
|
||||
#### Setup
|
||||
|
||||
1. Setup .env file
|
||||
|
||||
An example .env file is provided in the repository. You can copy it to .env file using the following command:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit the .env file and set the values for the environment variables, especially the database variables.
|
||||
|
||||
2. Run migrate
|
||||
|
||||
```bash
|
||||
hatch run dev:migrate
|
||||
```
|
||||
|
||||
3. Run the development server
|
||||
|
||||
```bash
|
||||
hatch run dev:server
|
||||
```
|
19
README.rst
Normal file
19
README.rst
Normal file
|
@ -0,0 +1,19 @@
|
|||
member.data.coop
|
||||
================
|
||||
|
||||
To start developing:
|
||||
|
||||
# Create a virtualenv with python 3
|
||||
$ mkvirtualenv -p python3 datacoop
|
||||
|
||||
# Run this make target, which installs all the requirements and sets up a
|
||||
# development database.
|
||||
$ make dev-setup
|
||||
|
||||
To run the Django development server:
|
||||
|
||||
$ python manage.py runserver
|
||||
|
||||
Before you push your stuff, run tests:
|
||||
|
||||
$ make test
|
28
accounting/admin.py
Normal file
28
accounting/admin.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
@admin.register(models.Order)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
|
||||
list_display = ('who', 'description', 'created', 'is_paid',)
|
||||
|
||||
def who(self, instance):
|
||||
return instance.user.get_full_name()
|
||||
who.short_description = _("Customer")
|
||||
|
||||
|
||||
@admin.register(models.Payment)
|
||||
class PaymentAdmin(admin.ModelAdmin):
|
||||
|
||||
list_display = ('who', 'description', 'order_id', 'created',)
|
||||
|
||||
def who(self, instance):
|
||||
return instance.order.user.get_full_name()
|
||||
who.short_description = _("Customer")
|
||||
|
||||
def order_id(self, instance):
|
||||
return instance.order.id
|
||||
order_id.short_description = _("Order ID")
|
5
accounting/apps.py
Normal file
5
accounting/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountingConfig(AppConfig):
|
||||
name = 'accounting'
|
84
accounting/migrations/0001_initial.py
Normal file
84
accounting/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
# Generated by Django 2.0.6 on 2018-06-23 19:51
|
||||
from decimal import Decimal
|
||||
|
||||
import django.db.models.deletion
|
||||
import djmoney.models.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Account',
|
||||
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')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Order',
|
||||
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')),
|
||||
('description', models.CharField(max_length=1024, verbose_name='description')),
|
||||
('price_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
|
||||
('price', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), max_digits=16, verbose_name='price (excl. VAT)')),
|
||||
('vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
|
||||
('vat', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), max_digits=16, verbose_name='VAT')),
|
||||
('is_paid', models.BooleanField(default=False, verbose_name='is paid')),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='accounting.Account')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Order',
|
||||
'verbose_name_plural': 'Orders',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Payment',
|
||||
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')),
|
||||
('amount_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
|
||||
('amount', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), max_digits=16)),
|
||||
('description', models.CharField(max_length=1024, verbose_name='description')),
|
||||
('stripe_charge_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='accounting.Order')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'payment',
|
||||
'verbose_name_plural': 'payments',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Transaction',
|
||||
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')),
|
||||
('amount_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
|
||||
('amount', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), help_text='This will include VAT', max_digits=16, verbose_name='amount')),
|
||||
('description', models.CharField(max_length=1024, verbose_name='description')),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transactions', to='accounting.Account')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,48 +1,44 @@
|
|||
"""Models for the accounting app."""
|
||||
|
||||
from hashlib import md5
|
||||
from typing import Self
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.db.models.aggregates import Sum
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import pgettext_lazy
|
||||
from djmoney.models.fields import MoneyField
|
||||
from djmoney.money import Money
|
||||
|
||||
|
||||
class CreatedModifiedAbstract(models.Model):
|
||||
"""Abstract model to track creation and modification of objects."""
|
||||
|
||||
modified = models.DateTimeField(auto_now=True, verbose_name=_("modified"))
|
||||
created = models.DateTimeField(auto_now_add=True, verbose_name=_("created"))
|
||||
modified = models.DateTimeField(
|
||||
auto_now=True,
|
||||
verbose_name=_("modified"),
|
||||
)
|
||||
created = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_("created"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Account(CreatedModifiedAbstract):
|
||||
"""An account for a user.
|
||||
|
||||
"""
|
||||
This is the model where we can give access to several users, such that they
|
||||
can decide which account to use to pay for something.
|
||||
"""
|
||||
|
||||
owner = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Account of {self.owner.get_full_name()}"
|
||||
owner = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
|
||||
|
||||
@property
|
||||
def balance(self) -> Money:
|
||||
"""Return the balance of the account."""
|
||||
return self.transactions.all().aggregate(Sum("amount")).get("amount", 0)
|
||||
def balance(self):
|
||||
return self.transactions.all().aggregate(Sum('amount')).get('amount', 0)
|
||||
|
||||
|
||||
class Transaction(CreatedModifiedAbstract):
|
||||
"""A transaction.
|
||||
|
||||
"""
|
||||
Tracks in and outgoing events of an account. When an order is received, an
|
||||
amount is subtracted, when a payment is received, an amount is added.
|
||||
"""
|
||||
|
@ -56,92 +52,106 @@ class Transaction(CreatedModifiedAbstract):
|
|||
verbose_name=_("amount"),
|
||||
max_digits=16,
|
||||
decimal_places=2,
|
||||
help_text=_("This will include VAT"),
|
||||
help_text=_("This will include VAT")
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=1024,
|
||||
verbose_name=_("description")
|
||||
)
|
||||
description = models.CharField(max_length=1024, verbose_name=_("description"))
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Transaction of {self.amount} for {self.account}"
|
||||
|
||||
|
||||
class Order(CreatedModifiedAbstract):
|
||||
"""An order.
|
||||
|
||||
"""
|
||||
Scoped out: Contents of invoices will have to be tracked either here or in
|
||||
a separate Invoice model. This is undecided because we are not generating
|
||||
invoices at the moment.
|
||||
"""
|
||||
|
||||
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
||||
user = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
|
||||
account = models.ForeignKey(Account, on_delete=models.PROTECT)
|
||||
is_paid = models.BooleanField(default=False)
|
||||
|
||||
description = models.CharField(max_length=1024, verbose_name=_("description"))
|
||||
description = models.CharField(
|
||||
max_length=1024,
|
||||
verbose_name=_("description")
|
||||
)
|
||||
|
||||
price = MoneyField(
|
||||
verbose_name=_("price (excl. VAT)"),
|
||||
max_digits=16,
|
||||
decimal_places=2,
|
||||
)
|
||||
vat = MoneyField(verbose_name=_("VAT"), max_digits=16, decimal_places=2)
|
||||
vat = MoneyField(
|
||||
verbose_name=_("VAT"),
|
||||
max_digits=16,
|
||||
decimal_places=2,
|
||||
)
|
||||
|
||||
is_paid = models.BooleanField(default=False, verbose_name=_("is paid"))
|
||||
is_paid = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("is paid"),
|
||||
)
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
return self.price + self.vat
|
||||
|
||||
@property
|
||||
def display_id(self):
|
||||
return str(self.id).zfill(6)
|
||||
|
||||
@property
|
||||
def payment_token(self):
|
||||
pk = str(self.pk).encode("utf-8")
|
||||
x = md5()
|
||||
x.update(pk)
|
||||
extra_hash = (settings.SECRET_KEY + 'blah').encode('utf-8')
|
||||
x.update(extra_hash)
|
||||
return x.hexdigest()
|
||||
|
||||
class Meta:
|
||||
verbose_name = pgettext_lazy("accounting term", "Order")
|
||||
verbose_name_plural = pgettext_lazy("accounting term", "Orders")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Order ID {self.display_id}"
|
||||
|
||||
@property
|
||||
def total(self) -> Money:
|
||||
"""Return the total price of the order."""
|
||||
return self.price + self.vat
|
||||
|
||||
@property
|
||||
def display_id(self) -> str:
|
||||
"""Return an id for the order."""
|
||||
return str(self.id).zfill(6)
|
||||
|
||||
@property
|
||||
def payment_token(self) -> str:
|
||||
"""Return a token for the payment."""
|
||||
pk = str(self.pk).encode("utf-8")
|
||||
x = md5() # noqa: S324
|
||||
x.update(pk)
|
||||
extra_hash = (settings.SECRET_KEY + "blah").encode("utf-8")
|
||||
x.update(extra_hash)
|
||||
return x.hexdigest()
|
||||
def __str__(self):
|
||||
return "Order ID {id}".format(id=self.display_id)
|
||||
|
||||
|
||||
class Payment(CreatedModifiedAbstract):
|
||||
"""A payment is a transaction that is made to pay for an order."""
|
||||
|
||||
amount = MoneyField(max_digits=16, decimal_places=2)
|
||||
amount = MoneyField(
|
||||
max_digits=16,
|
||||
decimal_places=2,
|
||||
)
|
||||
order = models.ForeignKey(Order, on_delete=models.PROTECT)
|
||||
|
||||
description = models.CharField(max_length=1024, verbose_name=_("description"))
|
||||
description = models.CharField(
|
||||
max_length=1024,
|
||||
verbose_name=_("description")
|
||||
)
|
||||
|
||||
stripe_charge_id = models.CharField(max_length=255, blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("payment")
|
||||
verbose_name_plural = _("payments")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Payment ID {self.display_id}"
|
||||
stripe_charge_id = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def display_id(self) -> str:
|
||||
"""Return an id for the payment."""
|
||||
def display_id(self):
|
||||
return str(self.id).zfill(6)
|
||||
|
||||
@classmethod
|
||||
def from_order(cls, order: Order) -> Self:
|
||||
"""Create a payment from an order."""
|
||||
def from_order(cls, order):
|
||||
return cls.objects.create(
|
||||
order=order,
|
||||
user=order.user,
|
||||
amount=order.total,
|
||||
description=order.description,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "Payment ID {id}".format(id=self.display_id)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("payment")
|
||||
verbose_name_plural = _("payments")
|
|
@ -7,9 +7,10 @@ from . import models
|
|||
# def test():
|
||||
# do stuff
|
||||
|
||||
|
||||
@pytest.mark.django_db()
|
||||
def test_balance() -> None:
|
||||
@pytest.mark.django_db
|
||||
def test_balance():
|
||||
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
|
|
@ -1,30 +0,0 @@
|
|||
---
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
- REQUIREMENTS_FILE=requirements/requirements-dev.txt
|
||||
command: python /app/src/manage.py runserver 0.0.0.0:8000
|
||||
tty: true
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./:/app/
|
||||
depends_on:
|
||||
- postgres
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
postgres:
|
||||
image: postgres:13-alpine
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data/
|
||||
ports:
|
||||
- 5432:5432
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
...
|
|
@ -1,21 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo "Waiting for postgres..."
|
||||
|
||||
POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
||||
POSTGRES_HOST=${POSTGRES_HOST:-localhost}
|
||||
|
||||
while ! nc -z "$POSTGRES_HOST" "$POSTGRES_PORT"; do
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
echo "PostgreSQL started"
|
||||
|
||||
# Only migrate, collectstatic and compilemessages if we are NOT in development
|
||||
if [ -z "$DEBUG" ]; then
|
||||
python src/manage.py migrate;
|
||||
python src/manage.py collectstatic --no-input;
|
||||
python src/manage.py compilemessages;
|
||||
fi
|
||||
|
||||
exec "$@"
|
|
@ -6,10 +6,10 @@ if __name__ == "__main__":
|
|||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError:
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"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?",
|
||||
"forget to activate a virtual environment?"
|
||||
)
|
||||
execute_from_command_line(sys.argv)
|
|
@ -1,5 +1,8 @@
|
|||
"""Membership application.
|
||||
"""
|
||||
Membership application
|
||||
======================
|
||||
|
||||
This application's domain relate to organizational structures and
|
||||
implementation of statutes, policies etc.
|
||||
|
||||
"""
|
8
membership/admin.py
Normal file
8
membership/admin.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
@admin.register(models.Membership)
|
||||
class MembershipAdmin(admin.ModelAdmin):
|
||||
pass
|
5
membership/apps.py
Normal file
5
membership/apps.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MembershipConfig(AppConfig):
|
||||
name = 'membership'
|
101
membership/migrations/0001_initial.py
Normal file
101
membership/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
# Generated by Django 2.0.6 on 2018-06-23 19:07
|
||||
from decimal import Decimal
|
||||
|
||||
import django.db.models.deletion
|
||||
import djmoney.models.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Membership',
|
||||
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')),
|
||||
('can_vote', models.BooleanField(default=False, help_text='Indicates that the user has a democratic membership of the organization.', verbose_name='can vote')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'membership',
|
||||
'verbose_name_plural': 'memberships',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Organization',
|
||||
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=64, verbose_name='name')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'organization',
|
||||
'verbose_name_plural': 'organizations',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Subscription',
|
||||
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')),
|
||||
('active', models.BooleanField(default=False, help_text='Automatically set by payment system.', verbose_name='active')),
|
||||
('starts', models.DateField()),
|
||||
('ends', models.DateField()),
|
||||
('renewed_subscription', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='membership.Subscription', verbose_name='renewed subscription')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'subscription',
|
||||
'verbose_name_plural': 'subscriptions',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SubscriptionType',
|
||||
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=64, verbose_name='name')),
|
||||
('fee_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
|
||||
('fee', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0.0'), max_digits=16)),
|
||||
('fee_vat_currency', djmoney.models.fields.CurrencyField(choices=[('DKK', 'DKK')], default='XYZ', editable=False, max_length=3)),
|
||||
('fee_vat', djmoney.models.fields.MoneyField(decimal_places=2, default=Decimal('0'), max_digits=16)),
|
||||
('duration', models.PositiveSmallIntegerField(choices=[(1, 'annual')], default=1, verbose_name='duration')),
|
||||
('organization', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.Organization')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'subscription type',
|
||||
'verbose_name_plural': 'subscription types',
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subscription',
|
||||
name='subscription_type',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='memberships', to='membership.SubscriptionType', verbose_name='subscription type'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='subscription',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='membership',
|
||||
name='organization',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='membership.Organization'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='membership',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
0
membership/migrations/__init__.py
Normal file
0
membership/migrations/__init__.py
Normal file
144
membership/models.py
Normal file
144
membership/models.py
Normal file
|
@ -0,0 +1,144 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from djmoney.models.fields import MoneyField
|
||||
|
||||
|
||||
class CreatedModifiedAbstract(models.Model):
|
||||
|
||||
modified = models.DateTimeField(
|
||||
auto_now=True,
|
||||
verbose_name=_("modified"),
|
||||
)
|
||||
created = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_("created"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class Organization(CreatedModifiedAbstract):
|
||||
"""
|
||||
This holds the data of the organization that someone is a member of. It is
|
||||
possible that we'll create more advanced features here.
|
||||
"""
|
||||
name = models.CharField(
|
||||
verbose_name=_("name"),
|
||||
max_length=64,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("organization")
|
||||
verbose_name_plural = _("organizations")
|
||||
|
||||
|
||||
class Membership(CreatedModifiedAbstract):
|
||||
"""
|
||||
A user remains a member of an organization even though the subscription is
|
||||
unpaid or renewed. This just changes the status/permissions etc. of the
|
||||
membership, thus we need to track subscription creation, expiry, renewals
|
||||
etc. and ensure that the membership is modified accordingly.
|
||||
|
||||
This expresses some
|
||||
"""
|
||||
|
||||
organization = models.ForeignKey(Organization, on_delete=models.PROTECT)
|
||||
user = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
|
||||
|
||||
can_vote = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("can vote"),
|
||||
help_text=_(
|
||||
"Indicates that the user has a democratic membership of the "
|
||||
"organization."
|
||||
)
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
||||
return _("{} is a member of {}").format(
|
||||
self.user.get_full_name(),
|
||||
self.organization.name,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("membership")
|
||||
verbose_name_plural = _("memberships")
|
||||
|
||||
|
||||
class SubscriptionType(CreatedModifiedAbstract):
|
||||
"""
|
||||
Properties of subscriptions are stored here. Should of course not be edited
|
||||
after subscriptions are created.
|
||||
"""
|
||||
|
||||
organization = models.ForeignKey(Organization, on_delete=models.PROTECT)
|
||||
|
||||
name = models.CharField(
|
||||
verbose_name=_("name"),
|
||||
max_length=64,
|
||||
)
|
||||
|
||||
fee = MoneyField(
|
||||
max_digits=16,
|
||||
decimal_places=2,
|
||||
)
|
||||
|
||||
fee_vat = MoneyField(
|
||||
max_digits=16,
|
||||
decimal_places=2,
|
||||
default=0,
|
||||
)
|
||||
|
||||
duration = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
choices=[(1, _("annual"))],
|
||||
verbose_name=_("duration"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("subscription type")
|
||||
verbose_name_plural = _("subscription types")
|
||||
|
||||
|
||||
class Subscription(CreatedModifiedAbstract):
|
||||
"""
|
||||
To not confuse other types of subscriptions, one can be a *subscribed*
|
||||
member of an organization, meaning that they are paying etc.
|
||||
|
||||
A subscription does not track payment, this is done in the accounting app.
|
||||
"""
|
||||
|
||||
subscription_type = models.ForeignKey(
|
||||
SubscriptionType,
|
||||
related_name='memberships',
|
||||
verbose_name=_("subscription type"),
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
user = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
|
||||
|
||||
active = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("active"),
|
||||
help_text=_("Automatically set by payment system.")
|
||||
)
|
||||
|
||||
starts = models.DateField()
|
||||
ends = models.DateField()
|
||||
|
||||
renewed_subscription = models.ForeignKey(
|
||||
'self',
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("renewed subscription"),
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("subscription")
|
||||
verbose_name_plural = _("subscriptions")
|
0
project/__init__.py
Normal file
0
project/__init__.py
Normal file
9
project/context_processors.py
Normal file
9
project/context_processors.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
"""Context processors for the membersystem app."""
|
||||
from django.contrib.sites.shortcuts import get_current_site
|
||||
|
||||
|
||||
def current_site(request):
|
||||
"""Include the current site in the context."""
|
||||
return {
|
||||
'site': get_current_site(request)
|
||||
}
|
14
project/settings/__init__.py
Normal file
14
project/settings/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
import warnings
|
||||
|
||||
from .base import * # noqa
|
||||
|
||||
try:
|
||||
from .local import * # noqa
|
||||
except ImportError:
|
||||
warnings.warn(
|
||||
"No settings.local, using a default SECRET_KEY 'hest'. You should "
|
||||
"write a custom local.py with this setting."
|
||||
)
|
||||
SECRET_KEY = "hest"
|
||||
DEBUG = True
|
||||
pass
|
137
project/settings/base.py
Normal file
137
project/settings/base.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
Django settings for membersystem project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 2.0.4.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/2.0/ref/settings/
|
||||
"""
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.sites',
|
||||
|
||||
'users',
|
||||
'accounting',
|
||||
'membership',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'project.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join("project", "templates")],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'project.context_processors.current_site',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'allauth.account.auth_backends.AuthenticationBackend',
|
||||
)
|
||||
|
||||
WSGI_APPLICATION = 'project.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', # noqa
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', # noqa
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', # noqa
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', # noqa
|
||||
},
|
||||
]
|
||||
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/2.0/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
CURRENCIES = ('DKK',)
|
||||
CURRENCY_CHOICES = [('DKK', 'DKK')]
|
79
project/static/css/membersystem.css
Normal file
79
project/static/css/membersystem.css
Normal file
|
@ -0,0 +1,79 @@
|
|||
/* General styles */
|
||||
html
|
||||
{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: sans-serif;
|
||||
font-size: 2.5vmin;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
background: #fff;
|
||||
color: #000;
|
||||
margin: 1em auto;
|
||||
max-width: 50em;
|
||||
padding: 0 1em;
|
||||
box-shadow: 0 0 2.5em rgba(0, 0, 0, 20%);
|
||||
}
|
||||
|
||||
header,
|
||||
footer
|
||||
{
|
||||
background: #eee;
|
||||
padding: .5em;
|
||||
margin: 0 -1em;
|
||||
}
|
||||
|
||||
footer
|
||||
{
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
|
||||
header h1
|
||||
{
|
||||
font-size: 1em;
|
||||
float: left;
|
||||
padding: .5em .5em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
header ul,
|
||||
footer ul
|
||||
{
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
header ul li,
|
||||
footer ul li
|
||||
{
|
||||
display: inline;
|
||||
}
|
||||
|
||||
header ul li a,
|
||||
footer ul li a
|
||||
{
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: .5em .5em;
|
||||
}
|
||||
|
||||
|
||||
/* Forms */
|
||||
label
|
||||
{
|
||||
display: block;
|
||||
padding: .5em 0;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea
|
||||
{
|
||||
font-size: inherit;
|
||||
}
|
49
project/templates/base.html
Normal file
49
project/templates/base.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
<!DOCTYPE html>
|
||||
{% load static %}
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block head_title %}{% endblock %} – {{ site.name }}</title>
|
||||
{% block extra_head %}{% endblock %}
|
||||
<link rel="stylesheet" href="{% static '/css/membersystem.css' %}" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>
|
||||
<a href="/">{{ site.name }}</a>
|
||||
</h1>
|
||||
<ul>
|
||||
{% if user.is_authenticated %}
|
||||
<li><a href="{% url 'users:email' %}">Change e-mail</a></li>
|
||||
<li><a href="{% url 'users:logout' %}">Sign out</a></li>
|
||||
{% else %}
|
||||
<li><a href="{% url 'users:login' %}">Sign in</a></li>
|
||||
<li><a href="{% url 'users:signup' %}">Sign up</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</header>
|
||||
{% block body %}
|
||||
{% if messages %}
|
||||
<ul id="messages">
|
||||
{% for message in messages %}
|
||||
<li>{{message}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
{% block extra_body %}
|
||||
{% endblock %}
|
||||
<footer>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://data.coop">data.coop</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://git.data.coop/data.coop/membersystem">source code</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
12
project/urls.py
Normal file
12
project/urls.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
"""URLs for the membersystem"""
|
||||
from django.contrib import admin
|
||||
from django.urls import include
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index),
|
||||
path("users/", include("users.urls")),
|
||||
path('admin/', admin.site.urls),
|
||||
]
|
5
project/views.py
Normal file
5
project/views.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.shortcuts import render_to_response
|
||||
|
||||
|
||||
def index(request):
|
||||
return render_to_response("index.html")
|
|
@ -1,11 +1,11 @@
|
|||
"""WSGI config for membersystem project.
|
||||
"""
|
||||
WSGI config for membersystem project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
154
pyproject.toml
154
pyproject.toml
|
@ -1,154 +0,0 @@
|
|||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "membersystem"
|
||||
description = ''
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
keywords = []
|
||||
authors = [
|
||||
{ name = "Víðir Valberg Guðmundsson", email = "valberg@orn.li" },
|
||||
]
|
||||
dependencies = [
|
||||
"Django~=5.0",
|
||||
"django-money~=3.5",
|
||||
"django-allauth~=0.63",
|
||||
"psycopg[binary]~=3.2",
|
||||
"environs[django]>=11,<12",
|
||||
"uvicorn~=0.30",
|
||||
"whitenoise~=6.7",
|
||||
"django-zen-queries~=2.1",
|
||||
"django-registries==0.0.3",
|
||||
"django-view-decorator==0.0.4",
|
||||
"django-oauth-toolkit~=2.4",
|
||||
]
|
||||
version = "0.0.1"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src"]
|
||||
|
||||
[tool.hatch.env]
|
||||
requires = ["hatch-pip-compile"]
|
||||
|
||||
[tool.hatch.envs.default]
|
||||
type = "pip-compile"
|
||||
|
||||
[tool.hatch.envs.dev]
|
||||
type = "pip-compile"
|
||||
dependencies = [
|
||||
"coverage[toml]==7.3.0",
|
||||
"pytest==7.2.2",
|
||||
"pytest-cov",
|
||||
"pytest-django==4.5.2",
|
||||
"mypy==1.1.1",
|
||||
"django-stubs==1.16.0",
|
||||
"pip-tools==7.3.0",
|
||||
"django-debug-toolbar==4.2.0",
|
||||
"django-browser-reload==1.7.0",
|
||||
"model-bakery==1.17.0",
|
||||
]
|
||||
|
||||
[[tool.hatch.envs.tests.matrix]]
|
||||
python = ["3.12"]
|
||||
django = ["5.0"]
|
||||
|
||||
[tool.hatch.envs.tests.overrides]
|
||||
matrix.django.dependencies = [
|
||||
{ value = "django~={matrix:django}" },
|
||||
]
|
||||
matrix.python.dependencies = [
|
||||
{ value = "typing_extensions==4.5.0", if = ["3.10"]},
|
||||
]
|
||||
|
||||
[tool.hatch.envs.dev.scripts]
|
||||
cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=src --cov=tests --cov=append {args}"
|
||||
no-cov = "cov --no-cov {args}"
|
||||
typecheck = "mypy --config-file=pyproject.toml ."
|
||||
requirements = "pip-compile --output-file requirements/base.txt pyproject.toml"
|
||||
server = "./src/manage.py runserver 0.0.0.0:8000"
|
||||
migrate = "./src/manage.py migrate"
|
||||
makemigrations = "./src/manage.py makemigrations"
|
||||
createsuperuser = "./src/manage.py createsuperuser"
|
||||
shell = "./src/manage.py shell"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
DJANGO_SETTINGS_MODULE="tests.settings"
|
||||
addopts = "--reuse-db"
|
||||
norecursedirs = "build dist docs .eggs/* *.egg-info htmlcov .git"
|
||||
python_files = "test*.py"
|
||||
testpaths = "tests"
|
||||
pythonpath = ". tests"
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
parallel = true
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"no cov",
|
||||
"if __name__ == .__main__.:",
|
||||
"if TYPE_CHECKING:",
|
||||
]
|
||||
|
||||
[tool.mypy]
|
||||
mypy_path = "src/"
|
||||
exclude = [
|
||||
"venv/",
|
||||
"dist/",
|
||||
"docs/",
|
||||
]
|
||||
namespace_packages = false
|
||||
show_error_codes = true
|
||||
strict = true
|
||||
warn_unreachable = true
|
||||
follow_imports = "normal"
|
||||
#plugins = ["mypy_django_plugin.main"]
|
||||
|
||||
[tool.django-stubs]
|
||||
#django_settings_module = "tests.settings"
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "tests.*"
|
||||
allow_untyped_defs = true
|
||||
|
||||
[tool.ruff]
|
||||
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
|
||||
"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)
|
||||
"D105", # Missing docstring in magic method
|
||||
"D106", # Missing docstring in public nested class
|
||||
"FIX", # TODO, FIXME, XXX
|
||||
"TD", # TODO, FIXME, XXX
|
||||
"ANN002", # Missing type annotation for `*args`
|
||||
"ANN003", # Missing type annotation for `**kwargs`
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
force-single-line = true
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests.py" = [
|
||||
"S101", # Use of assert
|
||||
"SLF001", # Private member access
|
||||
"D100", # Docstrings
|
||||
"D103", # Docstrings
|
||||
]
|
105
requirements.txt
105
requirements.txt
|
@ -1,103 +1,2 @@
|
|||
#
|
||||
# This file is autogenerated by hatch-pip-compile with Python 3.12
|
||||
#
|
||||
# - django-allauth==0.63.6
|
||||
# - django-money==3.5.2
|
||||
# - django-oauth-toolkit==2.4.0
|
||||
# - django-registries==0.0.3
|
||||
# - django-view-decorator==0.0.4
|
||||
# - django-zen-queries==2.1.0
|
||||
# - django==5.0.7
|
||||
# - environs[django]==11.0.0
|
||||
# - psycopg[binary]==3.2.1
|
||||
# - uvicorn==0.30.1
|
||||
# - whitenoise==6.7.0
|
||||
#
|
||||
|
||||
asgiref==3.8.1
|
||||
# via django
|
||||
babel==2.15.0
|
||||
# via py-moneyed
|
||||
certifi==2024.7.4
|
||||
# via requests
|
||||
cffi==1.16.0
|
||||
# via cryptography
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
click==8.1.7
|
||||
# via uvicorn
|
||||
cryptography==42.0.8
|
||||
# via jwcrypto
|
||||
dj-database-url==2.2.0
|
||||
# via environs
|
||||
dj-email-url==1.0.6
|
||||
# via environs
|
||||
django==5.0.7
|
||||
# via
|
||||
# hatch.envs.default
|
||||
# dj-database-url
|
||||
# django-allauth
|
||||
# django-money
|
||||
# django-oauth-toolkit
|
||||
# django-registries
|
||||
# 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.2
|
||||
# via hatch.envs.default
|
||||
django-oauth-toolkit==2.4.0
|
||||
# via hatch.envs.default
|
||||
django-registries==0.0.3
|
||||
# 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 uvicorn
|
||||
idna==3.7
|
||||
# via 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
|
||||
sqlparse==0.5.1
|
||||
# via django
|
||||
typing-extensions==4.12.2
|
||||
# via
|
||||
# dj-database-url
|
||||
# jwcrypto
|
||||
# psycopg
|
||||
# py-moneyed
|
||||
urllib3==2.2.2
|
||||
# via requests
|
||||
uvicorn==0.30.1
|
||||
# via hatch.envs.default
|
||||
whitenoise==6.7.0
|
||||
# via hatch.envs.default
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
||||
Django>=2.2,<2.3
|
||||
django-money==0.15
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile --output-file=requirements/base.txt pyproject.toml
|
||||
#
|
||||
asgiref==3.8.1
|
||||
# via django
|
||||
babel==2.15.0
|
||||
# via py-moneyed
|
||||
certifi==2024.7.4
|
||||
# via 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.0.7
|
||||
# via
|
||||
# dj-database-url
|
||||
# django-allauth
|
||||
# django-money
|
||||
# django-oauth-toolkit
|
||||
# django-registries
|
||||
# django-view-decorator
|
||||
# django-zen-queries
|
||||
# membersystem (pyproject.toml)
|
||||
django-allauth==0.63.6
|
||||
# via membersystem (pyproject.toml)
|
||||
django-cache-url==3.4.5
|
||||
# via environs
|
||||
django-money==3.5.2
|
||||
# via membersystem (pyproject.toml)
|
||||
django-oauth-toolkit==2.4.0
|
||||
# via membersystem (pyproject.toml)
|
||||
django-registries==0.0.3
|
||||
# via membersystem (pyproject.toml)
|
||||
django-view-decorator==0.0.4
|
||||
# via membersystem (pyproject.toml)
|
||||
django-zen-queries==2.1.0
|
||||
# via membersystem (pyproject.toml)
|
||||
environs[django]==11.0.0
|
||||
# via
|
||||
# environs
|
||||
# membersystem (pyproject.toml)
|
||||
h11==0.14.0
|
||||
# via uvicorn
|
||||
idna==3.7
|
||||
# via 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[binary]==3.2.1
|
||||
# via
|
||||
# membersystem (pyproject.toml)
|
||||
# psycopg
|
||||
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
|
||||
sqlparse==0.5.1
|
||||
# via django
|
||||
typing-extensions==4.12.2
|
||||
# via
|
||||
# dj-database-url
|
||||
# jwcrypto
|
||||
# psycopg
|
||||
# py-moneyed
|
||||
urllib3==2.2.2
|
||||
# via requests
|
||||
uvicorn==0.30.3
|
||||
# via membersystem (pyproject.toml)
|
||||
whitenoise==6.7.0
|
||||
# via membersystem (pyproject.toml)
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# setuptools
|
|
@ -1,183 +0,0 @@
|
|||
#
|
||||
# This file is autogenerated by hatch-pip-compile with Python 3.12
|
||||
#
|
||||
# - coverage[toml]==7.3.0
|
||||
# - pytest==7.2.2
|
||||
# - pytest-cov
|
||||
# - pytest-django==4.5.2
|
||||
# - mypy==1.1.1
|
||||
# - django-stubs==1.16.0
|
||||
# - pip-tools==7.3.0
|
||||
# - django-debug-toolbar==4.2.0
|
||||
# - django-browser-reload==1.7.0
|
||||
# - model-bakery==1.17.0
|
||||
# - django-allauth~=0.63
|
||||
# - django-money~=3.5
|
||||
# - django-oauth-toolkit~=2.4
|
||||
# - django-registries==0.0.3
|
||||
# - django-view-decorator==0.0.4
|
||||
# - django-zen-queries~=2.1
|
||||
# - django~=5.0
|
||||
# - environs[django]<12,>=11
|
||||
# - psycopg[binary]~=3.2
|
||||
# - uvicorn~=0.30
|
||||
# - whitenoise~=6.7
|
||||
#
|
||||
|
||||
asgiref==3.8.1
|
||||
# via django
|
||||
attrs==23.2.0
|
||||
# via pytest
|
||||
babel==2.15.0
|
||||
# via py-moneyed
|
||||
build==1.2.1
|
||||
# via pip-tools
|
||||
certifi==2024.7.4
|
||||
# via requests
|
||||
cffi==1.16.0
|
||||
# via cryptography
|
||||
charset-normalizer==3.3.2
|
||||
# via requests
|
||||
click==8.1.7
|
||||
# via
|
||||
# pip-tools
|
||||
# uvicorn
|
||||
coverage==7.3.0
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# coverage
|
||||
# pytest-cov
|
||||
cryptography==42.0.8
|
||||
# via jwcrypto
|
||||
dj-database-url==2.2.0
|
||||
# via environs
|
||||
dj-email-url==1.0.6
|
||||
# via environs
|
||||
django==5.0.7
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# dj-database-url
|
||||
# django-allauth
|
||||
# django-browser-reload
|
||||
# django-debug-toolbar
|
||||
# django-money
|
||||
# django-oauth-toolkit
|
||||
# django-registries
|
||||
# django-stubs
|
||||
# django-stubs-ext
|
||||
# django-view-decorator
|
||||
# django-zen-queries
|
||||
# model-bakery
|
||||
django-allauth==0.63.6
|
||||
# via hatch.envs.dev
|
||||
django-browser-reload==1.7.0
|
||||
# via hatch.envs.dev
|
||||
django-cache-url==3.4.5
|
||||
# via environs
|
||||
django-debug-toolbar==4.2.0
|
||||
# via hatch.envs.dev
|
||||
django-money==3.5.2
|
||||
# via hatch.envs.dev
|
||||
django-oauth-toolkit==2.4.0
|
||||
# via hatch.envs.dev
|
||||
django-registries==0.0.3
|
||||
# via hatch.envs.dev
|
||||
django-stubs==1.16.0
|
||||
# via hatch.envs.dev
|
||||
django-stubs-ext==5.0.2
|
||||
# via django-stubs
|
||||
django-view-decorator==0.0.4
|
||||
# via hatch.envs.dev
|
||||
django-zen-queries==2.1.0
|
||||
# via hatch.envs.dev
|
||||
environs==11.0.0
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# environs
|
||||
h11==0.14.0
|
||||
# via uvicorn
|
||||
idna==3.7
|
||||
# via requests
|
||||
iniconfig==2.0.0
|
||||
# via pytest
|
||||
jwcrypto==1.5.6
|
||||
# via django-oauth-toolkit
|
||||
marshmallow==3.21.3
|
||||
# via environs
|
||||
model-bakery==1.17.0
|
||||
# via hatch.envs.dev
|
||||
mypy==1.1.1
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# django-stubs
|
||||
mypy-extensions==1.0.0
|
||||
# via mypy
|
||||
oauthlib==3.2.2
|
||||
# via django-oauth-toolkit
|
||||
packaging==24.1
|
||||
# via
|
||||
# build
|
||||
# marshmallow
|
||||
# pytest
|
||||
pip-tools==7.3.0
|
||||
# via hatch.envs.dev
|
||||
pluggy==1.5.0
|
||||
# via pytest
|
||||
psycopg==3.2.1
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# psycopg
|
||||
psycopg-binary==3.2.1
|
||||
# via psycopg
|
||||
py-moneyed==3.0
|
||||
# via django-money
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
pyproject-hooks==1.1.0
|
||||
# via build
|
||||
pytest==7.2.2
|
||||
# via
|
||||
# hatch.envs.dev
|
||||
# pytest-cov
|
||||
# pytest-django
|
||||
pytest-cov==5.0.0
|
||||
# via hatch.envs.dev
|
||||
pytest-django==4.5.2
|
||||
# via hatch.envs.dev
|
||||
python-dotenv==1.0.1
|
||||
# via environs
|
||||
pytz==2024.1
|
||||
# via django-oauth-toolkit
|
||||
requests==2.32.3
|
||||
# via django-oauth-toolkit
|
||||
sqlparse==0.5.1
|
||||
# via
|
||||
# django
|
||||
# django-debug-toolbar
|
||||
tomli==2.0.1
|
||||
# via django-stubs
|
||||
types-pytz==2024.1.0.20240417
|
||||
# via django-stubs
|
||||
types-pyyaml==6.0.12.20240311
|
||||
# via django-stubs
|
||||
typing-extensions==4.12.2
|
||||
# via
|
||||
# dj-database-url
|
||||
# django-stubs
|
||||
# django-stubs-ext
|
||||
# jwcrypto
|
||||
# mypy
|
||||
# psycopg
|
||||
# py-moneyed
|
||||
urllib3==2.2.2
|
||||
# via requests
|
||||
uvicorn==0.30.1
|
||||
# via hatch.envs.dev
|
||||
wheel==0.43.0
|
||||
# via pip-tools
|
||||
whitenoise==6.7.0
|
||||
# via hatch.envs.dev
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
# pip
|
||||
# setuptools
|
3
requirements_dev.txt
Normal file
3
requirements_dev.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
pytest
|
||||
pytest-django
|
||||
pre-commit
|
13
setup.cfg
Normal file
13
setup.cfg
Normal file
|
@ -0,0 +1,13 @@
|
|||
[flake8]
|
||||
ignore = E226,E302,E41
|
||||
max-line-length = 160
|
||||
max-complexity = 10
|
||||
exclude = */migrations/*
|
||||
|
||||
[isort]
|
||||
atomic = true
|
||||
multi_line_output = 5
|
||||
line_length = 160
|
||||
indent = ' '
|
||||
combine_as_imports = true
|
||||
skip = wsgi.py,.eggs,setup.py
|
|
@ -1 +0,0 @@
|
|||
"""Accounting app."""
|
|
@ -1,36 +0,0 @@
|
|||
"""Admin for the accounting app."""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Order
|
||||
from .models import Payment
|
||||
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(admin.ModelAdmin):
|
||||
"""Admin for the Order model."""
|
||||
|
||||
list_display = ("who", "description", "created", "is_paid")
|
||||
|
||||
@admin.display(description=_("Customer"))
|
||||
def who(self, instance: Order) -> str:
|
||||
"""Return the full name of the user who made the order."""
|
||||
return instance.user.get_full_name()
|
||||
|
||||
|
||||
@admin.register(Payment)
|
||||
class PaymentAdmin(admin.ModelAdmin):
|
||||
"""Admin for the Payment model."""
|
||||
|
||||
list_display = ("who", "description", "order_id", "created")
|
||||
|
||||
@admin.display(description=_("Customer"))
|
||||
def who(self, instance: Payment) -> str:
|
||||
"""Return the full name of the user who made the payment."""
|
||||
return instance.order.user.get_full_name()
|
||||
|
||||
@admin.display(description=_("Order ID"))
|
||||
def order_id(self, instance: Payment) -> int:
|
||||
"""Return the ID of the order."""
|
||||
return instance.order.id
|
|
@ -1,9 +0,0 @@
|
|||
"""Accounting app configuration."""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountingConfig(AppConfig):
|
||||
"""Accounting app config."""
|
||||
|
||||
name = "accounting"
|
|
@ -1,233 +0,0 @@
|
|||
# Generated by Django 3.1.7 on 2021-02-27 20:06
|
||||
import django.db.models.deletion
|
||||
import djmoney.models.fields
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Account",
|
||||
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="oprettet"),
|
||||
),
|
||||
(
|
||||
"owner",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Order",
|
||||
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="oprettet"),
|
||||
),
|
||||
(
|
||||
"description",
|
||||
models.CharField(max_length=1024, verbose_name="description"),
|
||||
),
|
||||
(
|
||||
"price_currency",
|
||||
djmoney.models.fields.CurrencyField(
|
||||
choices=[("DKK", "DKK")],
|
||||
default="XYZ",
|
||||
editable=False,
|
||||
max_length=3,
|
||||
),
|
||||
),
|
||||
(
|
||||
"price",
|
||||
djmoney.models.fields.MoneyField(
|
||||
decimal_places=2,
|
||||
max_digits=16,
|
||||
verbose_name="price (excl. VAT)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"vat_currency",
|
||||
djmoney.models.fields.CurrencyField(
|
||||
choices=[("DKK", "DKK")],
|
||||
default="XYZ",
|
||||
editable=False,
|
||||
max_length=3,
|
||||
),
|
||||
),
|
||||
(
|
||||
"vat",
|
||||
djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16, verbose_name="VAT"),
|
||||
),
|
||||
("is_paid", models.BooleanField(default=False, verbose_name="is paid")),
|
||||
(
|
||||
"account",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="accounting.account",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Order",
|
||||
"verbose_name_plural": "Orders",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Transaction",
|
||||
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="oprettet"),
|
||||
),
|
||||
(
|
||||
"amount_currency",
|
||||
djmoney.models.fields.CurrencyField(
|
||||
choices=[("DKK", "DKK")],
|
||||
default="XYZ",
|
||||
editable=False,
|
||||
max_length=3,
|
||||
),
|
||||
),
|
||||
(
|
||||
"amount",
|
||||
djmoney.models.fields.MoneyField(
|
||||
decimal_places=2,
|
||||
help_text="This will include VAT",
|
||||
max_digits=16,
|
||||
verbose_name="amount",
|
||||
),
|
||||
),
|
||||
(
|
||||
"description",
|
||||
models.CharField(max_length=1024, verbose_name="description"),
|
||||
),
|
||||
(
|
||||
"account",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="transactions",
|
||||
to="accounting.account",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Payment",
|
||||
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="oprettet"),
|
||||
),
|
||||
(
|
||||
"amount_currency",
|
||||
djmoney.models.fields.CurrencyField(
|
||||
choices=[("DKK", "DKK")],
|
||||
default="XYZ",
|
||||
editable=False,
|
||||
max_length=3,
|
||||
),
|
||||
),
|
||||
(
|
||||
"amount",
|
||||
djmoney.models.fields.MoneyField(decimal_places=2, max_digits=16),
|
||||
),
|
||||
(
|
||||
"description",
|
||||
models.CharField(max_length=1024, verbose_name="description"),
|
||||
),
|
||||
(
|
||||
"stripe_charge_id",
|
||||
models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
(
|
||||
"order",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="accounting.order",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "payment",
|
||||
"verbose_name_plural": "payments",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,41 +0,0 @@
|
|||
# Generated by Django 5.0.1 on 2024-01-14 11:14
|
||||
|
||||
import djmoney.models.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("accounting", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="order",
|
||||
name="price_currency",
|
||||
field=djmoney.models.fields.CurrencyField(
|
||||
choices=[("DKK", "DKK")], default=None, editable=False, max_length=3
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="order",
|
||||
name="vat_currency",
|
||||
field=djmoney.models.fields.CurrencyField(
|
||||
choices=[("DKK", "DKK")], default=None, editable=False, max_length=3
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="payment",
|
||||
name="amount_currency",
|
||||
field=djmoney.models.fields.CurrencyField(
|
||||
choices=[("DKK", "DKK")], default=None, editable=False, max_length=3
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="transaction",
|
||||
name="amount_currency",
|
||||
field=djmoney.models.fields.CurrencyField(
|
||||
choices=[("DKK", "DKK")], default=None, editable=False, max_length=3
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 5.0.6 on 2024-07-14 22:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('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),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
|
@ -1,22 +0,0 @@
|
|||
"""Admin configuration for membership app."""
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Membership
|
||||
from .models import MembershipType
|
||||
from .models import SubscriptionPeriod
|
||||
|
||||
|
||||
@admin.register(Membership)
|
||||
class MembershipAdmin(admin.ModelAdmin):
|
||||
"""Admin for Membership model."""
|
||||
|
||||
|
||||
@admin.register(MembershipType)
|
||||
class MembershipTypeAdmin(admin.ModelAdmin):
|
||||
"""Admin for MembershipType model."""
|
||||
|
||||
|
||||
@admin.register(SubscriptionPeriod)
|
||||
class SubscriptionPeriodAdmin(admin.ModelAdmin):
|
||||
"""Admin for SubscriptionPeriod model."""
|
|
@ -1,16 +0,0 @@
|
|||
"""Membership app configuration."""
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_migrate
|
||||
|
||||
|
||||
class MembershipConfig(AppConfig):
|
||||
"""Membership app config."""
|
||||
|
||||
name = "membership"
|
||||
|
||||
def ready(self) -> None:
|
||||
"""Ready method."""
|
||||
from .permissions import persist_permissions
|
||||
|
||||
post_migrate.connect(persist_permissions, sender=self)
|
|
@ -1,92 +0,0 @@
|
|||
# Generated by Django 3.1.7 on 2021-02-28 21:09
|
||||
import django.contrib.postgres.fields.ranges
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="MembershipType",
|
||||
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="oprettet"),
|
||||
),
|
||||
("name", models.CharField(max_length=64, verbose_name="navn")),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "membership type",
|
||||
"verbose_name_plural": "membership types",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Membership",
|
||||
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="oprettet"),
|
||||
),
|
||||
(
|
||||
"period",
|
||||
django.contrib.postgres.fields.ranges.DateTimeRangeField(
|
||||
help_text="The duration this subscription is for. "
|
||||
),
|
||||
),
|
||||
(
|
||||
"membership_type",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="memberships",
|
||||
to="membership.membershiptype",
|
||||
verbose_name="subscription type",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "membership",
|
||||
"verbose_name_plural": "memberships",
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,61 +0,0 @@
|
|||
# Generated by Django 4.1 on 2023-01-02 21:05
|
||||
|
||||
import django.contrib.postgres.constraints
|
||||
import django.contrib.postgres.fields.ranges
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("membership", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SubscriptionPeriod",
|
||||
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"),
|
||||
),
|
||||
(
|
||||
"period",
|
||||
django.contrib.postgres.fields.ranges.DateRangeField(verbose_name="period"),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="membership",
|
||||
name="period",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="membership",
|
||||
name="created",
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name="created"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="membershiptype",
|
||||
name="created",
|
||||
field=models.DateTimeField(auto_now_add=True, verbose_name="created"),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="subscriptionperiod",
|
||||
constraint=django.contrib.postgres.constraints.ExclusionConstraint(
|
||||
expressions=[("period", "&&")], name="exclude_overlapping_periods"
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 4.1 on 2023-01-02 21:05
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("membership", "0002_subscriptionperiod_remove_membership_period_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="membership",
|
||||
name="period",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="membership.subscriptionperiod",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,22 +0,0 @@
|
|||
# Generated by Django 4.1 on 2023-01-02 21:06
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("membership", "0003_membership_period"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="membership",
|
||||
name="period",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="membership.subscriptionperiod",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
# Generated by Django 4.1.5 on 2023-09-16 14:09
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("membership", "0004_alter_membership_period"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Member",
|
||||
fields=[],
|
||||
options={
|
||||
"proxy": True,
|
||||
"indexes": [],
|
||||
"constraints": [],
|
||||
},
|
||||
bases=("auth.user",),
|
||||
),
|
||||
]
|
|
@ -1,141 +0,0 @@
|
|||
"""Models for the membership app."""
|
||||
|
||||
from typing import ClassVar
|
||||
from typing import Self
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.postgres.constraints import ExclusionConstraint
|
||||
from django.contrib.postgres.fields import DateRangeField
|
||||
from django.contrib.postgres.fields import RangeOperators
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from utils.mixins import CreatedModifiedAbstract
|
||||
|
||||
|
||||
class NoSubscriptionPeriodFoundError(Exception):
|
||||
"""Raised when no subscription period is found."""
|
||||
|
||||
|
||||
class Member(User):
|
||||
"""Proxy model for the User model to add some convenience methods."""
|
||||
|
||||
class QuerySet(models.QuerySet):
|
||||
"""QuerySet for the Member model."""
|
||||
|
||||
def annotate_membership(self) -> Self:
|
||||
"""Annotate whether the user has an active membership."""
|
||||
from .selectors import get_current_subscription_period
|
||||
|
||||
current_subscription_period = get_current_subscription_period()
|
||||
|
||||
if not current_subscription_period:
|
||||
raise NoSubscriptionPeriodFoundError
|
||||
|
||||
return self.annotate(
|
||||
active_membership=models.Exists(
|
||||
Membership.objects.filter(
|
||||
user=models.OuterRef("pk"),
|
||||
period=current_subscription_period.id,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
objects = QuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
|
||||
class SubscriptionPeriod(CreatedModifiedAbstract):
|
||||
"""A subscription period.
|
||||
|
||||
Denotes a period for which members should pay their membership fee for.
|
||||
"""
|
||||
|
||||
period = DateRangeField(verbose_name=_("period"))
|
||||
|
||||
class Meta:
|
||||
constraints: ClassVar = [
|
||||
ExclusionConstraint(
|
||||
name="exclude_overlapping_periods",
|
||||
expressions=[
|
||||
("period", RangeOperators.OVERLAPS),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
|
||||
|
||||
|
||||
class Membership(CreatedModifiedAbstract):
|
||||
"""A membership.
|
||||
|
||||
Tracks that a user has membership of a given type for a given period.
|
||||
"""
|
||||
|
||||
class QuerySet(models.QuerySet):
|
||||
"""QuerySet for the Membership model."""
|
||||
|
||||
def for_member(self, member: Member) -> Self:
|
||||
"""Filter memberships for a given member."""
|
||||
return self.filter(user=member)
|
||||
|
||||
def _current(self) -> Self:
|
||||
"""Filter memberships for the current period."""
|
||||
return self.filter(period__period__contains=timezone.now())
|
||||
|
||||
def current(self) -> "Membership | None":
|
||||
"""Get the current membership."""
|
||||
try:
|
||||
return self._current().get()
|
||||
except self.model.DoesNotExist:
|
||||
return None
|
||||
|
||||
def previous(self) -> list["Membership"]:
|
||||
"""Get previous memberships."""
|
||||
# A naïve way to get previous by just excluding the current. This
|
||||
# means that there must be some protection against "future"
|
||||
# memberships.
|
||||
return list(self.all().difference(self._current()))
|
||||
|
||||
objects = QuerySet.as_manager()
|
||||
|
||||
user = models.ForeignKey("auth.User", on_delete=models.PROTECT)
|
||||
|
||||
membership_type = models.ForeignKey(
|
||||
"membership.MembershipType",
|
||||
related_name="memberships",
|
||||
verbose_name=_("subscription type"),
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
period = models.ForeignKey(
|
||||
"membership.SubscriptionPeriod",
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("membership")
|
||||
verbose_name_plural = _("memberships")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.user} - {self.period}"
|
||||
|
||||
|
||||
class MembershipType(CreatedModifiedAbstract):
|
||||
"""A membership type.
|
||||
|
||||
Models membership types. Currently only a name, but will in the future
|
||||
possibly contain more information like fees.
|
||||
"""
|
||||
|
||||
name = models.CharField(verbose_name=_("name"), max_length=64)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("membership type")
|
||||
verbose_name_plural = _("membership types")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
|
@ -1,54 +0,0 @@
|
|||
"""Permissions for the membership app."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from django.contrib.auth.models import Permission as DjangoPermission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
PERMISSIONS = []
|
||||
|
||||
|
||||
def persist_permissions(*args, **kwargs) -> None: # type: ignore[no-untyped-def] # noqa: ARG001
|
||||
"""Persist all permissions."""
|
||||
for permission in PERMISSIONS:
|
||||
permission.persist_permission()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Permission:
|
||||
"""Dataclass to define a permission."""
|
||||
|
||||
name: str
|
||||
codename: str
|
||||
app_label: str
|
||||
model: str
|
||||
|
||||
def __post_init__(self, *args, **kwargs) -> None:
|
||||
"""Post init method."""
|
||||
PERMISSIONS.append(self)
|
||||
|
||||
@property
|
||||
def path(self) -> str:
|
||||
"""Return the path of the permission."""
|
||||
return f"{self.app_label}.{self.codename}"
|
||||
|
||||
def persist_permission(self) -> None:
|
||||
"""Persist the permission."""
|
||||
content_type, _ = ContentType.objects.get_or_create(
|
||||
app_label=self.app_label,
|
||||
model=self.model,
|
||||
)
|
||||
DjangoPermission.objects.get_or_create(
|
||||
content_type=content_type,
|
||||
codename=self.codename,
|
||||
defaults={"name": self.name},
|
||||
)
|
||||
|
||||
|
||||
ADMINISTRATE_MEMBERS = Permission(
|
||||
name=_("Can administrate members"),
|
||||
codename="administrate_members",
|
||||
app_label="membership",
|
||||
model="membership",
|
||||
)
|
|
@ -1,73 +0,0 @@
|
|||
"""Selectors for the membership app."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.db.models import Exists
|
||||
from django.db.models import OuterRef
|
||||
from django.utils import timezone
|
||||
|
||||
from membership.models import Member
|
||||
from membership.models import Membership
|
||||
from membership.models import SubscriptionPeriod
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models.query import QuerySet
|
||||
|
||||
|
||||
def get_subscription_periods(member: Member | None = None) -> list[SubscriptionPeriod]:
|
||||
"""Get all subscription periods."""
|
||||
subscription_periods = SubscriptionPeriod.objects.prefetch_related(
|
||||
"membership_set",
|
||||
"membership_set__user",
|
||||
).all()
|
||||
|
||||
if member:
|
||||
subscription_periods = subscription_periods.annotate(
|
||||
membership_exists=Exists(
|
||||
Membership.objects.filter(
|
||||
user=member,
|
||||
period=OuterRef("pk"),
|
||||
),
|
||||
),
|
||||
).filter(membership_exists=True)
|
||||
|
||||
return list(subscription_periods)
|
||||
|
||||
|
||||
def get_current_subscription_period() -> SubscriptionPeriod | None:
|
||||
"""Get the current subscription period."""
|
||||
with contextlib.suppress(SubscriptionPeriod.DoesNotExist):
|
||||
return SubscriptionPeriod.objects.prefetch_related(
|
||||
"membership_set",
|
||||
"membership_set__user",
|
||||
).get(period__contains=timezone.now())
|
||||
|
||||
|
||||
def get_memberships(
|
||||
*,
|
||||
member: Member | None = None,
|
||||
period: SubscriptionPeriod | None = None,
|
||||
) -> Membership.QuerySet:
|
||||
"""Get memberships."""
|
||||
memberships = Membership.objects.select_related("membership_type").all()
|
||||
|
||||
if member:
|
||||
memberships = memberships.for_member(member=member)
|
||||
|
||||
if period:
|
||||
memberships = memberships.filter(period=period)
|
||||
|
||||
return memberships
|
||||
|
||||
|
||||
def get_members() -> QuerySet[Member]:
|
||||
"""Get all members."""
|
||||
return Member.objects.all().annotate_membership().order_by("username")
|
||||
|
||||
|
||||
def get_member(*, member_id: int) -> Member:
|
||||
"""Get a member by id."""
|
||||
return get_members().get(id=member_id)
|
|
@ -1,45 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}
|
||||
{% trans "Member detail" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="content-view">
|
||||
<h1>
|
||||
{{ member.username }}
|
||||
</h1>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>{% trans "Membership" %}</h3>
|
||||
|
||||
{% if subscription_periods %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Start" %}</th>
|
||||
<th>{% trans "End" %}</th>
|
||||
<th>{% trans "Has membership" %}</th>
|
||||
<th>{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for period in subscription_periods %}
|
||||
<tr {% if not period.period.upper %}class="table-active"{% endif %}>
|
||||
<td>{{ period.period.lower }}</td>
|
||||
<td>{{ period.period.upper }}</td>
|
||||
<td>{{ period.membership_exists }}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
{% trans "No memberships" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,63 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block head_title %}
|
||||
{% trans "Membership" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="content-view">
|
||||
<h2>Membership settings</h2>
|
||||
{% if not current_membership %}
|
||||
<p>{% trans "You do not have an active membership!" %}</p>
|
||||
|
||||
<p>{% trans "You can become a member by depositing the membership fee to our bank account." %}</p>
|
||||
|
||||
<ul>
|
||||
<li>Reg. 8401 (Merkur)</li>
|
||||
<li>Kontonr. 1016866</li>
|
||||
<li>Tekst på overførslen: Your email</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>{% trans "You are a member!" %}</p>
|
||||
|
||||
{% trans "next general assembly" as next_general_assembly %}
|
||||
<p>{% trans "Period" %}: {{ current_period.lower|date:"SHORT_DATE_FORMAT" }} to {{ current_period.upper|date:"SHORT_DATE_FORMAT"|default:next_general_assembly }}</p>
|
||||
<p>{% trans "Type" %}: {{ current_membership.membership_type }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="content-view">
|
||||
<h2>Profile settings</h2>
|
||||
<form>
|
||||
<div>
|
||||
<label for="username">
|
||||
Username
|
||||
</label>
|
||||
<input id="username" type="text" value="{{user}}" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="first_name">
|
||||
First name
|
||||
</label>
|
||||
<input id="first_name" type="text" value="{{user.first_name}}" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="last_name">
|
||||
Last name
|
||||
</label>
|
||||
<input id="last_name" type="text" value="{{user.last_name}}" />
|
||||
</div>
|
||||
|
||||
<button>Update Profile</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="view-list">
|
||||
<h2>Email settings</h2>
|
||||
<button>Update Email</button>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,115 +0,0 @@
|
|||
"""Views for the membership app."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_view_decorator import namespaced_decorator_factory
|
||||
from utils.view_utils import RenderConfig
|
||||
from utils.view_utils import RowAction
|
||||
from utils.view_utils import render
|
||||
|
||||
from .permissions import ADMINISTRATE_MEMBERS
|
||||
from .selectors import get_member
|
||||
from .selectors import get_members
|
||||
from .selectors import get_memberships
|
||||
from .selectors import get_subscription_periods
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.http import HttpRequest
|
||||
from django.http import HttpResponse
|
||||
|
||||
member_view = namespaced_decorator_factory(namespace="member", base_path="membership")
|
||||
|
||||
|
||||
@member_view(
|
||||
paths="",
|
||||
name="membership-overview",
|
||||
login_required=True,
|
||||
)
|
||||
def membership_overview(request: HttpRequest) -> HttpResponse:
|
||||
"""View to show the membership overview."""
|
||||
memberships = get_memberships(member=request.user)
|
||||
current_membership = memberships.current()
|
||||
previous_memberships = memberships.previous()
|
||||
|
||||
current_period = current_membership.period.period if current_membership else None
|
||||
|
||||
context = {
|
||||
"current_membership": current_membership,
|
||||
"current_period": current_period,
|
||||
"previous_memberships": previous_memberships,
|
||||
}
|
||||
|
||||
return render(
|
||||
request=request,
|
||||
template_name="membership/membership_overview.html",
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
admin_members_view = namespaced_decorator_factory(
|
||||
namespace="admin-members",
|
||||
base_path="admin",
|
||||
)
|
||||
|
||||
|
||||
@admin_members_view(
|
||||
paths="members/",
|
||||
name="list",
|
||||
login_required=True,
|
||||
permissions=[ADMINISTRATE_MEMBERS.path],
|
||||
)
|
||||
def members_admin(request: HttpRequest) -> HttpResponse:
|
||||
"""View to list all members."""
|
||||
users = get_members()
|
||||
|
||||
render_config = RenderConfig(
|
||||
entity_name="member",
|
||||
entity_name_plural="members",
|
||||
paginate_by=20,
|
||||
objects=users,
|
||||
columns=[
|
||||
("username", _("Username")),
|
||||
("first_name", _("First name")),
|
||||
("last_name", _("Last name")),
|
||||
("email", _("Email")),
|
||||
("active_membership", _("Active membership")),
|
||||
],
|
||||
row_actions=[
|
||||
RowAction(
|
||||
label=_("View"),
|
||||
url_name="admin-members:detail",
|
||||
url_kwargs={"member_id": "id"},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
return render_config.render_list(
|
||||
request=request,
|
||||
)
|
||||
|
||||
|
||||
@admin_members_view(
|
||||
paths="<int:member_id>/",
|
||||
name="detail",
|
||||
login_required=True,
|
||||
permissions=[ADMINISTRATE_MEMBERS.path],
|
||||
)
|
||||
def members_admin_detail(request: HttpRequest, member_id: int) -> HttpResponse:
|
||||
"""View to show the details of a member."""
|
||||
member = get_member(member_id=member_id)
|
||||
subscription_periods = get_subscription_periods(member=member)
|
||||
|
||||
context = {
|
||||
"member": member,
|
||||
"subscription_periods": subscription_periods,
|
||||
"base_path": "admin-members:list",
|
||||
}
|
||||
|
||||
return render(
|
||||
request=request,
|
||||
template_name="membership/members_admin_detail.html",
|
||||
context=context,
|
||||
)
|
|
@ -1 +0,0 @@
|
|||
"""data.coop member system."""
|
|
@ -1,7 +0,0 @@
|
|||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
|
||||
|
||||
application = get_asgi_application()
|
|
@ -1,519 +0,0 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: \n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-03-04 09:06+0100\n"
|
||||
"PO-Revision-Date: 2021-03-04 09:06+0100\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: da\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Poedit 2.4.1\n"
|
||||
|
||||
#: src/accounting/admin.py:15 src/accounting/admin.py:26
|
||||
msgid "Customer"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/admin.py:31
|
||||
msgid "Order ID"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/models.py:13 src/membership/models.py:11
|
||||
msgid "modified"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/models.py:14 src/membership/models.py:12
|
||||
msgid "created"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/models.py:43
|
||||
msgid "amount"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/models.py:46
|
||||
msgid "This will include VAT"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/models.py:48 src/accounting/models.py:61
|
||||
#: src/accounting/models.py:100
|
||||
msgid "description"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/models.py:64
|
||||
msgid "price (excl. VAT)"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/models.py:66
|
||||
msgid "VAT"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/models.py:68
|
||||
msgid "is paid"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/models.py:88
|
||||
msgctxt "accounting term"
|
||||
msgid "Order"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/models.py:89
|
||||
msgctxt "accounting term"
|
||||
msgid "Orders"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/models.py:121
|
||||
msgid "payment"
|
||||
msgstr ""
|
||||
|
||||
#: src/accounting/models.py:122
|
||||
msgid "payments"
|
||||
msgstr ""
|
||||
|
||||
#: src/membership/models.py:45
|
||||
msgid "membership"
|
||||
msgstr "medlemskab"
|
||||
|
||||
#: src/membership/models.py:46
|
||||
msgid "memberships"
|
||||
msgstr "medlemskaber"
|
||||
|
||||
#: src/membership/models.py:53
|
||||
msgid "subscription type"
|
||||
msgstr ""
|
||||
|
||||
#: src/membership/models.py:57
|
||||
msgid "The duration this subscription is for. "
|
||||
msgstr ""
|
||||
|
||||
#: src/membership/models.py:70
|
||||
msgid "membership type"
|
||||
msgstr ""
|
||||
|
||||
#: src/membership/models.py:71
|
||||
msgid "membership types"
|
||||
msgstr ""
|
||||
|
||||
#: src/membership/models.py:73
|
||||
msgid "name"
|
||||
msgstr "navn"
|
||||
|
||||
#: src/membership/templates/membership_overview.html:7
|
||||
msgid "You do not have an active membership!"
|
||||
msgstr "Du har ikke et aktivt medlemskab!"
|
||||
|
||||
#: src/membership/templates/membership_overview.html:9
|
||||
msgid ""
|
||||
"You can become a member by depositing the membership fee to our bank account."
|
||||
msgstr ""
|
||||
|
||||
#: src/membership/templates/membership_overview.html:17
|
||||
msgid "You are a member!"
|
||||
msgstr "Du er medlem!"
|
||||
|
||||
#: src/membership/templates/membership_overview.html:19
|
||||
msgid "Period"
|
||||
msgstr "Periode"
|
||||
|
||||
#: src/membership/templates/membership_overview.html:20
|
||||
msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
#: src/project/settings.py:131
|
||||
msgid "Danish"
|
||||
msgstr "Dansk"
|
||||
|
||||
#: src/project/settings.py:132
|
||||
msgid "English"
|
||||
msgstr "Engelsk"
|
||||
|
||||
#: src/project/templates/account/account_inactive.html:5
|
||||
#: src/project/templates/account/account_inactive.html:8
|
||||
msgid "Account Inactive"
|
||||
msgstr "Inaktiv konto"
|
||||
|
||||
#: src/project/templates/account/account_inactive.html:10
|
||||
msgid "This account is inactive."
|
||||
msgstr "Denne konto er inaktiv."
|
||||
|
||||
#: src/project/templates/account/email.html:5
|
||||
#: src/project/templates/account/email.html:16
|
||||
msgid "E-mail Addresses"
|
||||
msgstr "E-mail adresser"
|
||||
|
||||
#: src/project/templates/account/email.html:21
|
||||
msgid "The following e-mail addresses are associated with your account:"
|
||||
msgstr "De følgende e-mail adresser er tilknyttet din konto:"
|
||||
|
||||
#: src/project/templates/account/email.html:31
|
||||
msgid "Address"
|
||||
msgstr "Adresse"
|
||||
|
||||
#: src/project/templates/account/email.html:32
|
||||
msgid "Status"
|
||||
msgstr "Status"
|
||||
|
||||
#: src/project/templates/account/email.html:33
|
||||
msgid "Primary"
|
||||
msgstr "Primær"
|
||||
|
||||
#: src/project/templates/account/email.html:90
|
||||
msgid "Warning:"
|
||||
msgstr "Advarsel:"
|
||||
|
||||
#: src/project/templates/account/email.html:91
|
||||
msgid ""
|
||||
"You currently do not have any e-mail address set up. You should really add "
|
||||
"an e-mail address so you can receive notifications, reset your password, etc."
|
||||
msgstr ""
|
||||
"Du har lige nu ingen e-mail adresse tilknyttet. Du burde virkelig tilføje en "
|
||||
"e-mail adresse så du kan modtage notifikationer, nulstille dit kodeord, osv."
|
||||
|
||||
#: src/project/templates/account/email.html:99
|
||||
#: src/project/templates/account/email.html:107
|
||||
msgid "Add E-mail"
|
||||
msgstr "Tilføj e-mail"
|
||||
|
||||
#: src/project/templates/account/email.html:118
|
||||
msgid "Do you really want to remove the selected e-mail address?"
|
||||
msgstr "Vil du virkelig fjerne den valgte e-mail?"
|
||||
|
||||
#: src/project/templates/account/email/base_message.txt:1
|
||||
#, python-format
|
||||
msgid "Hello from %(site_name)s!"
|
||||
msgstr "Hej fra %(site_name)s!"
|
||||
|
||||
#: src/project/templates/account/email/base_message.txt:5
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Thank you for using %(site_name)s!\n"
|
||||
"%(site_domain)s"
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/email/email_confirmation_message.txt:5
|
||||
#, python-format
|
||||
msgid ""
|
||||
"You're receiving this e-mail because user %(user_display)s has given your e-"
|
||||
"mail address to register an account on %(site_domain)s.\n"
|
||||
"\n"
|
||||
"To confirm this is correct, go to %(activate_url)s"
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/email/email_confirmation_subject.txt:3
|
||||
msgid "Please Confirm Your E-mail Address"
|
||||
msgstr "Venligst bekræft din e-mail adresse"
|
||||
|
||||
#: src/project/templates/account/email/password_reset_key_message.txt:4
|
||||
msgid ""
|
||||
"You're receiving this e-mail because you or someone else has requested a "
|
||||
"password for your user account.\n"
|
||||
"It can be safely ignored if you did not request a password reset. Click the "
|
||||
"link below to reset your password."
|
||||
msgstr ""
|
||||
"Du modtager denne e-mail fordi du, eller nogen anden, har anmodet om "
|
||||
"nulstilling af dit kodeord.\n"
|
||||
"Du kan trygt ignorere dette hvis det ikke var dig der anmodede om "
|
||||
"nulstillingen. Klik på linket herunder for at nulstille dit kodeord."
|
||||
|
||||
#: src/project/templates/account/email/password_reset_key_message.txt:9
|
||||
#, python-format
|
||||
msgid "In case you forgot, your username is %(username)s."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/email/password_reset_key_subject.txt:3
|
||||
msgid "Password Reset E-mail"
|
||||
msgstr "Nulstilling af password"
|
||||
|
||||
#: src/project/templates/account/email_confirm.html:6
|
||||
#: src/project/templates/account/email_confirm.html:10
|
||||
msgid "Confirm E-mail Address"
|
||||
msgstr "Bekræft e-mail adresse"
|
||||
|
||||
#: src/project/templates/account/email_confirm.html:16
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Please confirm that <a href=\"mailto:%(email)s\">%(email)s</a> is an e-mail "
|
||||
"address for user %(user_display)s."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/email_confirm.html:20
|
||||
msgid "Confirm"
|
||||
msgstr "Bekræft"
|
||||
|
||||
#: src/project/templates/account/email_confirm.html:27
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This e-mail confirmation link expired or is invalid. Please <a href="
|
||||
"\"%(email_url)s\">issue a new e-mail confirmation request</a>."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/login.html:20
|
||||
#: src/project/templates/account/login.html:26
|
||||
#: src/project/templates/account/signup.html:26
|
||||
#: src/project/templates/account/signup.html:32
|
||||
msgid "E-mail"
|
||||
msgstr "E-mail"
|
||||
|
||||
#: src/project/templates/account/login.html:31
|
||||
#: src/project/templates/account/login.html:37
|
||||
#: src/project/templates/account/signup.html:44
|
||||
#: src/project/templates/account/signup.html:50
|
||||
msgid "Password"
|
||||
msgstr "Kodeord"
|
||||
|
||||
#: src/project/templates/account/login.html:41
|
||||
msgid "Sign in"
|
||||
msgstr "Log ind"
|
||||
|
||||
#: src/project/templates/account/login.html:45
|
||||
msgid "Forgot password?"
|
||||
msgstr "Glemt kodeord?"
|
||||
|
||||
#: src/project/templates/account/login.html:48
|
||||
msgid "Or"
|
||||
msgstr "Eller"
|
||||
|
||||
#: src/project/templates/account/login.html:53
|
||||
#: src/project/templates/account/signup.html:6
|
||||
msgid "Become a member"
|
||||
msgstr "Bliv medlem"
|
||||
|
||||
#: src/project/templates/account/logout.html:5
|
||||
#: src/project/templates/account/logout.html:8
|
||||
#: src/project/templates/account/logout.html:17
|
||||
msgid "Sign Out"
|
||||
msgstr "Log ud"
|
||||
|
||||
#: src/project/templates/account/logout.html:10
|
||||
msgid "Are you sure you want to sign out?"
|
||||
msgstr "Er du sikker på at du vil logge ind?"
|
||||
|
||||
#: src/project/templates/account/messages/cannot_delete_primary_email.txt:2
|
||||
#, python-format
|
||||
msgid "You cannot remove your primary e-mail address (%(email)s)."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/messages/email_confirmation_sent.txt:2
|
||||
#, python-format
|
||||
msgid "Confirmation e-mail sent to %(email)s."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/messages/email_confirmed.txt:2
|
||||
#, python-format
|
||||
msgid "You have confirmed %(email)s."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/messages/email_deleted.txt:2
|
||||
#, python-format
|
||||
msgid "Removed e-mail address %(email)s."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/messages/logged_in.txt:4
|
||||
#, python-format
|
||||
msgid "Successfully signed in as %(name)s."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/messages/logged_out.txt:2
|
||||
msgid "You have signed out."
|
||||
msgstr "Du er nu logget ud."
|
||||
|
||||
#: src/project/templates/account/messages/password_changed.txt:2
|
||||
msgid "Password successfully changed."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/messages/password_set.txt:2
|
||||
msgid "Password successfully set."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/messages/primary_email_set.txt:2
|
||||
msgid "Primary e-mail address set."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/messages/unverified_primary_email.txt:2
|
||||
msgid "Your primary e-mail address must be verified."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/password_change.html:5
|
||||
#: src/project/templates/account/password_change.html:8
|
||||
#: src/project/templates/account/password_change.html:13
|
||||
#: src/project/templates/account/password_reset_from_key.html:4
|
||||
#: src/project/templates/account/password_reset_from_key.html:7
|
||||
#: src/project/templates/account/password_reset_from_key_done.html:4
|
||||
#: src/project/templates/account/password_reset_from_key_done.html:7
|
||||
msgid "Change Password"
|
||||
msgstr "Skift kodeord"
|
||||
|
||||
#: src/project/templates/account/password_change.html:14
|
||||
msgid "Forgot Password?"
|
||||
msgstr "Glemt kodeord?"
|
||||
|
||||
#: src/project/templates/account/password_reset.html:6
|
||||
#: src/project/templates/account/password_reset.html:10
|
||||
#: src/project/templates/account/password_reset_done.html:6
|
||||
#: src/project/templates/account/password_reset_done.html:9
|
||||
msgid "Password Reset"
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/password_reset.html:16
|
||||
msgid ""
|
||||
"Forgotten your password? Enter your e-mail address below, and we'll send you "
|
||||
"an e-mail allowing you to reset it."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/password_reset.html:21
|
||||
#: src/project/templates/account/password_reset.html:27
|
||||
msgid "E-mail address"
|
||||
msgstr "E-mail adresser"
|
||||
|
||||
#: src/project/templates/account/password_reset.html:38
|
||||
msgid "Reset My Password"
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/password_reset.html:41
|
||||
msgid "Please contact us if you have any trouble resetting your password."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/password_reset.html:47
|
||||
#: src/project/templates/account/signup.html:72
|
||||
msgid "To login"
|
||||
msgstr "Til login"
|
||||
|
||||
#: src/project/templates/account/password_reset_done.html:15
|
||||
msgid ""
|
||||
"We have sent you an e-mail. Please contact us if you do not receive it "
|
||||
"within a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/password_reset_from_key.html:7
|
||||
msgid "Bad Token"
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/password_reset_from_key.html:11
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The password reset link was invalid, possibly because it has already been "
|
||||
"used. Please request a <a href=\"%(passwd_reset_url)s\">new password reset</"
|
||||
"a>."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/password_reset_from_key.html:17
|
||||
msgid "change password"
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/password_reset_from_key.html:20
|
||||
#: src/project/templates/account/password_reset_from_key_done.html:8
|
||||
msgid "Your password is now changed."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/password_set.html:5
|
||||
#: src/project/templates/account/password_set.html:8
|
||||
#: src/project/templates/account/password_set.html:13
|
||||
msgid "Set Password"
|
||||
msgstr "Sæt kodeord"
|
||||
|
||||
#: src/project/templates/account/signup.html:19
|
||||
msgid "To become a member, you need to have an account. Create one here."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/signup.html:66
|
||||
msgid "Sign up"
|
||||
msgstr "Bliv medlem"
|
||||
|
||||
#: src/project/templates/account/signup_closed.html:5
|
||||
#: src/project/templates/account/signup_closed.html:8
|
||||
msgid "Sign Up Closed"
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/signup_closed.html:10
|
||||
msgid "We are sorry, but the sign up is currently closed."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/snippets/already_logged_in.html:5
|
||||
msgid "Note"
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/snippets/already_logged_in.html:5
|
||||
#, python-format
|
||||
msgid "you are already logged in as %(user_display)s."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/verification_sent.html:5
|
||||
#: src/project/templates/account/verification_sent.html:8
|
||||
#: src/project/templates/account/verified_email_required.html:5
|
||||
#: src/project/templates/account/verified_email_required.html:8
|
||||
msgid "Verify Your E-mail Address"
|
||||
msgstr "Verificér din e-mail adresse"
|
||||
|
||||
#: src/project/templates/account/verification_sent.html:10
|
||||
msgid ""
|
||||
"We have sent an e-mail to you for verification. Follow the link provided to "
|
||||
"finalize the signup process. Please contact us if you do not receive it "
|
||||
"within a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/verified_email_required.html:12
|
||||
msgid ""
|
||||
"This part of the site requires us to verify that\n"
|
||||
"you are who you claim to be. For this purpose, we require that you\n"
|
||||
"verify ownership of your e-mail address. "
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/verified_email_required.html:16
|
||||
msgid ""
|
||||
"We have sent an e-mail to you for\n"
|
||||
"verification. Please click on the link inside this e-mail. Please\n"
|
||||
"contact us if you do not receive it within a few minutes."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/account/verified_email_required.html:20
|
||||
#, python-format
|
||||
msgid ""
|
||||
"<strong>Note:</strong> you can still <a href=\"%(email_url)s\">change your e-"
|
||||
"mail address</a>."
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/base.html:140
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
|
||||
#: src/project/templates/base.html:146
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
#: src/project/templates/base.html:152
|
||||
msgid "Details"
|
||||
msgstr "Detaljer"
|
||||
|
||||
#: src/project/templates/base.html:158
|
||||
msgid "Emails"
|
||||
msgstr "Emails"
|
||||
|
||||
#: src/project/templates/base.html:164
|
||||
msgid "Membership"
|
||||
msgstr "Medlemskab"
|
||||
|
||||
#: src/project/templates/base.html:171 src/project/templates/base.html:184
|
||||
msgid "Overview"
|
||||
msgstr "Oversigt"
|
||||
|
||||
#: src/project/templates/base.html:177
|
||||
msgid "Services"
|
||||
msgstr "Tjenester"
|
||||
|
||||
#: src/project/templates/base.html:191
|
||||
msgid "Admin"
|
||||
msgstr "Admin"
|
||||
|
||||
#: src/project/templates/base.html:197
|
||||
msgid "Members"
|
||||
msgstr "Medlemmer"
|
||||
|
||||
#~ msgid "OR"
|
||||
#~ msgstr "Eller"
|
|
@ -1,190 +0,0 @@
|
|||
"""Settings for the project."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from environs import Env
|
||||
|
||||
env = Env()
|
||||
env.read_env()
|
||||
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
PROJECT_DIR = BASE_DIR / "project"
|
||||
|
||||
SECRET_KEY = env.str("SECRET_KEY", default="something-very-secret")
|
||||
|
||||
DEBUG = env.bool("DEBUG", default=False)
|
||||
|
||||
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["*"])
|
||||
CSRF_TRUSTED_ORIGINS = env.list(
|
||||
"CSRF_TRUSTED_ORIGINS",
|
||||
default=["http://localhost:8000"],
|
||||
)
|
||||
|
||||
ADMINS = [tuple(x.split(":")) for x in env.list("DJANGO_ADMINS", default=[])]
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
# Application definition
|
||||
|
||||
DJANGO_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.sites",
|
||||
]
|
||||
|
||||
THIRD_PARTY_APPS = [
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"django_view_decorator",
|
||||
]
|
||||
|
||||
LOCAL_APPS = [
|
||||
"utils",
|
||||
"accounting",
|
||||
"membership",
|
||||
]
|
||||
|
||||
INSTALLED_APPS = [
|
||||
*DJANGO_APPS,
|
||||
*THIRD_PARTY_APPS,
|
||||
*LOCAL_APPS,
|
||||
]
|
||||
|
||||
DATABASES = {"default": env.dj_db_url("DATABASE_URL")}
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"allauth.account.middleware.AccountMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "project.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [PROJECT_DIR / "templates"],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
"builtins": [
|
||||
"django.templatetags.i18n",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"allauth.account.auth_backends.AuthenticationBackend",
|
||||
)
|
||||
|
||||
WSGI_APPLICATION = "project.wsgi.application"
|
||||
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = []
|
||||
|
||||
LANGUAGE_CODE = "da-dk"
|
||||
|
||||
TIME_ZONE = "Europe/Copenhagen"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = "/static/"
|
||||
STATICFILES_DIRS = [PROJECT_DIR / "static"]
|
||||
STATIC_ROOT = BASE_DIR / "static"
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
|
||||
EMAIL_BACKEND = env.str(
|
||||
"EMAIL_BACKEND",
|
||||
default="django.core.mail.backends.console.EmailBackend",
|
||||
)
|
||||
DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL", default="")
|
||||
# Parse email URLs, e.g. "smtp://"
|
||||
email = env.dj_email_url("EMAIL_URL", default="smtp://")
|
||||
EMAIL_HOST = email["EMAIL_HOST"]
|
||||
EMAIL_PORT = email["EMAIL_PORT"]
|
||||
EMAIL_HOST_PASSWORD = email["EMAIL_HOST_PASSWORD"]
|
||||
EMAIL_HOST_USER = email["EMAIL_HOST_USER"]
|
||||
EMAIL_USE_TLS = email["EMAIL_USE_TLS"]
|
||||
|
||||
# Always show DDT in development for any IP, not just 127.0.0.1 or
|
||||
# settings.INTERNAL_IPS. This is useful in a docker setup where the
|
||||
# requesting IP isn't static.
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
"SHOW_TOOLBAR_CALLBACK": lambda _x: DEBUG,
|
||||
}
|
||||
|
||||
CURRENCIES = ("DKK",)
|
||||
CURRENCY_CHOICES = [("DKK", "DKK")]
|
||||
|
||||
LANGUAGES = [
|
||||
("da", _("Danish")),
|
||||
("en", _("English")),
|
||||
]
|
||||
|
||||
# We store all translations in one location
|
||||
LOCALE_PATHS = [PROJECT_DIR / "locale"]
|
||||
|
||||
# Allauth configuration
|
||||
ACCOUNT_AUTHENTICATION_METHOD = "email"
|
||||
ACCOUNT_EMAIL_REQUIRED = True
|
||||
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE = False
|
||||
ACCOUNT_USERNAME_REQUIRED = False
|
||||
|
||||
# Logging
|
||||
# We want to log everything to stdout in docker
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"": {
|
||||
"handlers": ["console"],
|
||||
"level": "DEBUG",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if DEBUG:
|
||||
INSTALLED_APPS += ["debug_toolbar", "django_browser_reload"]
|
||||
MIDDLEWARE += [
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
"django_browser_reload.middleware.BrowserReloadMiddleware",
|
||||
]
|
||||
# Always show DDT in development for any IP, not just 127.0.0.1 or
|
||||
# settings.INTERNAL_IPS. This is useful in a docker setup where the
|
||||
# requesting IP isn't static.
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
"SHOW_TOOLBAR_CALLBACK": lambda _x: DEBUG,
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
html.dark body {
|
||||
--splash: #5b47e0;
|
||||
background: var(--dark-dark);
|
||||
color: var(--medium-dust)
|
||||
}
|
||||
|
||||
html.dark h1,
|
||||
html.dark h2,
|
||||
html.dark h3,
|
||||
html.dark h4,
|
||||
html.dark h5,
|
||||
html.dark h6,
|
||||
html.dark footer,
|
||||
html.dark nav ol li a {
|
||||
color: var(--medium-dust);
|
||||
}
|
||||
|
||||
html.dark nav ol li a:not(.current):hover {
|
||||
border-color: var(--medium-dust);
|
||||
}
|
||||
|
||||
html.dark header,
|
||||
html.dark main aside,
|
||||
html.dark nav {
|
||||
background: #1d1d1d;
|
||||
}
|
||||
|
||||
html.dark nav {
|
||||
border: none;
|
||||
}
|
||||
|
||||
html.dark hr {
|
||||
border-color: var(--twilight);
|
||||
}
|
||||
|
||||
html.dark main aside div,
|
||||
html.dark article div.content-view {
|
||||
background: var(--dark-twilight);
|
||||
}
|
||||
|
||||
html.dark article table tbody {
|
||||
background: var(--dark-twilight);
|
||||
}
|
||||
|
||||
html.dark article table tbody tr:nth-child(2n+1) {
|
||||
background: var(--dark);
|
||||
}
|
||||
|
||||
html.dark article table tbody tr:nth-child(2n+1) td {
|
||||
border-top: 1px solid var(--dark-dark);
|
||||
border-bottom: 1px solid var(--dark-dark);
|
||||
}
|
||||
|
||||
html.dark article table tbody tr:last-child td {
|
||||
border-bottom: var(--half-space) solid var(--twilight);
|
||||
}
|
||||
|
||||
html.dark form>div>input[type="text"],
|
||||
html.dark form>div>input[type="password"],
|
||||
html.dark input[type="email"] {
|
||||
border: 2px solid var(--twilight);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
background: var(--dark-dark);
|
||||
width: 100%;
|
||||
color: var(--light-dust);
|
||||
}
|
Binary file not shown.
Binary file not shown.
|
@ -1,559 +0,0 @@
|
|||
/* Reset */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
img,
|
||||
picture,
|
||||
video,
|
||||
canvas,
|
||||
svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
#root,
|
||||
#__next {
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/* Variables */
|
||||
:root {
|
||||
/* Colors */
|
||||
--light: #ffffff;
|
||||
--light-dust: #fefef9;
|
||||
--dust: #f4f1ef;
|
||||
--medium-dust: #dadada;
|
||||
--dark-dust: #bfbfbf;
|
||||
--fade: #878787;
|
||||
--twilight: #4a4a4a;
|
||||
--dark-twilight: #2f2f2f;
|
||||
--dark: #2a2a2a;
|
||||
--dark-dark: #121212;
|
||||
--light-custard: #eee7d5;
|
||||
--custard: #f0dcac;
|
||||
--dark-custard: #d4c7a9;
|
||||
--water: #a8f3f4;
|
||||
--splash: #4b3aba;
|
||||
|
||||
/* Sizes */
|
||||
--space: 12px;
|
||||
--double-space: calc(var(--space) * 2);
|
||||
--half-space: calc(var(--space) / 2);
|
||||
--quarter-space: calc(var(--space) / 4);
|
||||
--outer-space: var(--double-space);
|
||||
}
|
||||
|
||||
@media (min-width: 1380px) {
|
||||
:root {
|
||||
--outer-space: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
font-size: 1.05em;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
color: var(--twilight);
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: var(--splash);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: var(--double-space) 0;
|
||||
height: 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid var(--dark-custard);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--custard);
|
||||
font-family: Inter;
|
||||
font-weight: 400;
|
||||
line-height: 1.6;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
padding: var(--double-space) var(--outer-space);
|
||||
background: var(--light);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
header>h1 {
|
||||
font-size: 1.44em;
|
||||
}
|
||||
|
||||
#switch-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 0 var(--space);
|
||||
top: -2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#switch-icon #layer1 path {
|
||||
fill: var(--twilight);
|
||||
}
|
||||
|
||||
header>div>a#logout {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--twilight);
|
||||
text-decoration: none;
|
||||
color: var(--dust);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
header>div>a#logout:hover {
|
||||
background: var(--splash);
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
aside {
|
||||
padding: 0 var(--outer-space) var(--double-space) var(--outer-space);
|
||||
background: var(--light);
|
||||
}
|
||||
|
||||
aside>div {
|
||||
background: var(--dust);
|
||||
padding: var(--double-space);
|
||||
border-radius: var(--quarter-space);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
aside>div>h2 {
|
||||
font-size: 1.22em;
|
||||
margin: 0 0 6px 0;
|
||||
}
|
||||
|
||||
aside>div>figure {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 1px solid var(--dark-dust);
|
||||
float: left;
|
||||
margin-right: var(--double-space);
|
||||
}
|
||||
|
||||
aside>div>dl {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
aside>div>dl>dt {
|
||||
float: left;
|
||||
clear: left;
|
||||
margin: 0 var(--double-space) 0 0;
|
||||
width: 180px;
|
||||
font-weight: 600;
|
||||
color: var(--twilight);
|
||||
}
|
||||
|
||||
nav {
|
||||
display: block;
|
||||
border-bottom: 1px solid var(--dark-custard);
|
||||
background: var(--light);
|
||||
}
|
||||
|
||||
nav ol {
|
||||
margin: 0 calc(var(--outer-space));
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
nav ol li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
float: left;
|
||||
}
|
||||
|
||||
nav>ol>li>a {
|
||||
display: block;
|
||||
padding: var(--half-space) var(--half-space) var(--quarter-space);
|
||||
margin: 0 var(--space);
|
||||
border-bottom: var(--half-space) solid transparent;
|
||||
text-decoration: none;
|
||||
color: var(--dark);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
nav>ol>li:first-child>a {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
nav ol li a:hover {
|
||||
border-color: rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
nav ol li a.current {
|
||||
font-weight: bold;
|
||||
border-color: var(--splash);
|
||||
color: var(--splash);
|
||||
}
|
||||
|
||||
article {
|
||||
padding: var(--double-space) var(--outer-space);
|
||||
}
|
||||
|
||||
article div.content-view {
|
||||
background: var(--dust);
|
||||
padding: var(--double-space);
|
||||
margin-bottom: var(--space);
|
||||
}
|
||||
|
||||
div.content-view>h2 {
|
||||
margin: 0 0 var(--space) 0;
|
||||
}
|
||||
|
||||
div.services {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--double-space);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
div.services>div,
|
||||
div.infobox {
|
||||
background: var(--light);
|
||||
padding: var(--double-space);
|
||||
border-radius: 6px;
|
||||
flex: 240px;
|
||||
max-width: 420px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
div.infobox button {
|
||||
margin-top: var(--double-space);
|
||||
}
|
||||
|
||||
div.services>div>div.description {
|
||||
margin-bottom: var(--double-space);
|
||||
}
|
||||
|
||||
div.services>div>div.description>p {
|
||||
margin-top: var(--half-space);
|
||||
}
|
||||
|
||||
div.services>div>a,
|
||||
a.button,
|
||||
button {
|
||||
display: block;
|
||||
color: var(--light);
|
||||
background: var(--splash);
|
||||
padding: var(--space) var(--double-space);
|
||||
border-radius: var(--quarter-space);
|
||||
opacity: 0.85;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
border: 0;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
button.small {
|
||||
font-size: 0.78em;
|
||||
padding: var(--half-space) var(--space);
|
||||
}
|
||||
|
||||
div.services>div>a:hover,
|
||||
a.button:hover,
|
||||
button:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
background: var(--twilight);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: var(--twilight);
|
||||
}
|
||||
|
||||
article table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
margin: var(--space) 0;
|
||||
}
|
||||
|
||||
article table thead th {
|
||||
background: var(--twilight);
|
||||
color: var(--medium-dust);
|
||||
}
|
||||
|
||||
article table thead th a {
|
||||
color: var(--light);
|
||||
}
|
||||
|
||||
article table thead th:first-child {
|
||||
border-radius: var(--half-space) 0 0 0;
|
||||
}
|
||||
|
||||
article table thead th:last-child {
|
||||
border-radius: 0 var(--half-space) 0 0;
|
||||
}
|
||||
|
||||
article table tbody {
|
||||
background: var(--light-dust);
|
||||
}
|
||||
|
||||
article table tbody tr:nth-child(odd) {
|
||||
background: var(--light-custard);
|
||||
}
|
||||
|
||||
article table tbody tr:nth-child(odd) td {
|
||||
border-top: 1px solid var(--dark-custard);
|
||||
border-bottom: 1px solid var(--dark-custard);
|
||||
}
|
||||
|
||||
article table tbody tr:last-child td {
|
||||
border-bottom: var(--half-space) solid var(--twilight);
|
||||
}
|
||||
|
||||
article table thead th,
|
||||
article table tbody td {
|
||||
padding: var(--space);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
article table#user_email_table tbody tr td:first-child {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
form>div {
|
||||
margin: 0 0 var(--double-space);
|
||||
}
|
||||
|
||||
form>div>label {
|
||||
display: block;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
form>div>input[type="text"],
|
||||
form>div>input[type="password"],
|
||||
input[type="email"] {
|
||||
border: 2px solid var(--twilight);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
background: var(--light-dust);
|
||||
width: 100%;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
form fieldset {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
form div.buttonHolder button {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#email-add-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#email-add-overlay .content-view {
|
||||
width: 600px;
|
||||
padding: var(--double-space);
|
||||
}
|
||||
|
||||
#email-add-overlay .content-view p {
|
||||
margin: var(--double-space) 0;
|
||||
}
|
||||
|
||||
#login {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#loginbox {
|
||||
border-radius: var(--space);
|
||||
border: 6px solid white;
|
||||
width: 800px;
|
||||
height: 500px;
|
||||
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
|
||||
#loginbox>div {
|
||||
padding: var(--double-space);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#loginbox label {
|
||||
color: var(--twilight);
|
||||
}
|
||||
|
||||
#loginbox>div.login {
|
||||
background: var(--light-dust);
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#loginbox>div.signup {
|
||||
background: var(--water);
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#loginbox>div:first-child {
|
||||
border-radius: var(--half-space) 0 0 var(--half-space);
|
||||
}
|
||||
|
||||
#loginbox>div:last-child {
|
||||
border-radius: 0 var(--half-space) var(--half-space) 0;
|
||||
}
|
||||
|
||||
#loginbox>div:last-child>* {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#loginbox div.new_here {
|
||||
margin-top: var(--double-space);
|
||||
}
|
||||
|
||||
#loginbox div.new_here h2 {
|
||||
margin: var(--double-space) 0;
|
||||
}
|
||||
|
||||
#loginbox button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#loginbox img {
|
||||
padding: 0 var(--double-space);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin: var(--space) var(--outer-space);
|
||||
padding: var(--space);
|
||||
border-radius: var(--quarter-space);
|
||||
background: var(--dark);
|
||||
color: var(--dust);
|
||||
font-size: 0.78em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
span.time_remaining {
|
||||
color: var(--fade);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
list-style: none;
|
||||
padding: var(--half-space) 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pagination>li {
|
||||
margin: 0 var(--half-space);
|
||||
}
|
||||
|
||||
.pagination>li:first-child {
|
||||
margin-right: var(--double-space);
|
||||
}
|
||||
|
||||
.pagination>li:last-child {
|
||||
margin-left: var(--double-space);
|
||||
}
|
||||
|
||||
.pagination .page-item {
|
||||
border: 1px solid var(--fade);
|
||||
padding: var(--quarter-space) var(--half-space);
|
||||
border-radius: var(--half-space);
|
||||
background: var(--light-dust);
|
||||
font-size: 0.78em;
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
padding: var(--half-space);
|
||||
color: var(--twilight);
|
||||
}
|
||||
|
||||
.pagination .page-item.active {
|
||||
background: var(--twilight);
|
||||
}
|
||||
|
||||
.pagination .page-item.active .page-link {
|
||||
color: var(--light-dust);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pagination .page-item.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.pagination .page-item.disabled .page-link {
|
||||
cursor: default;
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue