Create groupes

This commit is contained in:
augustin64 2023-10-11 17:15:49 +02:00
parent d35fd063bd
commit 8ff2f29617
11 changed files with 631 additions and 21 deletions

View File

@ -9,7 +9,7 @@ from flask import Flask, g, redirect, render_template, request, send_file, flash
from werkzeug.security import generate_password_hash from werkzeug.security import generate_password_hash
from .modules.utils import User, Album, get_all_albums from .modules.utils import User, Album, get_all_albums
from .modules import albums, auth, partition, admin from .modules import albums, auth, partition, admin, groupe
from .modules.auth import admin_required, login_required from .modules.auth import admin_required, login_required
from .modules.db import get_db from .modules.db import get_db
@ -26,6 +26,7 @@ else:
app.register_blueprint(auth.bp) app.register_blueprint(auth.bp)
app.register_blueprint(admin.bp) app.register_blueprint(admin.bp)
app.register_blueprint(groupe.bp)
app.register_blueprint(albums.bp) app.register_blueprint(albums.bp)
app.register_blueprint(partition.bp) app.register_blueprint(partition.bp)

View File

@ -0,0 +1,118 @@
from ..db import get_db
from .album import Album
class Groupe():
def __init__(self, uuid):
db=get_db()
self.uuid = uuid
data = db.execute(
"""
SELECT * FROM groupe
WHERE uuid = ?
""",
(self.uuid,)
).fetchone()
if data is None:
raise LookupError
self.name = data["name"]
self.id = data["id"]
self.users = None
self.albums = None
self.admins = None
def delete(self):
"""
Supprime le groupe, et les albums laissés orphelins (sans utilisateur)
"""
db = get_db()
db.execute(
"""
DELETE FROM groupe
WHERE id = ?
""",
(self.id,)
)
db.execute(
"""
DELETE FROM groupe_contient_user
WHERE groupe_id = ?
""",
(self.id,)
)
db.execute(
"""
DELETE FROM groupe_contient_album
WHERE groupe_id = ?
""",
(self.id,)
)
db.commit()
# Supprime tous les albums laissés orphelins (maintenant ou plus tôt)
data = db.execute(
"""
SELECT id FROM album
LEFT JOIN groupe_contient_album
LEFT JOIN contient_user
ON groupe_contient_album.album_id=album.id
AND contient_user.album_id=album.id
WHERE user_id IS NULL AND groupe_id IS NULL
"""
).fetchall()
for i in data:
album = Album(id=i["id"])
album.delete()
def get_users(self):
"""
Renvoie les data["id"] des utilisateurs liés au groupe
TODO: uniformiser le tout
"""
db = get_db()
return db.execute(
"""
SELECT * FROM user
JOIN groupe_contient_user ON user_id = user.id
JOIN groupe ON groupe.id = groupe_id
WHERE groupe.id = ?
""",
(self.id,)
).fetchall()
def get_albums(self, force_reload=False):
"""
Renvoie les uuids des albums liés au groupe
"""
if self.albums is None or force_reload:
db = get_db()
data = db.execute(
"""
SELECT * FROM album
JOIN groupe_contient_album ON album_id = album.id
JOIN groupe ON groupe.id = groupe_id
WHERE groupe.id = ?
""",
(self.id,)
).fetchall()
self.albums = [Album(uuid=i["uuid"]) for i in data]
return self.albums
def get_admins(self):
"""
Renvoie les ids utilisateurs administrateurs liés au groupe
"""
db = get_db()
data = db.execute(
"""
SELECT user.id FROM user
JOIN groupe_contient_user ON user_id = user.id
JOIN groupe ON groupe.id = groupe_id
WHERE is_admin=1 AND groupe.id = ?
""",
(self.id,)
).fetchall()
return [i["id"] for i in data]

View File

