mirror of
https://github.com/almet/copanier.git
synced 2025-04-28 11:32:38 +02:00
iwyu
This commit is contained in:
parent
cd12223d6d
commit
edd17e6630
27 changed files with 1876 additions and 2 deletions
3
.flake8
Normal file
3
.flake8
Normal file
|
@ -0,0 +1,3 @@
|
|||
[flake8]
|
||||
# Black crazyness.
|
||||
max-line-length = 88
|
24
kaba/config.py
Normal file
24
kaba/config.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
|
||||
getenv = os.environ.get
|
||||
|
||||
DATA_ROOT = Path(__file__).parent.parent / "db"
|
||||
SECRET = "sikretfordevonly"
|
||||
# JWT_ALGORITHM = "HS256"
|
||||
SEND_EMAILS = False
|
||||
SMTP_HOST = "smtp.gmail.com"
|
||||
SMTP_PASSWORD = ""
|
||||
FROM_EMAIL = "contact@epinamap.org"
|
||||
|
||||
|
||||
def init():
|
||||
for key, value in globals().items():
|
||||
if key.isupper():
|
||||
env_key = "KABA_" + key
|
||||
typ = type(value)
|
||||
if env_key in os.environ:
|
||||
globals()[key] = typ(os.environ[env_key])
|
||||
|
||||
|
||||
init()
|
32
kaba/emails.py
Normal file
32
kaba/emails.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
|
||||
from . import config
|
||||
|
||||
|
||||
ACCESS_GRANTED = """Hey ho!
|
||||
|
||||
Voici le sésame:
|
||||
|
||||
https://{hostname}/sésame/{token}
|
||||
|
||||
Les gentils gens d'Épinamap
|
||||
"""
|
||||
|
||||
|
||||
def send(to, subject, body):
|
||||
msg = EmailMessage()
|
||||
msg.set_content(body)
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = config.FROM_EMAIL
|
||||
msg["To"] = to
|
||||
if not config.SEND_EMAILS:
|
||||
return print("Sending email", str(msg))
|
||||
try:
|
||||
server = smtplib.SMTP_SSL(config.SMTP_HOST)
|
||||
server.login(config.FROM_EMAIL, config.SMTP_PASSWORD)
|
||||
server.send_message(msg)
|
||||
except smtplib.SMTPException:
|
||||
raise RuntimeError
|
||||
finally:
|
||||
server.quit()
|
168
kaba/models.py
Normal file
168
kaba/models.py
Normal file
|
@ -0,0 +1,168 @@
|
|||
import inspect
|
||||
import os
|
||||
import threading
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
|
||||
import yaml
|
||||
|
||||
from . import config, utils
|
||||
|
||||
|
||||
class DoesNotExist(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def datetime_field(value):
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
if isinstance(value, int):
|
||||
return datetime.fromtimestamp(value)
|
||||
if isinstance(value, str):
|
||||
return datetime.fromisoformat(value)
|
||||
raise ValueError
|
||||
|
||||
|
||||
def price_field(value):
|
||||
if isinstance(value, str):
|
||||
value = value.replace(",", ".")
|
||||
return float(value)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Base:
|
||||
|
||||
@classmethod
|
||||
def create(cls, data=None, **kwargs):
|
||||
if isinstance(data, Base):
|
||||
return data
|
||||
return cls(**(data or kwargs))
|
||||
|
||||
def __post_init__(self):
|
||||
for name, field_ in self.__dataclass_fields__.items():
|
||||
value = getattr(self, name)
|
||||
type_ = field_.type
|
||||
if not isinstance(value, Base): # Do not recast our classes.
|
||||
try:
|
||||
setattr(self, name, self.cast(type_, value))
|
||||
except (TypeError, ValueError):
|
||||
raise
|
||||
raise ValueError(f"Wrong value for field `{name}`: `{value}`")
|
||||
|
||||
def cast(self, type, value):
|
||||
if hasattr(type, "_name"):
|
||||
if type._name == "List":
|
||||
if type.__args__:
|
||||
args = type.__args__
|
||||
type = lambda v: [self.cast(args[0], s) for s in v]
|
||||
else:
|
||||
type = list
|
||||
elif type._name == "Dict":
|
||||
if type.__args__:
|
||||
args = type.__args__
|
||||
type = lambda o: {
|
||||
self.cast(args[0], k): self.cast(args[1], v)
|
||||
for k, v in o.items()
|
||||
}
|
||||
else:
|
||||
type = dict
|
||||
elif inspect.isclass(type) and issubclass(type, Base):
|
||||
type = type.create
|
||||
return type(value)
|
||||
|
||||
def dump(self):
|
||||
return yaml.dump(asdict(self), allow_unicode=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Person(Base):
|
||||
email: str
|
||||
first_name: str = ""
|
||||
last_name: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Product(Base):
|
||||
name: str
|
||||
ref: str
|
||||
price: price_field
|
||||
weight: str = ""
|
||||
description: str = ""
|
||||
url: str = ""
|
||||
img: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProductOrder(Base):
|
||||
wanted: int
|
||||
ordered: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class Order(Base):
|
||||
products: Dict[str, ProductOrder] = field(default_factory=lambda *a, **k: {})
|
||||
|
||||
def get_quantity(self, product):
|
||||
choice = self.products.get(product.ref)
|
||||
return choice.wanted if choice else 0
|
||||
|
||||
def total(self, products):
|
||||
products = {p.ref: p for p in products}
|
||||
return round(
|
||||
sum(p.wanted * products[ref].price for ref, p in self.products.items()), 2
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Delivery(Base):
|
||||
|
||||
__root__ = "delivery"
|
||||
__lock__ = threading.Lock()
|
||||
|
||||
producer: str
|
||||
when: datetime_field
|
||||
order_before: datetime_field
|
||||
description: str = ""
|
||||
where: str = "Marché de la Briche"
|
||||
products: List[Product] = field(default_factory=lambda *a, **k: [])
|
||||
orders: Dict[str, Order] = field(default_factory=lambda *a, **k: {})
|
||||
id: str = field(default_factory=lambda *a, **k: uuid.uuid4().hex)
|
||||
|
||||
@property
|
||||
def total(self):
|
||||
return round(sum(o.total(self.products) for o in self.orders.values()), 2)
|
||||
|
||||
@property
|
||||
def is_open(self):
|
||||
return self.order_before > utils.utcnow()
|
||||
|
||||
@classmethod
|
||||
def get_root(cls):
|
||||
return Path(config.DATA_ROOT) / cls.__root__
|
||||
|
||||
@classmethod
|
||||
def load(cls, id):
|
||||
path = cls.get_root() / f"{id}.yml"
|
||||
if not path.exists():
|
||||
raise DoesNotExist
|
||||
return cls(**yaml.safe_load(path.read_text()))
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
for path in cls.get_root().glob("*.yml"):
|
||||
yield Delivery.load(path.stem)
|
||||
|
||||
def persist(self):
|
||||
with self.__lock__:
|
||||
path = self.get_root() / f"{self.id}.yml"
|
||||
path.write_text(self.dump())
|
||||
|
||||
def product_wanted(self, product):
|
||||
total = 0
|
||||
for order in self.orders.values():
|
||||
if product.ref in order.products:
|
||||
total += order.products[product.ref].wanted
|
||||
return total
|
372
kaba/static/app.css
Normal file
372
kaba/static/app.css
Normal file
|
@ -0,0 +1,372 @@
|
|||
:root {
|
||||
--primary-color: #0062b7;
|
||||
--primary-color-light: #e6f0fa;
|
||||
--secondary-color: #e10055;
|
||||
--text-color: #414664;
|
||||
--border-color: #e6e6eb;
|
||||
--primary-background-color: #fff;
|
||||
--secondary-background-color: #fafafb;
|
||||
--disease: #7846af;
|
||||
--disease-light: #f5ebfa;
|
||||
--country: #0f8796;
|
||||
--country-light: #e6f5f5;
|
||||
--group: #d03800;
|
||||
--group-light: #fff5eb;
|
||||
--keyword: #8c5a2d;
|
||||
--keyword-light: #f5f0eb;
|
||||
--kind: #cd0073;
|
||||
--kind-light: #faf0f5;
|
||||
--ern: #32009b;
|
||||
--ern-light: #ebebf5;
|
||||
}
|
||||
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Work Sans Light'), local('WorkSans-Light'), url(./fonts/WorkSans/WorkSansLightLatinExt.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Work Sans Light'), local('WorkSans-Light'), url(./fonts/WorkSans/WorkSansLightLatin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Work Sans'), local('WorkSans-Regular'), url(./fonts/WorkSans/WorkSansRegularLatinExt.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Work Sans'), local('WorkSans-Regular'), url(./fonts/WorkSans/WorkSansRegularLatin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Work Sans SemiBold'), local('WorkSans-SemiBold'), url(./fonts/WorkSans/WorkSansSemiBoldLatinExt.woff2) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Work Sans SemiBold'), local('WorkSans-SemiBold'), url(./fonts/WorkSans/WorkSansSemiBoldLatin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Work Sans Bold'), local('WorkSans-Bold'), url(./fonts/WorkSans/WorkSansBoldLatinExt.woff) format('woff2');
|
||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Work Sans Bold'), local('WorkSans-Bold'), url(./fonts/WorkSans/WorkSansBoldLatin.woff2) format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
}
|
||||
*, ::after, ::before {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
html {
|
||||
font-size: 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
body {
|
||||
color: var(--text-color);
|
||||
font-size: .8rem;
|
||||
font-family: 'Work Sans', sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
header {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 20px;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
legend {
|
||||
/*margin: 0;*/
|
||||
color: var(--primary-color);
|
||||
line-height: 1;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
h1 { font-size: 2.2rem }
|
||||
h2 { font-size: 1.8rem }
|
||||
h3 { font-size: 1.4rem }
|
||||
h4 { font-size: 1.1rem }
|
||||
|
||||
a {
|
||||
color: #00d1b2;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
-webkit-transition: none 86ms ease-out;
|
||||
transition: none 86ms ease-out;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #363636;
|
||||
}
|
||||
|
||||
main a {
|
||||
padding: 0 .1rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
background-color: transparent;
|
||||
border-bottom: 1px solid var(--primary-color);
|
||||
transition: all .3s;
|
||||
}
|
||||
main a:hover {
|
||||
padding-bottom: 1px;
|
||||
color: var(--primary-color);
|
||||
background-color: #e2eaf1;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
a.button,
|
||||
input[type=submit] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: auto;
|
||||
height: 1.6rem;
|
||||
padding: .1rem .8rem;
|
||||
color: var(--primary-color);
|
||||
font-size: .6rem;
|
||||
font-weight: 500;
|
||||
line-height: 2;
|
||||
outline: 0;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
vertical-align: middle;
|
||||
background-color: transparent;
|
||||
white-space: nowrap;
|
||||
border: .05rem solid var(--primary-color);
|
||||
border-radius: 0.1rem;
|
||||
transition: all .2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: #fff;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
|
||||
button.primary,
|
||||
a.button.primary,
|
||||
input[type=submit].primary {
|
||||
color: #fff;
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
button.danger,
|
||||
a.button.danger,
|
||||
input[type=submit].danger {
|
||||
color: #d9534f;
|
||||
border-color: #d9534f;
|
||||
}
|
||||
|
||||
|
||||
/* Forms */
|
||||
|
||||
fieldset {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
legend {
|
||||
width: 100%;
|
||||
/* margin-top: 2rem;
|
||||
margin-bottom: 1rem; */
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
display: block;
|
||||
position: relative;
|
||||
height: 1rem;
|
||||
padding: .4rem .8rem;
|
||||
color: #50596c;
|
||||
font-size: .8rem;
|
||||
line-height: 1rem;
|
||||
background-color: #fff;
|
||||
border: .05rem solid #bbc;
|
||||
border-radius: .1rem;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 13.2rem;
|
||||
}
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
min-width: 4rem;
|
||||
height: 2rem;
|
||||
padding: .25rem .4rem;
|
||||
padding-right: 1.2rem;
|
||||
color: inherit;
|
||||
font-size: .8rem;
|
||||
line-height: 1.2rem;
|
||||
background: #fff url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%204%205'%3E%3Cpath%20fill='%23667189'%20d='M2%200L0%202h4zm0%205L0%203h4z'/%3E%3C/svg%3E") no-repeat right .35rem center/.4rem .5rem;
|
||||
border: .05rem solid #caced7;
|
||||
border-radius: .1rem;
|
||||
outline: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
[type="file"] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus {
|
||||
box-shadow: 0 0 .1rem var(--primary-color);
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-bottom: 1px solid #aaa;
|
||||
overflow-x: auto;
|
||||
}
|
||||
tr {
|
||||
height: 30px;
|
||||
}
|
||||
td,
|
||||
th {
|
||||
padding: 0 5px;
|
||||
line-height: 1rem;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
td + td {
|
||||
border-left: 1px solid white;
|
||||
}
|
||||
th + td,
|
||||
td + th {
|
||||
border-left: 1px solid #aaa;
|
||||
}
|
||||
th {
|
||||
color: #363636;
|
||||
}
|
||||
th.person {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
th.product {
|
||||
width: 15%;
|
||||
text-align: left;
|
||||
}
|
||||
th.price {
|
||||
width: 5%;
|
||||
text-align: center;
|
||||
}
|
||||
th.amount {
|
||||
width: 5%;
|
||||
}
|
||||
td.total,
|
||||
th.total {
|
||||
background-color: #bbb;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background-color: #ddd;
|
||||
}
|
||||
tr:nth-child(1) {
|
||||
background-color: #3498db;
|
||||
}
|
||||
tr:nth-child(1) * {
|
||||
color: #f1f1f1;
|
||||
}
|
||||
hr {
|
||||
background-color: #dbdbdb;
|
||||
border: none;
|
||||
display: block;
|
||||
height: 1px;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.carrelage {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px , 1fr));
|
||||
grid-gap: 10px;
|
||||
}
|
||||
.carrelage li {
|
||||
list-style: none;
|
||||
border: 1px solid #eee;
|
||||
text-align: center;
|
||||
padding: 5px;
|
||||
}
|
||||
.carrelage li input {
|
||||
/*max-width: 80%;*/
|
||||
}
|
||||
.carrelage li img {
|
||||
width: 50%;
|
||||
}
|
||||
.notification {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #f1f1f1;
|
||||
line-height: : 3rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.notification.success {
|
||||
background-color: #0f8796;
|
||||
}
|
||||
.toggle {
|
||||
display: none;
|
||||
}
|
||||
.toggle-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle-container {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(50% - 200px);
|
||||
left: calc(50% - 200px);
|
||||
height: 400px;
|
||||
width: 400px;
|
||||
border: 1px solid #999;
|
||||
background: white;
|
||||
padding: 5px;
|
||||
}
|
||||
.toggle:checked ~ .toggle-container {
|
||||
display: block;
|
||||
}
|
BIN
kaba/static/fonts/WorkSans/WorkSansBoldLatin.woff2
Normal file
BIN
kaba/static/fonts/WorkSans/WorkSansBoldLatin.woff2
Normal file
Binary file not shown.
BIN
kaba/static/fonts/WorkSans/WorkSansBoldLatinExt.woff2
Normal file
BIN
kaba/static/fonts/WorkSans/WorkSansBoldLatinExt.woff2
Normal file
Binary file not shown.
BIN
kaba/static/fonts/WorkSans/WorkSansLightLatin.woff2
Normal file
BIN
kaba/static/fonts/WorkSans/WorkSansLightLatin.woff2
Normal file
Binary file not shown.
BIN
kaba/static/fonts/WorkSans/WorkSansLightLatinExt.woff2
Normal file
BIN
kaba/static/fonts/WorkSans/WorkSansLightLatinExt.woff2
Normal file
Binary file not shown.
BIN
kaba/static/fonts/WorkSans/WorkSansRegularLatin.woff2
Normal file
BIN
kaba/static/fonts/WorkSans/WorkSansRegularLatin.woff2
Normal file
Binary file not shown.
BIN
kaba/static/fonts/WorkSans/WorkSansRegularLatinExt.woff2
Normal file
BIN
kaba/static/fonts/WorkSans/WorkSansRegularLatinExt.woff2
Normal file
Binary file not shown.
BIN
kaba/static/fonts/WorkSans/WorkSansSemiBoldLatin.woff2
Normal file
BIN
kaba/static/fonts/WorkSans/WorkSansSemiBoldLatin.woff2
Normal file
Binary file not shown.
BIN
kaba/static/fonts/WorkSans/WorkSansSemiBoldLatinExt.woff2
Normal file
BIN
kaba/static/fonts/WorkSans/WorkSansSemiBoldLatinExt.woff2
Normal file
Binary file not shown.
BIN
kaba/static/fonts/icomoon/icomoon.eot
Executable file
BIN
kaba/static/fonts/icomoon/icomoon.eot
Executable file
Binary file not shown.
744
kaba/static/fonts/icomoon/icomoon.svg
Executable file
744
kaba/static/fonts/icomoon/icomoon.svg
Executable file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 758 KiB |
BIN
kaba/static/fonts/icomoon/icomoon.ttf
Executable file
BIN
kaba/static/fonts/icomoon/icomoon.ttf
Executable file
Binary file not shown.
BIN
kaba/static/fonts/icomoon/icomoon.woff
Executable file
BIN
kaba/static/fonts/icomoon/icomoon.woff
Executable file
Binary file not shown.
326
kaba/static/icomoon.css
Normal file
326
kaba/static/icomoon.css
Normal file
|
@ -0,0 +1,326 @@
|
|||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src: url('./fonts/icomoon/icomoon.eot?195opb');
|
||||
src: url('./fonts/icomoon/icomoon.eot?195opb#iefix') format('embedded-opentype'),
|
||||
url('./fonts/icomoon/icomoon.ttf?195opb') format('truetype'),
|
||||
url('./fonts/icomoon/icomoon.woff?195opb') format('woff'),
|
||||
url('./fonts/icomoon/icomoon.svg?195opb#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[class^="icon-"], [class*=" icon-"] {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'icomoon' !important;
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.icon-mobile:before {
|
||||
content: "\e000";
|
||||
}
|
||||
.icon-laptop:before {
|
||||
content: "\e001";
|
||||
}
|
||||
.icon-desktop:before {
|
||||
content: "\e002";
|
||||
}
|
||||
.icon-tablet:before {
|
||||
content: "\e003";
|
||||
}
|
||||
.icon-phone:before {
|
||||
content: "\e004";
|
||||
}
|
||||
.icon-document:before {
|
||||
content: "\e005";
|
||||
}
|
||||
.icon-documents:before {
|
||||
content: "\e006";
|
||||
}
|
||||
.icon-search:before {
|
||||
content: "\e007";
|
||||
}
|
||||
.icon-clipboard:before {
|
||||
content: "\e008";
|
||||
}
|
||||
.icon-newspaper:before {
|
||||
content: "\e009";
|
||||
}
|
||||
.icon-notebook:before {
|
||||
content: "\e00a";
|
||||
}
|
||||
.icon-book-open:before {
|
||||
content: "\e00b";
|
||||
}
|
||||
.icon-browser:before {
|
||||
content: "\e00c";
|
||||
}
|
||||
.icon-calendar:before {
|
||||
content: "\e00d";
|
||||
}
|
||||
.icon-presentation:before {
|
||||
content: "\e00e";
|
||||
}
|
||||
.icon-picture:before {
|
||||
content: "\e00f";
|
||||
}
|
||||
.icon-pictures:before {
|
||||
content: "\e010";
|
||||
}
|
||||
.icon-video:before {
|
||||
content: "\e011";
|
||||
}
|
||||
.icon-camera:before {
|
||||
content: "\e012";
|
||||
}
|
||||
.icon-printer:before {
|
||||
content: "\e013";
|
||||
}
|
||||
.icon-toolbox:before {
|
||||
content: "\e014";
|
||||
}
|
||||
.icon-briefcase:before {
|
||||
content: "\e015";
|
||||
}
|
||||
.icon-wallet:before {
|
||||
content: "\e016";
|
||||
}
|
||||
.icon-gift:before {
|
||||
content: "\e017";
|
||||
}
|
||||
.icon-bargraph:before {
|
||||
content: "\e018";
|
||||
}
|
||||
.icon-grid:before {
|
||||
content: "\e019";
|
||||
}
|
||||
.icon-expand:before {
|
||||
content: "\e01a";
|
||||
}
|
||||
.icon-focus:before {
|
||||
content: "\e01b";
|
||||
}
|
||||
.icon-edit:before {
|
||||
content: "\e01c";
|
||||
}
|
||||
.icon-adjustments:before {
|
||||
content: "\e01d";
|
||||
}
|
||||
.icon-ribbon:before {
|
||||
content: "\e01e";
|
||||
}
|
||||
.icon-hourglass:before {
|
||||
content: "\e01f";
|
||||
}
|
||||
.icon-lock:before {
|
||||
content: "\e020";
|
||||
}
|
||||
.icon-megaphone:before {
|
||||
content: "\e021";
|
||||
}
|
||||
.icon-shield:before {
|
||||
content: "\e022";
|
||||
}
|
||||
.icon-trophy:before {
|
||||
content: "\e023";
|
||||
}
|
||||
.icon-flag:before {
|
||||
content: "\e024";
|
||||
}
|
||||
.icon-map:before {
|
||||
content: "\e025";
|
||||
}
|
||||
.icon-puzzle:before {
|
||||
content: "\e026";
|
||||
}
|
||||
.icon-basket:before {
|
||||
content: "\e027";
|
||||
}
|
||||
.icon-envelope:before {
|
||||
content: "\e028";
|
||||
}
|
||||
.icon-streetsign:before {
|
||||
content: "\e029";
|
||||
}
|
||||
.icon-telescope:before {
|
||||
content: "\e02a";
|
||||
}
|
||||
.icon-gears:before {
|
||||
content: "\e02b";
|
||||
}
|
||||
.icon-key:before {
|
||||
content: "\e02c";
|
||||
}
|
||||
.icon-paperclip:before {
|
||||
content: "\e02d";
|
||||
}
|
||||
.icon-attachment:before {
|
||||
content: "\e02e";
|
||||
}
|
||||
.icon-pricetags:before {
|
||||
content: "\e02f";
|
||||
}
|
||||
.icon-lightbulb:before {
|
||||
content: "\e030";
|
||||
}
|
||||
.icon-layers:before {
|
||||
content: "\e031";
|
||||
}
|
||||
.icon-pencil:before {
|
||||
content: "\e032";
|
||||
}
|
||||
.icon-tools:before {
|
||||
content: "\e033";
|
||||
}
|
||||
.icon-tools-:before {
|
||||
content: "\e034";
|
||||
}
|
||||
.icon-scissors:before {
|
||||
content: "\e035";
|
||||
}
|
||||
.icon-paintbrush:before {
|
||||
content: "\e036";
|
||||
}
|
||||
.icon-magnifying-glass:before {
|
||||
content: "\e037";
|
||||
}
|
||||
.icon-circle-compass:before {
|
||||
content: "\e038";
|
||||
}
|
||||
.icon-linegraph:before {
|
||||
content: "\e039";
|
||||
}
|
||||
.icon-mic:before {
|
||||
content: "\e03a";
|
||||
}
|
||||
.icon-strategy:before {
|
||||
content: "\e03b";
|
||||
}
|
||||
.icon-beaker:before {
|
||||
content: "\e03c";
|
||||
}
|
||||
.icon-caution:before {
|
||||
content: "\e03d";
|
||||
}
|
||||
.icon-recycle:before {
|
||||
content: "\e03e";
|
||||
}
|
||||
.icon-anchor:before {
|
||||
content: "\e03f";
|
||||
}
|
||||
.icon-profile-male:before {
|
||||
content: "\e040";
|
||||
}
|
||||
.icon-profile-female:before {
|
||||
content: "\e041";
|
||||
}
|
||||
.icon-bike:before {
|
||||
content: "\e042";
|
||||
}
|
||||
.icon-wine:before {
|
||||
content: "\e043";
|
||||
}
|
||||
.icon-hotairballoon:before {
|
||||
content: "\e044";
|
||||
}
|
||||
.icon-globe:before {
|
||||
content: "\e045";
|
||||
}
|
||||
.icon-genius:before {
|
||||
content: "\e046";
|
||||
}
|
||||
.icon-map-pin:before {
|
||||
content: "\e047";
|
||||
}
|
||||
.icon-dial:before {
|
||||
content: "\e048";
|
||||
}
|
||||
.icon-chat:before {
|
||||
content: "\e049";
|
||||
}
|
||||
.icon-heart:before {
|
||||
content: "\e04a";
|
||||
}
|
||||
.icon-cloud:before {
|
||||
content: "\e04b";
|
||||
}
|
||||
.icon-upload:before {
|
||||
content: "\e04c";
|
||||
}
|
||||
.icon-download:before {
|
||||
content: "\e04d";
|
||||
}
|
||||
.icon-target:before {
|
||||
content: "\e04e";
|
||||
}
|
||||
.icon-hazardous:before {
|
||||
content: "\e04f";
|
||||
}
|
||||
.icon-piechart:before {
|
||||
content: "\e050";
|
||||
}
|
||||
.icon-speedometer:before {
|
||||
content: "\e051";
|
||||
}
|
||||
.icon-global:before {
|
||||
content: "\e052";
|
||||
}
|
||||
.icon-compass:before {
|
||||
content: "\e053";
|
||||
}
|
||||
.icon-lifesaver:before {
|
||||
content: "\e054";
|
||||
}
|
||||
.icon-clock:before {
|
||||
content: "\e055";
|
||||
}
|
||||
.icon-aperture:before {
|
||||
content: "\e056";
|
||||
}
|
||||
.icon-quote:before {
|
||||
content: "\e057";
|
||||
}
|
||||
.icon-scope:before {
|
||||
content: "\e058";
|
||||
}
|
||||
.icon-alarmclock:before {
|
||||
content: "\e059";
|
||||
}
|
||||
.icon-refresh:before {
|
||||
content: "\e05a";
|
||||
}
|
||||
.icon-happy:before {
|
||||
content: "\e05b";
|
||||
}
|
||||
.icon-sad:before {
|
||||
content: "\e05c";
|
||||
}
|
||||
.icon-facebook:before {
|
||||
content: "\e05d";
|
||||
}
|
||||
.icon-twitter:before {
|
||||
content: "\e05e";
|
||||
}
|
||||
.icon-googleplus:before {
|
||||
content: "\e05f";
|
||||
}
|
||||
.icon-rss:before {
|
||||
content: "\e060";
|
||||
}
|
||||
.icon-tumblr:before {
|
||||
content: "\e061";
|
||||
}
|
||||
.icon-linkedin:before {
|
||||
content: "\e062";
|
||||
}
|
||||
.icon-dribbble:before {
|
||||
content: "\e063";
|
||||
}
|
||||
|
66
kaba/static/img/default_product.svg
Normal file
66
kaba/static/img/default_product.svg
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 278 285.12503"
|
||||
id="svg10"
|
||||
sodipodi:docname="default_product.svg"
|
||||
width="278"
|
||||
height="285.12503"
|
||||
inkscape:version="0.92.4 5da689c313, 2019-01-14">
|
||||
<metadata
|
||||
id="metadata16">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs14" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="3840"
|
||||
inkscape:window-height="2031"
|
||||
id="namedview12"
|
||||
showgrid="false"
|
||||
fit-margin-top="100"
|
||||
fit-margin-left="100"
|
||||
fit-margin-right="100"
|
||||
fit-margin-bottom="100"
|
||||
inkscape:zoom="1.888"
|
||||
inkscape:cx="-315.44915"
|
||||
inkscape:cy="127.94388"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="55"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg10" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path2"
|
||||
overflow="visible"
|
||||
display="inline"
|
||||
visibility="visible"
|
||||
marker="none"
|
||||
d="M 139,100 102.53125,115.9062 139,131.8125 175.46875,115.9062 Z m -39,19.1562 v 49.84384 l 37,16.125 V 135.2812 Z m 78,0 -37,16.125 v 49.84384 l 37,-16.125 z"
|
||||
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#666666;fill-opacity:1;stroke:none;enable-background:accumulate" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
|
@ -6,6 +6,7 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" type="text/css"> -->
|
||||
<link rel="stylesheet" type="text/css" href="/static/app.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/icomoon.css">
|
||||
{% block head %}
|
||||
{% endblock head %}
|
||||
|
||||
|
@ -15,6 +16,8 @@
|
|||
{% if message %}
|
||||
<div class="notification {{ message[1] }}">{{ message[0] }}</div>
|
||||
{% endif %}
|
||||
<h1><a href="/">Panio</a></h1>
|
||||
<h5>Commandes groupées Épinamap</h5><a href="/livraison/new">Nouvelle livraison</a>
|
||||
</header>
|
||||
{% block body %}
|
||||
{% endblock body %}
|
||||
|
|
51
kaba/templates/delivery.html
Normal file
51
kaba/templates/delivery.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<h3>{{ delivery.producer }}</h3>
|
||||
<p><i class="icon-basket"></i> Description: {{ delivery.description }}</p>
|
||||
<p><i class="icon-map-pin"></i> Lieu: {{ delivery.where }}</p>
|
||||
<p><i class="icon-clock"></i> Date: {{ delivery.when }}</p>
|
||||
<p><i class="icon-alarmclock"></i> Date limite de commande: {{ delivery.order_before.date() }}</p>
|
||||
<table class="delivery">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th class="product">Produit</th>
|
||||
<th class="price">Prix</th>
|
||||
{% for email, order in delivery.orders.items() %}
|
||||
<th class="person"><a href="/livraison/{{ delivery.id }}/commander?email={{ email }}" title="{{ email }}">{{ email }}</a></th>
|
||||
{% endfor %}
|
||||
<th class="amount">Total</th>
|
||||
</tr>
|
||||
{% for product in delivery.products %}
|
||||
<tr>
|
||||
<th class="product">{{ product.name }}</th>
|
||||
<td>{{ product.price }} €</td>
|
||||
{% for email, order in delivery.orders.items() %}
|
||||
{% if product.ref in order.products %}
|
||||
<td>{{ order.products[product.ref].wanted }}</td>
|
||||
{% else %}
|
||||
<td>—</td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<th>{{ delivery.product_wanted(product) }}</th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr><th class="total"><i class="icon-pricetags"></i> Total</th><td class="total">{{ delivery.total }} €</td>
|
||||
{% for email, order in delivery.orders.items() %}
|
||||
<td>{{ order.total(delivery.products) }} €</td>
|
||||
{% endfor %}
|
||||
<th>—</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<p><a href="/livraison/{{ delivery.id }}/edit">Modifier la livraison (admin)</a></p>
|
||||
<p><a href="/livraison/{{ delivery.id }}/rapport.xlsx">Générer un rapport</a></p>
|
||||
<h3>Importer une commande</h3>
|
||||
<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 une commande">
|
||||
</form>
|
||||
{% endblock body %}
|
40
kaba/templates/edit_delivery.html
Normal file
40
kaba/templates/edit_delivery.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p><a href="/">< Revenir aux livraisons</a></p>
|
||||
<h1>Nouvelle livraison</h1>
|
||||
<form method="post">
|
||||
<label>
|
||||
<h5>Producteur</h5>
|
||||
<input type="text" name="producer" value="{{ delivery.producer or '' }}">
|
||||
</label>
|
||||
<label>
|
||||
<h5>Description</h5>
|
||||
<input type="text" name="description" value="{{ delivery.description or '' }}">
|
||||
</label>
|
||||
<label>
|
||||
<h5>Lieu</h5>
|
||||
<input type="text" name="where" value="{{ delivery.where or '' }}">
|
||||
</label>
|
||||
<label>
|
||||
<h5>Date de livraison</h5>
|
||||
<input type="date" name="when" value="{{ delivery.when.date() if delivery.when else '' }}">
|
||||
<input type="time" name="when_time" value="{{ delivery.when.time() if delivery.when else '' }}">
|
||||
</label>
|
||||
<label>
|
||||
<h5>Date de limite de commande</h5>
|
||||
<input type="date" name="order_before" value="{{ delivery.order_before.date() if delivery.order_before else '' }}">
|
||||
</label>
|
||||
<div>
|
||||
<input type="submit" name="submit" value="Valider">
|
||||
</div>
|
||||
</form>
|
||||
{% if delivery %}
|
||||
<h3>Importer des produits (CSV)</h3>
|
||||
<p>Colonnes: ref*, name*, price*, description</p>
|
||||
<form action="/livraison/{{ delivery.id }}/importer/produits" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="data">
|
||||
<input type="submit" name="Importer des produits">
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock body %}
|
|
@ -13,5 +13,4 @@
|
|||
<a href="/livraison/{{ delivery.id }}">Résumé de la livraison</a>
|
||||
<hr>
|
||||
{% endfor %}
|
||||
<a href="/livraison/new">Nouvelle livraison</a>
|
||||
{% endblock body %}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<p><a href="/">< Commandes</a></p>
|
||||
<p>Producteur: {{ delivery.producer }} — Lieu: {{ delivery.where }} — Date: {{ delivery.when }} — Date limite de commande: {{ delivery.order_before }}</p>
|
||||
<p>Commande de «{{ person }}»</p>
|
||||
<form method="post">
|
||||
|
|
5
kaba/utils.py
Normal file
5
kaba/utils.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from datetime import datetime, timezone
|
||||
|
||||
|
||||
def utcnow():
|
||||
return datetime.now(timezone.utc)
|
26
tests/test_config.py
Normal file
26
tests/test_config.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from kaba import config
|
||||
|
||||
|
||||
def test_config_should_read_from_env():
|
||||
old_root = os.environ.get("KABA_DATA_ROOT", "")
|
||||
old_secret = os.environ.get("KABA_SECRET", "")
|
||||
assert config.SECRET == "sikretfordevonly"
|
||||
assert isinstance(config.DATA_ROOT, Path)
|
||||
os.environ["KABA_DATA_ROOT"] = "changeme"
|
||||
os.environ["KABA_SECRET"] = "ultrasecret"
|
||||
config.init()
|
||||
assert config.SECRET == "ultrasecret"
|
||||
assert isinstance(config.DATA_ROOT, Path)
|
||||
assert str(config.DATA_ROOT) == "changeme"
|
||||
if old_root:
|
||||
os.environ["KABA_DATA_ROOT"] = old_root
|
||||
else:
|
||||
del os.environ["KABA_DATA_ROOT"]
|
||||
if old_secret:
|
||||
os.environ["KABA_SECRET"] = old_secret
|
||||
else:
|
||||
del os.environ["KABA_SECRET"]
|
||||
config.init()
|
16
tests/test_views.py
Normal file
16
tests/test_views.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
import pytest
|
||||
|
||||
|
||||
pytestmark = pytest.mark.asyncio
|
||||
|
||||
|
||||
async def test_empty_home(client):
|
||||
resp = await client.get('/')
|
||||
assert resp.status == 200
|
||||
|
||||
|
||||
async def test_home_should_list_active_delivery(client, delivery):
|
||||
delivery.persist()
|
||||
resp = await client.get('/')
|
||||
assert resp.status == 200
|
||||
assert delivery.producer in resp.body.decode()
|
Loading…
Reference in a new issue