From fbe577ddda59fbacd322bc4ba04e6bc486854135 Mon Sep 17 00:00:00 2001 From: mcbloch Date: Mon, 17 May 2021 20:50:02 +0200 Subject: [PATCH] Init commit --- .gitignore | 237 +++++++++++++++++ Makefile | 6 + README.md | 8 + manage.py | 22 ++ mordor/__init__.py | 0 mordor/asgi.py | 16 ++ mordor/forms.py | 12 + mordor/migrations/0001_initial.py | 31 +++ mordor/migrations/__init__.py | 0 mordor/models.py | 34 +++ mordor/settings.py | 147 +++++++++++ mordor/templates/mordor/index.html | 12 + mordor/templates/mordor/orders.html | 29 +++ mordor/templates/mordor/winkel.html | 70 +++++ mordor/urls.py | 30 +++ mordor/views.py | 46 ++++ mordor/wsgi.py | 16 ++ oauth/__init__.py | 0 oauth/templates/oauth/failed.html | 11 + oauth/urls.py | 10 + oauth/views.py | 86 +++++++ static/main.css | 239 ++++++++++++++++++ static/mate33.jpg | Bin 0 -> 8842 bytes static/mate50.jpeg | Bin 0 -> 19505 bytes templates/base.html | 61 +++++ users/__init__.py | 0 users/admin.py | 39 +++ users/forms.py | 84 ++++++ users/managers.py | 33 +++ users/migrations/0001_initial.py | 77 ++++++ .../0002_seed_admin_and_zeus_board.py | 75 ++++++ users/migrations/0003_alter_customuser_id.py | 18 ++ users/migrations/__init__.py | 0 users/models.py | 25 ++ users/templates/users/profile.html | 19 ++ users/urls.py | 7 + users/views.py | 15 ++ 37 files changed, 1515 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100755 manage.py create mode 100644 mordor/__init__.py create mode 100644 mordor/asgi.py create mode 100644 mordor/forms.py create mode 100644 mordor/migrations/0001_initial.py create mode 100644 mordor/migrations/__init__.py create mode 100644 mordor/models.py create mode 100644 mordor/settings.py create mode 100644 mordor/templates/mordor/index.html create mode 100644 mordor/templates/mordor/orders.html create mode 100644 mordor/templates/mordor/winkel.html create mode 100644 mordor/urls.py create mode 100644 mordor/views.py create mode 100644 mordor/wsgi.py create mode 100644 oauth/__init__.py create mode 100644 oauth/templates/oauth/failed.html create mode 100644 oauth/urls.py create mode 100644 oauth/views.py create mode 100644 static/main.css create mode 100644 static/mate33.jpg create mode 100644 static/mate50.jpeg create mode 100644 templates/base.html create mode 100644 users/__init__.py create mode 100644 users/admin.py create mode 100644 users/forms.py create mode 100644 users/managers.py create mode 100644 users/migrations/0001_initial.py create mode 100644 users/migrations/0002_seed_admin_and_zeus_board.py create mode 100644 users/migrations/0003_alter_customuser_id.py create mode 100644 users/migrations/__init__.py create mode 100644 users/models.py create mode 100644 users/templates/users/profile.html create mode 100644 users/urls.py create mode 100644 users/views.py 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 0000000000000000000000000000000000000000..ef9a24f9b90bf010a862e72ea88a0fc0b4550719 GIT binary patch literal 8842 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 EAHey4_5c6? literal 0 HcmV?d00001 diff --git a/static/mate50.jpeg b/static/mate50.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1243c9e7b93a9f8ce5cea1e5750e6f79a29e173b GIT binary patch literal 19505 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 + + + + + {% 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"))