From 6db17fa56e609a8572d0efac5d038fe2e56652d3 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 23 Apr 2019 20:38:48 +0200 Subject: [PATCH 1/3] Allow to archive deliveries - Delivery.id is not persisted anymore: the file name and relaltive directory *is* the id, and we don't want to denormalize this info - Delivery.load will filter out non fields keys from the storage: this allows more resilient upgrade of the data model, and is usefull when dealing with multiple branches with different data models - Delivery.archive/unarchive will move the delivery storage from root to archive/ - Delivery.all now accepts "is_archived" to retrieve archived instead of active deliveries - import products is now moved to a modal - archived deliveries are now listed in a separate page --- copanier/__init__.py | 29 +++++++++ copanier/models.py | 54 +++++++++++++--- copanier/templates/archive.html | 5 ++ copanier/templates/delivery.html | 62 +++++++++++-------- copanier/templates/edit_delivery.html | 39 ++++-------- copanier/templates/home.html | 1 + .../templates/includes/delivery_head.html | 2 +- .../includes/modal_import_products.html | 32 ++++++++++ copanier/templates/includes/order_button.html | 2 +- tests/test_models.py | 27 +++++++- 10 files changed, 189 insertions(+), 64 deletions(-) create mode 100644 copanier/templates/archive.html create mode 100644 copanier/templates/includes/modal_import_products.html diff --git a/copanier/__init__.py b/copanier/__init__.py index 0b4e568..5684d2d 100644 --- a/copanier/__init__.py +++ b/copanier/__init__.py @@ -177,6 +177,35 @@ async def home(request, response): response.html("home.html", incoming=Delivery.incoming(), former=Delivery.former()) +@app.route("/archives", methods=["GET"]) +async def view_archives(request, response): + response.html("archive.html", {"deliveries": Delivery.all(is_archived=True)}) + + +@app.route("/livraison/archive/{id}", methods=["GET"]) +async def view_archive(request, response, id): + delivery = Delivery.load(f"archive/{id}") + response.html("delivery.html", {"delivery": delivery}) + + +@app.route("/livraison/{id}/archiver", methods=["GET"]) +@staff_only +async def archive_delivery(request, response, id): + delivery = Delivery.load(id) + delivery.archive() + response.message("La livraison a été archivée") + response.redirect = f"/livraison/{delivery.id}" + + +@app.route("/livraison/archive/{id}/désarchiver", methods=["GET"]) +@staff_only +async def unarchive_delivery(request, response, id): + delivery = Delivery.load(f"archive/{id}") + delivery.unarchive() + response.message("La livraison a été désarchivée") + response.redirect = f"/livraison/{delivery.id}" + + @app.route("/livraison", methods=["GET"]) async def new_delivery(request, response): response.html("edit_delivery.html", delivery={}) diff --git a/copanier/models.py b/copanier/models.py index a95b9d9..14b2bc2 100644 --- a/copanier/models.py +++ b/copanier/models.py @@ -151,6 +151,7 @@ class Delivery(Base): CLOSED = 0 OPEN = 1 ADJUSTMENT = 2 + ARCHIVED = 3 producer: str from_date: datetime_field @@ -162,11 +163,16 @@ class Delivery(Base): where: str = "Marché de la Briche" products: List[Product] = field(default_factory=list) orders: Dict[str, Order] = field(default_factory=dict) - id: str = field(default_factory=lambda *a, **k: uuid.uuid4().hex) infos_url: str = "" + def __post_init__(self): + self.id = None # Not a field because we don't want to persist it. + super().__post_init__() + @property def status(self): + if self.is_archived: + return self.ARCHIVED if self.is_open: return self.OPEN if self.needs_adjustment: @@ -197,9 +203,14 @@ class Delivery(Base): def needs_adjustment(self): return self.has_packing and any(self.product_missing(p) for p in self.products) + @property + def is_archived(self): + return self.id and self.id.startswith("archive/") + @classmethod def init_fs(cls): cls.get_root().mkdir(parents=True, exist_ok=True) + cls.get_root().joinpath("archive").mkdir(exist_ok=True) @classmethod def get_root(cls): @@ -210,12 +221,21 @@ class Delivery(Base): path = cls.get_root() / f"{id}.yml" if not path.exists(): raise DoesNotExist - return cls(**yaml.safe_load(path.read_text())) + data = yaml.safe_load(path.read_text()) + # Tolerate extra fields (but we'll lose them if instance is persisted) + data = {k: v for k, v in data.items() if k in cls.__dataclass_fields__} + delivery = cls(**data) + delivery.id = id + return delivery @classmethod - def all(cls): - for path in cls.get_root().glob("*.yml"): - yield Delivery.load(path.stem) + def all(cls, is_archived=False): + root = cls.get_root() + if is_archived: + root = root / "archive" + for path in root.glob("*.yml"): + id = str(path.relative_to(cls.get_root()))[:-4] + yield Delivery.load(id) @classmethod def incoming(cls): @@ -225,10 +245,30 @@ class Delivery(Base): def former(cls): return [d for d in cls.all() if not d.is_foreseen] + @property + def path(self): + assert self.id, "Cannot operate on unsaved deliveries" + return self.get_root() / f"{self.id}.yml" + def persist(self): with self.__lock__: - path = self.get_root() / f"{self.id}.yml" - path.write_text(self.dump()) + if not self.id: + self.id = uuid.uuid4().hex + self.path.write_text(self.dump()) + + def archive(self): + if self.is_archived: + raise ValueError("La livraison est déjà archivée") + current = self.path + self.id = f"archive/{self.id}" + current.rename(self.path) + + def unarchive(self): + if not self.is_archived: + raise ValueError("La livraison n'est pas archivée") + current = self.path + self.id = self.path.stem + current.rename(self.path) def product_wanted(self, product): total = 0 diff --git a/copanier/templates/archive.html b/copanier/templates/archive.html new file mode 100644 index 0000000..6daffec --- /dev/null +++ b/copanier/templates/archive.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} +{% block body %} +

