Add the ability to attach files to a partition

This commit is contained in:
augustin64 2023-10-26 14:14:40 +02:00
parent 971d54af41
commit f8670f4901
16 changed files with 337 additions and 30 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ partitioncloud/partitions
partitioncloud/search-partitions partitioncloud/search-partitions
partitioncloud/static/thumbnails partitioncloud/static/thumbnails
partitioncloud/static/search-thumbnails partitioncloud/static/search-thumbnails
partitioncloud/attachments

View File

@ -3,6 +3,7 @@
init () { init () {
mkdir -p "instance" mkdir -p "instance"
mkdir -p "partitioncloud/partitions" mkdir -p "partitioncloud/partitions"
mkdir -p "partitioncloud/attachments"
mkdir -p "partitioncloud/search-partitions" mkdir -p "partitioncloud/search-partitions"
mkdir -p "partitioncloud/static/thumbnails" mkdir -p "partitioncloud/static/thumbnails"
mkdir -p "partitioncloud/static/search-thumbnails" mkdir -p "partitioncloud/static/search-thumbnails"

View File

@ -65,10 +65,14 @@ class Album():
db = get_db() db = get_db()
return db.execute( return db.execute(
""" """
SELECT partition.uuid, partition.name, partition.author, partition.user_id FROM partition SELECT p.uuid, p.name, p.author, p.user_id,
JOIN contient_partition ON partition_uuid = partition.uuid CASE WHEN MAX(a.uuid) IS NOT NULL THEN 1 ELSE 0 END AS has_attachment
FROM partition AS p
JOIN contient_partition ON contient_partition.partition_uuid = p.uuid
JOIN album ON album.id = album_id JOIN album ON album.id = album_id
LEFT JOIN attachments AS a ON p.uuid = a.partition_uuid
WHERE album.uuid = ? WHERE album.uuid = ?
GROUP BY p.uuid, p.name, p.author, p.user_id
""", """,
(self.uuid,), (self.uuid,),
).fetchall() ).fetchall()

View File

@ -0,0 +1,33 @@
from flask import current_app
from ..db import get_db
class Attachment():
def __init__(self, uuid=None, data=None):
db = get_db()
if uuid is not None:
self.uuid = uuid
data = db.execute(
"""
SELECT * FROM attachments
WHERE uuid = ?
""",
(self.uuid,)
).fetchone()
if data is None:
raise LookupError
elif data is not None:
self.uuid = data["uuid"]
else:
raise LookupError
self.name = data["name"]
self.user_id = data["user_id"]
self.filetype = data["filetype"]
self.partition_uuid = data["partition_uuid"]
def __repr__(self):
return f"{self.name}.{self.filetype}"

View File

@ -3,6 +3,7 @@ from flask import current_app
from ..db import get_db from ..db import get_db
from .user import User from .user import User
from .attachment import Attachment
@ -25,6 +26,7 @@ class Partition():
self.body = data["body"] self.body = data["body"]
self.user_id = data["user_id"] self.user_id = data["user_id"]
self.source = data["source"] self.source = data["source"]
self.attachments = None
else: else:
raise LookupError raise LookupError
@ -95,3 +97,15 @@ class Partition():
""", """,
(self.uuid,), (self.uuid,),
).fetchall() ).fetchall()
def load_attachments(self):
db = get_db()
if self.attachments is None:
data = db.execute(
"""
SELECT * FROM attachments
WHERE partition_uuid = ?
""",
(self.uuid,)
)
self.attachments = [Attachment(data=i) for i in data]

View File