@ -2,6 +2,7 @@ from flask import current_app
from ..db import get_db from ..db import get_db
from .album import Album from .album import Album
from .groupe import Groupe
# Variables defined in the CSS # Variables defined in the CSS
@ -28,6 +29,7 @@ class User():
self.id = user_id self.id = user_id
self.username = name self.username = name
self.albums = None self.albums = None
self.groupes = None
self.partitions = None self.partitions = None
self.max_queries = 0 self.max_queries = 0
@ -64,10 +66,10 @@ class User():
self.max_queries = current_app.config["MAX_ONLINE_QUERIES"] self.max_queries = current_app.config["MAX_ONLINE_QUERIES"]
def is_participant(self, album_uuid): def is_participant(self, album_uuid, exclude_groupe=False):
db = get_db() db = get_db()
return len(db.execute( return (len(db.execute( # Is participant directly in the album
""" """
SELECT album.id FROM album SELECT album.id FROM album
JOIN contient_user ON album_id = album.id JOIN contient_user ON album_id = album.id
@ -75,16 +77,35 @@ class User():
WHERE user.id = ? AND album.uuid = ? WHERE user.id = ? AND album.uuid = ?
""", """,
(self.id, album_uuid) (self.id, album_uuid)
).fetchall()) == 1 ).fetchall()) == 1 or
# Is participant in a group that has this album
((not exclude_groupe) and (len(db.execute(
"""
SELECT album.id FROM album
JOIN groupe_contient_album
JOIN groupe_contient_user
JOIN user
ON user_id = user.id
AND groupe_contient_user.groupe_id = groupe_contient_album.groupe_id
AND album.id = album_id
WHERE user.id = ? AND album.uuid = ?
""",
(self.id, album_uuid)
).fetchall()) >= 1))
)
def get_albums(self, force_reload=False): def get_albums(self, force_reload=False):
if self.albums is None or force_reload: if self.albums is None or force_reload:
db = get_db() db = get_db()
if self.access_level == 1: if self.access_level == 1:
# On récupère tous les albums qui ne sont pas dans un groupe
self.albums = db.execute( self.albums = db.execute(
""" """
SELECT * FROM album SELECT * FROM album
LEFT JOIN groupe_contient_album
ON album_id=album.id
WHERE album_id IS NULL
""" """
).fetchall() ).fetchall()
else: else:
@ -100,6 +121,31 @@ class User():
return self.albums return self.albums
def get_groupes(self, force_reload=False):
if self.groupes is None or force_reload:
db = get_db()
if self.access_level == 1:
data = db.execute(
"""
SELECT uuid FROM groupe
"""
).fetchall()
else:
data = db.execute(
"""
SELECT uuid FROM groupe
JOIN groupe_contient_user ON groupe.id = groupe_id
JOIN user ON user_id = user.id
WHERE user.id = ?
""",
(self.id,),
).fetchall()
self.groupes = [Groupe(i["uuid"]) for i in data]
return self.groupes
def get_partitions(self, force_reload=False): def get_partitions(self, force_reload=False):
if self.partitions is None or force_reload: if self.partitions is None or force_reload:
db = get_db() db = get_db()
@ -133,6 +179,19 @@ class User():
) )
db.commit() db.commit()
def join_groupe(self, groupe_uuid):
db = get_db()
groupe = Groupe(uuid=groupe_uuid)
db.execute(
"""
INSERT INTO groupe_contient_user (groupe_id, user_id)
VALUES (?, ?)
""",
(groupe.id, self.id)
)
db.commit()
def quit_album(self, album_uuid): def quit_album(self, album_uuid):
db = get_db() db = get_db()
album = Album(uuid=album_uuid) album = Album(uuid=album_uuid)
@ -147,6 +206,20 @@ class User():
) )
db.commit() db.commit()
def quit_groupe(self, groupe_uuid):
db = get_db()
groupe = Groupe(uuid=groupe_uuid)
db.execute(
"""
DELETE FROM groupe_contient_user
WHERE user_id = ?
AND groupe_id = ?
""",
(self.id, groupe.id)
)
db.commit()
def get_color(self): def get_color(self):
if len(colors) == 0: if len(colors) == 0:

View File

