Properly remove the archive feature

This commit is contained in:
Alexis Métaireau 2020-11-21 14:41:52 +01:00
parent 7e558f479a
commit e839422bf6
18 changed files with 12 additions and 410 deletions

25
TODO
View file

@ -1,25 +0,0 @@
x Ajouter un menu pour revenir à l'élément parent facilement
x Repenser l'interface pour la rendre plus simple
x Refaire une interface plus simple (moins de boutons partout, qu'on s'y retrouve plus)
x Uniformiser le nom des choses. Livraison → Distribution
x Ajouter un numéro de téléphone de la personne référente de la commande
x Il semble que les prix trop précis ne sont pas acceptés
x Utiliser une version lowercase des emails
x Permettre l'ajout de produits
x Ajouter un moyen d'ajouter un⋅e producteurice
x Ajouter un moyen de changer le nom de la personne référente pour un producteur / productrice
x Faciliter la duplication de distribution
x Si un produit est en rupture de stock, alors il n'est pas compté dans les totaux
x Permettre la suppression de producteurs
x Ajouter une info « prix mis à jour » pour les référent⋅e⋅s
x Gérer les frais de livraison
x Ajouter une note explicative pour la répartition des chèques
x Permettre la supression des produits (terminer)
x Gérer le souci d'URL pour l'édition d'Apiluly
x Rendre plus visible l'action de modifier une commande
x Ajouter la trame (agenda dune distribution) dans la boite à outil du coordinateur
x Repasser sur les tests
x Changer les liens d'emergement, de paiements et ???, trouver de meilleurs dénominations
Faire un refactoring des modèles
Create an "url_for" utility to simplify URL management.

View file

@ -295,6 +295,7 @@ class Delivery(PersistedBase):
producers: Dict[str, Producer] = field(default_factory=dict)
orders: Dict[str, Order] = field(default_factory=dict)
shipping: Dict[str, price_field] = field(default_factory=dict)
is_archived: bool = False
def __post_init__(self):
self.id = None # Not a field because we don't want to persist it.
@ -365,10 +366,6 @@ class Delivery(PersistedBase):
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)
@ -387,14 +384,16 @@ class Delivery(PersistedBase):
return delivery
@classmethod
def all(cls, is_archived=False):
def all(cls):
root = cls.get_root()
if is_archived:
root = root / "archive"
for path in root.glob("*.yml"):
id_ = str(path.relative_to(cls.get_root())).replace(".yml", "")
yield Delivery.load(id_)
@classmethod
def archived(cls):
return [d for d in cls.all() if not d.is_archived]
@classmethod
def incoming(cls):
return sorted(
@ -419,18 +418,14 @@ class Delivery(PersistedBase):
def archive(self):
if self.is_archived:
raise ValueError("La distribution est déjà archivée")
current = self.path
self.id = f"archive/{self.id}"
current.rename(self.path)
self.is_archived = True
def unarchive(self):
if not self.is_archived:
raise ValueError(
"Impossible de désarchiver une distribution qui n'est pas archivée"
)
current = self.path
self.id = self.path.stem
current.rename(self.path)
self.is_archived = False
def product_wanted(self, product):
total = 0

View file

@ -6,11 +6,6 @@
<h1>Modifier la distribution</h1>
<div class="pure-menu pure-menu-horizontal">
<ul class="pure-menu-list">
{% if delivery.status == delivery.CLOSED %}
<li class="pure-menu-item">
<a class="pure-menu-link danger" href="{{ url_for('archive_delivery', id=delivery.id) }}"><i class="icon-layers"></i>&nbsp;Archiver</a>
</li>
{% endif %}
<li class="pure-menu-item">
<a class="pure-menu-link" href="{{ url_for('list_products', id=delivery.id) }}"><i class="icon-pencil"></i> Gérer les produits / product⋅eur⋅rice⋅s</a>
</li>

View file

@ -16,15 +16,4 @@
{% include "includes/delivery_small_list.html" %}
{% endwith %}
{% endif %}
{% if archives %}
<a href="/archives">Voir les distributions archivées</a>
<hr>
{% endif %}
<div class="pure-menu pure-menu-horizontal">
<ul class="pure-menu-list">
<li class="pure-menu-item">
<a class="pure-menu-link" href="{{ url_for('list_archives') }}"><i class="icon-telescope"></i>&nbsp;Aller aux distributions archivées</a>&nbsp;
</li>
</ul>
</div>
{% endblock body %}

View file

@ -9,5 +9,5 @@
{% endfor %}
</ul>
{% else %}
<p>Il n'y a aucune distribution à venir.</p>
<p>Il n'y a aucune distribution à venir. Vous pouvez <a href="{{ url_for('new_delivery') }}">en lancer une nouvelle</a> !
{% endif %}

View file

@ -23,21 +23,21 @@ async def home(request, response):
"delivery/list_deliveries.html",
incoming=Delivery.incoming(),
former=Delivery.former(),
archives=list(Delivery.all(is_archived=True)),
archives=list(Delivery.archived()),
)
@app.route("/archives", methods=["GET"])
async def list_archives(request, response):
response.html(
"delivery/list_archives.html", {"deliveries": Delivery.all(is_archived=True)}
"delivery/list_archives.html", {"deliveries": Delivery.archived()}
)
@app.route("/distribution/archive/{id}", methods=["GET"])
async def view_archive(request, response, id):
delivery = Delivery.load(f"archive/{id}")
response.html("delivery/show.html", {"delivery": delivery})
response.html("delivery/show_delivery.html", {"delivery": delivery})
@app.route("/distribution/{id}/archiver", methods=["GET"])