@ -3,11 +3,12 @@
Partition module Partition module
""" """
import os import os
from uuid import uuid4
from flask import Blueprint, abort, send_file, render_template, request, redirect, flash, session from flask import Blueprint, abort, send_file, render_template, request, redirect, flash, session
from .db import get_db from .db import get_db
from .auth import login_required, admin_required from .auth import login_required, admin_required
from .utils import get_all_partitions, User, Partition from .utils import get_all_partitions, User, Partition, Attachment
bp = Blueprint("partition", __name__, url_prefix="/partition") bp = Blueprint("partition", __name__, url_prefix="/partition")
@ -15,21 +16,110 @@ bp = Blueprint("partition", __name__, url_prefix="/partition")
@bp.route("/<uuid>") @bp.route("/<uuid>")
def partition(uuid): def partition(uuid):
db = get_db() db = get_db()
partition = db.execute( try:
""" partition = Partition(uuid=uuid)
SELECT * FROM partition except LookupError:
WHERE uuid = ?
""",
(uuid,)
).fetchone()
if partition is None:
abort(404) abort(404)
return send_file( return send_file(
os.path.join("partitions", f"{uuid}.pdf"), os.path.join("partitions", f"{uuid}.pdf"),
download_name = f"{partition['name']}.pdf" download_name = f"{partition.name}.pdf"
) )
@bp.route("/<uuid>/attachments")
def attachments(uuid):
db = get_db()
try:
partition = Partition(uuid=uuid)
except LookupError:
abort(404)
partition.load_attachments()
return render_template(
"partition/attachments.html",
partition=partition,
user=User(user_id=session.get("user_id"))
)
@bp.route("/<uuid>/add-attachment", methods=["POST"])
@login_required
def add_attachment(uuid):
db = get_db()
try:
partition = Partition(uuid=uuid)
except LookupError:
abort(404)
user = User(user_id=session.get("user_id"))
if user.id != partition.user_id and user.access_level != 1:
flash("Cette partition ne vous appartient pas")
return redirect(request.referrer)
error = None # À mettre au propre
if "file" not in request.files:
error = "Aucun fichier n'a été fourni."
else:
if "name" not in request.form or request.form["name"] == "":
name = ".".join(request.files["file"].filename.split(".")[:-1])
else:
name = request.form["name"]
if name == "":
error = "Pas de nom de fichier"
else:
filename = request.files["file"].filename
ext = filename.split(".")[-1]
if ext not in ["mid", "mp3"]:
error = "Extension de fichier non supportée"
if error is not None:
flash(error)
return redirect(request.referrer)
while True:
try:
attachment_uuid = str(uuid4())
db.execute(
"""
INSERT INTO attachments (uuid, name, filetype, partition_uuid, user_id)
VALUES (?, ?, ?, ?, ?)
""",
(attachment_uuid, name, ext, partition.uuid, user.id),
)
db.commit()
file = request.files["file"]
file.save(f"partitioncloud/attachments/{attachment_uuid}.{ext}")
break
except db.IntegrityError:
pass
return redirect(f"/partition/{partition.uuid}/attachments")
@bp.route("/attachment/<uuid>.<filetype>")
def attachment(uuid, filetype):
db = get_db()
try:
attachment = Attachment(uuid=uuid)
except LookupError:
abort(404)
assert filetype == attachment.filetype
return send_file(
os.path.join("attachments", f"{uuid}.{attachment.filetype}"),
download_name = f"{attachment.name}.{attachment.filetype}"
)
@bp.route("/<uuid>/edit", methods=["GET", "POST"]) @bp.route("/<uuid>/edit", methods=["GET", "POST"])
@login_required @login_required
def edit(uuid): def edit(uuid):

View File

@ -7,13 +7,20 @@ from .classes.user import User
from .classes.album import Album from .classes.album import Album
from .classes.groupe import Groupe from .classes.groupe import Groupe
from .classes.partition import Partition from .classes.partition import Partition
from .classes.attachment import Attachment
def get_all_partitions(): def get_all_partitions():
db = get_db() db = get_db()
partitions = db.execute( partitions = db.execute(
""" """
SELECT * FROM partition SELECT p.uuid, p.name, p.author, p.body, p.user_id,
CASE WHEN MAX(a.uuid) IS NOT NULL THEN 1 ELSE 0 END AS has_attachment
FROM partition AS p
JOIN contient_partition ON contient_partition.partition_uuid = p.uuid
JOIN album ON album.id = album_id
LEFT JOIN attachments AS a ON p.uuid = a.partition_uuid
GROUP BY p.uuid, p.name, p.author, p.user_id
""" """
) )
# Transform sql object to dictionary usable in any thread # Transform sql object to dictionary usable in any thread
@ -23,7 +30,8 @@ def get_all_partitions():
"name": p["name"], "name": p["name"],
"author": p["author"], "author": p["author"],
"body": p["body"], "body": p["body"],
"user_id": p["user_id"] "user_id": p["user_id"],
"has_attachment": p["has_attachment"]
} for p in partitions } for p in partitions
] ]

View File

