From 57ff25198a82b3f6f413440e4005f0ade8dfb8d8 Mon Sep 17 00:00:00 2001 From: pacien Date: Thu, 29 Jul 2021 18:04:48 +0200 Subject: app: render and serve proper web pages --- app/app.py | 4 + app/app_account.py | 39 ++++-- app/app_sessions.py | 27 ++++ app/app_templating.py | 51 ++++++++ app/app_wallet.py | 46 +++++-- flake.nix | 2 + readme.md | 10 +- static/images/favicon.svg | 93 +++++++++++++ static/stylesheets/grids-responsive-min.css | 7 + static/stylesheets/laundry.css | 139 ++++++++++++++++++++ static/stylesheets/main.css | 195 ++++++++++++++++++++++++++++ static/stylesheets/pepal.css | 40 ++++++ static/stylesheets/pure-min.css | 11 ++ templates/_base.html.jinja | 71 ++++++++++ templates/_fragments.html.jinja | 34 +++++ templates/homepage.html.jinja | 95 ++++++++++++++ templates/launder.html.jinja | 38 ++++++ templates/wallet.html.jinja | 140 ++++++++++++++++++++ 18 files changed, 1016 insertions(+), 26 deletions(-) create mode 100644 app/app_templating.py create mode 100644 static/images/favicon.svg create mode 100644 static/stylesheets/grids-responsive-min.css create mode 100644 static/stylesheets/laundry.css create mode 100644 static/stylesheets/main.css create mode 100644 static/stylesheets/pepal.css create mode 100644 static/stylesheets/pure-min.css create mode 100644 templates/_base.html.jinja create mode 100644 templates/_fragments.html.jinja create mode 100644 templates/homepage.html.jinja create mode 100644 templates/launder.html.jinja create mode 100644 templates/wallet.html.jinja diff --git a/app/app.py b/app/app.py index 3bf0337..b22b3cd 100644 --- a/app/app.py +++ b/app/app.py @@ -3,6 +3,7 @@ # Licence: EUPL-1.2 from fastapi import FastAPI, status +from fastapi.staticfiles import StaticFiles import app_sessions import app_account import app_wallet @@ -15,3 +16,6 @@ main.add_middleware(app_sessions.SessionManager) # Register our request handlers main.include_router(app_account.router) main.include_router(app_wallet.router) + +# Handler for static resource files (CSS, JS, ...) +main.mount('/', StaticFiles(directory='./static/'), name='static') diff --git a/app/app_account.py b/app/app_account.py index 3f4869d..e3d6433 100644 --- a/app/app_account.py +++ b/app/app_account.py @@ -3,6 +3,7 @@ # Licence: EUPL-1.2 from fastapi import APIRouter, Depends, Request, Form, status +from fastapi.responses import RedirectResponse, HTMLResponse from passlib.context import CryptContext import re @@ -10,8 +11,9 @@ import re from embrace.exceptions import IntegrityError from psycopg2.errors import UniqueViolation -from app_sessions import UserSession +from app_sessions import UserSession, FlashMessageQueue from app_database import db_transaction +from app_templating import TemplateRenderer # Password hashing context. @@ -20,42 +22,51 @@ password_ctx = CryptContext(schemes=['bcrypt'], deprecated='auto') username_pattern = re.compile(r'^[a-zA-Z0-9-_]{4,16}$') +to_homepage = RedirectResponse('/', status_code=status.HTTP_303_SEE_OTHER) +to_wallet = RedirectResponse('/wallet', status_code=status.HTTP_303_SEE_OTHER) + router = APIRouter() -@router.get('/') +@router.get('/', response_class=HTMLResponse) def homepage( session: UserSession=Depends(UserSession), + render: TemplateRenderer=Depends(TemplateRenderer), ): if session.is_logged_in(): - return 'Welcome!' + return to_wallet - return 'Homepage here.' + return render('homepage.html.jinja') @router.post('/account/register') def account_register( session: UserSession=Depends(UserSession), + messages: FlashMessageQueue=Depends(FlashMessageQueue), username: str=Form(...), password: str=Form(...), ): try: if username_pattern.match(username) is None: - return 'error: Invalid username format.' + messages.add('error', 'Invalid username format.') + return to_homepage if not 4 <= len(password) <= 32: - return 'error: Invalid password length.' + messages.add('error', 'Invalid password length.') + return to_homepage hash = password_ctx.hash(password) with db_transaction() as tx: user = tx.create_account(username=username, password_hash=hash) session.login(user.id) - return 'Account succesfully created. Welcome!' + messages.add('success', 'Account succesfully created. Welcome!') + return to_wallet except IntegrityError as exception: if isinstance(exception.__cause__, UniqueViolation): - return 'error: This username is already taken.' + messages.add('error', 'This username is already taken.') + return to_homepage else: raise exception @@ -63,6 +74,7 @@ def account_register( @router.post('/account/login') def session_login( session: UserSession=Depends(UserSession), + messages: FlashMessageQueue=Depends(FlashMessageQueue), username: str=Form(...), password: str=Form(...), ): @@ -71,17 +83,20 @@ def session_login( if user is not None and password_ctx.verify(password, user.password_hash): session.login(user.id) - return 'Welcome back!' + messages.add('info', 'Welcome back!') + return to_wallet else: - return 'error: Invalid credentials.' + messages.add('error', 'Invalid credentials.') + return to_homepage @router.post('/account/logout') def session_logout( session: UserSession=Depends(UserSession), + messages: FlashMessageQueue=Depends(FlashMessageQueue), ): if session.is_logged_in(): session.logout() - return 'You have been successfully logged out.' + messages.add('info', 'You have been successfully logged out.') - return 'Nothing to do' + return to_homepage diff --git a/app/app_sessions.py b/app/app_sessions.py index 89521fb..7a931d5 100644 --- a/app/app_sessions.py +++ b/app/app_sessions.py @@ -15,6 +15,33 @@ cookie_key = environ['COOKIE_SECRET_KEY'] SessionManager = partial(SessionMiddleware, secret_key=cookie_key) +class FlashMessageQueue: + """ + Session decorator for managing session flash messages to be displayed to + the user from one page to another. This suits confirmation and error + messages. Messages are stored in the session cookie, which is limited in + size to about 4kb. + """ + + def __init__(self, request: Request): + if 'messages' not in request.session: + request.session['messages'] = [] + + self._messages = request.session['messages'] + + def add(self, class_: str, message: str): + self._messages.append((class_, message)) + + def __iter__(self): + return self + + def __next__(self): + if not self._messages: + raise StopIteration + + return self._messages.pop(0) + + class UserSession: """ Session decorator for managing user login sessions. diff --git a/app/app_templating.py b/app/app_templating.py new file mode 100644 index 0000000..427170b --- /dev/null +++ b/app/app_templating.py @@ -0,0 +1,51 @@ +# UGE / L2 / Intro to relational databases / Python project prototype +# Author: Pacien TRAN-GIRARD +# Licence: EUPL-1.2 + +from typing import Optional, NamedTuple +from decimal import Decimal + +from fastapi import Depends, Request +from fastapi.templating import Jinja2Templates + +from app_sessions import UserSession, FlashMessageQueue +from app_database import db_transaction + + +# Load and parse Jinja HTML templates pages and fragments. +bare_templates = Jinja2Templates(directory='./templates/') + + +class TemplateRenderer: + """ + Template renderer using dependency injection to populate the template + parameters used by all pages. + """ + + def __init__( + self, + request: Request, + messages: FlashMessageQueue=Depends(FlashMessageQueue), + session: UserSession=Depends(UserSession), + ): + self._request = request + self._messages = messages + self._session = session + + def _get_user(self) -> Optional[NamedTuple]: + if not self._session.is_logged_in(): + return None + + with db_transaction() as tx: + return tx.fetch_account(user_id=self._session.get_user_id()) + + def _shared_template_args(self) -> dict: + return { + 'request': self._request, + 'messages': self._messages, + 'user': self._get_user(), + } + + def __call__(self, template: str, **kwargs): + template_args = self._shared_template_args() | kwargs + return bare_templates.TemplateResponse(template, template_args) diff --git a/app/app_wallet.py b/app/app_wallet.py index 9dffbd5..922c683 100644 --- a/app/app_wallet.py +++ b/app/app_wallet.py @@ -5,36 +5,45 @@ from decimal import Decimal from fastapi import APIRouter, Form, Depends, status +from fastapi.responses import RedirectResponse, HTMLResponse from embrace.exceptions import IntegrityError from psycopg2.errors import CheckViolation from psycopg2.extensions import ISOLATION_LEVEL_SERIALIZABLE -from app_sessions import UserSession +from app_sessions import UserSession, FlashMessageQueue from app_database import db_transaction +from app_templating import TemplateRenderer +to_wallet = RedirectResponse('/wallet', status_code=status.HTTP_303_SEE_OTHER) + router = APIRouter() # TODO: add paging for the transaction history -@router.get('/wallet') +@router.get('/wallet', response_class=HTMLResponse) def wallet( session: UserSession=Depends(UserSession.authenticated), + messages: FlashMessageQueue=Depends(FlashMessageQueue), + render: TemplateRenderer=Depends(TemplateRenderer), ): with db_transaction() as tx: history = tx.fetch_transactions(user_id=session.get_user_id()) - return list(history) + # rendering done in the transaction to make use the iteration cursor + return render('wallet.html.jinja', transactions=history) -@router.post('/wallet/transfer') +@router.post('/wallet/transfer', response_class=HTMLResponse) def wallet_transfer( session: UserSession=Depends(UserSession.authenticated), + messages: FlashMessageQueue=Depends(FlashMessageQueue), recipient: str=Form(...), amount: Decimal=Form(...), ): if amount <= 0: - return 'error: Invalid transaction amount.' + messages.add('error', 'Invalid transaction amount.') + return to_wallet try: with db_transaction(ISOLATION_LEVEL_SERIALIZABLE) as tx: @@ -48,14 +57,17 @@ def wallet_transfer( amount=amount, fee=amount * Decimal(0.10)) - return 'Your business is appreciated.' + messages.add('success', 'Your business is appreciated.') + return to_wallet except LookupError as exception: - return 'error: ' + str(exception) + messages.add('error', str(exception)) + return to_wallet except IntegrityError as exception: if isinstance(exception.__cause__, CheckViolation): - return 'error: Insufficient funds.' + messages.add('error', 'Insufficient funds.') + return to_wallet else: raise exception @@ -63,33 +75,41 @@ def wallet_transfer( @router.post('/wallet/deposit') def wallet_deposit( session: UserSession=Depends(UserSession.authenticated), + messages: FlashMessageQueue=Depends(FlashMessageQueue), amount: Decimal=Form(...), ): if amount <= 0: - return 'error: Invalid transaction amount.' + messages.add('error', 'Invalid transaction amount.') + return to_wallet with db_transaction(ISOLATION_LEVEL_SERIALIZABLE) as tx: tx.deposit(user_id=session.get_user_id(), amount=amount) - return 'Your business is appreciated.' + messages.add('success', 'Your business is appreciated.') + return to_wallet @router.post('/wallet/withdraw') def wallet_withdraw( session: UserSession=Depends(UserSession.authenticated), + messages: FlashMessageQueue=Depends(FlashMessageQueue), + render: TemplateRenderer=Depends(TemplateRenderer), amount: Decimal=Form(...), ): if amount <= 0: - return 'error: Invalid transaction amount.' + messages.add('error', 'Invalid transaction amount.') + return to_wallet try: with db_transaction(ISOLATION_LEVEL_SERIALIZABLE) as tx: tx.withdraw(user_id=session.get_user_id(), amount=amount) - return 'Annnnnd... It\'s gone.' + messages.add('success', 'Annnnnd... It\'s gone.') + return render('launder.html.jinja') except IntegrityError as exception: if isinstance(exception.__cause__, CheckViolation): - return 'error: Insufficient funds.' + messages.add('error', 'Insufficient funds.') + return to_wallet else: raise exception diff --git a/flake.nix b/flake.nix index b49951e..4e10a12 100644 --- a/flake.nix +++ b/flake.nix @@ -20,7 +20,9 @@ pythonWithDependencies = python.withPackages (ps: with ps; [ uvicorn # server for the web app fastapi # simple Python framework to build web apps + aiofiles # to let fastapi serve static resources (CSS, JS, ...) python-multipart # to let fastapi handle form submissions + jinja2 # HTML templating engine passlib # for account password hashing psycopg2 # PostgreSQL driver for Python embrace # bridges raw SQL queries to Python functions diff --git a/readme.md b/readme.md index 1508142..b2841a6 100644 --- a/readme.md +++ b/readme.md @@ -159,9 +159,11 @@ Because the use of an ORM is not desirable in this project for the reasons detailed in a previous section, the choices of frameworks is limited to these light-weight frameworks. Here, FastAPI is preferred over Flask due to its more modern architecture, using parameters and [dependency injection] over -thread-local global variables. +thread-local global variables. The chosen templating engine is [Jinja] for its +simplicity. [dependency injection]: https://en.wikipedia.org/wiki/Dependency_injection +[Jinja]: https://jinja.palletsprojects.com/en/2.0.x ### Project structure overview @@ -172,9 +174,12 @@ thread-local global variables. * `./app/` * `app_database.py`: database connection pool and transaction helper * `app_sessions.py`: (signed cookies) session data management helpers + * `app_templating.py`: template rendering helper * `app_{account,wallet}.py`: page-specific request handlers * `app.py`: FastAPI web application entry point +* `./templates/`: Jinja HTML templates +* `./static/`: static web resources (stylesheets) * `./flake.nix`: project runtime and development environment description ### Security considerations @@ -182,6 +187,9 @@ thread-local global variables. * SQL injections are prevented by using proper query parameters substitution, automatically handled by the embrace and psycopg libraries. +* Injections in rendered views are prevented by the automatic sanitisation of + inserted variables by the Jinja templating engine. + * Passwords are salted and hashed when stored in the database to ensure some minimal protection of the [data at rest]. The hashing is handled by the [passlib] library, which also covers algorithm migrations. diff --git a/static/images/favicon.svg b/static/images/favicon.svg new file mode 100644 index 0000000..981870b --- /dev/null +++ b/static/images/favicon.svg @@ -0,0 +1,93 @@ + + + + + + + + image/svg+xml + + + + + + + + P + P + ` + + + diff --git a/static/stylesheets/grids-responsive-min.css b/static/stylesheets/grids-responsive-min.css new file mode 100644 index 0000000..a7c1828 --- /dev/null +++ b/static/stylesheets/grids-responsive-min.css @@ -0,0 +1,7 @@ +/*! +Pure v2.0.6 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/pure-css/pure/blob/master/LICENSE +*/ +@media screen and (min-width:35.5em){.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-1-12,.pure-u-sm-1-2,.pure-u-sm-1-24,.pure-u-sm-1-3,.pure-u-sm-1-4,.pure-u-sm-1-5,.pure-u-sm-1-6,.pure-u-sm-1-8,.pure-u-sm-10-24,.pure-u-sm-11-12,.pure-u-sm-11-24,.pure-u-sm-12-24,.pure-u-sm-13-24,.pure-u-sm-14-24,.pure-u-sm-15-24,.pure-u-sm-16-24,.pure-u-sm-17-24,.pure-u-sm-18-24,.pure-u-sm-19-24,.pure-u-sm-2-24,.pure-u-sm-2-3,.pure-u-sm-2-5,.pure-u-sm-20-24,.pure-u-sm-21-24,.pure-u-sm-22-24,.pure-u-sm-23-24,.pure-u-sm-24-24,.pure-u-sm-3-24,.pure-u-sm-3-4,.pure-u-sm-3-5,.pure-u-sm-3-8,.pure-u-sm-4-24,.pure-u-sm-4-5,.pure-u-sm-5-12,.pure-u-sm-5-24,.pure-u-sm-5-5,.pure-u-sm-5-6,.pure-u-sm-5-8,.pure-u-sm-6-24,.pure-u-sm-7-12,.pure-u-sm-7-24,.pure-u-sm-7-8,.pure-u-sm-8-24,.pure-u-sm-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-sm-1-24{width:4.1667%}.pure-u-sm-1-12,.pure-u-sm-2-24{width:8.3333%}.pure-u-sm-1-8,.pure-u-sm-3-24{width:12.5%}.pure-u-sm-1-6,.pure-u-sm-4-24{width:16.6667%}.pure-u-sm-1-5{width:20%}.pure-u-sm-5-24{width:20.8333%}.pure-u-sm-1-4,.pure-u-sm-6-24{width:25%}.pure-u-sm-7-24{width:29.1667%}.pure-u-sm-1-3,.pure-u-sm-8-24{width:33.3333%}.pure-u-sm-3-8,.pure-u-sm-9-24{width:37.5%}.pure-u-sm-2-5{width:40%}.pure-u-sm-10-24,.pure-u-sm-5-12{width:41.6667%}.pure-u-sm-11-24{width:45.8333%}.pure-u-sm-1-2,.pure-u-sm-12-24{width:50%}.pure-u-sm-13-24{width:54.1667%}.pure-u-sm-14-24,.pure-u-sm-7-12{width:58.3333%}.pure-u-sm-3-5{width:60%}.pure-u-sm-15-24,.pure-u-sm-5-8{width:62.5%}.pure-u-sm-16-24,.pure-u-sm-2-3{width:66.6667%}.pure-u-sm-17-24{width:70.8333%}.pure-u-sm-18-24,.pure-u-sm-3-4{width:75%}.pure-u-sm-19-24{width:79.1667%}.pure-u-sm-4-5{width:80%}.pure-u-sm-20-24,.pure-u-sm-5-6{width:83.3333%}.pure-u-sm-21-24,.pure-u-sm-7-8{width:87.5%}.pure-u-sm-11-12,.pure-u-sm-22-24{width:91.6667%}.pure-u-sm-23-24{width:95.8333%}.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-24-24,.pure-u-sm-5-5{width:100%}}@media screen and (min-width:48em){.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-1-12,.pure-u-md-1-2,.pure-u-md-1-24,.pure-u-md-1-3,.pure-u-md-1-4,.pure-u-md-1-5,.pure-u-md-1-6,.pure-u-md-1-8,.pure-u-md-10-24,.pure-u-md-11-12,.pure-u-md-11-24,.pure-u-md-12-24,.pure-u-md-13-24,.pure-u-md-14-24,.pure-u-md-15-24,.pure-u-md-16-24,.pure-u-md-17-24,.pure-u-md-18-24,.pure-u-md-19-24,.pure-u-md-2-24,.pure-u-md-2-3,.pure-u-md-2-5,.pure-u-md-20-24,.pure-u-md-21-24,.pure-u-md-22-24,.pure-u-md-23-24,.pure-u-md-24-24,.pure-u-md-3-24,.pure-u-md-3-4,.pure-u-md-3-5,.pure-u-md-3-8,.pure-u-md-4-24,.pure-u-md-4-5,.pure-u-md-5-12,.pure-u-md-5-24,.pure-u-md-5-5,.pure-u-md-5-6,.pure-u-md-5-8,.pure-u-md-6-24,.pure-u-md-7-12,.pure-u-md-7-24,.pure-u-md-7-8,.pure-u-md-8-24,.pure-u-md-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-md-1-24{width:4.1667%}.pure-u-md-1-12,.pure-u-md-2-24{width:8.3333%}.pure-u-md-1-8,.pure-u-md-3-24{width:12.5%}.pure-u-md-1-6,.pure-u-md-4-24{width:16.6667%}.pure-u-md-1-5{width:20%}.pure-u-md-5-24{width:20.8333%}.pure-u-md-1-4,.pure-u-md-6-24{width:25%}.pure-u-md-7-24{width:29.1667%}.pure-u-md-1-3,.pure-u-md-8-24{width:33.3333%}.pure-u-md-3-8,.pure-u-md-9-24{width:37.5%}.pure-u-md-2-5{width:40%}.pure-u-md-10-24,.pure-u-md-5-12{width:41.6667%}.pure-u-md-11-24{width:45.8333%}.pure-u-md-1-2,.pure-u-md-12-24{width:50%}.pure-u-md-13-24{width:54.1667%}.pure-u-md-14-24,.pure-u-md-7-12{width:58.3333%}.pure-u-md-3-5{width:60%}.pure-u-md-15-24,.pure-u-md-5-8{width:62.5%}.pure-u-md-16-24,.pure-u-md-2-3{width:66.6667%}.pure-u-md-17-24{width:70.8333%}.pure-u-md-18-24,.pure-u-md-3-4{width:75%}.pure-u-md-19-24{width:79.1667%}.pure-u-md-4-5{width:80%}.pure-u-md-20-24,.pure-u-md-5-6{width:83.3333%}.pure-u-md-21-24,.pure-u-md-7-8{width:87.5%}.pure-u-md-11-12,.pure-u-md-22-24{width:91.6667%}.pure-u-md-23-24{width:95.8333%}.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-24-24,.pure-u-md-5-5{width:100%}}@media screen and (min-width:64em){.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-1-12,.pure-u-lg-1-2,.pure-u-lg-1-24,.pure-u-lg-1-3,.pure-u-lg-1-4,.pure-u-lg-1-5,.pure-u-lg-1-6,.pure-u-lg-1-8,.pure-u-lg-10-24,.pure-u-lg-11-12,.pure-u-lg-11-24,.pure-u-lg-12-24,.pure-u-lg-13-24,.pure-u-lg-14-24,.pure-u-lg-15-24,.pure-u-lg-16-24,.pure-u-lg-17-24,.pure-u-lg-18-24,.pure-u-lg-19-24,.pure-u-lg-2-24,.pure-u-lg-2-3,.pure-u-lg-2-5,.pure-u-lg-20-24,.pure-u-lg-21-24,.pure-u-lg-22-24,.pure-u-lg-23-24,.pure-u-lg-24-24,.pure-u-lg-3-24,.pure-u-lg-3-4,.pure-u-lg-3-5,.pure-u-lg-3-8,.pure-u-lg-4-24,.pure-u-lg-4-5,.pure-u-lg-5-12,.pure-u-lg-5-24,.pure-u-lg-5-5,.pure-u-lg-5-6,.pure-u-lg-5-8,.pure-u-lg-6-24,.pure-u-lg-7-12,.pure-u-lg-7-24,.pure-u-lg-7-8,.pure-u-lg-8-24,.pure-u-lg-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-lg-1-24{width:4.1667%}.pure-u-lg-1-12,.pure-u-lg-2-24{width:8.3333%}.pure-u-lg-1-8,.pure-u-lg-3-24{width:12.5%}.pure-u-lg-1-6,.pure-u-lg-4-24{width:16.6667%}.pure-u-lg-1-5{width:20%}.pure-u-lg-5-24{width:20.8333%}.pure-u-lg-1-4,.pure-u-lg-6-24{width:25%}.pure-u-lg-7-24{width:29.1667%}.pure-u-lg-1-3,.pure-u-lg-8-24{width:33.3333%}.pure-u-lg-3-8,.pure-u-lg-9-24{width:37.5%}.pure-u-lg-2-5{width:40%}.pure-u-lg-10-24,.pure-u-lg-5-12{width:41.6667%}.pure-u-lg-11-24{width:45.8333%}.pure-u-lg-1-2,.pure-u-lg-12-24{width:50%}.pure-u-lg-13-24{width:54.1667%}.pure-u-lg-14-24,.pure-u-lg-7-12{width:58.3333%}.pure-u-lg-3-5{width:60%}.pure-u-lg-15-24,.pure-u-lg-5-8{width:62.5%}.pure-u-lg-16-24,.pure-u-lg-2-3{width:66.6667%}.pure-u-lg-17-24{width:70.8333%}.pure-u-lg-18-24,.pure-u-lg-3-4{width:75%}.pure-u-lg-19-24{width:79.1667%}.pure-u-lg-4-5{width:80%}.pure-u-lg-20-24,.pure-u-lg-5-6{width:83.3333%}.pure-u-lg-21-24,.pure-u-lg-7-8{width:87.5%}.pure-u-lg-11-12,.pure-u-lg-22-24{width:91.6667%}.pure-u-lg-23-24{width:95.8333%}.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-24-24,.pure-u-lg-5-5{width:100%}}@media screen and (min-width:80em){.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-1-12,.pure-u-xl-1-2,.pure-u-xl-1-24,.pure-u-xl-1-3,.pure-u-xl-1-4,.pure-u-xl-1-5,.pure-u-xl-1-6,.pure-u-xl-1-8,.pure-u-xl-10-24,.pure-u-xl-11-12,.pure-u-xl-11-24,.pure-u-xl-12-24,.pure-u-xl-13-24,.pure-u-xl-14-24,.pure-u-xl-15-24,.pure-u-xl-16-24,.pure-u-xl-17-24,.pure-u-xl-18-24,.pure-u-xl-19-24,.pure-u-xl-2-24,.pure-u-xl-2-3,.pure-u-xl-2-5,.pure-u-xl-20-24,.pure-u-xl-21-24,.pure-u-xl-22-24,.pure-u-xl-23-24,.pure-u-xl-24-24,.pure-u-xl-3-24,.pure-u-xl-3-4,.pure-u-xl-3-5,.pure-u-xl-3-8,.pure-u-xl-4-24,.pure-u-xl-4-5,.pure-u-xl-5-12,.pure-u-xl-5-24,.pure-u-xl-5-5,.pure-u-xl-5-6,.pure-u-xl-5-8,.pure-u-xl-6-24,.pure-u-xl-7-12,.pure-u-xl-7-24,.pure-u-xl-7-8,.pure-u-xl-8-24,.pure-u-xl-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-xl-1-24{width:4.1667%}.pure-u-xl-1-12,.pure-u-xl-2-24{width:8.3333%}.pure-u-xl-1-8,.pure-u-xl-3-24{width:12.5%}.pure-u-xl-1-6,.pure-u-xl-4-24{width:16.6667%}.pure-u-xl-1-5{width:20%}.pure-u-xl-5-24{width:20.8333%}.pure-u-xl-1-4,.pure-u-xl-6-24{width:25%}.pure-u-xl-7-24{width:29.1667%}.pure-u-xl-1-3,.pure-u-xl-8-24{width:33.3333%}.pure-u-xl-3-8,.pure-u-xl-9-24{width:37.5%}.pure-u-xl-2-5{width:40%}.pure-u-xl-10-24,.pure-u-xl-5-12{width:41.6667%}.pure-u-xl-11-24{width:45.8333%}.pure-u-xl-1-2,.pure-u-xl-12-24{width:50%}.pure-u-xl-13-24{width:54.1667%}.pure-u-xl-14-24,.pure-u-xl-7-12{width:58.3333%}.pure-u-xl-3-5{width:60%}.pure-u-xl-15-24,.pure-u-xl-5-8{width:62.5%}.pure-u-xl-16-24,.pure-u-xl-2-3{width:66.6667%}.pure-u-xl-17-24{width:70.8333%}.pure-u-xl-18-24,.pure-u-xl-3-4{width:75%}.pure-u-xl-19-24{width:79.1667%}.pure-u-xl-4-5{width:80%}.pure-u-xl-20-24,.pure-u-xl-5-6{width:83.3333%}.pure-u-xl-21-24,.pure-u-xl-7-8{width:87.5%}.pure-u-xl-11-12,.pure-u-xl-22-24{width:91.6667%}.pure-u-xl-23-24{width:95.8333%}.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-24-24,.pure-u-xl-5-5{width:100%}} \ No newline at end of file diff --git a/static/stylesheets/laundry.css b/static/stylesheets/laundry.css new file mode 100644 index 0000000..c89964e --- /dev/null +++ b/static/stylesheets/laundry.css @@ -0,0 +1,139 @@ +/* from https://codepen.io/aitchiss/pen/GRpwOGK + * by Suzanne Aitchison (https://codepen.io/aitchiss) + */ + +:root { + --background-color: #EE5B52; + --machine-white: #F8F8F8; + --machine-feature-gray: #A3A1A1; + --accent-green: #5FBD9D; + --darkest-gray: #636161; +} + +/* Main machine body */ +.machine { + position: relative; + margin: auto; + width: 300px; + height: 400px; + background: var(--machine-white); + border-radius: 2%; +} + +/* Powder drawer */ +.drawer { + position: absolute; + top: 10px; + left: 10px; + width: 100px; + height: 50px; + border: 2px solid var(--machine-feature-gray); + border-radius: 0 0 20% 0; +} + +/* Drawer handle */ +.drawer::after { + content: ''; + position: absolute; + width: 70px; + height: 15px; + background: var(--machine-feature-gray); + bottom: 5px; + right: 5px; + border-radius: 0 0 30% 0; +} + +/* Small LED display */ +.panel { + position: absolute; + width: 40px; + height: 15px; + background: var(--darkest-gray); + left: 150px; + top: 30px; +} + +/* Light indicator in panel */ +.panel::before { + content: ''; + position: absolute; + width: 8px; + height: 10px; + background: var(--accent-green); + right: 5px; + top: 2px; +} + +/* Machine dial */ +.panel::after { + content: ''; + position: absolute; + left: 80px; + top: -10px; + width: 35px; + height: 35px; + border: 2px solid var(--machine-feature-gray); + border-radius: 50%; +} + +/* Center of washer door, width border forming the frame */ +.door { + position: absolute; + background: var(--machine-white); + left: 60px; + bottom: 90px; + width: 170px; + height: 170px; + border-radius: 50%; + border: 5px solid var(--machine-feature-gray); + overflow: hidden; +} + +/* Reflection on door surface */ +.door::after { + content: ''; + position: absolute; + top: 5px; + left: 5px; + width: 160px; + height: 160px; + background: radial-gradient(transparent 30%, white); +} + +/* Washing machine drum */ +.drum { + position: absolute; + left: 25px; + top: 25px; + width: 120px; + height: 120px; + border-radius: 50%; + background: var(--darkest-gray); + overflow: hidden; + animation: drumRotate 2s infinite; +} + +/* Money (instead of clothes) inside machine */ +.drum::before { + content: '§'; + text-align: right; + padding-right: 2px; + + + position: absolute; + width: 50px; + height: 30px; + background: var(--accent-green); + bottom: 0; +} + +@keyframes drumRotate { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + diff --git a/static/stylesheets/main.css b/static/stylesheets/main.css new file mode 100644 index 0000000..93001ac --- /dev/null +++ b/static/stylesheets/main.css @@ -0,0 +1,195 @@ +/* UGE / L2 / Intro to relational databases / Python project prototype + * Author: Pacien TRAN-GIRARD + * Licence: EUPL-1.2 + */ + +p { + line-height: 1.6em; +} + +.page { + color: #526066; + padding: 0 1em; + max-width: 1200px; + margin: 0 auto; +} + +.full-width { + width: 100%; +} + +.l-box { + padding: .75em; +} + +.centered { + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-right { + text-align: right; +} + +.align-center { + text-align: center; +} + +.input-invalid { + border: 1px solid #a94442 !important; + box-shadow: inset 0 1px 3px #f79291 !important; +} + +table { + width: 100%; +} + +.text-bold { + font-weight: bold; +} + +/***** DISCLAIMER BAR *****/ + +.disclaimer-bar { + color: white; + text-align: center; + font-size: 1.25em; + padding: .5em; +} + +/***** LOGO *****/ + +.branding h1, +.branding h2 { + display: inline-block; +} + +.branding h1 { + font-size: 4em; + /*padding-right: .5em;*/ + /*border-right: 3px solid #819096;*/ +} + +.branding h2 { + padding-left: .5em; +} + +.branding a { + text-decoration: inherit; + color: inherit; +} + +/***** ACCOUNT-INFO *****/ + +@media screen and (min-width: 768px) { + .account-info { + margin: 4em 0; + text-align: right; + } + + .account-info > * { + display: inline; + } + + .account-info .pure-button { + margin: .3em; + } + + .account-info .pure-button:last-child { + margin-right: 0; + } +} + +@media screen and (max-width: 768px) { + .account-info { + margin: 1em 0; + } + + .account-info .pure-button { + box-sizing: border-box; + width: 100%; + margin: .3em 0; + } +} + +/***** FLASH MESSAGES *****/ + +.flash-message > * { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; +} + +.flash-message > .success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.flash-message > .info { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.flash-message > .warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} + +.flash-message > .error { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} + +/***** FOOTER *****/ + +footer { + border-top: 1px solid #819096; + margin-top: 4em; + padding-top: 1em; + margin-bottom: 1em; +} + +@media screen and (min-width: 64em) { + footer .footer-links { + float: right; + } +} + +@media screen and (max-width: 64em) { + footer > * { + display: block; + } +} + +/***** ACTION BUTTONS *****/ + +.action-buttons * { + box-sizing: border-box; + color: white; +} + +.action-buttons a { + width: 100%; + border-radius: 0; + padding: 1.25em; +} + +.action-buttons a > * { + display: block; +} + +.action-buttons a > span { + margin-top: 1em; + + letter-spacing: 0.25em; + text-transform: uppercase; + font-weight: 600; +} diff --git a/static/stylesheets/pepal.css b/static/stylesheets/pepal.css new file mode 100644 index 0000000..d95fed0 --- /dev/null +++ b/static/stylesheets/pepal.css @@ -0,0 +1,40 @@ +/* UGE / L2 / Intro to relational databases / Python project prototype + * Author: Pacien TRAN-GIRARD + * Licence: EUPL-1.2 + */ + +/***** PEPAL LOGO COLOURING *****/ + +.pepal-logo { + font-style: italic; + font-weight: bold; +} + +.dark-blue { + color: #013088; +} + +.dark-blue-bg { + background-color: #013088; +} + +.light-blue { + color: #029de0; +} + +.light-blue-bg { + background-color: #029de0; +} + +.amount-positive { + color: darkgreen; +} + +.amount-negative { + color: darkred; +} + +.wallet-actions a { + width: 100%; + box-sizing: border-box; +} diff --git a/static/stylesheets/pure-min.css b/static/stylesheets/pure-min.css new file mode 100644 index 0000000..5b2aaf8 --- /dev/null +++ b/static/stylesheets/pure-min.css @@ -0,0 +1,11 @@ +/*! +Pure v2.0.6 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/pure-css/pure/blob/master/LICENSE +*/ +/*! +normalize.css v | MIT License | git.io/normalize +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-line-pack:start;align-content:flex-start}@media all and (-ms-high-contrast:none),(-ms-high-contrast:active){table .pure-g{display:block}}.opera-only :-o-prefocus,.pure-g{word-spacing:-0.43em}.pure-u{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class*=pure-u]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-0.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:-webkit-gradient(linear,left top,left bottom,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{-webkit-box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;-webkit-box-shadow:none;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-button-group .pure-button{margin:0;border-radius:0;border-right:1px solid rgba(0,0,0,.2)}.pure-button-group .pure-button:first-child{border-top-left-radius:2px;border-bottom-left-radius:2px}.pure-button-group .pure-button:last-child{border-top-right-radius:2px;border-bottom-right-radius:2px;border-right:none}.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;-webkit-box-shadow:inset 0 1px 3px #ddd;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=color]:focus,.pure-form input[type=date]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=email]:focus,.pure-form input[type=month]:focus,.pure-form input[type=number]:focus,.pure-form input[type=password]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=text]:focus,.pure-form input[type=time]:focus,.pure-form input[type=url]:focus,.pure-form input[type=week]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129fea}.pure-form input:not([type]):focus{outline:0;border-color:#129fea}.pure-form input[type=checkbox]:focus,.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=color][disabled],.pure-form input[type=date][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=email][disabled],.pure-form input[type=month][disabled],.pure-form input[type=number][disabled],.pure-form input[type=password][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=text][disabled],.pure-form input[type=time][disabled],.pure-form input[type=url][disabled],.pure-form input[type=week][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form select:focus:invalid,.pure-form textarea:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=checkbox]:focus:invalid:focus,.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=color],.pure-form-stacked input[type=date],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=email],.pure-form-stacked input[type=file],.pure-form-stacked input[type=month],.pure-form-stacked input[type=number],.pure-form-stacked input[type=password],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=text],.pure-form-stacked input[type=time],.pure-form-stacked input[type=url],.pure-form-stacked input[type=week],.pure-form-stacked label,.pure-form-stacked select,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned select,.pure-form-aligned textarea,.pure-form-message-inline{display:inline-block;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form .pure-input-rounded,.pure-form input.pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-3-4{width:75%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=color],.pure-form input[type=date],.pure-form input[type=datetime-local],.pure-form input[type=datetime],.pure-form input[type=email],.pure-form input[type=month],.pure-form input[type=number],.pure-form input[type=password],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=text],.pure-form input[type=time],.pure-form input[type=url],.pure-form input[type=week],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=color],.pure-group input[type=date],.pure-group input[type=datetime-local],.pure-group input[type=datetime],.pure-group input[type=email],.pure-group input[type=month],.pure-group input[type=number],.pure-group input[type=password],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=text],.pure-group input[type=time],.pure-group input[type=url],.pure-group input[type=week]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0 0}.pure-form-message,.pure-form-message-inline{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-item,.pure-menu-list{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-heading,.pure-menu-link{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-separator{display:inline-block;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-active>.pure-menu-children,.pure-menu-allow-hover:hover>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;padding:.5em 0}.pure-menu-horizontal .pure-menu-children .pure-menu-separator,.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-horizontal .pure-menu-children .pure-menu-separator{display:block;width:auto}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-heading,.pure-menu-link{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent;cursor:default}.pure-menu-active>.pure-menu-link,.pure-menu-link:focus,.pure-menu-link:hover{background-color:#eee}.pure-menu-selected>.pure-menu-link,.pure-menu-selected>.pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px 0;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} \ No newline at end of file diff --git a/templates/_base.html.jinja b/templates/_base.html.jinja new file mode 100644 index 0000000..5649718 --- /dev/null +++ b/templates/_base.html.jinja @@ -0,0 +1,71 @@ +{# + UGE / L2 / Intro to relational databases / Python project prototype + Author: Pacien TRAN-GIRARD + Licence: EUPL-1.2 +#} + +{% import '_fragments.html.jinja' as fragments %} + + + + + + + + + + {% block headers %} + + + + + + {% block title %}{% endblock %} - PèPal + {% endblock %} + + + +
+
+
+ + + +
+
+ +
+ {% block flash_messages %} + {{ fragments.flash_messages(messages) }} + {% endblock %} +
+ +
+ {% block content %}{% endblock %} +
+ + +
+ + + diff --git a/templates/_fragments.html.jinja b/templates/_fragments.html.jinja new file mode 100644 index 0000000..e5b3175 --- /dev/null +++ b/templates/_fragments.html.jinja @@ -0,0 +1,34 @@ +{# + UGE / L2 / Intro to relational databases / Python project prototype + Author: Pacien TRAN-GIRARD + Licence: EUPL-1.2 +#} + + +{% macro logo() %} + +{% endmacro %} + + +{% macro user_header(user) %} + + 👤 {{ user.username }} + + + + 👛 {{ user.balance }} § + + +
+ +
+{% endmacro %} + + +{% macro flash_messages(messages) %} + {% for class, message in messages %} +
{{ message }}
+ {% endfor %} +{% endmacro %} diff --git a/templates/homepage.html.jinja b/templates/homepage.html.jinja new file mode 100644 index 0000000..39ed94f --- /dev/null +++ b/templates/homepage.html.jinja @@ -0,0 +1,95 @@ +{# + UGE / L2 / Intro to relational databases / Python project prototype + Author: Pacien TRAN-GIRARD + Licence: EUPL-1.2 +#} + +{% extends '_base.html.jinja' %} + +{% block title %}Homepage{% endblock %} + +{% block content %} +
+ +
+ +

Create a new account

+ +
+
+ + +
+
+ + +
+
+ + + +
+
+
+ +
+ +

Log in into your account

+ +
+
+ + +
+
+ +
+
+ +
+
+
+ +
+{% endblock %} diff --git a/templates/launder.html.jinja b/templates/launder.html.jinja new file mode 100644 index 0000000..4d4cda7 --- /dev/null +++ b/templates/launder.html.jinja @@ -0,0 +1,38 @@ +{# + UGE / L2 / Intro to relational databases / Python project prototype + Author: Pacien TRAN-GIRARD + Licence: EUPL-1.2 +#} + +{% extends '_base.html.jinja' %} + +{% block title %}Transfer in progress...{% endblock %} + +{% block headers %} + {{ super() }} + + + +{% endblock %} + +{# delay notifications to the next page #} +{% block flash_messages %}{% endblock %} + +{% block content %} +
+
+
+
+
+
+
+
+
+
+ +
+

Laundering in progress...

+

The financial cycle will be completed soon.

+
+
+{% endblock %} diff --git a/templates/wallet.html.jinja b/templates/wallet.html.jinja new file mode 100644 index 0000000..86919e4 --- /dev/null +++ b/templates/wallet.html.jinja @@ -0,0 +1,140 @@ +{# + UGE / L2 / Intro to relational databases / Python project prototype + Author: Pacien TRAN-GIRARD + Licence: EUPL-1.2 +#} + +{% extends '_base.html.jinja' %} + +{% block title %}My wallet{% endblock %} + +{% macro format_operation(transaction) %} + {% if transaction.operation == 'transfer' %} + {% if transaction.amount > 0 %} + Transfer from {{ transaction.source }} + {% else %} + Transfer to {{ transaction.recipient }} + {% endif %} + {% else %} + {{ transaction.operation.capitalize() }} + {% endif %} +{% endmacro %} + +{% macro amount_class(transaction) %} + {{ 'amount-positive' if transaction.amount > 0 else 'amount-negative'}} +{% endmacro %} + +{% block content %} +
+ +
+

Deposit

+
+
+ + +
+
+
+ +
+

Withdraw

+
+
+ + +
+
+
+ +
+

Transfer

+
+
+ + + +
+
+
+ +
+ +

Transaction history

+ + + + + + + + + + + {% for transaction in transactions %} + + + + + + {% else %} + + + + {% endfor %} + +
DateOperationAmount
{{ transaction.datetime.strftime('%Y-%m-%d %H:%M:%S') }}{{ format_operation(transaction) }} + {{ transaction.amount }} § +
No past transaction.
+{% endblock %} -- cgit v1.2.3