View file

@ -1,213 +0,0 @@
import os
import sys
from io import StringIO
import minicli
from usine import chown, config, connect, env, exists, mkdir, put, run, sudo, template
@minicli.cli
def pip(*command):
"""Run a pip command on the remote server.
"""
with sudo(user="copanier"):
run(f"/srv/copanier/venv/bin/pip {' '.join(command)}")
@minicli.cli
def system():
"""Setup the system."""
with sudo():
run("apt update")
run(
"apt install -y nginx git software-properties-common gcc "
"python3.7 python3.7-dev python3.7-venv pkg-config"
)
mkdir("/srv/copanier/logs")
run("useradd -N copanier -d /srv/copanier/ || exit 0")
chown("copanier:users", "/srv/copanier/")
run("chsh -s /bin/bash copanier")
@minicli.cli
def venv():
"""Setup the python virtualenv."""
path = "/srv/copanier/venv/"
if not exists(path):
with sudo(user="copanier"):
run(f"python3.7 -m venv {path}")
pip("install pip -U")
@minicli.cli
def http():
"""Configure Nginx and letsencrypt."""
# When we'll have a domain.
put("remote/nginx-snippet.conf", "/etc/nginx/snippets/copanier.conf")
put("remote/letsencrypt.conf", "/etc/nginx/snippets/letsencrypt.conf")
put("remote/ssl.conf", "/etc/nginx/snippets/ssl.conf")
domain = config.domains[0]
pempath = f"/etc/letsencrypt/live/{domain}/fullchain.pem"
with sudo():
if exists(pempath):
print(f"{pempath} found, using https configuration")
conf = template(
"remote/nginx-https.conf",
domains=" ".join(config.domains),
domain=domain,
)
else:
print(f"{pempath} not found, using http configuration")
# Before letsencrypt.
conf = template(
"remote/nginx-http.conf",
domains=" ".join(config.domains),
domain=domain,
)
put(conf, "/etc/nginx/sites-enabled/copanier.conf")
restart("nginx")
@minicli.cli
def letsencrypt():
"""Configure letsencrypt."""
with sudo():
run("add-apt-repository --yes ppa:certbot/certbot")
run("apt update")
run("apt install -y certbot")
mkdir("/var/www/letsencrypt/.well-known/acme-challenge")
domains = ",".join(list(config.domains))
certbot_conf = template("remote/certbot.ini", domains=domains)
put(certbot_conf, "/var/www/certbot.ini")
run("certbot certonly -c /var/www/certbot.ini --non-interactive " "--agree-tos")
@minicli.cli
def bootstrap():
"""Bootstrap the system."""
system()
venv()
service()
http()
@minicli.cli
def cli(command):
"""Run the copanier executable on the remote server.
"""
with sudo(user="copanier"), env(COPANIER_DATA_ROOT="/srv/copanier/data"):
run(f"/srv/copanier/venv/bin/copanier {command}")
@minicli.cli
def service():
"""Deploy/update the copanier systemd service."""
with sudo():
put("remote/copanier.service", "/etc/systemd/system/copanier.service")
systemctl("enable copanier.service")
@minicli.cli
def deploy():
"""Deploy/update the copanier code base."""
with sudo(user="copanier"):
put("remote/gunicorn.conf", "/srv/copanier/gunicorn.conf")
pip("install gunicorn")
base = "https://framagit.org/ybon/copanier"
pip(f"install -U git+{base}")
restart()
@minicli.cli
def restart(*services):
"""Restart the systemd services."""
services = services or ["copanier", "nginx"]
with sudo():
systemctl(f"restart {' '.join(services)}")
@minicli.cli
def systemctl(*args):
"""Run a systemctl command on the remote server.
:command: the systemctl command to run.
"""
run(f'systemctl {" ".join(args)}')
@minicli.cli
def logs(lines=50):
"""Display the copanier logs.
:lines: number of lines to retrieve
"""
with sudo():
run(f"journalctl --lines {lines} --unit copanier --follow")
@minicli.cli
def status():
"""Get the services status."""
systemctl("status nginx copanier")
@minicli.cli
def access_logs():
"""See the nginx access logs."""
with sudo():
run("tail -F /var/log/nginx/access.log")
@minicli.cli
def error_logs():
"""See the nginx error logs."""
with sudo():
run("tail -F /var/log/nginx/error.log")
@minicli.cli
def data_logs():
"""See the app data logs."""
with sudo():
run("tail -F /srv/copanier/logs/copanier-requests.log")
@minicli.cli
def upload_env():
"""Upload environment vars to the server.
Use those to deal with info not commitable.
"""
vars_ = {
"COPANIER_DATA_ROOT": "/srv/copanier/data",
"COPANIER_LOG_ROOT": "/srv/copanier/logs",
"COPANIER_SEND_EMAILS": "1",
"COPANIER_SMTP_PASSWORD": None,
"COPANIER_SMTP_LOGIN": None,
"COPANIER_SMTP_HOST": None,
"COPANIER_STAFF": None,
}
content = ""
table = str.maketrans({'"': r"\""})
for key, value in vars_.items():
value = value or os.environ[key]
try:
content += f'{key}="{str(value).translate(table)}"\n'
except KeyError:
sys.exit(f"The {key} environment variable does not exist.")
path = "/srv/copanier/env"
if exists(path):
run(f"cat {path}")
put(StringIO(content), path)
if exists(path):
run(f"cat {path}")
@minicli.wrap
def wrapper(hostname, configpath):
with connect(hostname=hostname, configpath=configpath):
yield
if __name__ == "__main__":
minicli.run(hostname="qa", configpath="remote/config.yml")