@ -7,6 +7,7 @@ DROP TABLE IF EXISTS search_results;
DROP TABLE IF EXISTS groupe; DROP TABLE IF EXISTS groupe;
DROP TABLE IF EXISTS groupe_contient_user; DROP TABLE IF EXISTS groupe_contient_user;
DROP TABLE IF EXISTS groupe_contient_album; DROP TABLE IF EXISTS groupe_contient_album;
DROP TABLE IF EXISTS attachments;
CREATE TABLE user ( CREATE TABLE user (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -66,3 +67,11 @@ CREATE TABLE groupe_contient_album (
album_id INTEGER NOT NULL, album_id INTEGER NOT NULL,
PRIMARY KEY (groupe_id, album_id) PRIMARY KEY (groupe_id, album_id)
); );
CREATE TABLE attachments (
uuid TEXT(36) PRIMARY KEY,
name TEXT NOT NULL,
filetype TEXT NOT NULL DEFAULT 'mp3',
partition_uuid INTEGER NOT NULL,
user_id INTEGER NOT NULL
);

View File

@ -342,13 +342,18 @@ img.partition-thumbnail {
min-height: 50px; min-height: 50px;
} }
.edit-button { .partition-action {
float: right; padding: 7px;
transform: translateX(-96%) translateY(-162%); margin: 3px;
padding: 2%;
border-radius: 3px; border-radius: 3px;
box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.2); box-shadow: 1px 2px 2px rgba(0, 0, 0, 0.2);
background-color: var(--color-blue); background-color: #cdd6f4;
}
.partition-buttons {
float: right;
display: flex;
transform: translateX(-22px) translateY(-115px);
} }
@ -705,3 +710,43 @@ td {
.x-scrollable { .x-scrollable {
overflow-x: auto; overflow-x: auto;
} }
/** Attachment page */
#pdf-embed {
margin: auto;
width: 100%;
width: -moz-available;
width: -webkit-fill-available;
width: stretch;
height: 50vh;
}
midi-visualizer {
background-color: white;
border-radius: 3px;
}
midi-player {
color: black;
}
#attachments {
overflow-y: scroll;
}
#attachments > table {
border: none;
}
#attachments > table > tbody > tr > td {
border: none;
min-width: fit-content;
}
.centered {
justify-content: center;
display: flex;
}

View File

@ -18,7 +18,12 @@
</div> </div>
</div> </div>
</a> </a>
<a href="/partition/{{ partition['uuid'] }}/details"><div class="edit-button">🔍</div></a> <div class="partition-buttons">
{% if partition["has_attachment"] %}
<a href="/partition/{{ partition['uuid'] }}/attachments"><div class="partition-action">📎</div></a>
{% endif %}
<a href="/partition/{{ partition['uuid'] }}/details"><div class="edit-button partition-action">🔍</div></a>
</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@ -10,7 +10,7 @@
<input name="name" type="text" required="" placeholder="Titre"><br/> <input name="name" type="text" required="" placeholder="Titre"><br/>
<input name="author" type="text" placeholder="Auteur"><br/> <input name="author" type="text" placeholder="Auteur"><br/>
<textarea id="paroles" name="body" type="text" placeholder="Paroles"></textarea><br/> <textarea id="paroles" name="body" type="text" placeholder="Paroles"></textarea><br/>
<input name="file" type="file" required=""><br/> <input name="file" type="file" accept=".pdf" required=""><br/>
<input type="submit" value="Ajouter"> <input type="submit" value="Ajouter">
</form> </form>
<a href="#!" class="close-dialog">Close</a> <a href="#!" class="close-dialog">Close</a>
@ -75,9 +75,14 @@
</div> </div>
</div> </div>
</a> </a>
{% if partition["user_id"] == g.user.id or g.user.access_level == 1 %} <div class="partition-buttons">
<a href="/partition/{{ partition['uuid'] }}/edit"><div class="edit-button">✏️</div></a> {% if partition["has_attachment"] %}
<a href="/partition/{{ partition['uuid'] }}/attachments"><div class="partition-action">📎</div></a>
{% endif %} {% endif %}
{% if partition["user_id"] == g.user.id or g.user.access_level == 1 %}
<a href="/partition/{{ partition['uuid'] }}/edit"><div class="partition-action">✏️</div></a>
{% endif %}
</div>
</div> </div>
{% endfor %} {% endfor %}
</section> </section>

View File

@ -17,6 +17,11 @@
</div> </div>
</div> </div>
</a> </a>
<div class="partition-buttons">
{% if partition["has_attachment"] %}
<a href="/partition/{{ partition['uuid'] }}/attachments"><div class="partition-action">📎</div></a>
{% endif %}
</div>
<form action="/albums/add-partition" class="add-partition-form" method="post"> <form action="/albums/add-partition" class="add-partition-form" method="post">
<select name="album-uuid"> <select name="album-uuid">
{% for album in user.albums %} {% for album in user.albums %}