@ -0,0 +1,247 @@
#!/usr/bin/python3
"""
Groupe module
"""
import os
from uuid import uuid4
from flask import (Blueprint, abort, flash, redirect, render_template, request,
send_file, session, current_app)
from .auth import login_required
from .db import get_db
from .utils import User, Album, get_all_partitions, Groupe
from . import search
bp = Blueprint("groupe", __name__, url_prefix="/groupe")
@bp.route("/")
def index():
return redirect("/")
@bp.route("/<uuid>")
def groupe(uuid):
"""
Groupe page
"""
try:
groupe = Groupe(uuid=uuid)
groupe.users = [User(user_id=i["id"]) for i in groupe.get_users()]
groupe.get_albums()
user = User(user_id=session.get("user_id"))
if user.id is None:
# On ne propose pas aux gens non connectés de rejoindre l'album
not_participant = False
else:
not_participant = not user.id in [i.id for i in groupe.users]
return render_template(
"groupe/index.html",
groupe=groupe,
not_participant=not_participant,
user=user
)
except LookupError:
return abort(404)
@bp.route("/create-groupe", methods=["POST"])
@login_required
def create_groupe():
current_user = User(user_id=session.get("user_id"))
name = request.form["name"]
db = get_db()
error = None
if not name or name.strip() == "":
error = "Un nom est requis. Le groupe n'a pas été créé"
if error is None:
while True:
try:
uuid = str(uuid4())
db.execute(
"""
INSERT INTO groupe (uuid, name)
VALUES (?, ?)
""",
(uuid, name),
)
db.commit()
groupe = Groupe(uuid=uuid)
db.execute(
"""
INSERT INTO groupe_contient_user (user_id, groupe_id, is_admin)
VALUES (?, ?, 1)
""",
(session.get("user_id"), groupe.id),
)
db.commit()
break
except db.IntegrityError:
pass
return redirect(f"/groupe/{uuid}")
flash(error)
return redirect(request.referrer)
@bp.route("/<uuid>/join")
@login_required
def join_groupe(uuid):
user = User(user_id=session.get("user_id"))
try:
user.join_groupe(uuid)
except LookupError:
flash("Ce groupe n'existe pas.")
return redirect(f"/groupe/{uuid}")
flash("Groupe ajouté à la collection.")
return redirect(f"/groupe/{uuid}")
@bp.route("/<uuid>/quit")
@login_required
def quit_groupe(uuid):
user = User(user_id=session.get("user_id"))
groupe = Groupe(uuid=uuid)
users = groupe.get_users()
if user.id not in [u["id"] for u in users]:
flash("Vous ne faites pas partie de ce groupe")
return redirect(f"/groupe/{uuid}")
if len(users) == 1:
flash("Vous êtes seul dans ce groupe, le quitter entraînera sa suppression.")
return redirect(f"/groupe/{uuid}#delete")
user.quit_groupe(groupe.uuid)
flash("Groupe quitté.")
return redirect(f"/albums")
@bp.route("/<uuid>/delete", methods=["POST"])
@login_required
def delete_groupe(uuid):
db = get_db()
groupe = Groupe(uuid=uuid)
user = User(user_id=session.get("user_id"))
error = None
users = groupe.get_users()
if len(users) > 1:
error = "Vous n'êtes pas seul dans ce groupe."
if user.access_level == 1 or user.id not in groupe.get_admins():
error = None
if error is not None:
flash(error)
return redirect(request.referrer)
groupe.delete()
flash("Groupe supprimé.")
return redirect("/albums")
@bp.route("/<groupe_uuid>/create-album", methods=["POST"])
@login_required
def create_album(groupe_uuid):
try:
groupe = Groupe(uuid=groupe_uuid)
except LookupError:
abort(404)
user = User(user_id=session.get("user_id"))
name = request.form["name"]
db = get_db()
error = None
if not name or name.strip() == "":
error = "Un nom est requis. L'album n'a pas été créé"
if user.id not in groupe.get_admins():
error ="Vous n'êtes pas administrateur de ce groupe"
if error is None:
while True:
try:
uuid = str(uuid4())
db.execute(
"""
INSERT INTO album (uuid, name)
VALUES (?, ?)
""",
(uuid, name),
)
db.commit()
album = Album(uuid=uuid)
db.execute(
"""
INSERT INTO groupe_contient_album (groupe_id, album_id)
VALUES (?, ?)
""",
(groupe.id, album.id)
)
db.commit()
break
except db.IntegrityError:
pass
return redirect(f"/groupe/{groupe.uuid}/{uuid}")
flash(error)
return redirect(request.referrer)
@bp.route("/<groupe_uuid>/<album_uuid>")
def album(groupe_uuid, album_uuid):
"""
Album page
"""
try:
groupe = Groupe(uuid=groupe_uuid)
except LookupError:
abort(404)
album_list = [a for a in groupe.get_albums() if a.uuid == album_uuid]
if len(album_list) == 0:
abort(404)
album = album_list[0]
user = User(user_id=session.get("user_id"))
# List of users without duplicate
users_id = list(set([i["id"] for i in album.get_users()+groupe.get_users()]))
album.users = [User(user_id=id) for id in users_id]
partitions = album.get_partitions()
if user.id is None:
# On ne propose pas aux gens non connectés de rejoindre l'album
not_participant = False
else:
not_participant = not user.is_participant(album.uuid, exclude_groupe=True)
return render_template(
"albums/album.html",
album=album,
groupe=groupe,
partitions=partitions,
not_participant=not_participant,
user=user
)

