diff --git a/partitioncloud/__init__.py b/partitioncloud/__init__.py index 7ac8f61..c1892de 100644 --- a/partitioncloud/__init__.py +++ b/partitioncloud/__init__.py @@ -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) diff --git a/partitioncloud/modules/classes/groupe.py b/partitioncloud/modules/classes/groupe.py new file mode 100644 index 0000000..9a318ad --- /dev/null +++ b/partitioncloud/modules/classes/groupe.py @@ -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] diff --git a/partitioncloud/modules/classes/user.py b/partitioncloud/modules/classes/user.py index 044b123..90110ad 100644 --- a/partitioncloud/modules/classes/user.py +++ b/partitioncloud/modules/classes/user.py @@ -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: diff --git a/partitioncloud/modules/groupe.py b/partitioncloud/modules/groupe.py new file mode 100644 index 0000000..bd62d3a --- /dev/null +++ b/partitioncloud/modules/groupe.py @@ -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("/") +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("//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("//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("//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("//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("//") +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 + ) \ No newline at end of file diff --git a/partitioncloud/modules/utils.py b/partitioncloud/modules/utils.py index 9013335..31c8d5c 100644 --- a/partitioncloud/modules/utils.py +++ b/partitioncloud/modules/utils.py @@ -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( diff --git a/partitioncloud/schema.sql b/partitioncloud/schema.sql index c0ede36..12678bd 100644 --- a/partitioncloud/schema.sql +++ b/partitioncloud/schema.sql @@ -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, @@ -43,4 +46,23 @@ CREATE TABLE search_results ( uuid TEXT(36) PRIMARY KEY, 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) ); \ No newline at end of file diff --git a/partitioncloud/static/style.css b/partitioncloud/static/style.css index eee523e..852b586 100644 --- a/partitioncloud/static/style.css +++ b/partitioncloud/static/style.css @@ -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; } diff --git a/partitioncloud/templates/albums/album.html b/partitioncloud/templates/albums/album.html index 1fa3e15..232c7b5 100644 --- a/partitioncloud/templates/albums/album.html +++ b/partitioncloud/templates/albums/album.html @@ -28,7 +28,11 @@ {% block content %}
-

{{ album.name }}

+

+ {% if groupe %}{{ groupe.name}} / + {% endif %} + {{ album.name }} +

{% if g.user %}
diff --git a/partitioncloud/templates/albums/search.html b/partitioncloud/templates/albums/search.html index 6b40213..16e8750 100644 --- a/partitioncloud/templates/albums/search.html +++ b/partitioncloud/templates/albums/search.html @@ -22,6 +22,11 @@ {% for album in user.albums %} {% endfor %} + {% for groupe in user.get_groupes() %} + {% for album in groupe.get_albums() %} + + {% endfor %} + {% endfor %} diff --git a/partitioncloud/templates/base.html b/partitioncloud/templates/base.html index d3b8a1c..9ab4936 100644 --- a/partitioncloud/templates/base.html +++ b/partitioncloud/templates/base.html @@ -27,6 +27,14 @@ Close + +

Créer un nouveau groupe

+
+
+ +
+ Close +
{% endif %}
@@ -60,26 +68,65 @@

Albums

{% if g.user %} -
+
Créer un album
+ +
+ Créer un groupe +
+
{% endif %} -
- {% if not g.user %} -
Connectez vous pour avoir accès à vos albums
- {% elif user.get_albums() | length == 0 %} + + {% if g.user %} + + {% else %} + + {% endif %}
{% if g.user %} diff --git a/partitioncloud/templates/groupe/index.html b/partitioncloud/templates/groupe/index.html new file mode 100644 index 0000000..68ff5a3 --- /dev/null +++ b/partitioncloud/templates/groupe/index.html @@ -0,0 +1,71 @@ +{% extends 'base.html' %} + +{% block title %}{{ groupe.name }}{% endblock %} + + +{% block dialogs %} + +

Créer un nouvel album dans le groupe {{ groupe.name }}

+
+
+ +
+ Close +
+ +

Supprimer le groupe

+ Ê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). +

+
+ +
+ Close +
+{% endblock %} + +{% block content %} +
+

{{ groupe.name }}

+ {% if g.user %} +
+
+ {% for groupe_user in groupe.users %} + + {% endfor %} +
+ +
+ {% endif %} +
+
+{% if groupe.albums|length != 0 %} +
+ {% for album in groupe.albums | reverse %} + +
+ {{ album.name }} +
+
+ {% endfor %} +
+{% else %} +
+
Aucun album disponible. En créer un
+{% endif %} +{% endblock %} \ No newline at end of file