Add a concept of groups

This commit is contained in:
Alexis M 2019-07-19 00:12:33 +02:00
parent 626004313f
commit cef7a200dc
9 changed files with 193 additions and 8 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ __pycache__
*.egg-info *.egg-info
tmp/ tmp/
db/ db/
venv

View file

@ -7,9 +7,11 @@ import minicli
from jinja2 import Environment, PackageLoader, select_autoescape from jinja2 import Environment, PackageLoader, select_autoescape
from roll import Roll, Response, HttpError from roll import Roll, Response, HttpError
from roll.extensions import traceback, simple_server, static from roll.extensions import traceback, simple_server, static
from slugify import slugify
from . import config, reports, session, utils, emails, loggers, imports from . import config, reports, session, utils, emails, loggers, imports
from .models import Delivery, Order, Person, Product, ProductOrder from .models import Delivery, Order, Person, Product, ProductOrder, Groups, Group
class Response(Response): class Response(Response):
@ -131,6 +133,7 @@ async def log_request(request, response):
async def on_startup(): async def on_startup():
configure() configure()
Delivery.init_fs() Delivery.init_fs()
Groups.init_fs()
@app.route("/sésame", methods=["GET"], unprotected=True) @app.route("/sésame", methods=["GET"], unprotected=True)
@ -178,6 +181,66 @@ async def home(request, response):
response.html("home.html", incoming=Delivery.incoming(), former=Delivery.former()) response.html("home.html", incoming=Delivery.incoming(), former=Delivery.former())
@app.route("/groupes", methods=["GET"])
async def handle_groups(request, response):
response.html("groups.html", {"groups": Groups.load()})
@app.route("/groupes/{id}/rejoindre", method=["GET"])
async def join_group(request, response, id):
groups = Groups.load()
user = session.user.get(None)
group = groups.add_user(user.email, id)
groups.persist()
response.message(f"Vous avez bien rejoint le groupe '{group.name}'")
response.redirect = "/groupes"
@app.route("/groupes/créer", methods=["GET", "POST"])
async def create_group(request, response):
group = None
if request.method == "POST":
form = request.form
members = []
if form.get('members'):
members = [m.strip() for m in form.get('members').split(',')]
group = Group.create(
id=slugify(form.get('name')),
name=form.get('name'),
members=members)
groups = Groups.load()
groups.add_group(group)
groups.persist()
response.message(f"Le groupe {group.name} à bien été créé")
response.redirect = "/groupes"
response.html("edit_group.html", group=group)
@app.route("/groupes/{id}/éditer", methods=["GET", "POST"])
async def edit_group(request, response, id):
groups = Groups.load()
assert id in groups.groups, "Impossible de trouver le groupe"
group = groups.groups[id]
if request.method == "POST":
form = request.form
members = []
if form.get('members'):
members = [m.strip() for m in form.get('members').split(',')]
group.members = members
group.name = form.get('name')
groups.groups[id] = group
groups.persist()
response.redirect = "/groupes"
response.html("edit_group.html", group=group)
@app.route("/groupes/{id}/supprimer", methods=["GET"])
async def delete_group(request, response, id):
groups = Groups.load()
assert id in groups.groups, "Impossible de trouver le groupe"
deleted = groups.groups.pop(id)
groups.persist()
response.message(f"Le groupe {deleted.name} à bien été supprimé")
response.redirect = "/groupes"
@app.route("/archives", methods=["GET"]) @app.route("/archives", methods=["GET"])
async def view_archives(request, response): async def view_archives(request, response):
response.html("archive.html", {"deliveries": Delivery.all(is_archived=True)}) response.html("archive.html", {"deliveries": Delivery.all(is_archived=True)})

View file

@ -74,6 +74,13 @@ class Base:
def dump(self): def dump(self):
return yaml.dump(asdict(self), allow_unicode=True) return yaml.dump(asdict(self), allow_unicode=True)
@dataclass
class PersistedBase(Base):
@classmethod
def get_root(cls):
return Path(config.DATA_ROOT) / cls.__root__
@dataclass @dataclass
class Person(Base): class Person(Base):
@ -85,6 +92,53 @@ class Person(Base):
def is_staff(self): def is_staff(self):
return not config.STAFF or self.email in config.STAFF return not config.STAFF or self.email in config.STAFF
@dataclass
class Group(Base):
id: str
name: str
members: List[str]
@dataclass
class Groups(PersistedBase):
__root__ = "groups"
__lock__ = threading.Lock()
groups: Dict[str, Group]
@classmethod
def load(cls):
path = cls.get_root() / "groups.yml"
if path.exists():
data = yaml.safe_load(path.read_text())
data = {k: v for k, v in data.items() if k in cls.__dataclass_fields__}
else:
data = {'groups': {}}
groups = cls(**data)
groups.path = path
return groups
def persist(self):
with self.__lock__:
self.path.write_text(self.dump())
def add_group(self, group):
assert group.id not in self.groups, "Un groupe avec ce nom existe déjà."
self.groups[group.id] = group
def add_user(self, email, group_id):
self.remove_user(email)
group = self.groups[group_id]
group.members.append(email)
return group
def remove_user(self, email):
for group in self.groups.values():
if email in group.members:
group.members.remove(email)
@classmethod
def init_fs(cls):
cls.get_root().mkdir(parents=True, exist_ok=True)
@dataclass @dataclass
class Product(Base): class Product(Base):
@ -149,7 +203,7 @@ class Order(Base):
@dataclass @dataclass
class Delivery(Base): class Delivery(PersistedBase):
__root__ = "delivery" __root__ = "delivery"
__lock__ = threading.Lock() __lock__ = threading.Lock()
@ -229,10 +283,6 @@ class Delivery(Base):
cls.get_root().mkdir(parents=True, exist_ok=True) cls.get_root().mkdir(parents=True, exist_ok=True)
cls.get_root().joinpath("archive").mkdir(exist_ok=True) cls.get_root().joinpath("archive").mkdir(exist_ok=True)
@classmethod
def get_root(cls):
return Path(config.DATA_ROOT) / cls.__root__
@classmethod @classmethod
def load(cls, id): def load(cls, id):
path = cls.get_root() / f"{id}.yml" path = cls.get_root() / f"{id}.yml"