View File

@ -5,10 +5,10 @@ from .db import get_db
from .classes.user import User from .classes.user import User
from .classes.album import Album from .classes.album import Album
from .classes.groupe import Groupe
from .classes.partition import Partition from .classes.partition import Partition
def get_all_partitions(): def get_all_partitions():
db = get_db() db = get_db()
partitions = db.execute( partitions = db.execute(

View File

@ -4,6 +4,9 @@ DROP TABLE IF EXISTS album;
DROP TABLE IF EXISTS contient_partition; DROP TABLE IF EXISTS contient_partition;
DROP TABLE IF EXISTS contient_user; DROP TABLE IF EXISTS contient_user;
DROP TABLE IF EXISTS search_results; DROP TABLE IF EXISTS search_results;
DROP TABLE IF EXISTS groupe;
DROP TABLE IF EXISTS groupe_contient_user;
DROP TABLE IF EXISTS groupe_contient_album;
CREATE TABLE user ( CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -44,3 +47,22 @@ CREATE TABLE search_results (
url TEXT, url TEXT,
creation_time TEXT NULL DEFAULT (datetime('now', 'localtime')) creation_time TEXT NULL DEFAULT (datetime('now', 'localtime'))
); );
CREATE TABLE groupe (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
uuid TEXT(36) NOT NULL
);
CREATE TABLE groupe_contient_user (
groupe_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (groupe_id, user_id)
);
CREATE TABLE groupe_contient_album (
groupe_id INTEGER NOT NULL,
album_id INTEGER NOT NULL,
PRIMARY KEY (groupe_id, album_id)
);

View File

@ -164,7 +164,7 @@ body {
padding: 5px; padding: 5px;
} }
.album-cover { .album-cover, .groupe-cover {
padding: 5px; padding: 5px;
margin: 5px; margin: 5px;
border-radius: 3px; border-radius: 3px;
@ -172,10 +172,21 @@ body {
overflow-x: hidden; overflow-x: hidden;
} }
.album-cover:hover { .album-cover:hover, .groupe-album-cover:hover {
background-color: var(--color-base); background-color: var(--color-base);
} }
.groupe-cover {
background-color: var(--color-crust);
}
.groupe-albums-cover {
background-color: var(--color-mantle);
border-radius: 3px;
margin: -1px;
margin-top: 10px;
}
/** Sidebar toggle */ /** Sidebar toggle */
#sidebar { #sidebar {
@ -341,8 +352,19 @@ img.partition-thumbnail {
} }
/** Albums grid in groupe view */
#albums-grid > a > .album {
padding: 10px;
border-radius: 3px;
}
#albums-grid > a > .album:hover {
background-color: var(--color-surface0);
}
/** Sidebar content */ /** Sidebar content */
#new-album-button { .create-button {
text-align: center; text-align: center;
margin: 10px; margin: 10px;
background-color: var(--color-surface1); background-color: var(--color-surface1);
@ -352,18 +374,18 @@ img.partition-thumbnail {
border: 2px solid var(--color-overlay0); border: 2px solid var(--color-overlay0);
} }
#new-album-button:hover { .create-button:hover {
border-color: var(--color-blue); border-color: var(--color-blue);
background-color: var(--color-surface0); background-color: var(--color-surface0);
} }
#albums { #sidebar-navigation {
overflow: scroll; overflow: scroll;
height: calc(100% - 375px); /* we don't want it hidden behind settings */ height: calc(100% - 400px); /* we don't want it hidden behind settings */
padding: 0 5px; padding: 0 5px;
} }
#albums div { #albums div, #groupes div {
padding: 5px; padding: 5px;
} }

