Merge pull request 'Refactoring things and doing stuff in a MVP way.' (#15) from vidir_refactor into master

Reviewed-on: data.coop/membersystem#15
This commit is contained in:
valberg 2021-03-12 16:20:50 +00:00
commit 50e2ba642f
106 changed files with 2339 additions and 1339 deletions

33
.drone.yml Normal file
View file

@ -0,0 +1,33 @@
---
kind: pipeline
type: docker
name: default
steps:
- name: docker
image: plugins/docker
environment:
DJANGO_ENV: production
settings:
repo: docker.data.coop/member.data.coop
registry: docker.data.coop
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_PASSWORD
tags:
- "${DRONE_BUILD_NUMBER}"
- "vidir_refactor"
when:
branch:
- vidir_refactor
# - name: notify
# image: plugins/matrix
# settings:
# homeserver: https://data.coop
# roomid: plKSghHbepWeUEtbHE:data.coop
# username:
# from_secret: matrix_username
# password:
# from_secret: matrix_password

3
.gitignore vendored
View file

@ -2,5 +2,6 @@ __pycache__/
*.pyc
*.sw*
db.sqlite3
project/settings/local.py
.pytest_cache
.idea/
*.mo

View file

@ -21,6 +21,7 @@ RUN apt-get update \
libgdk-pixbuf2.0-0 \
libffi-dev \
shared-mime-info \
gettext \
&& pip install "poetry==$POETRY_VERSION"
WORKDIR /app
@ -34,10 +35,13 @@ RUN poetry export -f requirements.txt $(test "$DJANGO_ENV" != production && echo
RUN groupadd -g 1000 www
RUN useradd -u 1000 -ms /bin/bash -g www www
COPY --chown=www:www ./ /app/
RUN mkdir /app/static && chown www:www /app/static
RUN mkdir /app/src/static && chown www:www /app/src/static
ARG BUILD
ENV BUILD ${BUILD}
ENTRYPOINT ["./entrypoint.sh"]
CMD ["uvicorn", "config.asgi:application", "--host", "0.0.0.0", "--port", "8080", "--workers", "3", "--lifespan", "off"]
EXPOSE 8000
CMD ["uvicorn", "project.asgi:application", "--host", "0.0.0.0", "--port", "8000", "--workers", "3", "--lifespan", "off", "--app-dir", "/app/src"]

View file

@ -1,13 +1,39 @@
# These are just some make targets, expressing how things
# are supposed to be run, but feel free to change them!
dev-setup:
poetry run pre-commit install
poetry run python manage.py migrate
poetry run python manage.py createsuperuser
DOCKER_COMPOSE = COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose
DOCKER_RUN = ${DOCKER_COMPOSE} run -e UID=`id -u` -e GID=`id -g`
MANAGE = ${DOCKER_RUN} backend python /app/src/manage.py
lint:
poetry run pre-commit run --all
run:
${DOCKER_COMPOSE} up --build
build:
${DOCKER_COMPOSE} build
makemessages:
${MANAGE} makemessages -a
makemigrations:
${MANAGE} makemigrations ${EXTRA_ARGS}
migrate:
${MANAGE} migrate ${EXTRA_ARGS}
createsuperuser:
${MANAGE} createsuperuser
shell:
${MANAGE} shell
manage_command:
${MANAGE} ${COMMAND}
test:
poetry run pytest
${DOCKER_RUN} backend pytest src/
add_dependency:
${DOCKER_RUN} backend poetry add --lock ${DEPENDENCY}
add_dev_dependency:
${DOCKER_RUN} backend poetry add -D --lock ${DEPENDENCY}

View file

@ -1,24 +1,35 @@
# member.data.coop
To start developing:
## Development requirements
Get poetry
- Docker
- Docker compose
- pre-commit (preferred for contributions)
$ python3 -m pip install --user pipx
$ pipx install poetry
## Start local server
Run poetry to setup environment
Given that the requirements above are installed, it should be as easy as:
$ poetry install
$ make migrate
Run this make target, which installs all the requirements and sets up a development database.
This will setup the database. Next run:
$ make dev-setup
$ make run
To run the Django development server:
This will build the docker image and start the member system on http://localhost:8000.
$ poetry run python manage.py runserver
You can create a superuser by running:
Before you push your stuff, run tests:
$ make createsuperuser
Make migrations:
$ make makemigrations
Make messages:
$ make makemessages
Running tests:
$ make test

37
docker-compose.yml Normal file
View file

@ -0,0 +1,37 @@
version: '3.7'
services:
backend:
build:
context: .
dockerfile: Dockerfile
user: $UID:$GID
command: python /app/src/manage.py runserver 0.0.0.0:8000
tty: true
ports:
- "8000:8000"
volumes:
- ./:/app/
links:
- redis
- postgres
env_file:
- env
postgres:
image: postgres:13-alpine
volumes:
- postgres_data:/var/lib/postgresql/data/
ports:
- 5432:5432
env_file:
- env
redis:
image: redis:latest
ports:
- "6379:6379"
volumes:
postgres_data:

View file

@ -11,11 +11,11 @@ done
echo "PostgreSQL started"
# Only migrate and collectstatic if we are NOT in development
# Only migrate, collectstatic and compilemessages if we are NOT in development
if [ -z "$DEBUG" ]; then
python manage.py migrate
python manage.py collectstatic --no-input;
python src/manage.py migrate;
python src/manage.py collectstatic --no-input;
python src/manage.py compilemessages;
fi
exec "$@"

7
env Normal file
View file

@ -0,0 +1,7 @@
SECRET_KEY=something-very-random
POSTGRES_HOST=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_PORT=5432
DATABASE_URL=postgres://postgres:postgres@postgres:5432/postgres
DEBUG=True
DJANGO_ENV=all

View file

@ -1,8 +0,0 @@
from django.contrib import admin
from . import models
@admin.register(models.Membership)
class MembershipAdmin(admin.ModelAdmin):
pass

View file

@ -1,227 +0,0 @@
# 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
),
),
]

View file

@ -1,123 +0,0 @@
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")

378
poetry.lock generated
View file