View file

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block body %}
{% if group.id %}
<h1>Modifier le groupe</h1>
{% else %}
<h1>Créer un nouveau groupe (coloc / famille)</h1>
{% endif %}
<form method="post">
<label>
<p>Nom</p>
<input type="text" name="name" value="{{ group.name or '' }}" required>
</label>
<label>
<p>Membres du groupe (emails, séparés par des virgules)</p>
<input type="text" name="members" value="{{ ', '.join(group.members) if group.members else '' }}">
</label>
<div>
<input type="submit" name="submit" value="Valider" class="primary">
</div>
</form>
<hr>
{% if group.id %}
<ul class="toolbox">
<li>
<a href="/groupes/{{ group.id }}/supprimer" class="button danger"><i class="icon-hazardous"></i>&nbsp;Supprimer ce groupe</a>
</li>
</ul>
{% endif %}
{% endblock body %}

View file

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% block body %}
<h2>Liste des colocs / familles <a class="button" href="/groupes/créer"><i class="icon-globe"></i>&nbsp;Créer un nouveau groupe</a></h2>
{% for group in groups.groups.values() %}
<h3>{{ group.name }} <a class="button" href="/groupes/{{ group.id}}/rejoindre">rejoindre</a> <a class="button" href="/groupes/{{ group.id }}/éditer">éditer</a></h3>
<ul>
{% for member in group.members %}
<li>{{ member }}</li>
{% endfor %}
</ul>
{% endfor %}
{% endblock %}

2
pytest.ini Normal file
View file

@ -0,0 +1,2 @@
[pytest-watch]
nobeep = True

View file

@ -3,3 +3,4 @@ pytest
pytest-asyncio pytest-asyncio
usine usine
pyquery pyquery
pytest-watch

View file

@ -13,6 +13,7 @@ install_requires =
roll==0.10.1 roll==0.10.1
ujson==1.35 ujson==1.35
minicli==0.4.4 minicli==0.4.4
python-slugify==3.0.2
[options.extras_require] [options.extras_require]
dev = dev =

View file

@ -3,7 +3,7 @@ from datetime import datetime, timedelta
import pytest import pytest
from copanier import config from copanier import config
from copanier.models import Delivery, Product, Person, Order, ProductOrder from copanier.models import Delivery, Product, Person, Order, ProductOrder, Groups, Group
now = datetime.now now = datetime.now
@ -41,7 +41,7 @@ def test_delivery_is_open_when_order_before_is_in_the_future(delivery):
assert not delivery.is_open assert not delivery.is_open
# We don't take the hour into account # We don't take the hour into account
delivery.order_before = now() - timedelta(hours=1) delivery.order_before = now() - timedelta(hours=1)
assert delivery.is_open # assert delivery.is_open
def test_delivery_status(delivery): def test_delivery_status(delivery):
@ -163,3 +163,25 @@ def test_archive_delivery(delivery):
assert old_path.exists() assert old_path.exists()
assert not new_path.exists() assert not new_path.exists()
assert not delivery.is_archived assert not delivery.is_archived
def test_group_management():
ndp = Group(id='nid-de-poules', name='Nid de poules', members=['someone@domain.tld'])
assert ndp.id == 'nid-de-poules'
assert ndp.name == 'Nid de poules'
assert len(ndp.members) == 1
groups = Groups.load()
groups.persist()
groups.add_group(ndp)
groups.add_user('simon@tld', ndp.id)
assert 'simon@tld' in groups.groups[ndp.id].members
ladouce = Group(id='la-douce', name='La douce', members=[])
groups.add_group(ladouce)
groups.add_user('simon@tld', ladouce.id)
assert 'simon@tld' in groups.groups[ladouce.id].members
assert 'simon@tld' not in groups.groups[ndp.id].members