commit fbe577ddda59fbacd322bc4ba04e6bc486854135 Author: mcbloch Date: Mon May 17 20:50:02 2021 +0200 Init commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de331aa --- /dev/null +++ b/.gitignore @@ -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. +# /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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2a59a8a --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +run: + python manage.py runserver +makemigrations: + python manage.py makemigrations mordor +migrate: + python manage.py migrate diff --git a/README.md b/README.md new file mode 100644 index 0000000..59117b9 --- /dev/null +++ b/README.md @@ -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! + diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..5d9d263 --- /dev/null +++ b/manage.py @@ -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() diff --git a/mordor/__init__.py b/mordor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mordor/asgi.py b/mordor/asgi.py new file mode 100644 index 0000000..bd6aaad --- /dev/null +++ b/mordor/asgi.py @@ -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() diff --git a/mordor/forms.py b/mordor/forms.py new file mode 100644 index 0000000..7a5451a --- /dev/null +++ b/mordor/forms.py @@ -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"] diff --git a/mordor/migrations/0001_initial.py b/mordor/migrations/0001_initial.py new file mode 100644 index 0000000..b0d889e --- /dev/null +++ b/mordor/migrations/0001_initial.py @@ -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, + }, + ), + ] diff --git a/mordor/migrations/__init__.py b/mordor/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mordor/models.py b/mordor/models.py new file mode 100644 index 0000000..044e50a --- /dev/null +++ b/mordor/models.py @@ -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" diff --git a/mordor/settings.py b/mordor/settings.py new file mode 100644 index 0000000..92cf6c7 --- /dev/null +++ b/mordor/settings.py @@ -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"), +} diff --git a/mordor/templates/mordor/index.html b/mordor/templates/mordor/index.html new file mode 100644 index 0000000..f5c86de --- /dev/null +++ b/mordor/templates/mordor/index.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% block title %}Home{% endblock %} + +{% block content %} + + + + + + +{% endblock %} diff --git a/mordor/templates/mordor/orders.html b/mordor/templates/mordor/orders.html new file mode 100644 index 0000000..7bd7de8 --- /dev/null +++ b/mordor/templates/mordor/orders.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %}Orders{% endblock %} + +{% block content %} +
+ {% if orders %} + {% for order in orders %} +
+ {{ order.created_at.date }} +
+
+
{{ order.amount_33 }}x 33cl = €{{ order.price_33 }}
+
{{ order.amount_50 }}x 50cl = €{{ order.price_50 }}
+
+
+ € {{ order.total_price }}
+
+
+ {% csrf_token %} + +
+
+ {% endfor %} + {% else %} +

Nog geen orders geplaatst

+ {% endif %} +
+{% endblock %} diff --git a/mordor/templates/mordor/winkel.html b/mordor/templates/mordor/winkel.html new file mode 100644 index 0000000..09c5b13 --- /dev/null +++ b/mordor/templates/mordor/winkel.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} + +{% load static %} + +{% block title %}Winkel{% endblock %} + +{% block content %} + + +
+ {% csrf_token %} +
+ +
+
+
+
+ = + 0 +
+ + +
+
+ + + + x + €25 +
+
+ =€ + 0 +
+ + +
+
+ + + + x + €30 +
+
+ =€ + 0 +
+
+{% endblock %} diff --git a/mordor/urls.py b/mordor/urls.py new file mode 100644 index 0000000..12962cc --- /dev/null +++ b/mordor/urls.py @@ -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/", views.remove_order, name="remove_order"), + path("admin/", admin.site.urls), + path("login/zeus/", include("oauth.urls")), + path("user/", include("users.urls")), +] diff --git a/mordor/views.py b/mordor/views.py new file mode 100644 index 0000000..2f6f067 --- /dev/null +++ b/mordor/views.py @@ -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}) diff --git a/mordor/wsgi.py b/mordor/wsgi.py new file mode 100644 index 0000000..9795536 --- /dev/null +++ b/mordor/wsgi.py @@ -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() diff --git a/oauth/__init__.py b/oauth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/oauth/templates/oauth/failed.html b/oauth/templates/oauth/failed.html new file mode 100644 index 0000000..aee4cd6 --- /dev/null +++ b/oauth/templates/oauth/failed.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block title %}Login Failed{% endblock %} + +{% block content %} +