View File

@ -28,7 +28,11 @@
{% block content %} {% block content %}
<header id="album-header"> <header id="album-header">
<h2 id="album-title">{{ album.name }}</h2> <h2 id="album-title">
{% if groupe %}<a href="/groupe/{{ groupe.uuid }}">{{ groupe.name}}</a> /
{% endif %}
{{ album.name }}
</h2>
{% if g.user %} {% if g.user %}
<div id="header-actions"> <div id="header-actions">
<section id="users"> <section id="users">

View File

@ -22,6 +22,11 @@
{% for album in user.albums %} {% for album in user.albums %}
<option value="{{ album['uuid'] }}">{{ album["name"] }}</option> <option value="{{ album['uuid'] }}">{{ album["name"] }}</option>
{% endfor %} {% endfor %}
{% for groupe in user.get_groupes() %}
{% for album in groupe.get_albums() %}
<option value="{{ album['uuid'] }}">{{ groupe.name }}/{{ album["name"] }}</option>
{% endfor %}
{% endfor %}
</select> </select>
<input type="hidden" value="{{ partition['uuid'] }}" name="partition-uuid"> <input type="hidden" value="{{ partition['uuid'] }}" name="partition-uuid">
<input type="hidden" value="local_file" name="partition-type"> <input type="hidden" value="local_file" name="partition-type">

View File

@ -27,6 +27,14 @@
</form> </form>
<a href="#!" class="close-dialog">Close</a> <a href="#!" class="close-dialog">Close</a>
</dialog> </dialog>
<dialog id="create-groupe">
<h2>Créer un nouveau groupe</h2>
<form action="/groupe/create-groupe" method="post">
<input type="text" name="name" id="name" placeholder="Nom" required><br/>
<input type="submit" value="Créer">
</form>
<a href="#!" class="close-dialog">Close</a>
</dialog>
{% endif %} {% endif %}
<div class="mask" id="!"></div> <div class="mask" id="!"></div>
</div> </div>
@ -60,26 +68,65 @@
<h2>Albums</h2> <h2>Albums</h2>
{% if g.user %} {% if g.user %}
<a href="#create-album"> <a href="#create-album">
<div id="new-album-button"> <div class="create-button">
Créer un album Créer un album
</div> </div>
</a> </a>
<a href="#create-groupe">
<div class="create-button">
Créer un groupe
</div>
</a>
{% endif %} {% endif %}
<section id="albums">
{% if not g.user %} {% if g.user %}
<div style="text-align: center;"><i>Connectez vous pour avoir accès à vos albums</i></div> <section id="sidebar-navigation">
{% elif user.get_albums() | length == 0 %} <section id="groupes">
{% if user.get_groupes() | length > 0 %}
{% for groupe in user.groupes %}
<div class="groupe-cover">
<details>
<summary>
<a href="/groupe/{{ groupe.uuid }}">{{ groupe.name }}</a>
</summary>
<div class="groupe-albums-cover">
{% if groupe.get_albums() | length == 0 %}
Aucun album
{% else %}
{% for album in groupe.get_albums() %}
<a href="/groupe/{{ groupe.uuid }}/{{ album["uuid"] }}">
<div class="groupe-album-cover">
{{ album["name"] }}
</div>
</a>
{% endfor %}
{% endif %}
</div>
</details>
</div>
{% endfor %}
{% endif %}
</section>
<section id="albums">
{% if user.get_albums() | length == 0 %}
<div style="text-align: center;"><i>Aucun album disponible</i></div> <div style="text-align: center;"><i>Aucun album disponible</i></div>
{% else %} {% else %}
{% for album in user.albums %} {% for album in user.albums %}
<a href="/albums/{{ album['uuid'] }}"> <a href="/albums/{{ album['uuid'] }}">
<div class="album-cover" id="album-1"> <div class="album-cover">
{{ album["name"] }} {{ album["name"] }}
</div> </div>
</a> </a>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</section>
</section> </section>
{% else %}
<section id="sidebar-navigation">
<div style="text-align: center;"><i>Connectez vous pour avoir accès à vos albums</i></div>
</section>
{% endif %}
<div id="settings-container"> <div id="settings-container">
{% if g.user %} {% if g.user %}

