diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..84b9e6c --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +# Black crazyness. +max-line-length = 88 diff --git a/kaba/config.py b/kaba/config.py new file mode 100644 index 0000000..e6336f2 --- /dev/null +++ b/kaba/config.py @@ -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() diff --git a/kaba/emails.py b/kaba/emails.py new file mode 100644 index 0000000..95eb4e0 --- /dev/null +++ b/kaba/emails.py @@ -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() diff --git a/kaba/models.py b/kaba/models.py new file mode 100644 index 0000000..49e679c --- /dev/null +++ b/kaba/models.py @@ -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 diff --git a/kaba/static/app.css b/kaba/static/app.css new file mode 100644 index 0000000..e120fd0 --- /dev/null +++ b/kaba/static/app.css @@ -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; +} diff --git a/kaba/static/fonts/WorkSans/WorkSansBoldLatin.woff2 b/kaba/static/fonts/WorkSans/WorkSansBoldLatin.woff2 new file mode 100644 index 0000000..032ff91 Binary files /dev/null and b/kaba/static/fonts/WorkSans/WorkSansBoldLatin.woff2 differ diff --git a/kaba/static/fonts/WorkSans/WorkSansBoldLatinExt.woff2 b/kaba/static/fonts/WorkSans/WorkSansBoldLatinExt.woff2 new file mode 100644 index 0000000..3f8bbc4 Binary files /dev/null and b/kaba/static/fonts/WorkSans/WorkSansBoldLatinExt.woff2 differ diff --git a/kaba/static/fonts/WorkSans/WorkSansLightLatin.woff2 b/kaba/static/fonts/WorkSans/WorkSansLightLatin.woff2 new file mode 100644 index 0000000..7d63252 Binary files /dev/null and b/kaba/static/fonts/WorkSans/WorkSansLightLatin.woff2 differ diff --git a/kaba/static/fonts/WorkSans/WorkSansLightLatinExt.woff2 b/kaba/static/fonts/WorkSans/WorkSansLightLatinExt.woff2 new file mode 100644 index 0000000..876f71a Binary files /dev/null and b/kaba/static/fonts/WorkSans/WorkSansLightLatinExt.woff2 differ diff --git a/kaba/static/fonts/WorkSans/WorkSansRegularLatin.woff2 b/kaba/static/fonts/WorkSans/WorkSansRegularLatin.woff2 new file mode 100644 index 0000000..3f9d555 Binary files /dev/null and b/kaba/static/fonts/WorkSans/WorkSansRegularLatin.woff2 differ diff --git a/kaba/static/fonts/WorkSans/WorkSansRegularLatinExt.woff2 b/kaba/static/fonts/WorkSans/WorkSansRegularLatinExt.woff2 new file mode 100644 index 0000000..44c66f1 Binary files /dev/null and b/kaba/static/fonts/WorkSans/WorkSansRegularLatinExt.woff2 differ diff --git a/kaba/static/fonts/WorkSans/WorkSansSemiBoldLatin.woff2 b/kaba/static/fonts/WorkSans/WorkSansSemiBoldLatin.woff2 new file mode 100644 index 0000000..e77a3cd Binary files /dev/null and b/kaba/static/fonts/WorkSans/WorkSansSemiBoldLatin.woff2 differ diff --git a/kaba/static/fonts/WorkSans/WorkSansSemiBoldLatinExt.woff2 b/kaba/static/fonts/WorkSans/WorkSansSemiBoldLatinExt.woff2 new file mode 100644 index 0000000..3902b7e Binary files /dev/null and b/kaba/static/fonts/WorkSans/WorkSansSemiBoldLatinExt.woff2 differ diff --git a/kaba/static/fonts/icomoon/icomoon.eot b/kaba/static/fonts/icomoon/icomoon.eot new file mode 100755 index 0000000..4f28a10 Binary files /dev/null and b/kaba/static/fonts/icomoon/icomoon.eot differ diff --git a/kaba/static/fonts/icomoon/icomoon.svg b/kaba/static/fonts/icomoon/icomoon.svg new file mode 100755 index 0000000..dc48847 --- /dev/null +++ b/kaba/static/fonts/icomoon/icomoon.svg @@ -0,0 +1,744 @@ + + + \ No newline at end of file diff --git a/kaba/static/fonts/icomoon/icomoon.ttf b/kaba/static/fonts/icomoon/icomoon.ttf new file mode 100755 index 0000000..68c9808 Binary files /dev/null and b/kaba/static/fonts/icomoon/icomoon.ttf differ diff --git a/kaba/static/fonts/icomoon/icomoon.woff b/kaba/static/fonts/icomoon/icomoon.woff new file mode 100755 index 0000000..66f3759 Binary files /dev/null and b/kaba/static/fonts/icomoon/icomoon.woff differ diff --git a/kaba/static/icomoon.css b/kaba/static/icomoon.css new file mode 100644 index 0000000..f98c3b8 --- /dev/null +++ b/kaba/static/icomoon.css @@ -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"; +} + diff --git a/kaba/static/img/default_product.svg b/kaba/static/img/default_product.svg new file mode 100644 index 0000000..4265c76 --- /dev/null +++ b/kaba/static/img/default_product.svg @@ -0,0 +1,66 @@ + + diff --git a/kaba/templates/base.html b/kaba/templates/base.html index 4190401..5a7da60 100644 --- a/kaba/templates/base.html +++ b/kaba/templates/base.html @@ -6,6 +6,7 @@ + {% block head %} {% endblock head %} @@ -15,6 +16,8 @@ {% if message %}
{% endif %} +Description: {{ delivery.description }}
+Lieu: {{ delivery.where }}
+Date: {{ delivery.when }}
+Date limite de commande: {{ delivery.order_before.date() }}
+Produit | +Prix | + {% for email, order in delivery.orders.items() %} +{{ email }} | + {% endfor %} +Total | +|
---|---|---|---|---|
{{ product.name }} | +{{ product.price }} € | + {% for email, order in delivery.orders.items() %} + {% if product.ref in order.products %} +{{ order.products[product.ref].wanted }} | + {% else %} +— | + {% endif %} + {% endfor %} +{{ delivery.product_wanted(product) }} | +
Total | {{ delivery.total }} € | + {% for email, order in delivery.orders.items() %} +{{ order.total(delivery.products) }} € | + {% endfor %} +— | +
Colonnes: ref*, wanted*
+ +{% endblock body %} diff --git a/kaba/templates/edit_delivery.html b/kaba/templates/edit_delivery.html new file mode 100644 index 0000000..6647deb --- /dev/null +++ b/kaba/templates/edit_delivery.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block body %} + +Colonnes: ref*, name*, price*, description
+ +{% endif %} +{% endblock body %} diff --git a/kaba/templates/home.html b/kaba/templates/home.html index da758b8..c614925 100644 --- a/kaba/templates/home.html +++ b/kaba/templates/home.html @@ -13,5 +13,4 @@ Résumé de la livraisonProducteur: {{ delivery.producer }} — Lieu: {{ delivery.where }} — Date: {{ delivery.when }} — Date limite de commande: {{ delivery.order_before }}
Commande de «{{ person }}»