Login Failed

+ + {% if error %} +

{{ error }}

+ {% endif %} +{% endblock %} diff --git a/oauth/urls.py b/oauth/urls.py new file mode 100644 index 0000000..11afb10 --- /dev/null +++ b/oauth/urls.py @@ -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), +] diff --git a/oauth/views.py b/oauth/views.py new file mode 100644 index 0000000..6cca9c1 --- /dev/null +++ b/oauth/views.py @@ -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() diff --git a/static/main.css b/static/main.css new file mode 100644 index 0000000..bd7d731 --- /dev/null +++ b/static/main.css @@ -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; +} diff --git a/static/mate33.jpg b/static/mate33.jpg new file mode 100644 index 0000000..ef9a24f Binary files /dev/null and b/static/mate33.jpg differ diff --git a/static/mate50.jpeg b/static/mate50.jpeg new file mode 100644 index 0000000..1243c9e Binary files /dev/null and b/static/mate50.jpeg differ diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..9ef2ee5 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,61 @@ +{% load static %} + + + + + + {% block title %}{%endblock%} - Mordor + + + + + + {% block styles %}{% endblock %} + + + + +
+

Mordor

+
Mate order tool
+ +
+
+ {% block content %}{% endblock %} +
+ + diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/admin.py b/users/admin.py new file mode 100644 index 0000000..ed78d41 --- /dev/null +++ b/users/admin.py @@ -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) diff --git a/users/forms.py b/users/forms.py new file mode 100644 index 0000000..09800a3 --- /dev/null +++ b/users/forms.py @@ -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) diff --git a/users/managers.py b/users/managers.py new file mode 100644 index 0000000..bdf2ec7 --- /dev/null +++ b/users/managers.py @@ -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) diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py new file mode 100644 index 0000000..b7c039c --- /dev/null +++ b/users/migrations/0001_initial.py @@ -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, + }, + ), + ] diff --git a/users/migrations/0002_seed_admin_and_zeus_board.py b/users/migrations/0002_seed_admin_and_zeus_board.py new file mode 100644 index 0000000..904e572 --- /dev/null +++ b/users/migrations/0002_seed_admin_and_zeus_board.py @@ -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), + ] diff --git a/users/migrations/0003_alter_customuser_id.py b/users/migrations/0003_alter_customuser_id.py new file mode 100644 index 0000000..d575122 --- /dev/null +++ b/users/migrations/0003_alter_customuser_id.py @@ -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'), + ), + ] diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/models.py b/users/models.py new file mode 100644 index 0000000..dd33200 --- /dev/null +++ b/users/models.py @@ -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 "".format(self.username) diff --git a/users/templates/users/profile.html b/users/templates/users/profile.html new file mode 100644 index 0000000..fced0b7 --- /dev/null +++ b/users/templates/users/profile.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %}Profiel{% endblock %} + +{% block content %} +

Profiel

+ {% if user.is_authenticated %} +

Gebruikersnaam: {{ user.username }}

+ +
+ {% csrf_token %} + {{ form }} + +
+ {% else %} +

Niet ingelogd

+ {% endif %} +{% endblock %} + diff --git a/users/urls.py b/users/urls.py new file mode 100644 index 0000000..c3a2a65 --- /dev/null +++ b/users/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import views + +app_name = "users" + +urlpatterns = [path("logout", views.logout_view, name="logout")] diff --git a/users/views.py b/users/views.py new file mode 100644 index 0000000..c6ecb56 --- /dev/null +++ b/users/views.py @@ -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"))