View File

@ -170,9 +170,11 @@
</div> </div>
</div> </div>
<div id="content-container"> <div id="content-container">
{% if not DISABLE_HEADER %}
<header id="page-header"> <header id="page-header">
<h1>PartitionCloud</h1> <h1>PartitionCloud</h1>
</header> </header>
{% endif %}
{% for message in get_flashed_messages() %} {% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div> <div class="flash">{{ message }}</div>
{% endfor %} {% endfor %}

View File

@ -0,0 +1,63 @@
{% set DISABLE_HEADER=true %}
{% extends 'base.html' %}
{% block title %}Attachments de {{ partition.name }}{% endblock %}
{% block dialogs %}
<dialog id="create-attachment">
<h2>Ajouter un attachment à {{ partition.name }}</h2>
<form action="/partition/{{ partition.uuid }}/add-attachment" method="post" enctype="multipart/form-data">
<input type="text" name="name" id="name" placeholder="Nom"><br/>
<input name="file" type="file" accept=".mp3,.mid" required=""><br/>
<input type="submit" value="Ajouter">
</form>
<a href="#!" class="close-dialog">Close</a>
</dialog>
{% endblock %}
{% block content %}
<object id="pdf-embed" width="400" height="500" type="application/pdf" data="/partition/{{ partition.uuid }}">
<p>
Impossible d'afficher le pdf dans ce navigateur.
Il est conseillé d'utiliser Firefox sur Android.
</p>
</object>
<script src="https://cdn.jsdelivr.net/combine/npm/tone@14.7.58,npm/@magenta/music@1.23.1/es6/core.js,npm/focus-visible@5,npm/html-midi-player@1.5.0"></script>
<midi-visualizer type="staff" id="midi-visualizer"></midi-visualizer>
{% if partition.attachments | length > 0 %}
<div id="attachments">
<table>
<tbody>
{% for attachment in partition.attachments %}
<tr>
{% if attachment.filetype == "mp3" %}
<td><audio controls src="/partition/attachment/{{ attachment.uuid }}.mp3"></td>
<td>🎙️ {{ attachment.name }}</td>
{% elif attachment.filetype == "mid" %}
<td><midi-player
src="/partition/attachment/{{ attachment.uuid }}.mid"
sound-font visualizer="#midi-visualizer" data-js-focus-visible>
</midi-player>
<noscript>MIDI support needs JavaScript</noscript>
</td>
<td>🎵 {{ attachment.name }}</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<br/>
{% if user %}
<div class="centered">
<a href="#create-attachment"><button>Ajouter un attachment</button></a>
</div>
{% endif %}
{% endblock %}

View File

@ -64,6 +64,17 @@
<td>Paroles</td> <td>Paroles</td>
<td><textarea id="paroles" name="body" type="text" placeholder="Paroles">{{ partition.body }}</textarea><br/></td> <td><textarea id="paroles" name="body" type="text" placeholder="Paroles">{{ partition.body }}</textarea><br/></td>
</tr> </tr>
<tr>
<td>Pièces jointes</td>
{% set _ = partition.load_attachments() %}
<td><a href="/partition/{{ partition.uuid }}/attachments">
{% if partition.attachments %}
Oui, {{ partition.attachments | length }}
{% else %}
En rajouter
{% endif %}
</a></td>
</tr>
</tbody> </tbody>
</table> </table>
<input type="submit" value="Mettre à jour" /> <input type="submit" value="Mettre à jour" />

View File

@ -35,6 +35,17 @@
<td>Paroles</td> <td>Paroles</td>
<td><textarea id="paroles" name="body" type="text" placeholder="Paroles">{{ partition.body }}</textarea><br/></td> <td><textarea id="paroles" name="body" type="text" placeholder="Paroles">{{ partition.body }}</textarea><br/></td>
</tr> </tr>
<tr>
<td>Pièces jointes</td>
{% set _ = partition.load_attachments() %}
<td><a href="/partition/{{ partition.uuid }}/attachments">
{% if partition.attachments %}
Oui, {{ partition.attachments | length }}
{% else %}
En rajouter
{% endif %}
</a></td>
</tr>
</tbody> </tbody>
</table> </table>
<input type="submit" value="Mettre à jour" /> <input type="submit" value="Mettre à jour" />