mirror of
https://github.com/almet/copanier.git
synced 2025-04-28 19:42:37 +02:00
Basic auth management
This commit is contained in:
parent
8bb0dce145
commit
ab1710d1e4
21 changed files with 228 additions and 56 deletions
110
kaba/__init__.py
110
kaba/__init__.py
|
@ -1,7 +1,9 @@
|
||||||
import csv
|
import csv
|
||||||
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
|
|
||||||
|
import jwt
|
||||||
import ujson as json
|
import ujson as json
|
||||||
import hupper
|
import hupper
|
||||||
import minicli
|
import minicli
|
||||||
|
@ -9,7 +11,7 @@ from jinja2 import Environment, PackageLoader, select_autoescape
|
||||||
from roll import Roll, Response
|
from roll import Roll, Response
|
||||||
from roll.extensions import cors, options, traceback, simple_server, static
|
from roll.extensions import cors, options, traceback, simple_server, static
|
||||||
|
|
||||||
from . import config, reports
|
from . import config, reports, session, utils, emails
|
||||||
from .models import Delivery, Order, Person, Product, ProductOrder
|
from .models import Delivery, Order, Person, Product, ProductOrder
|
||||||
|
|
||||||
|
|
||||||
|
@ -59,6 +61,42 @@ cors(app, methods="*", headers="*")
|
||||||
options(app)
|
options(app)
|
||||||
|
|
||||||
|
|
||||||
|
def auth_required(view):
|
||||||
|
async def redirect(request, response, *a, **k):
|
||||||
|
# FIXME do not return a view when Roll allows it.
|
||||||
|
response.redirect = f"/sésame?next={request.path}"
|
||||||
|
|
||||||
|
def wrapper(request, response, *args, **kwargs):
|
||||||
|
token = request.cookies.get("token")
|
||||||
|
email = None
|
||||||
|
if token:
|
||||||
|
decoded = read_token(token)
|
||||||
|
email = decoded.get("sub")
|
||||||
|
if not email:
|
||||||
|
return redirect(request, response, *args, **kwargs)
|
||||||
|
user = Person(email=email)
|
||||||
|
request["user"] = user
|
||||||
|
session.user.set(user)
|
||||||
|
return view(request, response, *args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def create_token(email):
|
||||||
|
return jwt.encode(
|
||||||
|
{"sub": str(email), "exp": utils.utcnow() + timedelta(days=7)},
|
||||||
|
config.SECRET,
|
||||||
|
config.JWT_ALGORITHM,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_token(token):
|
||||||
|
try:
|
||||||
|
return jwt.decode(token, config.SECRET, algorithms=[config.JWT_ALGORITHM])
|
||||||
|
except (jwt.DecodeError, jwt.ExpiredSignatureError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
@app.listen("request")
|
@app.listen("request")
|
||||||
async def attach_request(request, response):
|
async def attach_request(request, response):
|
||||||
response.request = request
|
response.request = request
|
||||||
|
@ -69,17 +107,49 @@ async def on_startup():
|
||||||
configure()
|
configure()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/sésame", methods=["GET"])
|
||||||
|
async def sesame(request, response):
|
||||||
|
response.html("sesame.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/sésame", methods=["POST"])
|
||||||
|
async def send_sesame(request, response):
|
||||||
|
email = request.form.get("email")
|
||||||
|
token = create_token(email)
|
||||||
|
emails.send(
|
||||||
|
email,
|
||||||
|
"Sésame Panio",
|
||||||
|
emails.ACCESS_GRANTED.format(hostname=request.host, token=token.decode()),
|
||||||
|
)
|
||||||
|
response.message(f"Un sésame vous a été envoyé à l'adresse '{email}'")
|
||||||
|
response.redirect = "/"
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/sésame/{token}", methods=["GET"])
|
||||||
|
async def set_sesame(request, response, token):
|
||||||
|
decoded = read_token(token)
|
||||||
|
if not decoded:
|
||||||
|
response.message("Sésame invalide :(", status="error")
|
||||||
|
else:
|
||||||
|
response.message("Yay! Le sésame a fonctionné. Bienvenue à bord! :)")
|
||||||
|
response.cookies.set(name="token", value=token)
|
||||||
|
response.redirect = "/"
|
||||||
|
|
||||||
|
|
||||||
@app.route("/", methods=["GET"])
|
@app.route("/", methods=["GET"])
|
||||||
|
@auth_required
|
||||||
async def home(request, response):
|
async def home(request, response):
|
||||||
response.html("home.html", deliveries=Delivery.all())
|
response.html("home.html", deliveries=Delivery.all())
|
||||||
|
|
||||||
|
|
||||||
@app.route("/livraison/new", methods=["GET"])
|
@app.route("/livraison/new", methods=["GET"])
|
||||||
|
@auth_required
|
||||||
async def new_delivery(request, response):
|
async def new_delivery(request, response):
|
||||||
response.html("edit_delivery.html", delivery={})
|
response.html("edit_delivery.html", delivery={})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/livraison/new", methods=["POST"])
|
@app.route("/livraison/new", methods=["POST"])
|
||||||
|
@auth_required
|
||||||
async def create_delivery(request, response):
|
async def create_delivery(request, response):
|
||||||
form = request.form
|
form = request.form
|
||||||
data = {}
|
data = {}
|
||||||
|
@ -93,6 +163,7 @@ async def create_delivery(request, response):
|
||||||
|
|
||||||
|
|
||||||
@app.route("/livraison/{id}/importer/produits", methods=["POST"])
|
@app.route("/livraison/{id}/importer/produits", methods=["POST"])
|
||||||
|
@auth_required
|
||||||
async def import_products(request, response, id):
|
async def import_products(request, response, id):
|
||||||
delivery = Delivery.load(id)
|
delivery = Delivery.load(id)
|
||||||
delivery.products = []
|
delivery.products = []
|
||||||
|
@ -107,12 +178,14 @@ async def import_products(request, response, id):
|
||||||
|
|
||||||
|
|
||||||
@app.route("/livraison/{id}/edit", methods=["GET"])
|
@app.route("/livraison/{id}/edit", methods=["GET"])
|
||||||
|
@auth_required
|
||||||
async def edit_delivery(request, response, id):
|
async def edit_delivery(request, response, id):
|
||||||
delivery = Delivery.load(id)
|
delivery = Delivery.load(id)
|
||||||
response.html("edit_delivery.html", {"delivery": delivery})
|
response.html("edit_delivery.html", {"delivery": delivery})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/livraison/{id}/edit", methods=["POST"])
|
@app.route("/livraison/{id}/edit", methods=["POST"])
|
||||||
|
@auth_required
|
||||||
async def post_delivery(request, response, id):
|
async def post_delivery(request, response, id):
|
||||||
delivery = Delivery.load(id)
|
delivery = Delivery.load(id)
|
||||||
form = request.form
|
form = request.form
|
||||||
|
@ -125,30 +198,40 @@ async def post_delivery(request, response, id):
|
||||||
|
|
||||||
|
|
||||||
@app.route("/livraison/{id}", methods=["GET"])
|
@app.route("/livraison/{id}", methods=["GET"])
|
||||||
|
@auth_required
|
||||||
async def view_delivery(request, response, id):
|
async def view_delivery(request, response, id):
|
||||||
delivery = Delivery.load(id)
|
delivery = Delivery.load(id)
|
||||||
response.html("delivery.html", {"delivery": delivery})
|
response.html("delivery.html", {"delivery": delivery})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/livraison/{id}/commander", methods=["GET"])
|
@app.route("/livraison/{id}/commander", methods=["GET"])
|
||||||
|
@auth_required
|
||||||
async def order_form(request, response, id):
|
async def order_form(request, response, id):
|
||||||
delivery = Delivery.load(id)
|
delivery = Delivery.load(id)
|
||||||
email = request.query.get("email")
|
email = request.query.get("email", None)
|
||||||
order = delivery.orders.get(email) or Order()
|
if not email:
|
||||||
response.html(
|
user = session.user.get(None)
|
||||||
"place_order.html", {"delivery": delivery, "person": email, "order": order}
|
if user:
|
||||||
)
|
email = user.email
|
||||||
|
if email:
|
||||||
|
order = delivery.orders.get(email) or Order()
|
||||||
|
response.html(
|
||||||
|
"place_order.html", {"delivery": delivery, "person": email, "order": order}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response.message("Impossible de comprendre pour qui passer commande…", "error")
|
||||||
|
response.redirect = request.path
|
||||||
|
|
||||||
|
|
||||||
@app.route("/livraison/{id}/émargement", methods=["GET"])
|
@app.route("/livraison/{id}/émargement", methods=["GET"])
|
||||||
async def signing_list(request, response, id):
|
@auth_required
|
||||||
|
async def signing_sheet(request, response, id):
|
||||||
delivery = Delivery.load(id)
|
delivery = Delivery.load(id)
|
||||||
response.html(
|
response.html("signing_sheet.html", {"delivery": delivery})
|
||||||
"signing_list.html", {"delivery": delivery}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/livraison/{id}/commander", methods=["POST"])
|
@app.route("/livraison/{id}/commander", methods=["POST"])
|
||||||
|
@auth_required
|
||||||
async def place_order(request, response, id):
|
async def place_order(request, response, id):
|
||||||
delivery = Delivery.load(id)
|
delivery = Delivery.load(id)
|
||||||
email = request.query.get("email")
|
email = request.query.get("email")
|
||||||
|
@ -167,6 +250,7 @@ async def place_order(request, response, id):
|
||||||
|
|
||||||
|
|
||||||
@app.route("/livraison/{id}/importer/commande", methods=["POST"])
|
@app.route("/livraison/{id}/importer/commande", methods=["POST"])
|
||||||
|
@auth_required
|
||||||
async def import_commande(request, response, id):
|
async def import_commande(request, response, id):
|
||||||
email = request.form.get("email")
|
email = request.form.get("email")
|
||||||
order = Order()
|
order = Order()
|
||||||
|
@ -185,20 +269,22 @@ async def import_commande(request, response, id):
|
||||||
|
|
||||||
|
|
||||||
@app.route("/livraison/{id}/rapport.xlsx", methods=["GET"])
|
@app.route("/livraison/{id}/rapport.xlsx", methods=["GET"])
|
||||||
|
@auth_required
|
||||||
async def xls_report(request, response, id):
|
async def xls_report(request, response, id):
|
||||||
delivery = Delivery.load(id)
|
delivery = Delivery.load(id)
|
||||||
response.body = reports.summary(delivery)
|
response.body = reports.summary(delivery)
|
||||||
mimetype = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
mimetype = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
response.headers["Content-Disposition"] = f'attachment; filename="export.xlsx"'
|
response.headers["Content-Disposition"] = f'attachment; filename="epinamap.xlsx"'
|
||||||
response.headers["Content-Type"] = f"{mimetype}; charset=utf-8"
|
response.headers["Content-Type"] = f"{mimetype}; charset=utf-8"
|
||||||
|
|
||||||
|
|
||||||
@app.route("/livraison/{id}/rapport-complet.xlsx", methods=["GET"])
|
@app.route("/livraison/{id}/rapport-complet.xlsx", methods=["GET"])
|
||||||
|
@auth_required
|
||||||
async def xls_full_report(request, response, id):
|
async def xls_full_report(request, response, id):
|
||||||
delivery = Delivery.load(id)
|
delivery = Delivery.load(id)
|
||||||
response.body = reports.full(delivery)
|
response.body = reports.full(delivery)
|
||||||
mimetype = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
mimetype = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
response.headers["Content-Disposition"] = f'attachment; filename="export.xlsx"'
|
response.headers["Content-Disposition"] = f'attachment; filename="epinamap.xlsx"'
|
||||||
response.headers["Content-Type"] = f"{mimetype}; charset=utf-8"
|
response.headers["Content-Type"] = f"{mimetype}; charset=utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,11 @@ getenv = os.environ.get
|
||||||
|
|
||||||
DATA_ROOT = Path(__file__).parent.parent / "db"
|
DATA_ROOT = Path(__file__).parent.parent / "db"
|
||||||
SECRET = "sikretfordevonly"
|
SECRET = "sikretfordevonly"
|
||||||
# JWT_ALGORITHM = "HS256"
|
JWT_ALGORITHM = "HS256"
|
||||||
SEND_EMAILS = False
|
SEND_EMAILS = False
|
||||||
SMTP_HOST = "smtp.gmail.com"
|
SMTP_HOST = "mail.gandi.net"
|
||||||
SMTP_PASSWORD = ""
|
SMTP_PASSWORD = ""
|
||||||
|
SMTP_LOGIN = ""
|
||||||
FROM_EMAIL = "contact@epinamap.org"
|
FROM_EMAIL = "contact@epinamap.org"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ Voici le sésame:
|
||||||
|
|
||||||
https://{hostname}/sésame/{token}
|
https://{hostname}/sésame/{token}
|
||||||
|
|
||||||
Les gentils gens d'Épinamap
|
Les gentils copains d'Épinamap
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ def send(to, subject, body):
|
||||||
return print("Sending email", str(msg))
|
return print("Sending email", str(msg))
|
||||||
try:
|
try:
|
||||||
server = smtplib.SMTP_SSL(config.SMTP_HOST)
|
server = smtplib.SMTP_SSL(config.SMTP_HOST)
|
||||||
server.login(config.FROM_EMAIL, config.SMTP_PASSWORD)
|
server.login(config.SMTP_LOGIN, config.SMTP_PASSWORD)
|
||||||
server.send_message(msg)
|
server.send_message(msg)
|
||||||
except smtplib.SMTPException:
|
except smtplib.SMTPException:
|
||||||
raise RuntimeError
|
raise RuntimeError
|
||||||
|
|
|
@ -8,7 +8,7 @@ from typing import List, Dict
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from . import config, utils
|
from . import config
|
||||||
|
|
||||||
|
|
||||||
class DoesNotExist(ValueError):
|
class DoesNotExist(ValueError):
|
||||||
|
|
|
@ -5,7 +5,7 @@ from openpyxl.writer.excel import save_virtual_workbook
|
||||||
def summary(delivery):
|
def summary(delivery):
|
||||||
wb = Workbook()
|
wb = Workbook()
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
ws.title = f"Commande Epinamap - {delivery.producer} - {delivery.when.date()}"
|
ws.title = f"{delivery.producer} {delivery.when.date()}"
|
||||||
ws.append(["ref", "produit", "prix", "unités", "total"])
|
ws.append(["ref", "produit", "prix", "unités", "total"])
|
||||||
for product in delivery.products:
|
for product in delivery.products:
|
||||||
wanted = delivery.product_wanted(product)
|
wanted = delivery.product_wanted(product)
|
||||||
|
@ -25,7 +25,7 @@ def summary(delivery):
|
||||||
def full(delivery):
|
def full(delivery):
|
||||||
wb = Workbook()
|
wb = Workbook()
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
ws.title = f"Epinamap - {delivery.producer} - {delivery.when.date()}"
|
ws.title = f"{delivery.producer} {delivery.when.date()}"
|
||||||
headers = ["ref", "produit", "prix"] + [e for e in delivery.orders] + ["total"]
|
headers = ["ref", "produit", "prix"] + [e for e in delivery.orders] + ["total"]
|
||||||
ws.append(headers)
|
ws.append(headers)
|
||||||
for product in delivery.products:
|
for product in delivery.products:
|
||||||
|
|
3
kaba/session.py
Normal file
3
kaba/session.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import contextvars
|
||||||
|
|
||||||
|
user = contextvars.ContextVar("user")
|
|
@ -157,6 +157,10 @@ nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.logged-in {
|
||||||
|
text-decoration: underline;
|
||||||
|
font-variant: small-caps;
|
||||||
|
}
|
||||||
main {
|
main {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -164,7 +168,7 @@ main {
|
||||||
button,
|
button,
|
||||||
a.button,
|
a.button,
|
||||||
input[type=submit] {
|
input[type=submit] {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
@ -377,6 +381,9 @@ hr {
|
||||||
.notification.success {
|
.notification.success {
|
||||||
background-color: #0f8796;
|
background-color: #0f8796;
|
||||||
}
|
}
|
||||||
|
.notification.error {
|
||||||
|
background-color: #e10055;
|
||||||
|
}
|
||||||
.notification i {
|
.notification i {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,9 @@
|
||||||
<h1><a href="/">Panio</a> <small>Les paniers piano d'Épinamap</small></h1>
|
<h1><a href="/">Panio</a> <small>Les paniers piano d'Épinamap</small></h1>
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/livraison/new">Nouvelle livraison</a>
|
<a href="/livraison/new">Nouvelle livraison</a>
|
||||||
|
{% if request["user"] %}
|
||||||
|
⚫ {{ request["user"].email }}</span>
|
||||||
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
</section>
|
</section>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h3>{{ delivery.producer }}</h3>
|
<h3>{{ delivery.producer }} <a class="button" href="/livraison/{{ delivery.id }}/commander">Ma commande</a></h3>
|
||||||
{% include "includes/delivery_head.html" %}
|
{% include "includes/delivery_head.html" %}
|
||||||
<table class="delivery">
|
<table class="delivery">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -52,17 +52,14 @@
|
||||||
<a href="/livraison/{{ delivery.id }}/émargement" target="_blank"><i class="icon-document"></i> Liste d'émargement</a>
|
<a href="/livraison/{{ delivery.id }}/émargement" target="_blank"><i class="icon-document"></i> Liste d'émargement</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<label for="import-command" class="toggle-label"><i class="icon-paperclip"></i> Importer une commande</label>
|
{% with unique_id="import-command" %}
|
||||||
<input type="checkbox" id="import-command" class="toggle">
|
{% include "includes/modal_import_command.html" %}
|
||||||
<div class="toggle-container">
|
{% endwith %}
|
||||||
<label for="import-command" class="toggle-label">Fermer</label>
|
</li>
|
||||||
<p>Colonnes: ref*, wanted*</p>
|
<li>
|
||||||
<form action="/livraison/{{ delivery.id }}/importer/commande" method="post" enctype="multipart/form-data">
|
{% with unique_id="add-command" %}
|
||||||
<input type="file" name="data">
|
{% include "includes/modal_add_command.html" %}
|
||||||
<input type="email" name="email" placeholder="email">
|
{% endwith %}
|
||||||
<input type="submit" name="Importer une commande">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
|
@ -6,24 +6,24 @@
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<label>
|
<label>
|
||||||
<h5>Producteur</h5>
|
<h5>Producteur</h5>
|
||||||
<input type="text" name="producer" value="{{ delivery.producer or '' }}">
|
<input type="text" name="producer" value="{{ delivery.producer or '' }}" required>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<h5>Description</h5>
|
<h5>Description des produits</h5>
|
||||||
<input type="text" name="description" value="{{ delivery.description or '' }}">
|
<input type="text" name="description" value="{{ delivery.description or '' }}" required>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<h5>Lieu</h5>
|
<h5>Lieu</h5>
|
||||||
<input type="text" name="where" value="{{ delivery.where or '' }}">
|
<input type="text" name="where" value="{{ delivery.where or '' }}" required>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<h5>Date de livraison</h5>
|
<h5>Date de livraison</h5>
|
||||||
<input type="date" name="when" value="{{ delivery.when.date() if delivery.when else '' }}">
|
<input type="date" name="when" value="{{ delivery.when.date() if delivery.when else '' }}" required>
|
||||||
<input type="time" name="when_time" value="{{ delivery.when.time() if delivery.when else '' }}">
|
<input type="time" name="when_time" value="{{ delivery.when.time() if delivery.when else '' }}" required>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<h5>Date de limite de commande</h5>
|
<h5>Date de limite de commande</h5>
|
||||||
<input type="date" name="order_before" value="{{ delivery.order_before.date() if delivery.order_before else '' }}">
|
<input type="date" name="order_before" value="{{ delivery.order_before.date() if delivery.order_before else '' }}" required>
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<input type="submit" name="submit" value="Valider">
|
<input type="submit" name="submit" value="Valider">
|
||||||
|
|
|
@ -6,11 +6,6 @@
|
||||||
<li>
|
<li>
|
||||||
<h3><a href="/livraison/{{ delivery.id }}"><i class="icon-hotairballoon"></i> {{ delivery.producer }}</a></h3>
|
<h3><a href="/livraison/{{ delivery.id }}"><i class="icon-hotairballoon"></i> {{ delivery.producer }}</a></h3>
|
||||||
{% include "includes/delivery_head.html" %}
|
{% include "includes/delivery_head.html" %}
|
||||||
<!--form action="/livraison/{{ delivery.id }}/commander">
|
|
||||||
<label for="email">Commander</label>
|
|
||||||
<input type="email" name="email" placeholder="Mon courriel">
|
|
||||||
<input type="submit" value="Commander">
|
|
||||||
</form-->
|
|
||||||
</li>
|
</li>
|
||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<ul class="delivery-head">
|
<ul class="delivery-head">
|
||||||
<li><i class="icon-basket"></i> {{ delivery.description }}</li>
|
<li><i class="icon-basket"></i> <strong>Produits</strong> {{ delivery.description }}</li>
|
||||||
<li><i class="icon-map-pin"></i> <strong>Lieu</strong> {{ delivery.where }}</li>
|
<li><i class="icon-map-pin"></i> <strong>Lieu</strong> {{ delivery.where }}</li>
|
||||||
<li><i class="icon-clock"></i> <strong>Date de livraison</strong> <time datetime="{{ delivery.when }}">{{ delivery.when }}</time></li>
|
<li><i class="icon-clock"></i> <strong>Date de livraison</strong> <time datetime="{{ delivery.when }}">{{ delivery.when }}</time></li>
|
||||||
<li><i class="icon-hourglass"></i> {% if delivery.is_open %}<strong>Date limite de commande</strong> <time datetime="{{ delivery.order_before.date() }}">{{ delivery.order_before.date() }}</time>{% else %}<strong>Fermée</strong>{% endif %}</li>
|
<li><i class="icon-hourglass"></i> {% if delivery.is_open %}<strong>Date limite de commande</strong> <time datetime="{{ delivery.order_before.date() }}">{{ delivery.order_before.date() }}</time>{% else %}<strong>Fermée</strong>{% endif %}</li>
|
||||||
|
|
8
kaba/templates/includes/modal.html
Normal file
8
kaba/templates/includes/modal.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<label for="modal-{{ unique_id }}" class="toggle-label">{% block modal_label %}{% endblock %}</label>
|
||||||
|
<input type="checkbox" id="modal-{{ unique_id }}" class="toggle">
|
||||||
|
<label for="modal-{{ unique_id }}" class="toggle-background"></label>
|
||||||
|
<div class="toggle-container">
|
||||||
|
{% block modal_body %}
|
||||||
|
{% endblock modal_body %}
|
||||||
|
<label for="modal-{{ unique_id }}" class="toggle-label">Fermer</label>
|
||||||
|
</div>
|
10
kaba/templates/includes/modal_add_command.html
Normal file
10
kaba/templates/includes/modal_add_command.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{% extends "includes/modal.html" %}
|
||||||
|
|
||||||
|
{% block modal_label %}<i class="icon-hazardous"></i> Ajouter une commande{% endblock modal_label %}
|
||||||
|
{% block modal_body %}
|
||||||
|
<form action="/livraison/{{ delivery.id }}/commander">
|
||||||
|
<h4>Ajouter une commande pour quelqu'un d'autre</h4>
|
||||||
|
<input type="email" name="email" placeholder="Courriel" required>
|
||||||
|
<input type="submit" value="Commander">
|
||||||
|
</form>
|
||||||
|
{% endblock modal_body %}
|
12
kaba/templates/includes/modal_import_command.html
Normal file
12
kaba/templates/includes/modal_import_command.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "includes/modal.html" %}
|
||||||
|
|
||||||
|
{% block modal_label %}<i class="icon-paperclip"></i> Importer une commande{% endblock modal_label %}
|
||||||
|
{% block modal_body %}
|
||||||
|
<h4>Importer une commande</h4>
|
||||||
|
<p>Colonnes: ref*, wanted*</p>
|
||||||
|
<form action="/livraison/{{ delivery.id }}/importer/commande" method="post" enctype="multipart/form-data">
|
||||||
|
<input type="file" name="data">
|
||||||
|
<input type="email" name="email" placeholder="email">
|
||||||
|
<input type="submit" name="Importer">
|
||||||
|
</form>
|
||||||
|
{% endblock modal_body %}
|
9
kaba/templates/includes/modal_product.html
Normal file
9
kaba/templates/includes/modal_product.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{% extends "includes/modal.html" %}
|
||||||
|
|
||||||
|
{% block modal_label %}(Détails){% endblock modal_label %}
|
||||||
|
{% block modal_body %}
|
||||||
|
<p>{{ product.description }}</p>
|
||||||
|
<p>{% if product.img %}
|
||||||
|
<img src="{{ product.img }}">
|
||||||
|
{% endif %}</p>
|
||||||
|
{% endblock modal_body %}
|
|
@ -11,16 +11,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th class="product">{{ product.name }}
|
<th class="product">{{ product.name }}
|
||||||
{% if product.description or product.img %}
|
{% if product.description or product.img %}
|
||||||
<label for="toggleControl{{ loop.index }}" class="toggle-label">(Détails)</label>
|
{% with unique_id=loop.index %}
|
||||||
<input type="checkbox" id="toggleControl{{ loop.index }}" class="toggle">
|
{% include "includes/modal_product.html" %}
|
||||||
<label for="toggleControl{{ loop.index }}" class="toggle-background"></label>
|
{% endwith %}
|
||||||
<div class="toggle-container">
|
|
||||||
<p>{{ product.description }}</p>
|
|
||||||
<p>{% if product.img %}
|
|
||||||
<img src="{{ product.img }}">
|
|
||||||
{% endif %}</p>
|
|
||||||
<label for="toggleControl{{ loop.index }}" class="toggle-label">Fermer</label>
|
|
||||||
</div>
|
|
||||||
{% endif %}</p>
|
{% endif %}</p>
|
||||||
</th>
|
</th>
|
||||||
<td>{{ product.price }} €</td><td class="with-input"><input type="number" name="{{ product.ref }}" value="{{ order.get_quantity(product) }}"></td></tr>
|
<td>{{ product.price }} €</td><td class="with-input"><input type="number" name="{{ product.ref }}" value="{{ order.get_quantity(product) }}"></td></tr>
|
||||||
|
|
7
kaba/templates/sesame.html
Normal file
7
kaba/templates/sesame.html
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block body %}
|
||||||
|
<form method="post">
|
||||||
|
<input type="email" name="email" placeholder="Mon courriel" required>
|
||||||
|
<input type="submit" value="Envoyez-moi un sésame">
|
||||||
|
</form>
|
||||||
|
{% endblock body %}
|
|
@ -3,9 +3,11 @@ from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from roll.extensions import traceback
|
from roll.extensions import traceback
|
||||||
|
from roll.testing import Client as BaseClient
|
||||||
|
|
||||||
from kaba import app as kaba_app
|
from kaba import app as kaba_app
|
||||||
from kaba import config as kconfig
|
from kaba import config as kconfig
|
||||||
|
from kaba import create_token
|
||||||
from kaba.models import Delivery, Person
|
from kaba.models import Delivery, Person
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +22,39 @@ def pytest_runtest_setup(item):
|
||||||
path.unlink()
|
path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
class Client(BaseClient):
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
async def request(
|
||||||
|
self, path, method="GET", body=b"", headers=None, content_type=None
|
||||||
|
):
|
||||||
|
# TODO move this to Roll upstream?
|
||||||
|
headers = headers or {}
|
||||||
|
for key, value in self.headers.items():
|
||||||
|
headers.setdefault(key, value)
|
||||||
|
return await super().request(path, method, body, headers, content_type)
|
||||||
|
|
||||||
|
def login(self, email="foo@bar.org"):
|
||||||
|
token = create_token(email)
|
||||||
|
self.headers["Cookie"] = f"token={token.decode()}"
|
||||||
|
|
||||||
|
def logout(self):
|
||||||
|
try:
|
||||||
|
del self.headers["Cookie"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(app, event_loop):
|
||||||
|
app.loop = event_loop
|
||||||
|
app.loop.run_until_complete(app.startup())
|
||||||
|
client = Client(app)
|
||||||
|
client.login()
|
||||||
|
yield client
|
||||||
|
app.loop.run_until_complete(app.shutdown())
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def app(): # Requested by Roll testing utilities.
|
def app(): # Requested by Roll testing utilities.
|
||||||
traceback(kaba_app)
|
traceback(kaba_app)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
pytestmark = pytest.mark.asyncio
|
pytestmark = pytest.mark.asyncio
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,3 +13,10 @@ async def test_home_should_list_active_delivery(client, delivery):
|
||||||
resp = await client.get('/')
|
resp = await client.get('/')
|
||||||
assert resp.status == 200
|
assert resp.status == 200
|
||||||
assert delivery.producer in resp.body.decode()
|
assert delivery.producer in resp.body.decode()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_home_should_redirect_to_login_if_not_logged(client):
|
||||||
|
client.logout()
|
||||||
|
resp = await client.get('/')
|
||||||
|
assert resp.status == 302
|
||||||
|
assert resp.headers["Location"] == "/sésame?next=/"
|
||||||
|
|
Loading…
Reference in a new issue