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 .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.db import get_db
@ -26,6 +26,7 @@ else:
app.register_blueprint(auth.bp)
app.register_blueprint(admin.bp)
app.register_blueprint(groupe.bp)
app.register_blueprint(albums.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 .album import Album
from .groupe import Groupe
# Variables defined in the CSS
@ -28,6 +29,7 @@ class User():
self.id = user_id
self.username = name
self.albums = None
self.groupes = None
self.partitions = None
self.max_queries = 0
@ -64,10 +66,10 @@ class User():
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()
return len(db.execute(
return (len(db.execute( # Is participant directly in the album
"""
SELECT album.id FROM album
JOIN contient_user ON album_id = album.id
@ -75,16 +77,35 @@ class User():
WHERE user.id = ? AND 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):
if self.albums is None or force_reload:
db = get_db()
if self.access_level == 1:
# On récupère tous les albums qui ne sont pas dans un groupe
self.albums = db.execute(
"""
SELECT * FROM album
LEFT JOIN groupe_contient_album
ON album_id=album.id
WHERE album_id IS NULL
"""
).fetchall()
else:
@ -100,6 +121,31 @@ class User():
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):
if self.partitions is None or force_reload:
db = get_db()
@ -133,6 +179,19 @@ class User():
)
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):
db = get_db()
album = Album(uuid=album_uuid)
@ -147,6 +206,20 @@ class User():
)
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):
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.album import Album
from .classes.groupe import Groupe
from .classes.partition import Partition
def get_all_partitions():
db = get_db()
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_user;
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 (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -44,3 +47,22 @@ CREATE TABLE search_results (
url TEXT,
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;
}
.album-cover {
.album-cover, .groupe-cover {
padding: 5px;
margin: 5px;
border-radius: 3px;
@ -172,10 +172,21 @@ body {
overflow-x: hidden;
}
.album-cover:hover {
.album-cover:hover, .groupe-album-cover:hover {
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 {
@ -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 */
#new-album-button {
.create-button {
text-align: center;
margin: 10px;
background-color: var(--color-surface1);
@ -352,18 +374,18 @@ img.partition-thumbnail {
border: 2px solid var(--color-overlay0);
}
#new-album-button:hover {
.create-button:hover {
border-color: var(--color-blue);
background-color: var(--color-surface0);
}
#albums {
#sidebar-navigation {
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;
}
#albums div {
#albums div, #groupes div {
padding: 5px;
}

View File

@ -28,7 +28,11 @@
{% block content %}
<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 %}
<div id="header-actions">
<section id="users">

View File

@ -22,6 +22,11 @@
{% for album in user.albums %}
<option value="{{ album['uuid'] }}">{{ album["name"] }}</option>
{% 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>
<input type="hidden" value="{{ partition['uuid'] }}" name="partition-uuid">
<input type="hidden" value="local_file" name="partition-type">

View File

@ -27,6 +27,14 @@
</form>
<a href="#!" class="close-dialog">Close</a>
</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 %}
<div class="mask" id="!"></div>
</div>
@ -60,26 +68,65 @@
<h2>Albums</h2>
{% if g.user %}
<a href="#create-album">
<div id="new-album-button">
<div class="create-button">
Créer un album
</div>
</a>
<a href="#create-groupe">
<div class="create-button">
Créer un groupe
</div>
</a>
{% endif %}
<section id="albums">
{% if not g.user %}
<div style="text-align: center;"><i>Connectez vous pour avoir accès à vos albums</i></div>
{% elif user.get_albums() | length == 0 %}
{% if g.user %}
<section id="sidebar-navigation">
<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>
{% else %}
{% else %}
{% for album in user.albums %}
<a href="/albums/{{ album['uuid'] }}">
<div class="album-cover" id="album-1">
<div class="album-cover">
{{ album["name"] }}
</div>
</a>
{% endfor %}
{% endif %}
{% endif %}
</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">
{% 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 %}