View File

@ -0,0 +1,71 @@
{% extends 'base.html' %}
{% block title %}{{ groupe.name }}{% endblock %}
{% block dialogs %}
<dialog id="create-groupe-album">
<h2>Créer un nouvel album dans le groupe {{ groupe.name }}</h2>
<form action="/groupe/{{ groupe.uuid }}/create-album" method="post">
<input type="text" name="name" id="name" placeholder="Nom" required><br/>
<input type="submit" value="Ajouter">
</form>
<a href="#!" class="close-dialog">Close</a>
</dialog>
<dialog id="delete">
<h2>Supprimer le groupe</h2>
Êtes vous sûr de vouloir supprimer ce groupe ? Cela supprimera les albums
sous-jacents et leurs partitions si personne ne les a rejoints (indépendamment du groupe).
<br/><br/>
<form method="post" action="/groupe/{{ groupe.uuid }}/delete">
<input type="submit" style="background-color: var(--color-red);" value="Supprimer">
</form>
<a href="#!" class="close-dialog">Close</a>
</dialog>
{% endblock %}
{% block content %}
<header id="album-header">
<h2 id="groupe-title">{{ groupe.name }}</h2>
{% if g.user %}
<div id="header-actions">
<section id="users">
{% for groupe_user in groupe.users %}
<div class="user-profile-picture" style="background-color:{{ groupe_user.color }};" title="{{ groupe_user.username }}">
{{ groupe_user.username[0] | upper }}
</div>
{% endfor %}
</section>
<div class="dropdown dp1">
+
<div class="dropdown-content dp1">
{% if not_participant %}
<a href="/groupe/{{ groupe.uuid }}/join">Rejoindre</a>
{% elif groupe.users | length > 1 %}
<a href="/groupe/{{ groupe.uuid }}/quit">Quitter</a>
{% endif %}
{% if g.user.access_level == 1 or user.id in groupe.get_admins() %}
<a href="#create-groupe-album">Ajouter un album</a>
<a id="delete-album" href="#delete">Supprimer</a>
{% endif %}
</div>
</div>
</div>
{% endif %}
</header>
<hr/>
{% if groupe.albums|length != 0 %}
<section id="albums-grid">
{% for album in groupe.albums | reverse %}
<a href="/groupe/{{ groupe.uuid }}/{{ album.uuid }}">
<div class="album">
{{ album.name }}
</div>
</a>
{% endfor %}
</section>
{% else %}
<br/>
<div id="albums-grid" style="display: inline;">Aucun album disponible. <a href="#create-groupe-album">En créer un</a></div>
{% endif %}
{% endblock %}