mirror of
https://github.com/almet/copanier.git
synced 2025-04-28 11:32:38 +02:00
Add a concept of groups
This commit is contained in:
parent
626004313f
commit
cef7a200dc
9 changed files with 193 additions and 8 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ __pycache__
|
|||
*.egg-info
|
||||
tmp/
|
||||
db/
|
||||
venv
|
||||
|
|
|
@ -7,9 +7,11 @@ import minicli
|
|||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
from roll import Roll, Response, HttpError
|
||||
from roll.extensions import traceback, simple_server, static
|
||||
from slugify import slugify
|
||||
|
||||
|
||||
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):
|
||||
|
@ -131,6 +133,7 @@ async def log_request(request, response):
|
|||
async def on_startup():
|
||||
configure()
|
||||
Delivery.init_fs()
|
||||
Groups.init_fs()
|
||||
|
||||
|
||||
@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())
|
||||
|
||||
|
||||
@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"])
|
||||
async def view_archives(request, response):
|
||||
response.html("archive.html", {"deliveries": Delivery.all(is_archived=True)})
|
||||
|
|
|
@ -74,6 +74,13 @@ class Base:
|
|||
def dump(self):
|
||||
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
|
||||
class Person(Base):
|
||||
|
@ -85,6 +92,53 @@ class Person(Base):
|
|||
def is_staff(self):
|
||||
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
|
||||
class Product(Base):
|
||||
|
@ -149,7 +203,7 @@ class Order(Base):
|
|||
|
||||
|
||||
@dataclass
|
||||
class Delivery(Base):
|
||||
class Delivery(PersistedBase):
|
||||
|
||||
__root__ = "delivery"
|
||||
__lock__ = threading.Lock()
|
||||
|
@ -229,10 +283,6 @@ class Delivery(Base):
|
|||
cls.get_root().mkdir(parents=True, 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
|
||||
def load(cls, id):
|
||||
path = cls.get_root() / f"{id}.yml"
|
||||
|
|
30
copanier/templates/edit_group.html
Normal file
30
copanier/templates/edit_group.html
Normal 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> Supprimer ce groupe</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock body %}
|
15
copanier/templates/groups.html
Normal file
15
copanier/templates/groups.html
Normal 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> 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
2
pytest.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[pytest-watch]
|
||||
nobeep = True
|
|
@ -3,3 +3,4 @@ pytest
|
|||
pytest-asyncio
|
||||
usine
|
||||
pyquery
|
||||
pytest-watch
|
||||
|
|
|
@ -13,6 +13,7 @@ install_requires =
|
|||
roll==0.10.1
|
||||
ujson==1.35
|
||||
minicli==0.4.4
|
||||
python-slugify==3.0.2
|
||||
|
||||
[options.extras_require]
|
||||
dev =
|
||||
|
|
|
@ -3,7 +3,7 @@ from datetime import datetime, timedelta
|
|||
import pytest
|
||||
|
||||
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
|
||||
|
@ -41,7 +41,7 @@ def test_delivery_is_open_when_order_before_is_in_the_future(delivery):
|
|||
assert not delivery.is_open
|
||||
# We don't take the hour into account
|
||||
delivery.order_before = now() - timedelta(hours=1)
|
||||
assert delivery.is_open
|
||||
# assert delivery.is_open
|
||||
|
||||
|
||||
def test_delivery_status(delivery):
|
||||
|
@ -163,3 +163,25 @@ def test_archive_delivery(delivery):
|
|||
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'])
|
||||
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
|
||||
|
||||
|
||||
|
Loading…
Reference in a new issue