View file

@ -1,4 +0,0 @@
authenticator = webroot
webroot-path = /var/www/letsencrypt
domains = $$domains
email = yohanboniface@free.fr

View file

@ -1,4 +0,0 @@
domains:
- copanier.epinamap.org
username: ubuntu
hostname: 185.145.251.38

View file

@ -1,17 +0,0 @@
[Unit]
Description=COPANIER
After=syslog.target network.target
[Service]
Type=simple
User=copanier
ExecStart=/srv/copanier/venv/bin/gunicorn copanier:app --config gunicorn.conf
WorkingDirectory=/srv/copanier/
EnvironmentFile=/srv/copanier/env
Restart=always
RestartSec=5
StandardOutput=journal
SyslogIdentifier=COPANIER
[Install]
WantedBy=default.target

View file

@ -1,4 +0,0 @@
worker_class = 'roll.worker.Worker'
workers = 4
bind = 'unix:/tmp/copanier.sock'
log_level = 'debug'

View file

@ -1,5 +0,0 @@
location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
root /var/www/letsencrypt;
auth_basic off;
}

View file

@ -1,14 +0,0 @@
upstream copanier {
server unix:///tmp/copanier.sock;
}
server {
listen 80;
server_name $${domains};
charset utf-8;
client_max_body_size 1M;
include /etc/nginx/snippets/letsencrypt.conf;
include /etc/nginx/snippets/copanier.conf;
}

View file

@ -1,40 +0,0 @@
upstream copanier {
server unix:///tmp/copanier.sock;
}
server {
listen 80;
server_name $${domains};
include /etc/nginx/snippets/letsencrypt.conf;
location /nginx_status {
stub_status on;
access_log off;
allow 127.0.0.1;
deny all;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
server_name $${domains};
listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_certificate /etc/letsencrypt/live/$$domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/$$domain/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/$$domain/fullchain.pem;
include /etc/nginx/snippets/ssl.conf;
charset utf-8;
client_max_body_size 1M;
include /etc/nginx/snippets/letsencrypt.conf;
include /etc/nginx/snippets/copanier.conf;
}

View file

@ -1,11 +0,0 @@
root /srv/copanier;
location /static/ {
alias /srv/copanier/venv/lib/python3.7/site-packages/copanier/static/;
}
location / {
proxy_pass http://copanier/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
}

View file

@ -1,5 +0,0 @@
#!/bin/bash
certbot -c /srv/eurordis/certbot.ini renew
systemctl reload nginx

View file

@ -1,15 +0,0 @@
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_protocols TLSv1.2;
ssl_ciphers EECDH+AESGCM:EECDH+AES;
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;
ssl_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security "max-age=15768000; includeSubdomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;

View file

@ -163,26 +163,6 @@ def test_productorder_quantity():
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
def test_group_management():
ndp = Group(
id="nid-de-poules", name="Nid de poules", members=["someone@domain.tld"]