@ -47,6 +47,17 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "cffi"
version = "1.14.5"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
pycparser = "*"
[[package]]
name = "cfgv"
version = "3.2.0"
@ -63,6 +74,14 @@ category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "click"
version = "7.1.2"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "colorama"
version = "0.4.4"
@ -71,6 +90,25 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]]
name = "cryptography"
version = "3.4.6"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
cffi = ">=1.12"
[package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
sdist = ["setuptools-rust (>=0.11.4)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["pytest (>=6.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
[[package]]
name = "defusedxml"
version = "0.6.0"
@ -87,9 +125,25 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "dj-database-url"
version = "0.5.0"
description = "Use Database URLs in your Django Application."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "dj-email-url"
version = "1.0.2"
description = "Use an URL to configure email backend settings in your Django Application."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "django"
version = "3.1.5"
version = "3.1.7"
description = "A high-level Python Web framework that encourages rapid development and clean, pragmatic design."
category = "main"
optional = false
@ -106,21 +160,42 @@ bcrypt = ["bcrypt"]
[[package]]
name = "django-allauth"
version = "0.40.0"
version = "0.44.0"
description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
Django = ">=1.11"
Django = ">=2.0"
pyjwt = {version = ">=1.7", extras = ["crypto"]}
python3-openid = ">=3.0.8"
requests = "*"
requests-oauthlib = ">=0.3.0"
[[package]]
name = "django-cache-url"
version = "3.2.3"
description = "Use Cache URLs in your Django application."
category = "main"
optional = false
python-versions = "*"
[[package]]
name = "django-debug-toolbar"
version = "3.2"
description = "A configurable set of panels that display various debug information about the current request/response."
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
Django = ">=2.2"
sqlparse = ">=0.2.0"
[[package]]
name = "django-money"
version = "1.3"
version = "1.3.1"
description = "Adds support for using money and currency fields in django models and forms. Uses py-moneyed as the money implementation."
category = "main"
optional = false
@ -132,7 +207,28 @@ py-moneyed = ">=0.8,<1.0"
[package.extras]
exchange = ["certifi"]
test = ["pytest (>=3.1.0)", "pytest-django", "pytest-pythonpath", "pytest-cov", "django-reversion", "mixer"]
test = ["pytest (>=3.1.0)", "pytest-django", "pytest-pythonpath", "pytest-cov", "mixer"]
[[package]]
name = "environs"
version = "9.3.1"
description = "simplified environment variable parsing"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
dj-database-url = {version = "*", optional = true, markers = "extra == \"django\""}
dj-email-url = {version = "*", optional = true, markers = "extra == \"django\""}
django-cache-url = {version = "*", optional = true, markers = "extra == \"django\""}
marshmallow = ">=2.7.0"
python-dotenv = "*"
[package.extras]
dev = ["pytest", "dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==3.8.4)", "flake8-bugbear (==20.11.1)", "mypy (==0.800)", "pre-commit (>=2.4,<3.0)", "tox"]
django = ["dj-database-url", "dj-email-url", "django-cache-url"]
lint = ["flake8 (==3.8.4)", "flake8-bugbear (==20.11.1)", "mypy (==0.800)", "pre-commit (>=2.4,<3.0)"]
tests = ["pytest", "dj-database-url", "dj-email-url", "django-cache-url"]
[[package]]
name = "filelock"
@ -142,13 +238,21 @@ category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "h11"
version = "0.12.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "identify"
version = "1.5.13"
version = "2.0.0"
description = "File identification library for Python"
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
python-versions = ">=3.6.1"
[package.extras]
license = ["editdistance"]
@ -162,24 +266,22 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "importlib-metadata"
version = "3.4.0"
description = "Read metadata from Python packages"
category = "dev"
name = "marshmallow"
version = "3.10.0"
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
python-versions = ">=3.5"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
dev = ["pytest", "pytz", "simplejson", "mypy (==0.790)", "flake8 (==3.8.4)", "flake8-bugbear (==20.11.1)", "pre-commit (>=2.4,<3.0)", "tox"]
docs = ["sphinx (==3.3.1)", "sphinx-issues (==1.2.0)", "alabaster (==0.7.12)", "sphinx-version-warning (==1.1.2)", "autodocsumm (==0.2.2)"]
lint = ["mypy (==0.790)", "flake8 (==3.8.4)", "flake8-bugbear (==20.11.1)", "pre-commit (>=2.4,<3.0)"]
tests = ["pytest", "pytz", "simplejson"]
[[package]]
name = "more-itertools"
version = "8.6.0"
version = "8.7.0"
description = "More routines for operating on iterables, beyond itertools"
category = "dev"
optional = false
@ -208,7 +310,7 @@ signedtoken = ["cryptography", "pyjwt (>=1.0.0)"]
[[package]]
name = "packaging"
version = "20.8"
version = "20.9"
description = "Core utilities for Python packages"
category = "dev"
optional = false
@ -225,15 +327,12 @@ category = "dev"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
[package.extras]
dev = ["pre-commit", "tox"]
[[package]]
name = "pre-commit"
version = "2.9.3"
version = "2.10.1"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
category = "dev"
optional = false
@ -242,7 +341,6 @@ python-versions = ">=3.6.1"
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
toml = "*"
@ -275,6 +373,31 @@ python-versions = "*"
[package.extras]
tests = ["pytest (>=2.3.0)", "tox (>=1.6.0)"]
[[package]]
name = "pycparser"
version = "2.20"
description = "C parser in Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "pyjwt"
version = "2.0.1"
description = "JSON Web Token implementation in Python"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
cryptography = {version = ">=3.3.1,<4.0.0", optional = true, markers = "extra == \"crypto\""}
[package.extras]
crypto = ["cryptography (>=3.3.1,<4.0.0)"]
dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1,<4.0.0)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"]
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"]
[[package]]
name = "pyparsing"
version = "2.4.7"
@ -295,7 +418,6 @@ python-versions = ">=3.5"
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=17.4.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""}
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
more-itertools = ">=4.0.0"
packaging = "*"
pluggy = ">=0.12,<1.0"
@ -321,6 +443,17 @@ pytest = ">=3.6"
docs = ["sphinx", "sphinx-rtd-theme"]
testing = ["django", "django-configurations (>=2.0)", "six"]
[[package]]
name = "python-dotenv"
version = "0.15.0"
description = "Add .env support to your django/flask apps in development and deployments"
category = "main"
optional = false
python-versions = "*"
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python3-openid"
version = "3.2.0"
@ -338,7 +471,7 @@ postgresql = ["psycopg2"]
[[package]]
name = "pytz"
version = "2020.5"
version = "2021.1"
description = "World timezone definitions, modern and historical"
category = "main"
optional = false
@ -409,14 +542,6 @@ category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]]
name = "typing-extensions"
version = "3.7.4.3"
description = "Backported and Experimental Type Hints for Python 3.5+"
category = "dev"
optional = false
python-versions = "*"
[[package]]
name = "urllib3"
version = "1.26.3"
@ -430,9 +555,24 @@ brotli = ["brotlipy (>=0.6.0)"]
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "uvicorn"
version = "0.13.4"
description = "The lightning-fast ASGI server."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
click = ">=7.0.0,<8.0.0"
h11 = ">=0.8"
[package.extras]
standard = ["websockets (>=8.0.0,<9.0.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "httptools (>=0.1.0,<0.2.0)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"]
[[package]]
name = "virtualenv"
version = "20.4.0"
version = "20.4.2"
description = "Virtual Python Environment builder"
category = "dev"
optional = false
@ -442,7 +582,6 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
appdirs = ">=1.4.3,<2"
distlib = ">=0.3.1,<1"
filelock = ">=3.0.0,<4"
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
six = ">=1.9.0,<2"
[package.extras]
@ -458,21 +597,20 @@ optional = false
python-versions = "*"
[[package]]
name = "zipp"
version = "3.4.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "dev"
name = "whitenoise"
version = "5.2.0"
description = "Radically simplified static file serving for WSGI applications"
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.5, <4"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
brotli = ["brotli"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "29ee22cded289d14aaf1015cc00f5fd4b3e441f807c86b325e21c67c2314a274"
python-versions = "^3.9"
content-hash = "273466dbd53a484027b72a712aba600936507cd2d528c9a245464637c0c54207"
[metadata.files]
appdirs = [
@ -495,6 +633,45 @@ certifi = [
{file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
{file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
]
cffi = [
{file = "cffi-1.14.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:bb89f306e5da99f4d922728ddcd6f7fcebb3241fc40edebcb7284d7514741991"},
{file = "cffi-1.14.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:34eff4b97f3d982fb93e2831e6750127d1355a923ebaeeb565407b3d2f8d41a1"},
{file = "cffi-1.14.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99cd03ae7988a93dd00bcd9d0b75e1f6c426063d6f03d2f90b89e29b25b82dfa"},
{file = "cffi-1.14.5-cp27-cp27m-win32.whl", hash = "sha256:65fa59693c62cf06e45ddbb822165394a288edce9e276647f0046e1ec26920f3"},
{file = "cffi-1.14.5-cp27-cp27m-win_amd64.whl", hash = "sha256:51182f8927c5af975fece87b1b369f722c570fe169f9880764b1ee3bca8347b5"},
{file = "cffi-1.14.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:43e0b9d9e2c9e5d152946b9c5fe062c151614b262fda2e7b201204de0b99e482"},
{file = "cffi-1.14.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cbde590d4faaa07c72bf979734738f328d239913ba3e043b1e98fe9a39f8b2b6"},
{file = "cffi-1.14.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:5de7970188bb46b7bf9858eb6890aad302577a5f6f75091fd7cdd3ef13ef3045"},
{file = "cffi-1.14.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a465da611f6fa124963b91bf432d960a555563efe4ed1cc403ba5077b15370aa"},
{file = "cffi-1.14.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:d42b11d692e11b6634f7613ad8df5d6d5f8875f5d48939520d351007b3c13406"},
{file = "cffi-1.14.5-cp35-cp35m-win32.whl", hash = "sha256:72d8d3ef52c208ee1c7b2e341f7d71c6fd3157138abf1a95166e6165dd5d4369"},
{file = "cffi-1.14.5-cp35-cp35m-win_amd64.whl", hash = "sha256:29314480e958fd8aab22e4a58b355b629c59bf5f2ac2492b61e3dc06d8c7a315"},
{file = "cffi-1.14.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d3dd4c9e559eb172ecf00a2a7517e97d1e96de2a5e610bd9b68cea3925b4892"},
{file = "cffi-1.14.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:48e1c69bbacfc3d932221851b39d49e81567a4d4aac3b21258d9c24578280058"},
{file = "cffi-1.14.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:69e395c24fc60aad6bb4fa7e583698ea6cc684648e1ffb7fe85e3c1ca131a7d5"},
{file = "cffi-1.14.5-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:9e93e79c2551ff263400e1e4be085a1210e12073a31c2011dbbda14bda0c6132"},
{file = "cffi-1.14.5-cp36-cp36m-win32.whl", hash = "sha256:58e3f59d583d413809d60779492342801d6e82fefb89c86a38e040c16883be53"},
{file = "cffi-1.14.5-cp36-cp36m-win_amd64.whl", hash = "sha256:005a36f41773e148deac64b08f233873a4d0c18b053d37da83f6af4d9087b813"},
{file = "cffi-1.14.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2894f2df484ff56d717bead0a5c2abb6b9d2bf26d6960c4604d5c48bbc30ee73"},
{file = "cffi-1.14.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0857f0ae312d855239a55c81ef453ee8fd24136eaba8e87a2eceba644c0d4c06"},
{file = "cffi-1.14.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:cd2868886d547469123fadc46eac7ea5253ea7fcb139f12e1dfc2bbd406427d1"},
{file = "cffi-1.14.5-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:35f27e6eb43380fa080dccf676dece30bef72e4a67617ffda586641cd4508d49"},
{file = "cffi-1.14.5-cp37-cp37m-win32.whl", hash = "sha256:9ff227395193126d82e60319a673a037d5de84633f11279e336f9c0f189ecc62"},
{file = "cffi-1.14.5-cp37-cp37m-win_amd64.whl", hash = "sha256:9cf8022fb8d07a97c178b02327b284521c7708d7c71a9c9c355c178ac4bbd3d4"},
{file = "cffi-1.14.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b198cec6c72df5289c05b05b8b0969819783f9418e0409865dac47288d2a053"},
{file = "cffi-1.14.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ad17025d226ee5beec591b52800c11680fca3df50b8b29fe51d882576e039ee0"},
{file = "cffi-1.14.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c97d7350133666fbb5cf4abdc1178c812cb205dc6f41d174a7b0f18fb93337e"},
{file = "cffi-1.14.5-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ae6299f6c68de06f136f1f9e69458eae58f1dacf10af5c17353eae03aa0d827"},
{file = "cffi-1.14.5-cp38-cp38-win32.whl", hash = "sha256:b85eb46a81787c50650f2392b9b4ef23e1f126313b9e0e9013b35c15e4288e2e"},
{file = "cffi-1.14.5-cp38-cp38-win_amd64.whl", hash = "sha256:1f436816fc868b098b0d63b8920de7d208c90a67212546d02f84fe78a9c26396"},
{file = "cffi-1.14.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1071534bbbf8cbb31b498d5d9db0f274f2f7a865adca4ae429e147ba40f73dea"},
{file = "cffi-1.14.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9de2e279153a443c656f2defd67769e6d1e4163952b3c622dcea5b08a6405322"},
{file = "cffi-1.14.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:6e4714cc64f474e4d6e37cfff31a814b509a35cb17de4fb1999907575684479c"},
{file = "cffi-1.14.5-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:158d0d15119b4b7ff6b926536763dc0714313aa59e320ddf787502c70c4d4bee"},
{file = "cffi-1.14.5-cp39-cp39-win32.whl", hash = "sha256:afb29c1ba2e5a3736f1c301d9d0abe3ec8b86957d04ddfa9d7a6a42b9367e396"},
{file = "cffi-1.14.5-cp39-cp39-win_amd64.whl", hash = "sha256:f2d45f97ab6bb54753eab54fffe75aaf3de4ff2341c9daee1987ee1837636f1d"},
{file = "cffi-1.14.5.tar.gz", hash = "sha256:fd78e5fee591709f32ef6edb9a015b4aa1a5022598e36227500c8f4e02328d9c"},
]
cfgv = [
{file = "cfgv-3.2.0-py2.py3-none-any.whl", hash = "sha256:32e43d604bbe7896fe7c248a9c2276447dbef840feb28fe20494f62af110211d"},
{file = "cfgv-3.2.0.tar.gz", hash = "sha256:cf22deb93d4bcf92f345a5c3cd39d3d41d6340adc60c78bbbd6588c384fda6a1"},
@ -503,10 +680,28 @@ chardet = [
{file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
{file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
]
click = [
{file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
{file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
cryptography = [
{file = "cryptography-3.4.6-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:57ad77d32917bc55299b16d3b996ffa42a1c73c6cfa829b14043c561288d2799"},
{file = "cryptography-3.4.6-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:4169a27b818de4a1860720108b55a2801f32b6ae79e7f99c00d79f2a2822eeb7"},
{file = "cryptography-3.4.6-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:93cfe5b7ff006de13e1e89830810ecbd014791b042cbe5eec253be11ac2b28f3"},
{file = "cryptography-3.4.6-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:5ecf2bcb34d17415e89b546dbb44e73080f747e504273e4d4987630493cded1b"},
{file = "cryptography-3.4.6-cp36-abi3-manylinux2014_x86_64.whl", hash = "sha256:fec7fb46b10da10d9e1d078d1ff8ed9e05ae14f431fdbd11145edd0550b9a964"},
{file = "cryptography-3.4.6-cp36-abi3-win32.whl", hash = "sha256:df186fcbf86dc1ce56305becb8434e4b6b7504bc724b71ad7a3239e0c9d14ef2"},
{file = "cryptography-3.4.6-cp36-abi3-win_amd64.whl", hash = "sha256:66b57a9ca4b3221d51b237094b0303843b914b7d5afd4349970bb26518e350b0"},
{file = "cryptography-3.4.6-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:066bc53f052dfeda2f2d7c195cf16fb3e5ff13e1b6b7415b468514b40b381a5b"},
{file = "cryptography-3.4.6-pp36-pypy36_pp73-manylinux2014_x86_64.whl", hash = "sha256:600cf9bfe75e96d965509a4c0b2b183f74a4fa6f5331dcb40fb7b77b7c2484df"},
{file = "cryptography-3.4.6-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:0923ba600d00718d63a3976f23cab19aef10c1765038945628cd9be047ad0336"},
{file = "cryptography-3.4.6-pp37-pypy37_pp73-manylinux2014_x86_64.whl", hash = "sha256:9e98b452132963678e3ac6c73f7010fe53adf72209a32854d55690acac3f6724"},
{file = "cryptography-3.4.6.tar.gz", hash = "sha256:2d32223e5b0ee02943f32b19245b61a62db83a882f0e76cc564e1cec60d48f87"},
]
defusedxml = [
{file = "defusedxml-0.6.0-py2.py3-none-any.whl", hash = "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93"},
{file = "defusedxml-0.6.0.tar.gz", hash = "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"},
@ -515,36 +710,60 @@ distlib = [
{file = "distlib-0.3.1-py2.py3-none-any.whl", hash = "sha256:8c09de2c67b3e7deef7184574fc060ab8a793e7adbb183d942c389c8b13c52fb"},
{file = "distlib-0.3.1.zip", hash = "sha256:edf6116872c863e1aa9d5bb7cb5e05a022c519a4594dc703843343a9ddd9bff1"},
]
dj-database-url = [
{file = "dj-database-url-0.5.0.tar.gz", hash = "sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163"},
{file = "dj_database_url-0.5.0-py2.py3-none-any.whl", hash = "sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9"},
]
dj-email-url = [
{file = "dj-email-url-1.0.2.tar.gz", hash = "sha256:838fd4ded9deba53ae757debef431e25fa7fca31d3948b3c4808ccdc84fab2b7"},
{file = "dj_email_url-1.0.2-py2.py3-none-any.whl", hash = "sha256:15148141c6ef123636e4ca3663e95231ed94ca5ed267e91977e5a4397be8b34c"},
]
django = [
{file = "Django-3.1.5-py3-none-any.whl", hash = "sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9"},
{file = "Django-3.1.5.tar.gz", hash = "sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7"},
{file = "Django-3.1.7-py3-none-any.whl", hash = "sha256:baf099db36ad31f970775d0be5587cc58a6256a6771a44eb795b554d45f211b8"},
{file = "Django-3.1.7.tar.gz", hash = "sha256:32ce792ee9b6a0cbbec340123e229ac9f765dff8c2a4ae9247a14b2ba3a365a7"},
]
django-allauth = [
{file = "django-allauth-0.40.0.tar.gz", hash = "sha256:6a189fc4d3ee23596c3fd6e9f49c59b5b15618980118171a50675dd6a27cc589"},
{file = "django-allauth-0.44.0.tar.gz", hash = "sha256:e51af457466022f52154d74c8523ac69375120fad2acce6e239635d85e610b25"},
]
django-cache-url = [
{file = "django-cache-url-3.2.3.tar.gz", hash = "sha256:c1d45626ae8a206267c1263aa7a3461e2e186be2e939bcbd8c660e25851ddac8"},
{file = "django_cache_url-3.2.3-py2.py3-none-any.whl", hash = "sha256:5514ca3a2075c6b956b3d0a5c540654d32b004e76340d7bdabf6661135b5f218"},
]
django-debug-toolbar = [
{file = "django-debug-toolbar-3.2.tar.gz", hash = "sha256:84e2607d900dbd571df0a2acf380b47c088efb787dce9805aefeb407341961d2"},
{file = "django_debug_toolbar-3.2-py3-none-any.whl", hash = "sha256:9e5a25d0c965f7e686f6a8ba23613ca9ca30184daa26487706d4829f5cfb697a"},
]
django-money = [
{file = "django-money-1.3.tar.gz", hash = "sha256:da95f9a7174281eb2ef0f5f1584d5ee2670fc0d67707cd269816a73cae791eb3"},
{file = "django_money-1.3-py3-none-any.whl", hash = "sha256:09952d49f998d089b21eb0f552d6dcb40d82626ab674d1caf0535bbd83a1ea01"},
{file = "django-money-1.3.1.tar.gz", hash = "sha256:a363ce16a23e403befdafa9895b2f538a10f9d390b160f12140094a6dfd55246"},
{file = "django_money-1.3.1-py3-none-any.whl", hash = "sha256:3b8fc751c8ae27cf877b8f3770ade1b63af97ee49a32ac08a6a1bc6d8d59f089"},
]
environs = [
{file = "environs-9.3.1-py2.py3-none-any.whl", hash = "sha256:2da44b7c30114415aa858577fa6396ee326fc76a0a60f0f15e8260ba554f19dc"},
{file = "environs-9.3.1.tar.gz", hash = "sha256:3f6def554abb5455141b540e6e0b72fda3853404f2b0d31658aab1bf95410db3"},
]
filelock = [
{file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"},
{file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"},
]
h11 = [
{file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"},
{file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"},
]
identify = [
{file = "identify-1.5.13-py2.py3-none-any.whl", hash = "sha256:9dfb63a2e871b807e3ba62f029813552a24b5289504f5b071dea9b041aee9fe4"},
{file = "identify-1.5.13.tar.gz", hash = "sha256:70b638cf4743f33042bebb3b51e25261a0a10e80f978739f17e7fd4837664a66"},
{file = "identify-2.0.0-py2.py3-none-any.whl", hash = "sha256:9cdd81e5d2b6e76c3006d5226316dd947bd6324fbeebb881bec489202fa09d3a"},
{file = "identify-2.0.0.tar.gz", hash = "sha256:b99aa309329c4fea679463eb35d169f3fbe13e66e9dd6162ad1856cbeb03dcbd"},
]
idna = [
{file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
{file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
]
importlib-metadata = [
{file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"},
{file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"},
marshmallow = [
{file = "marshmallow-3.10.0-py2.py3-none-any.whl", hash = "sha256:eca81d53aa4aafbc0e20566973d0d2e50ce8bf0ee15165bb799bec0df1e50177"},
{file = "marshmallow-3.10.0.tar.gz", hash = "sha256:4ab2fdb7f36eb61c3665da67a7ce281c8900db08d72ba6bf0e695828253581f7"},
]
more-itertools = [
{file = "more-itertools-8.6.0.tar.gz", hash = "sha256:b3a9005928e5bed54076e6e549c792b306fddfe72b2d1d22dd63d42d5d3899cf"},
{file = "more_itertools-8.6.0-py3-none-any.whl", hash = "sha256:8e1a2a43b2f2727425f2b5839587ae37093f19153dc26c0927d1048ff6557330"},
{file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"},
{file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"},
]
nodeenv = [
{file = "nodeenv-1.5.0-py2.py3-none-any.whl", hash = "sha256:5304d424c529c997bc888453aeaa6362d242b6b4631e90f3d4bf1b290f1c84a9"},
@ -555,16 +774,16 @@ oauthlib = [
{file = "oauthlib-3.1.0.tar.gz", hash = "sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889"},
]
packaging = [
{file = "packaging-20.8-py2.py3-none-any.whl", hash = "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858"},
{file = "packaging-20.8.tar.gz", hash = "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"},
{file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
{file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
]
pluggy = [
{file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
]
pre-commit = [
{file = "pre_commit-2.9.3-py2.py3-none-any.whl", hash = "sha256:6c86d977d00ddc8a60d68eec19f51ef212d9462937acf3ea37c7adec32284ac0"},
{file = "pre_commit-2.9.3.tar.gz", hash = "sha256:ee784c11953e6d8badb97d19bc46b997a3a9eded849881ec587accd8608d74a4"},
{file = "pre_commit-2.10.1-py2.py3-none-any.whl", hash = "sha256:16212d1fde2bed88159287da88ff03796863854b04dc9f838a55979325a3d20e"},
{file = "pre_commit-2.10.1.tar.gz", hash = "sha256:399baf78f13f4de82a29b649afd74bef2c4e28eb4f021661fc7f29246e8c7a3a"},
]
psycopg2 = [
{file = "psycopg2-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:068115e13c70dc5982dfc00c5d70437fe37c014c808acce119b5448361c03725"},
@ -591,6 +810,14 @@ py-moneyed = [
{file = "py-moneyed-0.8.0.tar.gz", hash = "sha256:ec73795171919d537880a33c44d07fcdf0a5225e8368684fe02f0e75a6404742"},
{file = "py_moneyed-0.8.0-py2.py3-none-any.whl", hash = "sha256:c6691b914a5e4b5b2335cf113620479a52cc82988c0e143435a7c5c7d60cd4ad"},
]
pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
]
pyjwt = [
{file = "PyJWT-2.0.1-py3-none-any.whl", hash = "sha256:b70b15f89dc69b993d8a8d32c299032d5355c82f9b5b7e851d1a6d706dffe847"},
{file = "PyJWT-2.0.1.tar.gz", hash = "sha256:a5c70a06e1f33d81ef25eecd50d50bd30e34de1ca8b2b9fa3fe0daaabcf69bf7"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
@ -603,13 +830,17 @@ pytest-django = [
{file = "pytest-django-3.10.0.tar.gz", hash = "sha256:4de6dbd077ed8606616958f77655fed0d5e3ee45159475671c7fa67596c6dba6"},
{file = "pytest_django-3.10.0-py2.py3-none-any.whl", hash = "sha256:c33e3d3da14d8409b125d825d4e74da17bb252191bf6fc3da6856e27a8b73ea4"},
]
python-dotenv = [
{file = "python-dotenv-0.15.0.tar.gz", hash = "sha256:587825ed60b1711daea4832cf37524dfd404325b7db5e25ebe88c495c9f807a0"},
{file = "python_dotenv-0.15.0-py2.py3-none-any.whl", hash = "sha256:0c8d1b80d1a1e91717ea7d526178e3882732420b03f08afea0406db6402e220e"},
]
python3-openid = [
{file = "python3-openid-3.2.0.tar.gz", hash = "sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf"},
{file = "python3_openid-3.2.0-py3-none-any.whl", hash = "sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"},
]
pytz = [
{file = "pytz-2020.5-py2.py3-none-any.whl", hash = "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4"},
{file = "pytz-2020.5.tar.gz", hash = "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"},
{file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"},
{file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"},
]
pyyaml = [
{file = "PyYAML-5.4.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922"},
@ -655,24 +886,23 @@ toml = [
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
]
typing-extensions = [
{file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
{file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
{file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
]
urllib3 = [
{file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"},
{file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"},
]
uvicorn = [
{file = "uvicorn-0.13.4-py3-none-any.whl", hash = "sha256:7587f7b08bd1efd2b9bad809a3d333e972f1d11af8a5e52a9371ee3a5de71524"},
{file = "uvicorn-0.13.4.tar.gz", hash = "sha256:3292251b3c7978e8e4a7868f4baf7f7f7bb7e40c759ecc125c37e99cdea34202"},
]
virtualenv = [
{file = "virtualenv-20.4.0-py2.py3-none-any.whl", hash = "sha256:227a8fed626f2f20a6cdb0870054989f82dd27b2560a911935ba905a2a5e0034"},
{file = "virtualenv-20.4.0.tar.gz", hash = "sha256:219ee956e38b08e32d5639289aaa5bd190cfbe7dafcb8fa65407fca08e808f9c"},
{file = "virtualenv-20.4.2-py2.py3-none-any.whl", hash = "sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3"},
{file = "virtualenv-20.4.2.tar.gz", hash = "sha256:147b43894e51dd6bba882cf9c282447f780e2251cd35172403745fc381a0a80d"},
]
wcwidth = [
{file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
{file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
]
zipp = [
{file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"},
{file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"},
whitenoise = [
{file = "whitenoise-5.2.0-py2.py3-none-any.whl", hash = "sha256:05d00198c777028d72d8b0bbd234db605ef6d60e9410125124002518a48e515d"},
{file = "whitenoise-5.2.0.tar.gz", hash = "sha256:05ce0be39ad85740a78750c86a93485c40f08ad8c62a6006de0233765996e5c7"},
]

View file

@ -1,14 +0,0 @@
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

View file

@ -1,132 +0,0 @@
"""
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")]

View file

@ -1,79 +0,0 @@
/* 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;
}

View file

@ -1,49 +0,0 @@
<!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:password_change' %}">Change password</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>

View file

@ -1,12 +0,0 @@
"""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),
]

View file

@ -5,16 +5,20 @@ description = ""
authors = ["Your Name <you@example.com>"]
[tool.poetry.dependencies]
python = "^3.7"
python = "^3.9"
Django = "^3.1"
django-money = "^1.3"
django-allauth = "^0.40.0"
django-allauth = "^0.44.0"
psycopg2 = "^2.8.6"
environs = {extras = ["django"], version = "^9.3.1"}
uvicorn = "^0.13.4"
whitenoise = "^5.2.0"
[tool.poetry.dev-dependencies]
pre-commit = "^2.9.3"
pytest = "^5.1"
pytest-django = "^3.5"
django-debug-toolbar = "^3.2"
[build-system]
requires = ["poetry>=0.12"]

View file

@ -1,6 +1,4 @@
# Generated by Django 2.0.6 on 2018-06-23 19:51
from decimal import Decimal
# 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
@ -12,7 +10,9 @@ class Migration(migrations.Migration):
initial = True
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
@ -33,7 +33,7 @@ class Migration(migrations.Migration):
),
(
"created",
models.DateTimeField(auto_now_add=True, verbose_name="created"),
models.DateTimeField(auto_now_add=True, verbose_name="oprettet"),
),
(
"owner",
@ -43,7 +43,9 @@ class Migration(migrations.Migration):
),
),
],
options={"abstract": False},
options={
"abstract": False,
},
),
migrations.CreateModel(
name="Order",
@ -63,7 +65,7 @@ class Migration(migrations.Migration):
),
(
"created",
models.DateTimeField(auto_now_add=True, verbose_name="created"),
models.DateTimeField(auto_now_add=True, verbose_name="oprettet"),
),
(
"description",
@ -82,7 +84,6 @@ class Migration(migrations.Migration):
"price",
djmoney.models.fields.MoneyField(
decimal_places=2,
default=Decimal("0.0"),
max_digits=16,
verbose_name="price (excl. VAT)",
),
@ -99,10 +100,7 @@ class Migration(migrations.Migration):
(
"vat",
djmoney.models.fields.MoneyField(
decimal_places=2,
default=Decimal("0.0"),
max_digits=16,
verbose_name="VAT",
decimal_places=2, max_digits=16, verbose_name="VAT"
),
),
("is_paid", models.BooleanField(default=False, verbose_name="is paid")),
@ -110,7 +108,7 @@ class Migration(migrations.Migration):
"account",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
to="accounting.Account",
to="accounting.account",
),
),
(
@ -121,60 +119,10 @@ class Migration(migrations.Migration):
),
),
],
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"},
options={
"verbose_name": "Order",
"verbose_name_plural": "Orders",
},
),
migrations.CreateModel(
name="Transaction",
@ -194,7 +142,7 @@ class Migration(migrations.Migration):
),
(
"created",
models.DateTimeField(auto_now_add=True, verbose_name="created"),
models.DateTimeField(auto_now_add=True, verbose_name="oprettet"),
),
(
"amount_currency",
@ -209,7 +157,6 @@ class Migration(migrations.Migration):
"amount",
djmoney.models.fields.MoneyField(
decimal_places=2,
default=Decimal("0.0"),
help_text="This will include VAT",
max_digits=16,
verbose_name="amount",
@ -224,10 +171,66 @@ class Migration(migrations.Migration):
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="transactions",
to="accounting.Account",
to="accounting.account",
),
),
],
options={"abstract": False},
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",
},
),
]

View file

@ -1,7 +1,6 @@
from hashlib import md5
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 _
@ -24,7 +23,7 @@ class Account(CreatedModifiedAbstract):
can decide which account to use to pay for something.
"""
owner = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
owner = models.ForeignKey("auth.User", on_delete=models.PROTECT)
@property
def balance(self):
@ -56,9 +55,8 @@ class Order(CreatedModifiedAbstract):
invoices at the moment.
"""
user = models.ForeignKey(get_user_model(), on_delete=models.PROTECT)
user = models.ForeignKey("auth.User", 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"))

14
src/membership/admin.py Normal file
View file

@ -0,0 +1,14 @@
from django.contrib import admin
from .models import Membership
from .models import MembershipType
@admin.register(Membership)
class MembershipAdmin(admin.ModelAdmin):
pass
@admin.register(MembershipType)
class MembershipTypeAdmin(admin.ModelAdmin):
pass

View file

@ -0,0 +1,93 @@
# 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",
},
),
]

76
src/membership/models.py Normal file
View file

@ -0,0 +1,76 @@
from typing import Optional
from django.contrib.postgres.fields import DateTimeRangeField
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext as _
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 Membership(CreatedModifiedAbstract):
"""
Tracks that a user has membership of a given type for a given period.
"""
class QuerySet(models.QuerySet):
def for_user(self, user):
return self.filter(user=user)
def _current(self):
return self.filter(period__contains=timezone.now())
def current(self) -> Optional["Membership"]:
try:
return self._current().get()
except self.model.DoesNotExist:
return None
def previous(self):
# A naïve way to get previous by just excluding the current. This
# means that there must be some protection against "future"
# memberships.
return self.all().difference(self._current())
objects = QuerySet.as_manager()
class Meta:
verbose_name = _("membership")
verbose_name_plural = _("memberships")
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 = DateTimeRangeField(help_text=_("The duration this subscription is for. "))
def __str__(self):
return f"{self.user} - {self.period}"
class MembershipType(CreatedModifiedAbstract):
"""
Models membership types. Currently only a name, but will in the future
possibly contain more information like fees.
"""
class Meta:
verbose_name = _("membership type")
verbose_name_plural = _("membership types")
name = models.CharField(verbose_name=_("name"), max_length=64)
def __str__(self):
return self.name

View file

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
{% 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>
<p>{% trans "Period" %}: {{ current_membership.period.lower|date:"SHORT_DATE_FORMAT" }} to {{ current_membership.period.upper|date:"SHORT_DATE_FORMAT" }}</p>
<p>{% trans "Type" %}: {{ current_membership.membership_type }}</p>
{% endif %}
{% endblock %}

18
src/membership/views.py Normal file
View file

@ -0,0 +1,18 @@
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from .models import Membership
@login_required
def membership_overview(request):
memberships = Membership.objects.for_user(request.user)
current_membership = memberships.current()
previous_memberships = memberships.previous()
context = dict(
current_membership=current_membership,
previous_memberships=previous_memberships,
)
return render(request, "membership_overview.html", context)

7
src/project/asgi.py Normal file
View file

@ -0,0 +1,7 @@
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
application = get_asgi_application()

View file

@ -0,0 +1,519 @@
# 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"

142
src/project/settings.py Normal file
View file

@ -0,0 +1,142 @@
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=["*"])
ADMINS = [tuple(x.split(":")) for x in env.list("DJANGO_ADMINS", default=[])]
# 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",
"debug_toolbar",
"allauth",
"allauth.account",
"utils",
"accounting",
"membership",
]
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",
"debug_toolbar.middleware.DebugToolbarMiddleware",
]
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",
"project.context_processors.current_site",
]
},
}
]
AUTHENTICATION_BACKENDS = (
"django.contrib.auth.backends.ModelBackend",
"allauth.account.auth_backends.AuthenticationBackend",
)
WSGI_APPLICATION = "project.wsgi.application"
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
]
LANGUAGE_CODE = "da-dk"
TIME_ZONE = "Europe/Copenhagen"
USE_I18N = True
USE_L10N = 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,11 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Account Inactive" %}{% endblock %}
{% block content %}
<h1>{% trans "Account Inactive" %}</h1>
<p>{% trans "This account is inactive." %}</p>
{% endblock %}

View file

@ -0,0 +1,130 @@
{% extends 'account/base.html' %}
{% load i18n %}
{% block head_title %}{% trans "E-mail Addresses" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h4>{% trans "E-mail Addresses" %}</h4>
</div>
<div class="panel-body">
{% if user.emailaddress_set.all %}
<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>
<form action="{% url 'account_email' %}" class="email_list"
method="post">
{% csrf_token %}
<fieldset class="blockLabels">
<table class="table">
<thead>
<tr>
<th></th>
<th>{% trans "Address" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Primary" %}</th>
</tr>
</thead>
<tbody>
{% for emailaddress in user.emailaddress_set.all %}
<tr class="ctrlHolder">
<label for="email_radio_{{ forloop.counter }}"
class="{% if emailaddress.primary %}primary_email{% endif %}">
<td>
<input
id="email_radio_{{ forloop.counter }}"
type="radio"
name="email"
value="{{ emailaddress.email }}"
{% if emailaddress.primary or user.emailaddress_set.count == 1 %}
checked="checked"
{% endif %}
/>
</td>
<td>
{{ emailaddress.email }}
</td>
<td>
{% if emailaddress.verified %}
<span class="label label-success">Verified</span>
{% else %}
<span class="label label-danger">Unverified</span>
{% endif %}
</td>
<td>
{% if emailaddress.primary %}
<span class="label label-primary">Primary</span>{% endif %}
</td>
</label>
</tr>
{% endfor %}
</tbody>
</table>
<div class="buttonHolder">
<button class="btn btn-success" type="submit"
name="action_primary">Make Primary
</button>
<button class="btn btn-primary" type="submit"
name="action_send">Re-send Verification
</button>
<button class="btn btn-danger" type="submit"
name="action_remove">Remove
</button>
</div>
</fieldset>
</form>
{% else %}
<p>
<strong>{% trans 'Warning:' %}</strong>
{% trans "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." %}
</p>
{% endif %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4>{% trans "Add E-mail" %}</h4>
</div>
<div class="panel-body">
<form method="post" action="{% url 'account_email' %}"
class="add_email">
{% csrf_token %}
{{ form.as_p }}
<button name="action_add" class="btn btn-success" type="submit">
{% trans "Add E-mail" %}
</button>
</form>
</div>
</div>
</div>
</div>
<script type="text/javascript">
(function () {
let message = "{% trans 'Do you really want to remove the selected e-mail address?' %}";
let actions = document.getElementsByName('action_remove');
if (actions.length) {
actions[0].addEventListener("click", function (e) {
if (!confirm(message)) {
e.preventDefault();
}
});
}
})();
</script>
{% endblock %}

View file

@ -0,0 +1,7 @@
{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name %}Hello from {{ site_name }}!{% endblocktrans %}
{% block content %}{% endblock %}
{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you for using {{ site_name }}!
{{ site_domain }}{% endblocktrans %}
{% endautoescape %}

View file

@ -0,0 +1,7 @@
{% extends "account/email/base_message.txt" %}
{% load account %}
{% load i18n %}
{% block content %}{% autoescape off %}{% user_display user as user_display %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}You're receiving this e-mail because user {{ user_display }} has given your e-mail address to register an account on {{ site_domain }}.
To confirm this is correct, go to {{ activate_url }}{% endblocktrans %}{% endautoescape %}{% endblock %}

View file

@ -0,0 +1 @@
{% include "account/email/email_confirmation_message.txt" %}

View file

@ -0,0 +1 @@
{% include "account/email/email_confirmation_subject.txt" %}

View file

@ -0,0 +1,4 @@
{% load i18n %}
{% autoescape off %}
{% blocktrans %}Please Confirm Your E-mail Address{% endblocktrans %}
{% endautoescape %}

View file

@ -0,0 +1,9 @@
{% extends "account/email/base_message.txt" %}
{% load i18n %}
{% block content %}{% autoescape off %}{% blocktrans %}You're receiving this e-mail because you or someone else has requested a password for your user account.
It can be safely ignored if you did not request a password reset. Click the link below to reset your password.{% endblocktrans %}
{{ password_reset_url }}{% if username %}
{% blocktrans %}In case you forgot, your username is {{ username }}.{% endblocktrans %}{% endif %}{% endautoescape %}{% endblock %}

View file

@ -0,0 +1,4 @@
{% load i18n %}
{% autoescape off %}
{% blocktrans %}Password Reset E-mail{% endblocktrans %}
{% endautoescape %}

View file

@ -0,0 +1,31 @@
{% extends "account/base.html" %}
{% load i18n %}
{% load account %}
{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
{% block content %}
<h1>{% trans "Confirm E-mail Address" %}</h1>
{% if confirmation %}
{% user_display confirmation.email_address.user as user_display %}
<p>{% blocktrans with confirmation.email_address.email as email %}Please confirm that <a href="mailto:{{ email }}">{{ email }}</a> is an e-mail address for user {{ user_display }}.{% endblocktrans %}</p>
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
{% csrf_token %}
<button type="submit">{% trans 'Confirm' %}</button>
</form>
{% else %}
{% url 'account_email' as email_url %}
<p>{% blocktrans %}This e-mail confirmation link expired or is invalid. Please <a href="{{ email_url }}">issue a new e-mail confirmation request</a>.{% endblocktrans %}</p>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,56 @@
{% extends "account/pre_login_base.html" %}
{% load i18n %}
{% load static %}
{% block non_login_content %}
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endfor %}
{% endif %}
<form method="post" action="">
{% csrf_token %}
<label for="id_username"
class="visually-hidden">
{% trans "E-mail" %}
</label>
<input type="text"
id="id_username"
name="login"
class="form-control mb-lg-2"
placeholder="{% trans "E-mail" %}"
required
autofocus>
<label for="id_password" class="visually-hidden">
{% trans "Password" %}
</label>
<input type="password"
id="id_password"
name="password"
class="form-control mb-lg-2"
placeholder="{% trans "Password" %}"
required>
<button class="w-100 mb-lg-2 btn btn-lg btn-primary"
type="submit">{% trans "Sign in" %}</button>
</form>
<a href="{% url "account_reset_password" %}"
class="w-100 btn btn-lg btn-outline-info">
{% trans "Forgot password?" %}
</a>
<hr class="hr-text" data-content="{% trans "Or"|upper %}">
<a class="w-100 btn btn-lg btn-outline-success"
type="submit"
href="{% url "account_signup" %}">
{% trans "Become a member" %}
</a>
{% endblock %}

View file

@ -0,0 +1,21 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Sign Out" %}{% endblock %}
{% block content %}
<h1>{% trans "Sign Out" %}</h1>
<p>{% trans 'Are you sure you want to sign out?' %}</p>
<form method="post" action="{% url 'account_logout' %}">
{% csrf_token %}
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
{% endif %}
<button type="submit">{% trans 'Sign Out' %}</button>
</form>
{% endblock %}

View file

@ -0,0 +1,2 @@
{% load i18n %}
{% blocktrans %}You cannot remove your primary e-mail address ({{email}}).{% endblocktrans %}

View file

@ -0,0 +1,2 @@
{% load i18n %}
{% blocktrans %}Confirmation e-mail sent to {{email}}.{% endblocktrans %}

View file

@ -0,0 +1,2 @@
{% load i18n %}
{% blocktrans %}You have confirmed {{email}}.{% endblocktrans %}

View file

@ -0,0 +1,2 @@
{% load i18n %}
{% blocktrans %}Removed e-mail address {{email}}.{% endblocktrans %}

View file

@ -0,0 +1,4 @@
{% load account %}
{% load i18n %}
{% user_display user as name %}
{% blocktrans %}Successfully signed in as {{name}}.{% endblocktrans %}

View file

@ -0,0 +1,2 @@
{% load i18n %}
{% blocktrans %}You have signed out.{% endblocktrans %}

View file

@ -0,0 +1,2 @@
{% load i18n %}
{% blocktrans %}Password successfully changed.{% endblocktrans %}

View file

@ -0,0 +1,2 @@
{% load i18n %}
{% blocktrans %}Password successfully set.{% endblocktrans %}

View file

@ -0,0 +1,2 @@
{% load i18n %}
{% blocktrans %}Primary e-mail address set.{% endblocktrans %}

View file

@ -0,0 +1,2 @@
{% load i18n %}
{% blocktrans %}Your primary e-mail address must be verified.{% endblocktrans %}

View file

@ -0,0 +1,16 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Change Password" %}{% endblock %}
{% block content %}
<h1>{% trans "Change Password" %}</h1>
<form method="POST" action="{% url 'account_change_password' %}" class="password_change">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" name="action">{% trans "Change Password" %}</button>
<a href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
</form>
{% endblock %}

View file

@ -0,0 +1,49 @@
{% extends "account/pre_login_base.html" %}
{% load i18n %}
{% load account %}
{% block head_title %}{% trans "Password Reset" %}{% endblock %}
{% block non_login_content %}
<h1 class="h3 mb-3 fw-normal">{% trans "Password Reset" %}</h1>
{% if user.is_authenticated %}
{% include "account/snippets/already_logged_in.html" %}
{% endif %}
<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}</p>
<form method="POST" action="{% url 'account_reset_password' %}" class="password_reset">
{% csrf_token %}
<label for="id_email" class="visually-hidden">
{% trans "E-mail address" %}
</label>
<input type="email"
id="id_email"
name="email"
class="form-control mb-lg-2 {% if form.email.errors %}is-invalid{% elif form.is_bound %}is-valid{% endif %}"
placeholder="{% trans "E-mail address" %}"
required>
{% if form.email.errors %}
{% for error in form.email.errors %}
<div class="invalid-feedback mb-lg-2">
{{ error }}
</div>
{% endfor %}
{% endif %}
<input class="w-100 btn btn-lg btn-primary"
type="submit" value="{% trans 'Reset My Password' %}" />
</form>
<p>{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}</p>
<hr>
<a class="w-100"
type="submit"
href="{% url "account_login" %}">
{% trans "To login" %}
</a>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends "account/pre_login_base.html" %}
{% load i18n %}
{% load account %}
{% block head_title %}{% trans "Password Reset" %}{% endblock %}
{% block non_login_content %}
<h1>{% trans "Password Reset" %}</h1>
{% if user.is_authenticated %}
{% include "account/snippets/already_logged_in.html" %}
{% endif %}
<p>{% blocktrans %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktrans %}</p>
{% endblock %}

View file

@ -0,0 +1,24 @@
{% extends "account/pre_login_base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Change Password" %}{% endblock %}
{% block non_login_content %}
<h1>{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}</h1>
{% if token_fail %}
{% url 'account_reset_password' as passwd_reset_url %}
<p>{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a <a href="{{ passwd_reset_url }}">new password reset</a>.{% endblocktrans %}</p>
{% else %}
{% if form %}
<form method="POST" action="{{ action_url }}">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" name="action" value="{% trans 'change password' %}"/>
</form>
{% else %}
<p>{% trans 'Your password is now changed.' %}</p>
{% endif %}
{% endif %}
{% endblock %}

View file

@ -0,0 +1,9 @@
{% extends "account/pre_login_base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Change Password" %}{% endblock %}
{% block non_login_content %}
<h1>{% trans "Change Password" %}</h1>
<p>{% trans 'Your password is now changed.' %}</p>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Set Password" %}{% endblock %}
{% block content %}
<h1>{% trans "Set Password" %}</h1>
<form method="POST" action="{% url 'account_set_password' %}" class="password_set">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" name="action" value="{% trans 'Set Password' %}"/>
</form>
{% endblock %}

View file

@ -0,0 +1,75 @@
{% load i18n %}
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<title>Login {{ site.name }}</title>
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet"
integrity="sha384-8A6VqJCgKATsNvbzGwXr1Y1kRA+dQzm5znASKSOLMVRJ3s2FgO1h9J1HrtVp9pBF"
crossorigin="anonymous">
<style>
html,
body {
height: 100%;
}
body {
display: flex;
align-items: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #a8f3f4;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-control {
position: relative;
box-sizing: border-box;
height: auto;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.hr-text:after {
content: attr(data-content);
padding: 0 4px;
position: relative;
top: -13px;
background-color: #a8f3f4;
}
</style>
</head>
<body class="text-center">
<main class="form-signin">
<img class="mb-4" src="https://new.data.coop/static/img/logo_da.svg" alt=""
width="260" height="160">
{% block non_login_content %}
{% endblock %}
</main>
</body>
</html>

View file

@ -0,0 +1,75 @@
{% extends "account/pre_login_base.html" %}
{% load i18n %}
{% load static %}
{% block non_login_content %}
<h1 class="h3 mb-3 fw-normal">{% trans "Become a member" %}</h1>
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endfor %}
{% endif %}
{{ form.email.errors }}
{{ form.password1.errors }}
<p>{% trans "To become a member, you need to have an account. Create one here." %}</p>
<form method="post" action="">
{% csrf_token %}
<label for="id_email"
class="visually-hidden">
{% trans "E-mail" %}
</label>
<input type="email"
id="id_email"
name="email"
class="form-control mb-lg-2 {% if form.email.errors %}is-invalid{% elif form.is_bound %}is-valid{% endif %}"
placeholder="{% trans "E-mail" %}"
required
autofocus>
{% if form.email.errors %}
{% for error in form.email.errors %}
<div class="invalid-feedback">
{{ error }}
</div>
{% endfor %}
{% endif %}
<label for="id_password" class="visually-hidden">
{% trans "Password" %}
</label>
<input type="password"
id="id_password"
name="password1"
class="form-control mb-lg-2 {% if form.password1.errors %}is-invalid{% elif form.is_bound %}is-valid{% endif %}"
placeholder="{% trans "Password" %}"
required>
{% if form.password1.errors %}
{% for error in form.password1.errors %}
<div class="invalid-feedback">
{{ error }}
</div>
{% endfor %}
{% endif %}
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}"
value="{{ redirect_field_value }}"/>
{% endif %}
<button class="w-100 btn btn-lg btn-primary"
type="submit">{% trans "Sign up" %}</button>
</form>
<hr>
<a class="w-100"
type="submit"
href="{% url "account_login" %}">
{% trans "To login" %}
</a>
{% endblock %}

View file

@ -0,0 +1,11 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Sign Up Closed" %}{% endblock %}
{% block content %}
<h1>{% trans "Sign Up Closed" %}</h1>
<p>{% trans "We are sorry, but the sign up is currently closed." %}</p>
{% endblock %}

View file

@ -0,0 +1,5 @@
{% load i18n %}
{% load account %}
{% user_display user as user_display %}
<p><strong>{% trans "Note" %}:</strong> {% blocktrans %}you are already logged in as {{ user_display }}.{% endblocktrans %}</p>

View file

@ -0,0 +1,12 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %}
{% block content %}
<h1>{% trans "Verify Your E-mail Address" %}</h1>
<p>{% blocktrans %}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.{% endblocktrans %}</p>
{% endblock %}

View file

@ -0,0 +1,23 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Verify Your E-mail Address" %}{% endblock %}
{% block content %}
<h1>{% trans "Verify Your E-mail Address" %}</h1>
{% url 'account_email' as email_url %}
<p>{% blocktrans %}This part of the site requires us to verify that
you are who you claim to be. For this purpose, we require that you
verify ownership of your e-mail address. {% endblocktrans %}</p>
<p>{% blocktrans %}We have sent an e-mail to you for
verification. Please click on the link inside this e-mail. Please
contact us if you do not receive it within a few minutes.{% endblocktrans %}</p>
<p>{% blocktrans %}<strong>Note:</strong> you can still <a href="{{ email_url }}">change your e-mail address</a>.{% endblocktrans %}</p>
{% endblock %}

View file

@ -0,0 +1,212 @@
{% load utils %}
{% load i18n %}
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<title>{% block head_title %}{% endblock %} {{ site.name }}</title>
<link href="{% static "/css/bootstrap.min.css" %}" rel="stylesheet"
integrity="sha384-8A6VqJCgKATsNvbzGwXr1Y1kRA+dQzm5znASKSOLMVRJ3s2FgO1h9J1HrtVp9pBF"
crossorigin="anonymous">
<style>
body {
font-size: .875rem;
}
/*
* Sidebar
*/
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100; /* Behind the navbar */
padding: 48px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
@media (max-width: 767.98px) {
.sidebar {
top: 5rem;
}
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 48px);
padding-top: .5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link .feather {
margin-right: 4px;
color: #727272;
}
.sidebar .nav-link.active {
color: #007bff;
}
.sidebar .nav-link:hover .feather,
.sidebar .nav-link.active .feather {
color: inherit;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
/*
* Navbar
*/
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
font-size: 1rem;
background-color: rgba(0, 0, 0, .25);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}
.navbar .navbar-toggler {
top: .25rem;
right: 1rem;
}
.navbar .form-control {
padding: .75rem 1rem;
border-width: 0;
border-radius: 0;
}
.form-control-dark {
color: #fff;
background-color: rgba(255, 255, 255, .1);
border-color: rgba(255, 255, 255, .1);
}
.form-control-dark:focus {
border-color: transparent;
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
} </style>
<script src="{% static "js/bootstrap.bundle.min.js" %}"
integrity="sha384-161MkQeTxmFupfLvH3jbFeSkoKSweR+66Tfp/ot/TuqbbOq0qssz8c91dG8I8y19"
crossorigin="anonymous"></script>
</head>
<body>
<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="#">{{ site.name }}</a>
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button"
data-bs-toggle="collapse" data-bs-target="#sidebarMenu"
aria-controls="sidebarMenu" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<ul class="navbar-nav px-3">
<li class="nav-item text-nowrap">
<a class="nav-link" href="{% url "account_logout" %}">
<i class="fas fa-user"></i>
Sign out</a>
</li>
</ul>
</header>
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu"
class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {% active_path "index" "active" %}"
href="{% url "index" %}">
{% trans "Dashboard" %}
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>{% trans "Profile" %}</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="#">
{% trans "Details" %}
</a>
</li>
<li class="nav-item">
<a class="nav-link {% active_path "account_email" "active" %}"
aria-current="page" href="{% url "account_email" %}">
{% trans "Emails" %}
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>{% trans "Membership" %}</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {% active_path "membership-overview" "active" %}"
aria-current="page" href="{% url "membership-overview" %}">
{% trans "Overview" %}
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>{% trans "Services" %}</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link {% active_path "services-overview" "active" %}"
href="{% url "services-overview" %}">
{% trans "Overview" %}
</a>
</li>
</ul>
{% if user.is_staff %}
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>{% trans "Admin" %}</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="file-text"></span>
{% trans "Members" %}
</a>
</li>
</ul>
{% endif %}
</div>
</nav>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
{% block content %}
{% endblock %}
</main>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1 @@
{% extends "base.html" %}

View file

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block content %}
<p>
Services and signup to these will be
</p>
<p>
This is yet to be implemented.
</p>
{% endblock %}

19
src/project/urls.py Normal file
View file

@ -0,0 +1,19 @@
"""URLs for the membersystem"""
import debug_toolbar
from django.contrib import admin
from django.contrib.auth.decorators import login_required
from django.urls import include
from django.urls import path
from membership.views import membership_overview
from .views import index
from .views import services_overview
urlpatterns = [
path("", login_required(index), name="index"),
path("services/", login_required(services_overview), name="services-overview"),
path("membership/", membership_overview, name="membership-overview"),
path("accounts/", include("allauth.urls")),
path("admin/", admin.site.urls),
path("__debug__/", include(debug_toolbar.urls)),
]

View file

@ -3,3 +3,7 @@ from django.shortcuts import render
def index(request):
return render(request, "index.html")
def services_overview(request):
return render(request, "services_overview.html")

View file

@ -0,0 +1,13 @@
from django import template
from django.urls import reverse
register = template.Library()
@register.simple_tag(takes_context=True)
def active_path(context, path_name, class_name):
path = reverse(path_name)
request_path = context.get("request").path
if path == request_path or ("basepath" in context and context["basepath"] == path):
return class_name

View file

@ -1 +0,0 @@
from django.contrib import admin # noqa

View file

@ -1,5 +0,0 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = "users"

View file

@ -1,19 +0,0 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.tokens import default_token_generator
from django.utils.translation import gettext_lazy as _
from . import models
def get_confirm_code(email):
return default_token_generator(email)[:7]
class SignupForm(UserCreationForm):
username = forms.EmailField(label=_("Email"))
class Meta:
model = models.User
fields = ("username",)

View file

@ -1,73 +0,0 @@
# Generated by Django 2.2.4 on 2019-08-31 18:44
import uuid
from django.db import migrations
from django.db import models
class Migration(migrations.Migration):
initial = True
dependencies = [("auth", "0011_update_proxy_permissions")]
operations = [
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
("nick", models.CharField(blank=True, max_length=60, null=True)),
(
"email",
models.EmailField(
help_text="Your email address will be used for password resets and notification about your event/submissions.",
max_length=254,
unique=True,
verbose_name="E-Mail",
),
),
("is_active", models.BooleanField(default=True)),
("is_staff", models.BooleanField(default=False)),
("is_superuser", models.BooleanField(default=False)),
("token_uuid", models.UUIDField(default=uuid.uuid4, editable=False)),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.Group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.Permission",
verbose_name="user permissions",
),
),
],
options={"verbose_name": "User"},
)
]

View file

@ -1,60 +0,0 @@
import uuid
from django.contrib.auth.base_user import BaseUserManager
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.models import PermissionsMixin
from django.db import models
from django.utils.translation import gettext_lazy as _
class UserManager(BaseUserManager):
"""The user manager class."""
def create_user(self, password: str = None, **kwargs):
user = self.model(**kwargs)
user.set_password(password)
user.save()
return user
def create_superuser(self, password: str, **kwargs):
user = self.create_user(password=password, **kwargs)
user.is_staff = True
user.is_superuser = True
user.save(update_fields=["is_staff", "is_superuser"])
return user
class User(PermissionsMixin, AbstractBaseUser):
EMAIL_FIELD = "email"
USERNAME_FIELD = "email"
objects = UserManager()
nick = models.CharField(max_length=60, null=True, blank=True)
email = models.EmailField(
unique=True,
verbose_name=_("E-Mail"),
help_text=_(
"Your email address will be used for password resets and notification about your event/submissions."
),
)
is_active = models.BooleanField(default=True)
# For the Django admin...
is_staff = models.BooleanField(default=False)
is_superuser = models.BooleanField(default=False)
# Used for confirmations and password reminders to NOT disclose email in URL
token_uuid = models.UUIDField(default=uuid.uuid4, editable=False)
def __str__(self) -> str:
"""Use a useful string representation."""
return self.get_display_name()
def get_display_name(self) -> str:
return self.nick if self.nick else str(_("Unnamed user"))
class Meta:
verbose_name = _("User")

View file

@ -1,10 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<p>{% trans "Thanks for spending some quality time with the Web site today." %}</p>
<p><a href="{% url 'admin:index' %}">{% trans 'Log in again' %}</a></p>
{% endblock %}

View file

@ -1,39 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
{% if form.errors %}
<p>Your username and password didn't match. Please try again.</p>
{% endif %}
{% if next %}
{% if user.is_authenticated %}
<p>Your account doesn't have access to this page. To proceed,
please login with an account that has access.</p>
{% else %}
<p>Please login to see this page.</p>
{% endif %}
{% endif %}
<form method="post" action="{% url 'users:login' %}">
{% csrf_token %}
<table>
<tr>
<td>{{ form.username.label_tag }}</td>
<td>{{ form.username }}</td>
</tr>
<tr>
<td>{{ form.password.label_tag }}</td>
<td>{{ form.password }}</td>
</tr>
</table>
<input type="submit" value="login">
<input type="hidden" name="next" value="{{ next }}">
</form>
{# Assumes you setup the password_reset view in your URLconf #}
<p><a href="{% url 'users:password_reset' %}">Lost password?</a></p>
{% endblock %}

View file

@ -1,8 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
<p>{% trans 'Your password was changed.' %}</p>
{% endblock %}

View file

@ -1,52 +0,0 @@
{% extends "base.html" %}
{% load i18n static %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}<div id="content-main">
<form method="post">{% csrf_token %}
<div>
{% if form.errors %}
<p class="errornote">
{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
</p>
{% endif %}
<p>{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}</p>
<fieldset class="module aligned wide">
<div class="form-row">
{{ form.old_password.errors }}
{{ form.old_password.label_tag }} {{ form.old_password }}
</div>
<div class="form-row">
{{ form.new_password1.errors }}
{{ form.new_password1.label_tag }} {{ form.new_password1 }}
{% if form.new_password1.help_text %}
<div class="help">{{ form.new_password1.help_text|safe }}</div>
{% endif %}
</div>
<div class="form-row">
{{ form.new_password2.errors }}
{{ form.new_password2.label_tag }} {{ form.new_password2 }}
{% if form.new_password2.help_text %}
<div class="help">{{ form.new_password2.help_text|safe }}</div>
{% endif %}
</div>
</fieldset>
<div class="submit-row">
<input type="submit" value="{% trans 'Change my password' %}" class="default">
</div>
</div>
</form></div>
{% endblock %}

View file

@ -1,13 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
<p>{% trans "Your password has been set. You may go ahead and log in now." %}</p>
<p><a href="{{ login_url }}">{% trans 'Log in' %}</a></p>
{% endblock %}

View file

@ -1,34 +0,0 @@
{% extends "base.html" %}
{% load i18n static %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
{% if validlink %}
<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
<form method="post">{% csrf_token %}
<fieldset class="module aligned">
<div class="form-row field-password1">
{{ form.new_password1.errors }}
<label for="id_new_password1">{% trans 'New password:' %}</label>
{{ form.new_password1 }}
</div>
<div class="form-row field-password2">
{{ form.new_password2.errors }}
<label for="id_new_password2">{% trans 'Confirm password:' %}</label>
{{ form.new_password2 }}
</div>
<input type="submit" value="{% trans 'Change my password' %}">
</fieldset>
</form>
{% else %}
<p>{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}</p>
{% endif %}
{% endblock %}

View file

@ -1,12 +0,0 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block content_title %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
<p>{% trans "We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly." %}</p>
<p>{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}</p>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show more