Init commit
This commit is contained in:
commit
fbe577ddda
37 changed files with 1515 additions and 0 deletions
237
.gitignore
vendored
Normal file
237
.gitignore
vendored
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
*.log
|
||||||
|
tmp/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/django,python
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=django,python
|
||||||
|
|
||||||
|
### Django ###
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
media
|
||||||
|
|
||||||
|
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||||
|
# in your Git repository. Update and uncomment the following line accordingly.
|
||||||
|
# <django-project-name>/staticfiles/
|
||||||
|
|
||||||
|
### Django.Python Stack ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
pytestdebug.log
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
doc/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
# .env
|
||||||
|
.env/
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
pythonenv*
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# operating system-related files
|
||||||
|
# file properties cache/storage on macOS
|
||||||
|
*.DS_Store
|
||||||
|
# thumbnail cache on Windows
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# profiling data
|
||||||
|
.prof
|
||||||
|
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
# .env
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
|
||||||
|
# operating system-related files
|
||||||
|
# file properties cache/storage on macOS
|
||||||
|
# thumbnail cache on Windows
|
||||||
|
|
||||||
|
# profiling data
|
||||||
|
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/django,python
|
6
Makefile
Normal file
6
Makefile
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
run:
|
||||||
|
python manage.py runserver
|
||||||
|
makemigrations:
|
||||||
|
python manage.py makemigrations mordor
|
||||||
|
migrate:
|
||||||
|
python manage.py migrate
|
8
README.md
Normal file
8
README.md
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Mordor
|
||||||
|
|
||||||
|
Mate order tool
|
||||||
|
|
||||||
|
> Its Black Gates are protected by more than just Orcs. There is evil there that does not sleep, and the Eye of Sauron is ever watchful
|
||||||
|
|
||||||
|
> One does not simply walk into Mordor!
|
||||||
|
|
22
manage.py
Executable file
22
manage.py
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mordor.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
0
mordor/__init__.py
Normal file
0
mordor/__init__.py
Normal file
16
mordor/asgi.py
Normal file
16
mordor/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for mordor project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mordor.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
12
mordor/forms.py
Normal file
12
mordor/forms.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
|
||||||
|
from django.forms import ModelForm
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class OrderForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = models.Order
|
||||||
|
fields = ["amount_33", "amount_50"]
|
31
mordor/migrations/0001_initial.py
Normal file
31
mordor/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# Generated by Django 3.2 on 2021-05-17 18:41
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Order',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True)),
|
||||||
|
('amount_33', models.IntegerField(blank=True, default=0)),
|
||||||
|
('amount_50', models.IntegerField(blank=True, default=0)),
|
||||||
|
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
0
mordor/migrations/__init__.py
Normal file
0
mordor/migrations/__init__.py
Normal file
34
mordor/models.py
Normal file
34
mordor/models.py
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
from users.models import CustomUser
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class TimeStampMixin(models.Model):
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class Order(TimeStampMixin):
|
||||||
|
amount_33 = models.IntegerField(default=0, blank=True)
|
||||||
|
amount_50 = models.IntegerField(default=0, blank=True)
|
||||||
|
user = models.ForeignKey(CustomUser, null=True, on_delete=models.SET_NULL)
|
||||||
|
|
||||||
|
def price_33(self):
|
||||||
|
return self.amount_33 * 25
|
||||||
|
|
||||||
|
def price_50(self):
|
||||||
|
return self.amount_50 * 30
|
||||||
|
|
||||||
|
def total_price(self):
|
||||||
|
return self.price_33() + self.price_50()
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.amount_33 is None:
|
||||||
|
self.amount_33 = 0
|
||||||
|
if self.amount_50 is None:
|
||||||
|
self.amount_50 = 0
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Order {self.amount_33}x33cl, {self.amount_50}x50cl"
|
147
mordor/settings.py
Normal file
147
mordor/settings.py
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
"""
|
||||||
|
Django settings for mordor project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 3.2.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.2/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/3.2/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = "django-insecure-43206vz5-rhg6a2s$w&a2#*+--0)-#glqcgur=%glo9-x6(*2h"
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = os.getenv("DEBUG", "1") == "1"
|
||||||
|
|
||||||
|
_allowed_hosts = os.getenv("ALLOWED_HOSTS")
|
||||||
|
ALLOWED_HOSTS = _allowed_hosts.split(",") if _allowed_hosts else []
|
||||||
|
|
||||||
|
OWN_APPS = ["mordor", "users", "oauth"]
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
] + OWN_APPS
|
||||||
|
|
||||||
|
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 = "mordor.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [os.path.join(BASE_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",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "mordor.wsgi.application"
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.sqlite3",
|
||||||
|
"NAME": BASE_DIR / "db.sqlite3",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/3.2/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "nl-be"
|
||||||
|
|
||||||
|
TIME_ZONE = "Europe/Brussels"
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/3.2/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = "/static/"
|
||||||
|
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static")]
|
||||||
|
STATIC_ROOT = os.getenv("STATIC_ROOT")
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
# OAuth
|
||||||
|
|
||||||
|
BASE_URL = os.getenv("BASE_URL", "http://localhost:8000")
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = "users.CustomUser"
|
||||||
|
|
||||||
|
_BASE_OAUTH_URL = os.getenv("OAUTH_BASE_URL", "https://adams.ugent.be/oauth")
|
||||||
|
|
||||||
|
OAUTH = {
|
||||||
|
"USER_API_URI": f"{_BASE_OAUTH_URL}/api/current_user/",
|
||||||
|
"ACCESS_TOKEN_URI": f"{_BASE_OAUTH_URL}/oauth2/token/",
|
||||||
|
"AUTHORIZE_URI": f"{_BASE_OAUTH_URL}/oauth2/authorize/",
|
||||||
|
"REDIRECT_URI": f"{BASE_URL}/login/zeus/authorized",
|
||||||
|
"CLIENT_ID": os.getenv("OAUTH_CLIENT_ID", "tomtest"),
|
||||||
|
"CLIENT_SECRET": os.getenv("OAUTH_CLIENT_SECRET", "blargh"),
|
||||||
|
}
|
12
mordor/templates/mordor/index.html
Normal file
12
mordor/templates/mordor/index.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Home{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
|
||||||
|
<a href="{% url 'oauth:login' %}" >
|
||||||
|
<button style="margin-top: 4em;" class="big_but">Ik ben zeus</button>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{% endblock %}
|
29
mordor/templates/mordor/orders.html
Normal file
29
mordor/templates/mordor/orders.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Orders{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<main class="shop">
|
||||||
|
{% if orders %}
|
||||||
|
{% for order in orders %}
|
||||||
|
<div class="leftcell">
|
||||||
|
{{ order.created_at.date }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>{{ order.amount_33 }}x 33cl = €{{ order.price_33 }}</div>
|
||||||
|
<div>{{ order.amount_50 }}x 50cl = €{{ order.price_50 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="midcell">
|
||||||
|
€ {{ order.total_price }}</span></div>
|
||||||
|
<div class="rightcell">
|
||||||
|
<form action="{% url 'remove_order' order.id %}" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit">annuleer</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p>Nog geen orders geplaatst</p>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
70
mordor/templates/mordor/winkel.html
Normal file
70
mordor/templates/mordor/winkel.html
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Winkel{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<script>
|
||||||
|
let sum_33 = 0
|
||||||
|
let sum_55 = 0
|
||||||
|
let total_sum = 0
|
||||||
|
|
||||||
|
function input_changed() {
|
||||||
|
sum_33 = Number(document.getElementById(`amount_33`).value) * 25;
|
||||||
|
sum_50 = Number(document.getElementById(`amount_50`).value) * 30;
|
||||||
|
|
||||||
|
let total_field_33 = document.getElementById(`sum_33`);
|
||||||
|
total_field_33.innerText = sum_33;
|
||||||
|
let total_field_50 = document.getElementById(`sum_50`);
|
||||||
|
total_field_50.innerText = sum_50;
|
||||||
|
|
||||||
|
|
||||||
|
let total_sum = document.getElementById(`total_sum`);
|
||||||
|
total_sum.innerText = sum_33 + sum_50;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="cart" action="{% url 'winkel' %}" method='post'>
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="leftcell buy_button_cell">
|
||||||
|
<button class="buy_button" style="font-size:95%">Bestel</button>
|
||||||
|
</div>
|
||||||
|
<div class="total_quantity">
|
||||||
|
</div>
|
||||||
|
<div class="rightcell total_price">
|
||||||
|
<span class="bold equals">=</span><span> €</span>
|
||||||
|
<span id="total_sum">0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="leftcell"><img class="photo" src="{% static 'mate33.jpg' %}"></div>
|
||||||
|
<div class="midcell">
|
||||||
|
<span class="quantity">
|
||||||
|
<input id="amount_33" name="amount_33" type="number" value="0" placeholder="0" onfocus="this.placeholder=''"
|
||||||
|
onblur="this.placeholder='0'" oninput="input_changed()">
|
||||||
|
</span>
|
||||||
|
<span>x</span>
|
||||||
|
<span class="unit_price">€25</span>
|
||||||
|
</div>
|
||||||
|
<div class="rightcell price">
|
||||||
|
<span class="bold equals">=</span>€
|
||||||
|
<span id="sum_33">0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="leftcell"><img class="photo" src="{% static 'mate50.jpeg' %}"></div>
|
||||||
|
<div class="midcell">
|
||||||
|
<span class="quantity">
|
||||||
|
<input id="amount_50" name="amount_50" type="number" value="0" placeholder="0" onfocus="this.placeholder=''"
|
||||||
|
onblur="this.placeholder='0'" oninput="input_changed()">
|
||||||
|
</span>
|
||||||
|
<span>x</span>
|
||||||
|
<span class="unit_price">€30</span>
|
||||||
|
</div>
|
||||||
|
<div class="rightcell price">
|
||||||
|
<span class="bold equals">=</span>€
|
||||||
|
<span id="sum_50">0</span>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
30
mordor/urls.py
Normal file
30
mordor/urls.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
"""mordor URL Configuration
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/3.2/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "mordor"
|
||||||
|
urlpatterns = [
|
||||||
|
path("", views.index, name="index"),
|
||||||
|
path("winkel", views.winkel, name="winkel"),
|
||||||
|
path("orders", views.orders, name="orders"),
|
||||||
|
path("remove_order/<int:order_id>", views.remove_order, name="remove_order"),
|
||||||
|
path("admin/", admin.site.urls),
|
||||||
|
path("login/zeus/", include("oauth.urls")),
|
||||||
|
path("user/", include("users.urls")),
|
||||||
|
]
|
46
mordor/views.py
Normal file
46
mordor/views.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
from django.http import HttpResponse, HttpResponseRedirect
|
||||||
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from mordor.models import Order
|
||||||
|
from mordor.forms import OrderForm
|
||||||
|
|
||||||
|
|
||||||
|
def index(req):
|
||||||
|
if not req.user.is_authenticated:
|
||||||
|
return render(req, "mordor/index.html")
|
||||||
|
else:
|
||||||
|
return HttpResponseRedirect(reverse("winkel"))
|
||||||
|
|
||||||
|
|
||||||
|
def winkel(req):
|
||||||
|
if not req.user.is_authenticated:
|
||||||
|
return HttpResponseRedirect(reverse("index"))
|
||||||
|
|
||||||
|
if req.method == "POST":
|
||||||
|
order = Order(user=req.user)
|
||||||
|
form = OrderForm(req.POST, instance=order)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
return HttpResponseRedirect(reverse("orders"))
|
||||||
|
else:
|
||||||
|
return render(req, "mordor/winkel.html")
|
||||||
|
|
||||||
|
|
||||||
|
def remove_order(request, order_id):
|
||||||
|
if request.method != "POST":
|
||||||
|
return HttpResponse(status_code=405)
|
||||||
|
|
||||||
|
order = get_object_or_404(Order, id=order_id)
|
||||||
|
order.delete()
|
||||||
|
return HttpResponseRedirect(reverse("orders"))
|
||||||
|
|
||||||
|
|
||||||
|
def orders(req):
|
||||||
|
if not req.user.is_authenticated:
|
||||||
|
return HttpResponseRedirect(reverse("index"))
|
||||||
|
|
||||||
|
user_orders = Order.objects.filter(user=req.user)
|
||||||
|
|
||||||
|
return render(req, "mordor/orders.html", {"orders": user_orders})
|
16
mordor/wsgi.py
Normal file
16
mordor/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for mordor project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mordor.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
0
oauth/__init__.py
Normal file
0
oauth/__init__.py
Normal file
11
oauth/templates/oauth/failed.html
Normal file
11
oauth/templates/oauth/failed.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Login Failed{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Login Failed</h2>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<h3>{{ error }}</h3>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
10
oauth/urls.py
Normal file
10
oauth/urls.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from django.contrib.auth import logout
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "oauth"
|
||||||
|
urlpatterns = [
|
||||||
|
path('register', views.register, name='login'),
|
||||||
|
path('authorized', views.register_callback),
|
||||||
|
]
|
86
oauth/views.py
Normal file
86
oauth/views.py
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import login
|
||||||
|
from django.http.request import HttpRequest
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
|
|
||||||
|
import users
|
||||||
|
from users.models import CustomUser
|
||||||
|
|
||||||
|
logger = logging.getLogger(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def register(_):
|
||||||
|
RESPONSE_TYPE = 'code'
|
||||||
|
return redirect(f'{settings.OAUTH["AUTHORIZE_URI"]}?'
|
||||||
|
f'response_type={RESPONSE_TYPE}&'
|
||||||
|
f'client_id={settings.OAUTH["CLIENT_ID"]}&'
|
||||||
|
f'redirect_uri={settings.OAUTH["REDIRECT_URI"]}')
|
||||||
|
|
||||||
|
|
||||||
|
def register_callback(req: HttpRequest):
|
||||||
|
if 'code' not in req.GET or 'error' in req.GET:
|
||||||
|
error = req.GET['error'] if 'error' in req.GET else None
|
||||||
|
return login_fail(req, error)
|
||||||
|
|
||||||
|
try:
|
||||||
|
access_token = get_access_token(req.GET['code'])
|
||||||
|
user_info = get_user_info(access_token)
|
||||||
|
|
||||||
|
logger.debug(f'Succesfully authenticated user: {user_info["username"]} with id: {user_info["id"]}')
|
||||||
|
|
||||||
|
validated_user = validate_user(user_info['id'], user_info['username'])
|
||||||
|
login(req, validated_user)
|
||||||
|
return redirect('/')
|
||||||
|
except OAuthException as e:
|
||||||
|
logger.error(e)
|
||||||
|
return login_fail(req, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def login_fail(request, error: str = None):
|
||||||
|
return render(request, "oauth/failed.html", {'error': error})
|
||||||
|
|
||||||
|
|
||||||
|
def validate_user(zeus_id, username) -> CustomUser:
|
||||||
|
try:
|
||||||
|
user = CustomUser.objects.get(zeus_id=zeus_id)
|
||||||
|
user.username = username
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
except users.models.CustomUser.DoesNotExist:
|
||||||
|
return CustomUser.objects.create_user(zeus_id, username)
|
||||||
|
|
||||||
|
|
||||||
|
def get_access_token(code):
|
||||||
|
response = requests.post(
|
||||||
|
settings.OAUTH["ACCESS_TOKEN_URI"],
|
||||||
|
data={'code': code,
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'client_id': settings.OAUTH["CLIENT_ID"],
|
||||||
|
'client_secret': settings.OAUTH["CLIENT_SECRET"],
|
||||||
|
'redirect_uri': settings.OAUTH["REDIRECT_URI"]})
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise OAuthException(
|
||||||
|
f'Status code {response.status_code} when requesting access token.\nresponse: {response.text}')
|
||||||
|
if 'access_token' not in response.json():
|
||||||
|
raise OAuthException('Got status code 200 but no access_token')
|
||||||
|
return response.json()['access_token']
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_info(access_token):
|
||||||
|
response = requests.get(
|
||||||
|
settings.OAUTH["USER_API_URI"],
|
||||||
|
headers={'Authorization': f'Bearer {access_token}'},
|
||||||
|
)
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise OAuthException(
|
||||||
|
f'Status code {response.status_code} when requesting user info.\nresponse: {response.text}')
|
||||||
|
if 'username' not in response.json() or 'id' not in response.json():
|
||||||
|
raise OAuthException(f'username and id are expected values: {response.json()}')
|
||||||
|
return response.json()
|
239
static/main.css
Normal file
239
static/main.css
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
body {
|
||||||
|
font-family: "Helvetica", "Arial", "Roboto", sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #cf863e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*input {
|
||||||
|
width: 4em;
|
||||||
|
border: 0;
|
||||||
|
background-color: #CF863E;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-bottom: 5vh;
|
||||||
|
border-right: none;
|
||||||
|
border-top: none;
|
||||||
|
border-left: none;
|
||||||
|
background-color: transparent;
|
||||||
|
outline: none;
|
||||||
|
color: white;
|
||||||
|
caret-color: white;
|
||||||
|
width: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"] {
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
-webkit-appearance: textfield;
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-height: 500px) {
|
||||||
|
hgroup {
|
||||||
|
margin-top: 5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 125%;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emptiness {
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-height: 500px) {
|
||||||
|
.emptiness {
|
||||||
|
margin-top: 9em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
background: #ff7f00;
|
||||||
|
border: 0px solid #000;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
padding: 0.3em 0.8em 0.3em 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.big_but {
|
||||||
|
padding: 1em 2em;
|
||||||
|
min-width: 10em;
|
||||||
|
min-height: 4.7em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart {
|
||||||
|
margin: 3em auto 0;
|
||||||
|
font-weight: 400;
|
||||||
|
display: grid;
|
||||||
|
max-width: 20em;
|
||||||
|
grid-template-columns: auto 40% auto;
|
||||||
|
row-gap: 2em;
|
||||||
|
font-size: 125%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop {
|
||||||
|
margin: 3em auto 0;
|
||||||
|
font-weight: 400;
|
||||||
|
display: grid;
|
||||||
|
max-width: 30em;
|
||||||
|
grid-template-columns: auto 30% 20% auto;
|
||||||
|
row-gap: 2em;
|
||||||
|
font-size: 125%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop {
|
||||||
|
row-gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart > div,
|
||||||
|
.shop > div {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftcell {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.midcell {
|
||||||
|
height: 100px;
|
||||||
|
line-height: 100px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightcell {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop .midcell {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buy_button_cell,
|
||||||
|
.total_quantity,
|
||||||
|
.total_price {
|
||||||
|
padding-bottom: 2em;
|
||||||
|
margin-bottom: -1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total_price {
|
||||||
|
border-bottom: 1.5px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cart .quantity {
|
||||||
|
margin-right: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
border: 1.5px solid #000;
|
||||||
|
border-radius: 100%;
|
||||||
|
width: 5em;
|
||||||
|
height: 5em;
|
||||||
|
line-height: 5em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-size: 95%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop button {
|
||||||
|
padding: 0 1em 0.1em;
|
||||||
|
line-height: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.narrow {
|
||||||
|
display: inline-block;
|
||||||
|
transform: scaleX(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toonarrow {
|
||||||
|
display: inline-block;
|
||||||
|
transform: scaleX(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equals {
|
||||||
|
display: inline-block;
|
||||||
|
transform: scaleY(0.55);
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-right: 1em;
|
||||||
|
color: rgb(8, 8, 63);
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
nav a:before {
|
||||||
|
background-color: rgb(8, 8, 63);
|
||||||
|
content: "";
|
||||||
|
height: 2px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: -1px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
transition: width 0.2s ease-in-out;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
nav a:hover:before {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li {
|
||||||
|
list-style-type: none;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav span,
|
||||||
|
nav a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 1vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deemphasized {
|
||||||
|
color: #66351d;
|
||||||
|
}
|
BIN
static/mate33.jpg
Normal file
BIN
static/mate33.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
BIN
static/mate50.jpeg
Normal file
BIN
static/mate50.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
61
templates/base.html
Normal file
61
templates/base.html
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
{% load static %}
|
||||||
|
<!doctype html>
|
||||||
|
<html class="no-js" lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||||
|
<title>{% block title %}{%endblock%} - Mordor</title>
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'main.css' %}"/>
|
||||||
|
{% block styles %}{% endblock %}
|
||||||
|
<!-- Place favicon.ico in the root directory -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<hgroup>
|
||||||
|
<h1>Mordor</h1>
|
||||||
|
<div class="subtitle">Mate order tool</div>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'winkel' %}">Winkel</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'orders' %}">Mijn orders</a>
|
||||||
|
</li>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<li class="deemphasized">
|
||||||
|
<span>
|
||||||
|
Hallo, {{ user.username }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% if user.is_staff or user.is_superuser %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'admin:index' %}">
|
||||||
|
Admininterface
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'users:logout' %}">
|
||||||
|
Afmelden
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'oauth:login' %}">
|
||||||
|
Inloggen
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</hgroup>
|
||||||
|
<main>
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
0
users/__init__.py
Normal file
0
users/__init__.py
Normal file
39
users/admin.py
Normal file
39
users/admin.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
|
||||||
|
from .forms import CustomUserCreationForm, CustomUserChangeForm
|
||||||
|
from .models import CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserAdmin(UserAdmin):
|
||||||
|
add_form = CustomUserCreationForm
|
||||||
|
form = CustomUserChangeForm
|
||||||
|
model = CustomUser
|
||||||
|
list_display = ("username", "is_staff", "is_superuser")
|
||||||
|
list_filter = ("username", "is_staff")
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"username",
|
||||||
|
"password",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("Permissions", {"fields": ("is_staff", "is_superuser")}),
|
||||||
|
)
|
||||||
|
add_fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"classes": ("wide",),
|
||||||
|
"fields": ("username", "password1", "password2", "is_staff"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
search_fields = ("username",)
|
||||||
|
ordering = ("username",)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(CustomUser, CustomUserAdmin)
|
84
users/forms.py
Normal file
84
users/forms.py
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
|
||||||
|
from django.forms import ModelForm, TextInput
|
||||||
|
|
||||||
|
from .models import CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserCreationForm(UserCreationForm):
|
||||||
|
class Meta(UserCreationForm):
|
||||||
|
model = CustomUser
|
||||||
|
fields = ("username",)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserChangeForm(UserChangeForm):
|
||||||
|
class Meta:
|
||||||
|
model = CustomUser
|
||||||
|
fields = ("username",)
|
||||||
|
|
||||||
|
|
||||||
|
class UserMetaForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = CustomUser
|
||||||
|
fields = []
|
||||||
|
widgets = {}
|
||||||
|
|
||||||
|
|
||||||
|
# Add user to groups in django admin
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
# Create ModelForm based on the Group model.
|
||||||
|
class GroupAdminForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Group
|
||||||
|
exclude = []
|
||||||
|
|
||||||
|
# Add the users field.
|
||||||
|
users = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
required=False,
|
||||||
|
# Use the pretty 'filter_horizontal widget'.
|
||||||
|
widget=FilteredSelectMultiple("users", False),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# Do the normal form initialisation.
|
||||||
|
super(GroupAdminForm, self).__init__(*args, **kwargs)
|
||||||
|
# If it is an existing group (saved objects have a pk).
|
||||||
|
if self.instance.pk:
|
||||||
|
# Populate the users field with the current Group users.
|
||||||
|
self.fields["users"].initial = self.instance.user_set.all()
|
||||||
|
|
||||||
|
def save_m2m(self):
|
||||||
|
# Add the users to the Group.
|
||||||
|
self.instance.user_set.set(self.cleaned_data["users"])
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# Default save
|
||||||
|
instance = super(GroupAdminForm, self).save()
|
||||||
|
# Save many-to-many data
|
||||||
|
self.save_m2m()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
# Unregister the original Group admin.
|
||||||
|
admin.site.unregister(Group)
|
||||||
|
|
||||||
|
|
||||||
|
# Create a new Group admin.
|
||||||
|
class GroupAdmin(admin.ModelAdmin):
|
||||||
|
# Use our custom form.
|
||||||
|
form = GroupAdminForm
|
||||||
|
# Filter permissions horizontal as well.
|
||||||
|
filter_horizontal = ["permissions"]
|
||||||
|
|
||||||
|
|
||||||
|
# Register the new Group ModelAdmin.
|
||||||
|
admin.site.register(Group, GroupAdmin)
|
33
users/managers.py
Normal file
33
users/managers.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
from django.contrib.auth.base_user import BaseUserManager
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUserManager(BaseUserManager):
|
||||||
|
"""
|
||||||
|
Custom user model manager where email is the unique identifiers
|
||||||
|
for authentication instead of usernames.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def create_user(self, zeus_id, username, password=None, **extra_fields):
|
||||||
|
"""
|
||||||
|
Create and save a User with the given email and password.
|
||||||
|
"""
|
||||||
|
if zeus_id is None or username is None:
|
||||||
|
raise ValueError(_('The zeus_id and username must be set'))
|
||||||
|
user = self.model(zeus_id=zeus_id, username=username, password=password, **extra_fields)
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
def create_superuser(self, zeus_id, username, password, **extra_fields):
|
||||||
|
"""
|
||||||
|
Create and save a SuperUser with the given email and password.
|
||||||
|
"""
|
||||||
|
extra_fields.setdefault('is_staff', True)
|
||||||
|
extra_fields.setdefault('is_superuser', True)
|
||||||
|
|
||||||
|
if extra_fields.get('is_staff') is not True:
|
||||||
|
raise ValueError(_('Superuser must have is_staff=True.'))
|
||||||
|
if extra_fields.get('is_superuser') is not True:
|
||||||
|
raise ValueError(_('Superuser must have is_superuser=True.'))
|
||||||
|
return self.create_user(zeus_id, username, password, **extra_fields)
|
77
users/migrations/0001_initial.py
Normal file
77
users/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
# Generated by Django 3.0.8 on 2020-07-22 16:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("auth", "0011_update_proxy_permissions"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="CustomUser",
|
||||||
|
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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"is_superuser",
|
||||||
|
models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||||
|
verbose_name="superuser status",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("zeus_id", models.IntegerField(null=True, unique=True)),
|
||||||
|
("is_staff", models.BooleanField(default=False)),
|
||||||
|
("username", models.CharField(max_length=50, unique=True)),
|
||||||
|
(
|
||||||
|
"date_joined",
|
||||||
|
models.DateTimeField(default=django.utils.timezone.now),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"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={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
75
users/migrations/0002_seed_admin_and_zeus_board.py
Normal file
75
users/migrations/0002_seed_admin_and_zeus_board.py
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
# Created manually
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.management.sql import emit_post_migrate_signal
|
||||||
|
from django.db import migrations
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
ENV_USERNAME = "MORDOR_ADMIN_USERNAME"
|
||||||
|
ENV_PASSWORD = "MORDOR_ADMIN_PASSWORD"
|
||||||
|
|
||||||
|
|
||||||
|
def create_superuser(apps, schema_editor):
|
||||||
|
superuser = get_user_model()(
|
||||||
|
is_superuser=True,
|
||||||
|
is_staff=True,
|
||||||
|
username=os.environ.get(ENV_USERNAME, "admin"),
|
||||||
|
last_login=timezone.now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
dev_password = "admin"
|
||||||
|
password = os.environ.get(ENV_PASSWORD, dev_password)
|
||||||
|
if password == dev_password:
|
||||||
|
log = logger.warning if settings.DEBUG else logger.error
|
||||||
|
log(
|
||||||
|
f"Admin password is '{password}'. This is not for use in production. Set environment variable {ENV_PASSWORD} to choose a different password."
|
||||||
|
)
|
||||||
|
if not settings.DEBUG:
|
||||||
|
raise Exception("Development admin password used in production")
|
||||||
|
|
||||||
|
superuser.set_password(password)
|
||||||
|
superuser.save()
|
||||||
|
|
||||||
|
|
||||||
|
kers_group_permissions: Dict[str, List] = {
|
||||||
|
"Bestuur": [
|
||||||
|
# "add_event",
|
||||||
|
# "delete_event",
|
||||||
|
# "change_event",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def add_group_permissions(apps, schema_editor):
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
emit_post_migrate_signal(2, False, db_alias)
|
||||||
|
|
||||||
|
Group = apps.get_model("auth", "Group")
|
||||||
|
Permission = apps.get_model("auth", "Permission")
|
||||||
|
|
||||||
|
for group in kers_group_permissions:
|
||||||
|
role, created = Group.objects.get_or_create(name=group)
|
||||||
|
logger.info(f'{group} Group {"created" if created else "exists"}')
|
||||||
|
for perm in kers_group_permissions[group]:
|
||||||
|
role.permissions.add(Permission.objects.get(codename=perm))
|
||||||
|
logger.info(f"Permitting {group} to {perm}")
|
||||||
|
role.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("users", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(create_superuser),
|
||||||
|
migrations.RunPython(add_group_permissions),
|
||||||
|
]
|
18
users/migrations/0003_alter_customuser_id.py
Normal file
18
users/migrations/0003_alter_customuser_id.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2 on 2021-05-17 18:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0002_seed_admin_and_zeus_board'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='customuser',
|
||||||
|
name='id',
|
||||||
|
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||||
|
),
|
||||||
|
]
|
0
users/migrations/__init__.py
Normal file
0
users/migrations/__init__.py
Normal file
25
users/models.py
Normal file
25
users/models.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
from django.contrib.auth.base_user import AbstractBaseUser
|
||||||
|
from django.db import models
|
||||||
|
from django.contrib.auth.models import User, PermissionsMixin
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from users.managers import CustomUserManager
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUser(AbstractBaseUser, PermissionsMixin):
|
||||||
|
zeus_id = models.IntegerField(unique=True, null=True)
|
||||||
|
is_staff = models.BooleanField(default=False)
|
||||||
|
username = models.CharField(max_length=50, unique=True) # zeus username
|
||||||
|
|
||||||
|
date_joined = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
USERNAME_FIELD = "username"
|
||||||
|
REQUIRED_FIELDS = ["zeus_id"]
|
||||||
|
|
||||||
|
objects = CustomUserManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.username
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<User: {}>".format(self.username)
|
19
users/templates/users/profile.html
Normal file
19
users/templates/users/profile.html
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Profiel{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Profiel</h2>
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<p>Gebruikersnaam: {{ user.username }}</p>
|
||||||
|
|
||||||
|
<form action="/user/profile" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form }}
|
||||||
|
<button type="submit">Bijwerken</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<p>Niet ingelogd</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
7
users/urls.py
Normal file
7
users/urls.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
app_name = "users"
|
||||||
|
|
||||||
|
urlpatterns = [path("logout", views.logout_view, name="logout")]
|
15
users/views.py
Normal file
15
users/views.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
from django.contrib.auth import logout
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from users.forms import UserMetaForm
|
||||||
|
from users.models import CustomUser
|
||||||
|
|
||||||
|
|
||||||
|
def logout_view(request):
|
||||||
|
logout(request)
|
||||||
|
|
||||||
|
return redirect(reverse("index"))
|
Loading…
Reference in a new issue