Livraisons archivées

+ {% include "includes/delivery_list.html" %} +{% endblock body %} diff --git a/copanier/templates/delivery.html b/copanier/templates/delivery.html index 8738fec..17e94d7 100644 --- a/copanier/templates/delivery.html +++ b/copanier/templates/delivery.html @@ -58,6 +58,7 @@
{% endblock body %} diff --git a/copanier/templates/edit_delivery.html b/copanier/templates/edit_delivery.html index 9aa44a5..eb93aaf 100644 --- a/copanier/templates/edit_delivery.html +++ b/copanier/templates/edit_delivery.html @@ -45,32 +45,17 @@
{% if delivery %} -

Importer des produits

-

Formats pris en charge: xlsx, csv

-
- Détails des colonnes -
-
ref
-
Référence unique du produit (permet de mettre à jour les produits en cours de commande ou d'importer des commandes individuelles).
-
name
-
Nom du produit: mettre juste assez d'info pour distinguer les produits les uns des autres.
-
price
-
Prix d'une unité, en euros.
-
unit
-
Conditionnement d'une unité: 1kg, 33cl…
-
description
-
Plus de détails sur le produit.
-
packing
-
Contionnement final pour grouper les commandes, le cas échéant, en nombre d'unités.
-
url
-
Une URL éventuelle pointant sur une page présentant le produit.
-
img
-
Une URL éventuelle pointant sur une image du produit (attention, utiliser seulement des liens https).
-
-
-
- - -
+ {% endif %} {% endblock body %} diff --git a/copanier/templates/home.html b/copanier/templates/home.html index d08ae7a..d57ebb3 100644 --- a/copanier/templates/home.html +++ b/copanier/templates/home.html @@ -8,4 +8,5 @@ {% with deliveries=former %} {% include "includes/delivery_list.html" %} {% endwith %} + Voir les livraisons archivées {% endblock body %} diff --git a/copanier/templates/includes/delivery_head.html b/copanier/templates/includes/delivery_head.html index 2b5cbd8..0e9c9af 100644 --- a/copanier/templates/includes/delivery_head.html +++ b/copanier/templates/includes/delivery_head.html @@ -3,7 +3,7 @@
  • Lieu {{ delivery.where }}
  • Référent {{ delivery.contact }}
  • Date de livraison
  • -
  • {% if delivery.status == delivery.OPEN %}Date limite de commande {% elif delivery.status == delivery.ADJUSTMENT %}Ajustement en cours{% else %}Fermée{% endif %}
  • +
  • {% if delivery.status == delivery.OPEN %}Date limite de commande {% elif delivery.status == delivery.ADJUSTMENT %}Ajustement en cours{% elif delivery.status == delivery.CLOSED %}Fermée{% else %}Archivée{% endif %}
  • {% if delivery.instructions %}
  • À savoir {{ delivery.instructions }}
  • {% endif %} {% if delivery.infos_url %}
  • Plus d'infos {{ delivery.infos_url|truncate(20)}}
  • {% endif %} diff --git a/copanier/templates/includes/modal_import_products.html b/copanier/templates/includes/modal_import_products.html new file mode 100644 index 0000000..ca2f0cc --- /dev/null +++ b/copanier/templates/includes/modal_import_products.html @@ -0,0 +1,32 @@ +{% extends "includes/modal.html" %} + +{% block modal_label %} Importer les produits{% endblock modal_label %} +{% block modal_body %} +

    Importer des produits

    +

    Formats pris en charge: xlsx, csv

    +
    + Détails des colonnes +
    +
    ref
    +
    Référence unique du produit (permet de mettre à jour les produits en cours de commande ou d'importer des commandes individuelles).
    +
    name
    +
    Nom du produit: mettre juste assez d'info pour distinguer les produits les uns des autres.
    +
    price
    +
    Prix d'une unité, en euros.
    +
    unit
    +
    Conditionnement d'une unité: 1kg, 33cl…
    +
    description
    +
    Plus de détails sur le produit.
    +
    packing
    +
    Contionnement final pour grouper les commandes, le cas échéant, en nombre d'unités.
    +
    url
    +
    Une URL éventuelle pointant sur une page présentant le produit.
    +
    img
    +
    Une URL éventuelle pointant sur une image du produit (attention, utiliser seulement des liens https).
    +
    +
    +
    + + +
    +{% endblock modal_body %} diff --git a/copanier/templates/includes/order_button.html b/copanier/templates/includes/order_button.html index 56d41e5..bd45f48 100644 --- a/copanier/templates/includes/order_button.html +++ b/copanier/templates/includes/order_button.html @@ -1,4 +1,4 @@ -{% if delivery.status != delivery.CLOSED %} +{% if delivery.status == delivery.OPEN or delivery.status == delivery.ADJUSTMENT %} {% if delivery.status == delivery.ADJUSTMENT %} Ajuster ma commande diff --git a/tests/test_models.py b/tests/test_models.py index 941c74e..4cc531d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -20,7 +20,7 @@ def test_can_create_delivery(): assert delivery.producer == "Andines" assert delivery.where == "Marché de la Briche" assert delivery.from_date.year == now().year - assert delivery.id + assert not delivery.id def test_wrong_datetime_raise_valueerror(): @@ -106,7 +106,12 @@ def test_order_has_adjustments(): def test_can_persist_delivery(delivery): + with pytest.raises(AssertionError): + delivery.path + assert not delivery.id delivery.persist() + assert delivery.id + assert delivery.path.exists() def test_can_load_delivery(delivery): @@ -138,3 +143,23 @@ def test_productorder_quantity(): assert choice.quantity == 5 choice = ProductOrder(wanted=3, adjustment=-1) assert choice.quantity == 2 + + +def test_archive_delivery(delivery): + delivery.persist() + old_id = delivery.id + old_path = delivery.path + assert str(old_path).endswith(f"delivery/{delivery.id}.yml") + assert old_path.exists() + delivery.archive() + assert delivery.is_archived + assert delivery.id.startswith("archive/") + new_path = delivery.path + assert str(new_path).endswith(f"delivery/archive/{old_id}.yml") + assert not old_path.exists() + assert new_path.exists() + delivery.unarchive() + assert not delivery.id.startswith("archive/") + assert old_path.exists() + assert not new_path.exists() + assert not delivery.is_archived From 85521846e60b1796ad9333d417fcf4b7c4ff92a0 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Tue, 23 Apr 2019 22:52:39 +0200 Subject: [PATCH 2/3] danger button CSS --- copanier/static/app.css | 15 ++++++++++++++- copanier/templates/delivery.html | 2 +- copanier/templates/edit_delivery.html | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/copanier/static/app.css b/copanier/static/app.css index 25953c0..94aef00 100644 --- a/copanier/static/app.css +++ b/copanier/static/app.css @@ -208,6 +208,13 @@ input[type=submit].primary { background: var(--primary-color); } +button.primary:hover, +a.button.primary:hover, +input[type=submit].primary:hover { + background-color: #fff; + color: var(--primary-color); +} + button.danger, a.button.danger, input[type=submit].danger { @@ -215,6 +222,13 @@ input[type=submit].danger { border-color: #d9534f; } +button.danger:hover, +a.button.danger:hover, +input[type=submit].danger:hover { + background-color: #d9534f; + color: #fff; +} + /* Forms */ @@ -445,7 +459,6 @@ hr { background: white; padding: 5px; z-index: 100; - text-align: center; overflow-y: auto; white-space: normal; } diff --git a/copanier/templates/delivery.html b/copanier/templates/delivery.html index 17e94d7..e8e0e77 100644 --- a/copanier/templates/delivery.html +++ b/copanier/templates/delivery.html @@ -99,7 +99,7 @@ {% else %} {% if request.user and request.user.is_staff %}
  • - Désarchiver +  Désarchiver
  • {% endif %} {% endif %} diff --git a/copanier/templates/edit_delivery.html b/copanier/templates/edit_delivery.html index eb93aaf..2955081 100644 --- a/copanier/templates/edit_delivery.html +++ b/copanier/templates/edit_delivery.html @@ -48,7 +48,7 @@