Init commit

This commit is contained in:
mcbloch 2021-05-17 20:50:02 +02:00
commit fbe577ddda
37 changed files with 1515 additions and 0 deletions

237
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View File

16
mordor/asgi.py Normal file
View 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
View 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"]

View 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,
},
),
]

View File

34
mordor/models.py Normal file
View 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
View 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"),
}

View 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 %}

View 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 %}

View 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
View 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
View 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
View 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
View File

View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
static/mate50.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

61
templates/base.html Normal file
View 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
View File

39
users/admin.py Normal file
View 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
View 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
View 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)

View 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,
},
),
]

View 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),
]

View 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'),
),
]

View File

25
users/models.py Normal file
View 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)

View 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
View 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
View 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"))