Compare commits

...

41 Commits

Author SHA1 Message Date
bc91cec93f Fix groupe permissions 2025-03-28 19:41:57 +01:00
ffbf1907ad Generalization of drag & drop for attachments 2024-10-11 09:35:00 +02:00
009be93b7a Allow for drag & drop when creating partition 2024-10-10 10:54:46 +02:00
4fd789436b Fix migration hooks 2024-10-07 21:50:05 +02:00
b71953fd1b Improve search results
add new dependency: unidecode
2024-06-17 21:06:28 +02:00
9aa156a9b4 Fix bug: partition can be found multiple times with private search off 2024-06-16 17:04:49 +02:00
f6690f2013 Remove inappropriate "Quit" button 2024-04-19 18:48:19 +02:00
178a540791 Edit README 2024-04-19 18:48:00 +02:00
90c70a8d42 Add translation 2024-04-10 18:48:57 +02:00
dbe77d0ece Add UI button 2024-04-10 18:45:38 +02:00
d1812cdde7 Implement album and groupe zip download
Disabled by default for users not logged in
2024-04-10 18:38:47 +02:00
52894d37ea migration: Add --backup flag 2024-04-10 18:21:06 +02:00
1bd70c8653 Add Docker instructions 2024-03-03 22:56:49 +01:00
0a1d3f0f1c Translation typo 2024-03-03 22:55:44 +01:00
40becb01ce Set partitions to private by default (inaccessible from search for other users) 2024-02-29 13:14:28 +01:00
988f85b134 Update translations 2024-02-29 12:45:52 +01:00
7ae8a1939a config: add DISABLE_ACCOUNT_DELETION 2024-02-28 23:59:14 +01:00
bfb6a127f0 logging: Add "password change" and "account deletion" events 2024-02-28 23:38:14 +01:00
3cbc586c78 Add Dockerfile 2024-02-25 15:56:40 +01:00
5d0535ef70 Remove whitespaces 2024-02-25 15:54:48 +01:00
c702cb714e Move to gs to generate thumbnails 2024-02-25 15:53:51 +01:00
b9a5f92a56 Add /settings 2024-02-25 15:28:51 +01:00
3f83f1c44a check search pdfs 2024-02-21 15:53:49 +01:00
9a6d08d2e1 Fix logs incorrect formatting 2024-02-18 19:40:58 +01:00
ebc454f7a2 Add launch page 2024-01-29 18:37:30 +01:00
3f888c39d2 Add html lang attribute 2024-01-27 12:25:52 +01:00
eb5e1edf5e Fix bad mobile scaling 2024-01-27 12:25:27 +01:00
bf48ed29d7
Merge pull request #4 from partitioncloud/localization
Add localization
2024-01-26 19:32:48 +01:00
511a4b3626 Add automatic translations compilation 2024-01-26 19:32:22 +01:00
a97070eb2e Add flask-babel installation hook 2024-01-26 19:30:41 +01:00
c219f28a37 Fix recently introduced bugs 2024-01-26 11:14:43 +01:00
74444871c0 localization: Update all base strings to be in English 2024-01-26 09:48:11 +01:00
7acb446837 Localization: add templates strings 2024-01-25 16:22:04 +01:00
210ab6c0d3 Localization: add python strings 2024-01-22 16:06:03 +01:00
2ff7a515d5 Add ENABLED_LOGS config option 2024-01-19 13:48:23 +01:00
99c9781767 Add admin logs view 2024-01-19 13:38:05 +01:00
191ffebd7e Add logs server-side 2024-01-17 12:56:01 +01:00
5a7c3ed09d Hide qrcode loading 2024-01-16 21:39:28 +01:00
56d01ee3e2 Several minor changes (pylint) 2024-01-16 21:32:00 +01:00
fb13e396e5 Use lazy loading on all thumbnails 2024-01-16 21:25:32 +01:00
bb59dfe992 Change webapp background color 2024-01-16 21:24:42 +01:00
59 changed files with 2995 additions and 401 deletions

4
.gitignore vendored
View File

@ -1,6 +1,10 @@
# cache
**/__pycache__
# translations
**.mo
**.pot
# config
.vscode/

16
Dockerfile Normal file
View File

@ -0,0 +1,16 @@
FROM python:latest
WORKDIR /app
EXPOSE 5000
COPY . /app
RUN apt-get update
RUN apt-get install -y python3-pip sqlite3 ghostscript
RUN rm /app/instance -rf
RUN bash make.sh init
RUN pip3 install -r requirements.txt
RUN pip3 install gunicorn
CMD [ "bash", "make.sh" , "production"]

View File

@ -10,7 +10,7 @@ Serveur web (basé sur Flask) pour gérer sa collection de partitions musicales
- Thème sombre
- dashboard administrateur: gestion de tous les albums, partitions et utilisateurs
- [CLI](https://github.com/partitioncloud/partitioncloud-cli) uniquement à des fins de synchronisation. Il serait bon d'ajouter une BDD locale avec les UUIDs des partitions
- ~~Pas de Javascript~~ Complètement fonctionnel sans JavaScript, cela vient juste ajouter des [toutes petites améliorations](partitioncloud/static/main.js)
- ~~Pas de Javascript~~ Complètement fonctionnel sans JavaScript, cela vient juste ajouter des [toutes petites améliorations](partitioncloud/static/scripts)
## Points à noter
- Les partitions ajoutées sont accessibles à tous les utilisateurs depuis la recherche même si ils ne sont pas dans un album leur y donnant accès, pour limiter la redondance
@ -20,6 +20,26 @@ Serveur web (basé sur Flask) pour gérer sa collection de partitions musicales
## Installation
### Installation via Docker (recommandé)
```bash
# Clone this repo
git clone https://github.com/partitioncloud/partitioncloud-server.git
cd partitioncloud-server
# Create an image named "partitioncloud"
docker build -t partitioncloud .
# You can then run the container, replace $PORT with the port you want to be exposed
PORT=5000
docker run -d \
-p $PORT:5000 \
--restart=unless-stopped \
--name partitioncloud \
partitioncloud:latest
```
L'utilisateur par défaut est `root` avec le mot de passe `root`
### Installation manuelle
Installer le serveur
```bash
# Clone this repo
@ -27,6 +47,7 @@ git clone https://github.com/partitioncloud/partitioncloud-server.git
cd partitioncloud-server
# Install dependencies
pip install -r requirements.txt
pybabel compile -d partitioncloud/translations
# Create database and folders
./make.sh init
```
@ -66,9 +87,34 @@ Modifier le fichier de configuration créé dans `instance/`
![Recherche](https://github.com/partitioncloud/partitioncloud-server/assets/67148092/745bf3e3-37e9-40cd-80d2-14670bce1a45)
## Translations
### Créer une nouvelle traduction
```bash
# Extraire les données
pybabel extract -F babel.cfg -k _l -o partitioncloud/translations/messages.pot .
# Créer un nouveau fichier
pybabel init -i partitioncloud/translations/messages.pot -d partitioncloud/translations/ -l $COUNTRY_CODE
# Modifier translations/$COUNTRY_CODE/LC_MESSAGES/messages.po
# Ajouter $COUNTRY_CODE dans default_config.py: LANGUAGES
# Compiler les nouvelles translations avant de démarrer le serveur
pybabel compile -d partitioncloud/translations/
```
### Mettre à jour une traduction
```bash
# Récupérer les données les plus récentes
pybabel extract -F babel.cfg -k _l -o partitioncloud/translations/messages.pot .
# Les ajouter aux traductions
pybabel update -i partitioncloud/translations/messages.pot -d partitioncloud/translations/
```
## TODO
- [ ] Modifier son mot de passe
- [ ] Supprimer un utilisateur
- [x] Modifier son mot de passe
- [x] Supprimer un utilisateur
- [ ] Ajouter config:DISABLE_DARK_MODE
- [x] Ajouter config:DISABLE_REGISTER
- [ ] Ajouter config:ONLINE_SEARCH_BASE_QUERY pour la recherche google, actuellement 'filetype:pdf partition'
- [x] Ajouter un Dockerfile

5
babel.cfg Normal file
View File

@ -0,0 +1,5 @@
[extractors]
jinja2 = jinja2.ext:babel_extract
[python: partitioncloud/**.py]
[jinja2: partitioncloud/templates/**.html]

View File

@ -14,6 +14,12 @@ MAX_ONLINE_QUERIES=3
# Disable registration of new users via /auth/register (they can still be added by root)
DISABLE_REGISTER=False
# Disable account deletion for users (still possible for admins)
DISABLE_ACCOUNT_DELETION=False
# Set this to True if you want local search to be across all albums (not just those the user belong to)
PRIVATE_SEARCH=False
# Front URL of the application (for QRCodes generation)
BASE_URL="http://localhost:5000"
@ -24,3 +30,15 @@ MAX_AGE=31
# Keep in mind that this config option can only be loaded from default_config.py,
# as the custom config is stored in $INSTANCE_PATH/
INSTANCE_PATH="instance"
# Events to log
ENABLED_LOGS=["NEW_GROUPE", "NEW_ALBUM", "NEW_PARTITION", "NEW_USER", "PASSWORD_CHANGE", "DELETE_ACCOUNT", "SERVER_RESTART", "FAILED_LOGIN"]
# Available languages
LANGUAGES=['en', 'fr']
# Show Launch page
LAUNCH_PAGE=True
# Check if account is logged in before serving zipped album/groupe
ZIP_REQUIRE_LOGIN=True

37
make.sh
View File

@ -9,7 +9,7 @@ init () {
mkdir -p "$INSTANCE_PATH/search-partitions"
mkdir -p "$INSTANCE_PATH/cache/thumbnails"
mkdir -p "$INSTANCE_PATH/cache/search-thumbnails"
if ! test -f "$INSTANCE_PATH/config.py"; then
echo "SECRET_KEY=\"$(python3 -c 'import secrets; print(secrets.token_hex())')\"" > "$INSTANCE_PATH/config.py"
fi
@ -18,6 +18,7 @@ init () {
printf "Souhaitez vous supprimer la base de données existante ? [y/n] "
read -r CONFIRMATION
[[ $CONFIRMATION == y ]] || exit 1
rm "$INSTANCE_PATH/partitioncloud.sqlite"
fi
sqlite3 "$INSTANCE_PATH/partitioncloud.sqlite" '.read partitioncloud/schema.sql'
echo "Base de données créé"
@ -25,30 +26,54 @@ init () {
echo "Utilisateur root:root ajouté"
}
translations () {
# Rajouter les chaînes non traduites
pybabel extract -F babel.cfg -k _l -o partitioncloud/translations/messages.pot .
pybabel update -i partitioncloud/translations/messages.pot -d partitioncloud/translations/
# Compiler
pybabel compile -d partitioncloud/translations/
}
start () {
pybabel compile -d partitioncloud/translations/
flask run --port=$PORT
}
production () {
FLASK_APP=partitioncloud /usr/bin/gunicorn \
pybabel compile -d partitioncloud/translations/
FLASK_APP=partitioncloud gunicorn \
wsgi:app \
--bind 0.0.0.0:$PORT
}
load_config () {
# Load variables PORT and INSTANCE_PATH
eval $(cat $1 | grep -E "^PORT=")
eval $(cat $1 | grep -E "^INSTANCE_PATH=")
}
usage () {
echo "Usage:"
echo -e "\t$0 init"
echo -e "\t$0 start"
echo -e "\t$0 production"
echo -e "\t$0 translations"
}
if [[ $1 && $(type "$1") = *"is a"*"function"* || $(type "$1") == *"est une fonction"* ]]; then
RESULT=$(type "$1")
if [[ $1 && $RESULT = *"is a"*"function"* || $RESULT == *"est une fonction"* ]]; then
# Import config
source "default_config.py"
[[ ! -x "$INSTANCE_PATH/config.py" ]] && source "$INSTANCE_PATH/config.py"
load_config "default_config.py"
if test -f "instance/config.py"; then
load_config "instance/config.py"
fi
$1 ${*:2} # Call the function
else
usage
echo $(type "$1")
echo $RESULT
exit 1
fi

View File

@ -8,16 +8,23 @@ import datetime
import subprocess
import importlib.util
from flask import Flask, g, redirect, render_template, request, send_file, flash, session, abort
from flask import Flask, g, redirect, render_template, request, send_file, flash, session, abort, url_for
from werkzeug.security import generate_password_hash
from flask_babel import Babel, _
from .modules.utils import User, Album, get_all_albums
from .modules import albums, auth, partition, admin, groupe, thumbnails
from .modules.utils import User, Album, get_all_albums, user_count, partition_count
from .modules import albums, auth, partition, admin, groupe, thumbnails, logging, settings
from .modules.auth import admin_required, login_required
from .modules.db import get_db
app = Flask(__name__)
def get_locale():
return request.accept_languages.best_match(app.config['LANGUAGES'])
babel = Babel(app, locale_selector=get_locale)
def load_config():
app.config.from_object('default_config')
@ -33,14 +40,20 @@ def load_config():
".",
os.path.join(app.instance_path, "config.py")
)
if spec is None:
print("[ERROR] Failed to load $INSTANCE_PATH/config.py")
sys.exit(1)
user_config = importlib.util.module_from_spec(spec)
spec.loader.exec_module(user_config)
app.config.from_object(user_config)
if os.path.abspath(app.config["INSTANCE_PATH"]) != app.instance_path:
print("[ERROR] Using two different instance path. \
\nPlease modify INSTANCE_PATH only in default_config.py and remove it from $INSTANCE_PATH/config.py")
print(("[ERROR] Using two different instance path.\n"
"Please modify INSTANCE_PATH only in default_config.py ",
"and remove it from $INSTANCE_PATH/config.py"))
sys.exit(1)
else:
print("[WARNING] Using default config")
@ -50,6 +63,18 @@ def load_config():
)
def setup_logging():
logging.log_file = os.path.join(app.instance_path, "logs.txt")
enabled = []
for event in app.config["ENABLED_LOGS"]:
try:
enabled.append(logging.LogEntry.from_string(event))
except KeyError:
print(f"[ERROR] There is an error in your config: Unknown event {event}")
logging.enabled = enabled
def get_version():
try:
result = subprocess.run(["git", "describe", "--tags"], stdout=subprocess.PIPE, check=True)
@ -60,21 +85,37 @@ def get_version():
load_config()
setup_logging()
app.register_blueprint(auth.bp)
app.register_blueprint(admin.bp)
app.register_blueprint(groupe.bp)
app.register_blueprint(albums.bp)
app.register_blueprint(settings.bp)
app.register_blueprint(partition.bp)
app.register_blueprint(thumbnails.bp)
__version__ = get_version()
logging.log([], logging.LogEntry.SERVER_RESTART)
@app.route("/")
def home():
"""Redirect to home"""
return redirect("/albums/")
"""Show launch page if enabled"""
if g.user is None:
if app.config["LAUNCH_PAGE"]:
return redirect(url_for("launch_page"))
return redirect(url_for("auth.login"))
return redirect(url_for("albums.index"))
@app.route("/launch")
def launch_page():
"""Show launch page if enabled"""
if not app.config["LAUNCH_PAGE"]:
return home()
return render_template("launch.html", user_count=user_count(), partition_count=partition_count())
@app.route("/add-user", methods=["GET", "POST"])
@ -95,14 +136,20 @@ def add_user():
if error is None:
# Success, go to the login page.
user = User(name=username)
logging.log(
[user.username, user.id, True, current_user.username],
logging.LogEntry.NEW_USER
)
try:
if album_uuid != "":
user.join_album(album_uuid)
flash(f"Utilisateur {username} créé")
return redirect("/albums")
flash(_("Created user %(username)s", username=username))
return redirect(url_for("albums.index"))
except LookupError:
flash(f"Cet album n'existe pas. L'utilisateur {username} a été créé")
return redirect("/albums")
flash(_("This album does not exists, but user %(username)s has been created", username=username))
return redirect(url_for("albums.index"))
flash(error)
return render_template("auth/register.html", albums=get_all_albums(), user=current_user)
@ -119,7 +166,7 @@ def inject_default_variables():
"""Inject the version number in the template variables"""
if __version__ == "unknown":
return {"version": ''}
return {"version": __version__}
return {"version": __version__, "lang": get_locale()}
@app.after_request

View File

@ -2,7 +2,8 @@
"""
Admin Panel
"""
from flask import Blueprint, render_template, session
import os
from flask import Blueprint, render_template, session, current_app, send_file
from .db import get_db
from .auth import admin_required
@ -18,7 +19,6 @@ def index():
Admin panel home page
"""
current_user = User(user_id=session.get("user_id"))
current_user.get_albums() # We need to do that before we close the db
db = get_db()
users_id = db.execute(
"""
@ -35,3 +35,46 @@ def index():
users=users,
user=current_user
)
@bp.route("/user/<user_id>")
@admin_required
def user_inspect(user_id):
"""
Inspect user
"""
current_user = User(user_id=session.get("user_id"))
db = get_db()
inspected_user = User(user_id=user_id)
return render_template(
"settings/index.html",
skip_old_password=True,
inspected_user=inspected_user,
user=current_user,
deletion_allowed=True
)
@bp.route("/logs")
@admin_required
def logs():
"""
Admin panel logs page
"""
user = User(user_id=session.get("user_id"))
return render_template(
"admin/logs.html",
user=user
)
@bp.route("/logs.txt")
@admin_required
def logs_txt():
"""
Admin panel logs page
"""
return send_file(
os.path.join(current_app.instance_path, "logs.txt")
)

View File

@ -3,17 +3,20 @@
Albums module
"""
import os
import pypdf
import shutil
from uuid import uuid4
from typing import TypeVar
from flask import (Blueprint, abort, flash, redirect, render_template,
request, session, current_app)
request, session, current_app, send_file, g, url_for)
from werkzeug.utils import secure_filename
from flask_babel import _
from .auth import login_required
from .db import get_db
from .utils import User, Album
from . import search, utils
from . import search, utils, logging
bp = Blueprint("albums", __name__, url_prefix="/albums")
@ -37,15 +40,21 @@ def search_page():
Résultats de recherche
"""
if "query" not in request.form or request.form["query"] == "":
flash("Aucun terme de recherche spécifié.")
flash(_("Missing search query"))
return redirect("/albums")
user = User(user_id=session.get("user_id"))
query = request.form["query"]
nb_queries = abs(int(request.form["nb-queries"]))
search.flush_cache(current_app.instance_path)
partitions_local = search.local_search(query, utils.get_all_partitions())
user = User(user_id=session.get("user_id"))
partitions_list = None
if current_app.config["PRIVATE_SEARCH"]:
partitions_list = utils.get_all_partitions()
else:
partitions_list = user.get_accessible_partitions()
partitions_local = search.local_search(query, partitions_list)
if nb_queries > 0:
if user.access_level != 1:
@ -80,7 +89,7 @@ def get_album(uuid):
except LookupError:
return abort(404)
album.users = [User(user_id=i["id"]) for i in album.get_users()]
album.users = [User(user_id=u_id) for u_id in album.get_users()]
user = User(user_id=session.get("user_id"))
partitions = album.get_partitions()
if user.id is None:
@ -106,6 +115,30 @@ def qr_code(uuid):
return utils.get_qrcode(f"/albums/{uuid}")
@bp.route("/<uuid>/zip")
def zip_download(uuid):
"""
Télécharger un album comme fichier zip
"""
if g.user is None and current_app.config["ZIP_REQUIRE_LOGIN"]:
flash(_("You need to login to access this resource."))
return redirect(url_for("auth.login"))
try:
album = Album(uuid=uuid)
except LookupError:
try:
album = Album(uuid=utils.format_uuid(uuid))
return redirect(f"/albums/{utils.format_uuid(uuid)}")
except LookupError:
return abort(404)
return send_file(
album.to_zip(current_app.instance_path),
download_name=secure_filename(f"{album.name}.zip")
)
@bp.route("/create-album", methods=["POST"])
@login_required
def create_album_req():
@ -116,8 +149,10 @@ def create_album_req():
db = get_db()
error = None
user = User(user_id=session["user_id"])
if not name or name.strip() == "":
error = "Un nom est requis. L'album n'a pas été créé"
error = _("Missing name.")
if error is None:
uuid = utils.create_album(name)
@ -131,6 +166,8 @@ def create_album_req():
)
db.commit()
logging.log([album.name, album.uuid, user.username], logging.LogEntry.NEW_ALBUM)
if "response" in request.args and request.args["response"] == "json":
return {
"status": "ok",
@ -152,10 +189,10 @@ def join_album(uuid):
try:
user.join_album(uuid)
except LookupError:
flash("Cet album n'existe pas.")
flash(_("This album does not exist."))
return redirect(request.referrer)
flash("Album ajouté à la collection.")
flash(_("Album added to collection."))
return redirect(request.referrer)
@ -167,17 +204,18 @@ def quit_album(uuid):
"""
user = User(user_id=session.get("user_id"))
album = Album(uuid=uuid)
users = album.get_users()
if user.id not in [u["id"] for u in users]:
flash("Vous ne faites pas partie de cet album")
if user.id not in users:
flash(_("You are not a member of this album"))
return redirect(request.referrer)
if len(users) == 1:
flash("Vous êtes seul dans cet album, le quitter entraînera sa suppression.")
flash(_("You are alone here, quitting means deleting this album."))
return redirect(f"/albums/{uuid}#delete")
user.quit_album(uuid)
flash("Album quitté.")
flash(_("Album quitted."))
return redirect("/albums")
@ -196,9 +234,9 @@ def delete_album(uuid):
error = None
users = album.get_users()
if len(users) > 1:
error = "Vous n'êtes pas seul dans cet album."
elif len(users) == 1 and users[0]["id"] != user.id:
error = "Vous ne possédez pas cet album."
error = _("You are not alone in this album.")
elif len(users) == 1 and users[0] != user.id:
error = _("You don't own this album.")
if user.access_level == 1:
error = None
@ -209,7 +247,7 @@ def delete_album(uuid):
album.delete(current_app.instance_path)
flash("Album supprimé.")
flash(_("Album deleted."))
return redirect("/albums")
@ -217,7 +255,7 @@ def delete_album(uuid):
@login_required
def add_partition(album_uuid):
"""
Ajouter une partition à un album (par upload)
Ajouter une partition à un album (nouveau fichier)
"""
T = TypeVar("T")
def get_opt_string(dictionary: dict[T, str], key: T):
@ -232,15 +270,15 @@ def add_partition(album_uuid):
source = "upload" # source type: upload, unknown or url
if (not user.is_participant(album.uuid)) and (user.access_level != 1):
flash("Vous ne participez pas à cet album.")
flash(_("You are not a member of this album"))
return redirect(request.referrer)
error = None
if "name" not in request.form:
error = "Un titre est requis."
error = _("Missing title")
elif "file" not in request.files and "partition-uuid" not in request.form:
error = "Aucun fichier n'a été fourni."
error = _("Missing file")
elif "file" not in request.files:
partition_type = "uuid"
search_uuid = request.form["partition-uuid"]
@ -252,12 +290,18 @@ def add_partition(album_uuid):
(search_uuid,)
).fetchone()
if data is None:
error = "Les résultats de la recherche ont expiré."
error = _("Search results expired")
else:
source = data["url"]
else:
partition_type = "file"
try:
pypdf.PdfReader(request.files["file"])
request.files["file"].seek(0)
except (pypdf.errors.PdfReadError, pypdf.errors.PdfStreamError):
error = _("Invalid PDF file")
if error is not None:
flash(error)
return redirect(request.referrer)
@ -265,6 +309,7 @@ def add_partition(album_uuid):
author = get_opt_string(request.form, "author")
body = get_opt_string(request.form, "body")
partition_uuid: str
while True:
try:
partition_uuid = str(uuid4())
@ -307,12 +352,17 @@ def add_partition(album_uuid):
except db.IntegrityError:
pass
logging.log(
[request.form["name"], partition_uuid, user.username],
logging.LogEntry.NEW_PARTITION
)
if "response" in request.args and request.args["response"] == "json":
return {
"status": "ok",
"uuid": partition_uuid
}
flash(f"Partition {request.form['name']} ajoutée")
flash(_("Score %(partition_name)s added", partition_name=request.form['name']))
return redirect(f"/albums/{album.uuid}")
@ -320,19 +370,19 @@ def add_partition(album_uuid):
@login_required
def add_partition_from_search():
"""
Ajout d'une partition (depuis la recherche)
Ajout d'une partition (depuis la recherche locale)
"""
user = User(user_id=session.get("user_id"))
error = None
if "album-uuid" not in request.form:
error = "Il est nécessaire de sélectionner un album."
error = _("Selecting an album is mandatory.")
elif "partition-uuid" not in request.form:
error = "Il est nécessaire de sélectionner une partition."
error = _("Selecting a score is mandatory.")
elif "partition-type" not in request.form:
error = "Il est nécessaire de spécifier un type de partition."
error = _("Please specify a score type.")
elif (not user.is_participant(request.form["album-uuid"])) and (user.access_level != 1):
error = "Vous ne participez pas à cet album."
error = _("You are not a member of this album")
if error is not None:
flash(error)
@ -352,9 +402,9 @@ def add_partition_from_search():
if data is None:
album.add_partition(request.form["partition-uuid"])
flash("Partition ajoutée.")
flash(_("Score added"))
else:
flash("Partition déjà dans l'album.")
flash(_("Score is already in the album."))
return redirect(f"/albums/{album.uuid}")
@ -366,5 +416,5 @@ def add_partition_from_search():
user=user
)
flash("Type de partition inconnu.")
flash(_("Unknown score type."))
return redirect("/albums")

View File

@ -7,11 +7,13 @@ from typing import Optional
from flask import (Blueprint, flash, g, redirect, render_template,
request, session, url_for, current_app)
from flask_babel import _
from werkzeug.security import check_password_hash, generate_password_hash
from .db import get_db
from .utils import User
from . import logging
bp = Blueprint("auth", __name__, url_prefix="/auth")
@ -22,7 +24,7 @@ def login_required(view):
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None:
flash("Vous devez être connecté pour accéder à cette page.")
flash(_("You need to login to access this resource."))
return redirect(url_for("auth.login"))
return view(**kwargs)
@ -49,12 +51,12 @@ def admin_required(view):
@functools.wraps(view)
def wrapped_view(**kwargs):
if g.user is None:
flash("Vous devez être connecté pour accéder à cette page.")
flash(_("You need to login to access this resource."))
return redirect(url_for("auth.login"))
user = User(user_id=session.get("user_id"))
if user.access_level != 1:
flash("Droits insuffisants.")
flash(_("Missing rights."))
return redirect("/albums")
return view(**kwargs)
@ -80,9 +82,9 @@ def create_user(username: str, password: str) -> Optional[str]:
"""Adds a new user to the database"""
error = None
if not username:
error = "Un nom d'utilisateur est requis."
error = _("Missing username.")
elif not password:
error = "Un mot de passe est requis."
error = _("Missing password.")
try:
db = get_db()
@ -95,7 +97,7 @@ def create_user(username: str, password: str) -> Optional[str]:
except db.IntegrityError:
# The username was already taken, which caused the
# commit to fail. Show a validation error.
error = f"Le nom d'utilisateur {username} est déjà pris."
error = _("Username %(username)s is not available.", username=username)
return error # may be None
@ -108,7 +110,7 @@ def register():
password for security.
"""
if current_app.config["DISABLE_REGISTER"]:
flash("L'enregistrement de nouveaux utilisateurs a été désactivé par l'administrateur.")
flash(_("New users registration is disabled by owner."))
return redirect(url_for("auth.login"))
if request.method == "POST":
@ -120,7 +122,15 @@ def register():
if error is not None:
flash(error)
else:
flash("Utilisateur créé avec succès. Vous pouvez vous connecter.")
user = User(name=username)
flash(_("Successfully created new user. You can log in."))
logging.log(
[user.username, user.id, False],
logging.LogEntry.NEW_USER
)
return redirect(url_for("auth.login"))
return render_template("auth/register.html")
@ -139,12 +149,16 @@ def login():
).fetchone()
if (user is None) or not check_password_hash(user["password"], password):
error = "Nom d'utilisateur ou mot de passe incorrect."
logging.log([username], logging.LogEntry.FAILED_LOGIN)
error = _("Incorrect username or password")
if error is None:
# store the user id in a new session and return to the index
session.clear()
session["user_id"] = user["id"]
logging.log([username], logging.LogEntry.LOGIN)
return redirect(url_for("albums.index"))
flash(error)
@ -156,4 +170,4 @@ def login():
def logout():
"""Clear the current session, including the stored user id."""
session.clear()
return redirect(url_for("auth.login"))
return redirect("/")

View File

@ -2,6 +2,10 @@
Classe Album
"""
import os
import io
import zipfile
from werkzeug.utils import secure_filename
from ..db import get_db
from ..utils import new_uuid
@ -47,11 +51,11 @@ class Album():
def get_users(self, force_reload=False):
"""
Renvoie les utilisateurs liés à l'album
Renvoie les data["id"] des utilisateurs liés à l'album
"""
if self.users is None or force_reload:
db = get_db()
self.users = db.execute(
data = db.execute(
"""
SELECT * FROM user
JOIN contient_user ON user_id = user.id
@ -60,6 +64,7 @@ class Album():
""",
(self.uuid,)
).fetchall()
self.users = [i["id"] for i in data]
return self.users
def get_partitions(self):
@ -114,7 +119,7 @@ class Album():
"""
SELECT partition.uuid FROM partition
WHERE NOT EXISTS (
SELECT NULL FROM contient_partition
SELECT NULL FROM contient_partition
WHERE partition.uuid = partition_uuid
)
"""
@ -142,7 +147,7 @@ class Album():
WHERE uuid IN (
SELECT partition.uuid FROM partition
WHERE NOT EXISTS (
SELECT NULL FROM contient_partition
SELECT NULL FROM contient_partition
WHERE partition.uuid = partition_uuid
)
)
@ -165,6 +170,23 @@ class Album():
)
db.commit()
def to_zip(self, instance_path):
data = io.BytesIO()
with zipfile.ZipFile(data, mode="w") as z:
for partition in self.get_partitions():
z.write(os.path.join(
instance_path,
"partitions",
f"{partition['uuid']}.pdf"
), arcname=secure_filename(partition['name']+".pdf")
)
# Spooling back to the beginning of the buffer
data.seek(0)
return data
def create(name: str) -> str:
"""Créer un nouvel album"""

View File

@ -1,3 +1,12 @@
"""
Classe Groupe
"""
import io
import os
import zipfile
from werkzeug.utils import secure_filename
from ..db import get_db
from .album import Album
@ -66,21 +75,23 @@ class Groupe():
album.delete(instance_path)
def get_users(self):
def get_users(self, force_reload=False):
"""
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()
if self.users is None or force_reload:
db = get_db()
data = 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()
self.users = [i["id"] for i in data]
return self.users
def get_albums(self, force_reload=False):
"""
@ -116,3 +127,36 @@ class Groupe():
(self.id,)
).fetchall()
return [i["id"] for i in data]
def set_admin(self, user_id, value):
"""
Rend un utilisateur administrateur du groupe
"""
db = get_db()
data = db.execute(
"""
UPDATE groupe_contient_user
SET is_admin=?
WHERE user_id=? AND groupe_id=?
""",
(value, user_id, self.id)
)
db.commit()
def to_zip(self, instance_path):
data = io.BytesIO()
with zipfile.ZipFile(data, mode="w") as z:
for album in self.get_albums():
for partition in album.get_partitions():
z.write(os.path.join(
instance_path,
"partitions",
f"{partition['uuid']}.pdf"
), arcname=secure_filename(album.name)+"/"
+secure_filename(partition['name']+".pdf")
)
# Spooling back to the beginning of the buffer
data.seek(0)
return data

View File

@ -75,6 +75,16 @@ class Partition():
)
db.commit()
def update_file(self, file, instance_path):
partition_path = os.path.join(
instance_path,
"partitions",
f"{self.uuid}.pdf"
)
file.save(partition_path)
if os.path.exists(f"{instance_path}/cache/thumbnails/{self.uuid}.jpg"):
os.remove(f"{instance_path}/cache/thumbnails/{self.uuid}.jpg")
def get_user(self):
db = get_db()
user = db.execute(

View File

@ -1,4 +1,5 @@
from flask import current_app
from werkzeug.security import generate_password_hash
from ..db import get_db
from .album import Album
@ -28,9 +29,11 @@ class User():
def __init__(self, user_id=None, name=None):
self.id = user_id
self.username = name
self.password = None
self.albums = None
self.groupes = None
self.partitions = None
self.accessible_partitions = None
self.max_queries = 0
db = get_db()
@ -58,6 +61,7 @@ class User():
self.id = data["id"]
self.username = data["username"]
self.password = data["password"]
self.access_level = data["access_level"]
self.color = self.get_color()
if self.access_level == 1:
@ -166,6 +170,44 @@ class User():
).fetchall()
return self.partitions
def get_accessible_partitions(self, force_reload=False):
if self.accessible_partitions is None or force_reload:
db = get_db()
if self.access_level == 1:
self.accessible_partitions = db.execute(
"""
SELECT * FROM partition
"""
).fetchall()
else:
self.accessible_partitions = db.execute(
"""
SELECT DISTINCT partition.uuid, partition.name,
partition.author, partition.body,
partition.user_id, partition.source
FROM partition
JOIN album
JOIN contient_partition
ON album.id=album_id
AND partition.uuid=partition_uuid
WHERE album.id IN (
SELECT album.id FROM album
JOIN contient_user
ON contient_user.user_id=?
AND album_id=album.id
UNION
SELECT DISTINCT album.id FROM album
JOIN groupe_contient_user
JOIN groupe_contient_album
ON groupe_contient_user.user_id=?
AND groupe_contient_album.album_id=album.id
AND groupe_contient_user.groupe_id=groupe_contient_album.groupe_id
)
""",
(self.id, self.id,),
).fetchall()
return self.accessible_partitions
def join_album(self, album_uuid):
db = get_db()
album = Album(uuid=album_uuid)
@ -198,12 +240,10 @@ class User():
db.execute(
"""
DELETE FROM contient_user
JOIN album
ON album.id = album_id
WHERE user_id = ?
AND album.uuid = ?
WHERE album_id IN (SELECT id FROM album WHERE uuid = ?)
AND user_id = ?
""",
(self.id, album_uuid)
(album_uuid, self.id)
)
db.commit()
@ -221,6 +261,49 @@ class User():
)
db.commit()
def update_password(self, new_password):
db = get_db()
db.execute(
"""
UPDATE user SET password=?
WHERE id=?
""",
(generate_password_hash(new_password), self.id)
)
db.commit()
def delete(self):
instance_path = current_app.config["INSTANCE_PATH"]
for groupe in self.get_groupes():
self.quit_groupe(groupe.uuid)
if groupe.get_users() == []:
groupe.delete(instance_path)
for album_data in self.get_albums():
uuid = album_data["uuid"]
self.quit_album(uuid)
album = Album(uuid=uuid)
if album.get_users() == []:
album.delete(instance_path)
db = get_db()
db.execute(
"""
DELETE FROM user
WHERE id=?
""",
(self.id,)
)
db.commit()
def get_color(self):
if len(colors) == 0:

View File

@ -3,12 +3,15 @@
Groupe module
"""
from flask import (Blueprint, abort, flash, redirect, render_template,
request, session, current_app)
request, session, current_app, send_file, g, url_for)
from werkzeug.utils import secure_filename
from flask_babel import _
from .auth import login_required
from .db import get_db
from .utils import User, Album, Groupe
from . import utils
from . import logging
bp = Blueprint("groupe", __name__, url_prefix="/groupe")
@ -32,7 +35,7 @@ def get_groupe(uuid):
except LookupError:
return abort(404)
groupe.users = [User(user_id=i["id"]) for i in groupe.get_users()]
groupe.users = [User(user_id=u_id) for u_id in groupe.get_users()]
groupe.get_albums()
user = User(user_id=session.get("user_id"))
@ -63,8 +66,10 @@ def create_groupe():
db = get_db()
error = None
user = User(user_id=session["user_id"])
if not name or name.strip() == "":
error = "Un nom est requis. Le groupe n'a pas été créé"
error = _("Missing name.")
if error is None:
while True:
@ -93,6 +98,8 @@ def create_groupe():
except db.IntegrityError:
pass
logging.log([name, uuid, user.username], logging.LogEntry.NEW_GROUPE)
if "response" in request.args and request.args["response"] == "json":
return {
"status": "ok",
@ -111,10 +118,10 @@ def join_groupe(uuid):
try:
user.join_groupe(uuid)
except LookupError:
flash("Ce groupe n'existe pas.")
flash(_("Unknown group."))
return redirect(f"/groupe/{uuid}")
flash("Groupe ajouté à la collection.")
flash(_("Group added to collection."))
return redirect(f"/groupe/{uuid}")
@ -124,16 +131,21 @@ 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")
if user.id not in users:
flash(_("You are not a member of this group."))
return redirect(f"/groupe/{uuid}")
if len(users) == 1:
flash("Vous êtes seul dans ce groupe, le quitter entraînera sa suppression.")
flash(_("You are alone here, quitting means deleting this group."))
return redirect(f"/groupe/{uuid}#delete")
user.quit_groupe(groupe.uuid)
flash("Groupe quitté.")
if len(groupe.get_admins()) == 0: # On s'assure que le groupe contient toujours des administrateurs
for user_id in groupe.get_users(force_reload=True):
groupe.set_admin(user_id, True)
flash(_("Group quitted."))
return redirect("/albums")
@ -144,9 +156,8 @@ def delete_groupe(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 len(groupe.get_users()) > 1:
error = _("You are not alone in this group.")
if user.access_level == 1 or user.id not in groupe.get_admins():
error = None
@ -157,7 +168,7 @@ def delete_groupe(uuid):
groupe.delete(current_app.instance_path)
flash("Groupe supprimé.")
flash(_("Group deleted."))
return redirect("/albums")
@ -176,10 +187,10 @@ def create_album_req(groupe_uuid):
error = None
if not name or name.strip() == "":
error = "Un nom est requis. L'album n'a pas été créé"
error = _("Missing name.")
if user.id not in groupe.get_admins():
error ="Vous n'êtes pas administrateur de ce groupe"
if user.id not in groupe.get_admins() and user.access_level != 1:
error = _("You are not admin of this group.")
if error is None:
uuid = utils.create_album(name)
@ -194,6 +205,8 @@ def create_album_req(groupe_uuid):
)
db.commit()
logging.log([album.name, album.uuid, user.username], logging.LogEntry.NEW_ALBUM)
if "response" in request.args and request.args["response"] == "json":
return {
"status": "ok",
@ -232,7 +245,7 @@ def get_album(groupe_uuid, album_uuid):
user = User(user_id=session.get("user_id"))
# List of users without duplicate
users_id = list({i["id"] for i in album.get_users()+groupe.get_users()})
users_id = list(set(album.get_users()+groupe.get_users()))
album.users = [User(user_id=id) for id in users_id]
partitions = album.get_partitions()
@ -253,6 +266,31 @@ def get_album(groupe_uuid, album_uuid):
)
@bp.route("/<groupe_uuid>/zip")
def zip_download(groupe_uuid):
"""
Télécharger un groupe comme fichier zip
"""
if g.user is None and current_app.config["ZIP_REQUIRE_LOGIN"]:
flash(_("You need to login to access this resource."))
return redirect(url_for("auth.login"))
try:
groupe = Groupe(uuid=groupe_uuid)
except LookupError:
try:
groupe = Groupe(uuid=utils.format_uuid(groupe_uuid))
return redirect(f"/groupe/{utils.format_uuid(groupe_uuid)}/zip")
except LookupError:
return abort(404)
return send_file(
groupe.to_zip(current_app.instance_path),
download_name=secure_filename(f"{groupe.name}.zip")
)
@bp.route("/<groupe_uuid>/<album_uuid>/qr")
def groupe_qr_code(groupe_uuid, album_uuid):
return utils.get_qrcode(f"/groupe/{groupe_uuid}/{album_uuid}")

View File

@ -0,0 +1,90 @@
from datetime import datetime
from typing import Union
from enum import Enum
global log_file
global enabled
class LogEntry(Enum):
LOGIN = 1
NEW_GROUPE = 2
NEW_ALBUM = 3
NEW_PARTITION = 4
NEW_USER = 5
PASSWORD_CHANGE = 6
DELETE_ACCOUNT = 7
SERVER_RESTART = 8
FAILED_LOGIN = 9
def from_string(entry: str):
mapping = {
"LOGIN": LogEntry.LOGIN,
"NEW_GROUPE": LogEntry.NEW_GROUPE,
"NEW_ALBUM": LogEntry.NEW_ALBUM,
"NEW_PARTITION": LogEntry.NEW_PARTITION,
"NEW_USER": LogEntry.NEW_USER,
"PASSWORD_CHANGE": LogEntry.PASSWORD_CHANGE,
"DELETE_ACCOUNT": LogEntry.DELETE_ACCOUNT,
"SERVER_RESTART": LogEntry.SERVER_RESTART,
"FAILED_LOGIN": LogEntry.FAILED_LOGIN
}
# Will return KeyError if not available
return mapping[entry]
def add_entry(entry: str) -> None:
date = datetime.now().strftime('%d-%b-%Y %H:%M:%S')
with open(log_file, 'a', encoding="utf8") as f:
f.write(f"[{date}] {entry}\n")
def log(content: list[Union[str, bool, int]], log_type: LogEntry) -> None:
description: str = ""
if log_type not in enabled:
return
match log_type:
case LogEntry.LOGIN: # content = (user.name)
description = f"Successful login for {content[0]}"
case LogEntry.NEW_GROUPE: # content = (groupe.name, groupe.id, user.name)
description = f"{content[2]} added groupe '{content[0]}' ({content[1]})"
case LogEntry.NEW_ALBUM: # content = (album.name, album.id, user.name)
description = f"{content[2]} added album '{content[0]}' ({content[1]})"
case LogEntry.NEW_PARTITION: # content = (partition.name, partition.uuid, user.name)
description = f"{content[2]} added partition '{content[0]}' ({content[1]})"
case LogEntry.NEW_USER: # content = (user.name, user.id, from_register_page, admin.name if relevant)
if not content[2]:
description = f"New user {content[0]}[{content[1]}]"
else:
description = f"New user {content[0]}[{content[1]}] added by {content[3]}"
case LogEntry.PASSWORD_CHANGE: # content = (user.name, user.id, admin.name if relevant)
if len(content) == 2:
description = f"New password for {content[0]}[{content[1]}]"
else:
description = f"New password for {content[0]}[{content[1]}], changed by {content[2]}"
case LogEntry.DELETE_ACCOUNT: # content = (user.name, user.id, admin.name if relevant)
if len(content) == 2:
description = f"Account deleted {content[0]}[{content[1]}]"
else:
description = f"Account deleted {content[0]}[{content[1]}], by {content[2]}"
case LogEntry.SERVER_RESTART: # content = ()
description = "Server just restarted"
case LogEntry.FAILED_LOGIN: # content = (user.name)
description = f"Failed login for {content[0]}"
add_entry(description)
log_file = "logs.txt"
enabled = [i for i in LogEntry]

View File

@ -3,9 +3,11 @@
Partition module
"""
import os
import pypdf
from uuid import uuid4
from flask import (Blueprint, abort, send_file, render_template,
request, redirect, flash, session, current_app)
from flask_babel import _
from .db import get_db
from .auth import login_required, admin_required
@ -54,12 +56,12 @@ def add_attachment(uuid):
user = User(user_id=session.get("user_id"))
if user.id != partition.user_id and user.access_level != 1:
flash("Cette partition ne vous current_appartient pas")
flash(_("You don't own this score."))
return redirect(request.referrer)
error = None # À mettre au propre
if "file" not in request.files:
error = "Aucun fichier n'a été fourni."
error = _("Missing file")
else:
if "name" not in request.form or request.form["name"] == "":
name = ".".join(request.files["file"].filename.split(".")[:-1])
@ -67,12 +69,12 @@ def add_attachment(uuid):
name = request.form["name"]
if name == "":
error = "Pas de nom de fichier"
error = _("Missing filename.")
else:
filename = request.files["file"].filename
ext = filename.split(".")[-1]
if ext not in ["mid", "mp3"]:
error = "Extension de fichier non supportée"
error = _("Unsupported file type.")
if error is not None:
flash(error)
@ -140,7 +142,7 @@ def edit(uuid):
user = User(user_id=session.get("user_id"))
if user.access_level != 1 and partition.user_id != user.id:
flash("Vous n'êtes pas autorisé à modifier cette partition.")
flash(_("You are not allowed to edit this file."))
return redirect("/albums")
if request.method == "GET":
@ -149,23 +151,34 @@ def edit(uuid):
error = None
if "name" not in request.form or request.form["name"].strip() == "":
error = "Un titre est requis."
error = _("Missing title")
elif "author" not in request.form:
error = "Un nom d'auteur est requis (à minima nul)"
error = _("Missing author in request body (can be null).")
elif "body" not in request.form:
error = "Des paroles sont requises (à minima nulles)"
error = _("Missing lyrics (can be null).")
if error is not None:
flash(error)
return redirect(f"/partition/{ uuid }/edit")
if request.files.get('file', None):
new_file = request.files["file"]
try:
pypdf.PdfReader(new_file)
new_file.seek(0)
except (pypdf.errors.PdfReadError, pypdf.errors.PdfStreamError):
flash(_("Invalid PDF file"))
return redirect(request.referrer)
partition.update_file(new_file, current_app.instance_path)
partition.update(
name=request.form["name"],
author=request.form["author"],
body=request.form["body"]
)
flash(f"Partition {request.form['name']} modifiée avec succès.")
flash(_("Successfully modified %(name)s", name=request.form['name']))
return redirect("/albums")
@ -195,11 +208,11 @@ def details(uuid):
error = None
if "name" not in request.form or request.form["name"].strip() == "":
error = "Un titre est requis."
error = _("Missing title")
elif "author" not in request.form:
error = "Un nom d'auteur est requis (à minima nul)"
error = _("Missing author in request body (can be null).")
elif "body" not in request.form:
error = "Des paroles sont requises (à minima nulles)"
error = _("Missing lyrics (can be null).")
if error is not None:
flash(error)
@ -211,7 +224,7 @@ def details(uuid):
body=request.form["body"]
)
flash(f"Partition {request.form['name']} modifiée avec succès.")
flash(_("Successfully modified %(name)s", name=request.form['name']))
return redirect("/albums")
@ -226,7 +239,7 @@ def delete(uuid):
user = User(user_id=session.get("user_id"))
if user.access_level != 1 and partition.user_id != user.id:
flash("Vous n'êtes pas autorisé à supprimer cette partition.")
flash(_("You are not allowed to delete this score."))
return redirect("/albums")
if request.method == "GET":
@ -234,7 +247,7 @@ def delete(uuid):
partition.delete(current_app.instance_path)
flash("Partition supprimée.")
flash(_("Score deleted."))
return redirect("/albums")

View File

@ -8,7 +8,9 @@ import threading
import socket
import os
import pypdf
import googlesearch
from unidecode import unidecode
from .db import get_db
@ -19,20 +21,20 @@ def local_search(query, partitions):
"""
Renvoie les 5 résultats les plus pertinents parmi une liste donnée
"""
query_words = [word.lower() for word in query.split(" ")]
query_words = [word.lower() for word in unidecode(query).split()]
def score_attribution(partition):
score = 0
for word in query_words:
if word != "":
if word in partition["name"].lower():
if word in unidecode(partition["name"]).lower():
score += 6
elif word in partition["author"].lower():
elif word in unidecode(partition["author"]).lower():
score += 4
elif word in partition["body"].lower():
elif word in unidecode(partition["body"]).lower():
score += 2
else:
score -= 1
for word in partition["name"].split(" "):
score -= 6
for word in unidecode(partition["name"]).split():
if word != "" and word.lower() not in query_words:
score -= 1
return score
@ -52,12 +54,17 @@ def local_search(query, partitions):
def download_search_result(element, instance_path):
uuid = element["uuid"]
url = element["url"]
filename = f"{instance_path}/search-partitions/{uuid}.pdf"
try:
urllib.request.urlretrieve(url, f"{instance_path}/search-partitions/{uuid}.pdf")
urllib.request.urlretrieve(url, filename)
pypdf.PdfReader(filename)
except (urllib.error.HTTPError, urllib.error.URLError):
with open(f"{instance_path}/search-partitions/{uuid}.pdf", 'a', encoding="utf8") as _:
except (urllib.error.HTTPError, urllib.error.URLError,
pypdf.errors.PdfReadError, pypdf.errors.PdfStreamError):
if os.path.exists(filename):
os.remove(filename)
with open(filename, 'a', encoding="utf8") as _:
pass # Create empty file

View File

@ -0,0 +1,106 @@
#!/usr/bin/python3
"""
User Settings
"""
import os
from flask import Blueprint, render_template, session, current_app, send_file, request, flash, redirect
from werkzeug.security import check_password_hash
from flask_babel import _
from .db import get_db
from .auth import login_required
from .utils import User
from . import logging
bp = Blueprint("settings", __name__, url_prefix="/settings")
@bp.route("/")
@login_required
def index():
"""
Settings page
"""
user = User(user_id=session.get("user_id"))
return render_template(
"settings/index.html",
inspected_user=user,
user=user,
deletion_allowed=not current_app.config["DISABLE_ACCOUNT_DELETION"]
)
@bp.route("/delete-account", methods=["POST"])
@login_required
def delete_account():
log_data = None
if "user_id" not in request.form:
flash(_("Missing user id."))
return redirect(request.referrer)
cur_user = User(user_id=session.get("user_id"))
user_id = request.form["user_id"]
mod_user = User(user_id=user_id)
if cur_user.access_level != 1:
log_data = [mod_user.username, mod_user.id]
if cur_user.id != mod_user.id:
flash(_("Missing rights."))
return redirect(request.referrer)
if current_app.config["DISABLE_ACCOUNT_DELETION"]:
flash(_("You are not allowed to delete your account."))
return redirect(request.referrer)
else:
log_data = [mod_user.username, mod_user.id, cur_user.username]
mod_user.delete()
flash(_("User successfully deleted."))
logging.log(log_data, logging.LogEntry.DELETE_ACCOUNT)
if cur_user.id == mod_user.id:
return redirect("/")
return redirect("/admin")
@bp.route("/change-password", methods=["POST"])
@login_required
def change_password():
log_data = None
if "user_id" not in request.form:
flash(_("Missing user id."))
return redirect(request.referrer)
cur_user = User(user_id=session.get("user_id"))
user_id = request.form["user_id"]
mod_user = User(user_id=user_id)
if cur_user.access_level != 1:
log_data = [mod_user.username, mod_user.id]
if cur_user.id != mod_user.id:
flash(_("Missing rights."))
return redirect(request.referrer)
if "old_password" not in request.form:
flash(_("Missing old password."))
return redirect(request.referrer)
if not check_password_hash(mod_user.password, request.form["old_password"]):
flash(_("Incorrect password."))
return redirect(request.referrer)
else:
log_data = [mod_user.username, mod_user.id, cur_user.username]
if "new_password" not in request.form or "confirm_new_password" not in request.form:
flash(_("Missing password."))
return redirect(request.referrer)
if request.form["new_password"] != request.form["confirm_new_password"]:
flash(_("Password and its confirmation differ."))
return redirect(request.referrer)
mod_user.update_password(request.form["new_password"])
flash(_("Successfully updated password."))
logging.log(log_data, logging.LogEntry.PASSWORD_CHANGE)
return redirect(request.referrer)

View File

@ -2,10 +2,10 @@
Thumbnails
"""
import os
import pypdf
from flask import current_app, abort, Blueprint, send_file
from .db import get_db
from .auth import login_required
bp = Blueprint("thumbnails", __name__, url_prefix="/thumbnails")
@ -15,13 +15,18 @@ def generate_thumbnail(source, dest):
"""
Generates a thumbnail with 'convert' (ImageMagick)
"""
os.system(
f'/usr/bin/convert -thumbnail\
"178^>" -background white -alpha \
remove -crop 178x178+0+0 \
{source}[0] \
{dest}'
try:
pypdf.PdfReader(source) # Check if file is really a PDF
except (pypdf.errors.PdfReadError, pypdf.errors.PdfStreamError):
return
command = (
f"gs -dQUIET -dSAFER -dBATCH -dNOPAUSE -dNOPROMPT -dMaxBitmap=500000000 \
-dAlignToPixels=0 -dGridFitTT=2 -sDEVICE=png16m -dBackgroundColor=16#FFFFFF -dTextAlphaBits=4 \
-dGraphicsAlphaBits=4 -r72x72 -dPrinted=false -dFirstPage=1 -dPDFFitPage -g356x356 \
-dLastPage=1 -sOutputFile={dest} {source}"
)
os.system(command)
def serve_thumbnail(partition_file, thumbnail_file):
"""
@ -33,6 +38,9 @@ def serve_thumbnail(partition_file, thumbnail_file):
if not os.path.exists(thumbnail_file):
generate_thumbnail(partition_file, thumbnail_file)
if not os.path.exists(thumbnail_file):
abort(404)
return send_file(thumbnail_file)
@ -55,4 +63,4 @@ def regular_thumbnail(uuid):
return serve_thumbnail(
os.path.join(current_app.instance_path, "partitions", f"{uuid}.pdf"),
os.path.join(current_app.instance_path, "cache", "thumbnails", f"{uuid}.jpg")
)
)

View File

@ -72,3 +72,25 @@ def get_all_albums():
"uuid": a["uuid"]
} for a in albums
]
def user_count():
db = get_db()
count = db.execute(
"""
SELECT COUNT(*) as count FROM user
"""
).fetchone()
return count[0]
def partition_count():
db = get_db()
count = db.execute(
"""
SELECT COUNT(*) FROM partition
"""
).fetchone()
return count[0]

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@ -1,5 +1,5 @@
{
"background_color": "#eff1f5",
"background_color": "#1E1E2E",
"description": "Partitioncloud",
"display": "fullscreen",
"icons": [

View File

@ -0,0 +1,20 @@
let logsEmbed = document.getElementById("logs-embed");
logsEmbed.addEventListener("load", () => {
var cssLink = document.createElement("link");
cssLink.href = "/static/style/logs.css";
cssLink.rel = "stylesheet";
cssLink.type = "text/css";
// add css
logsEmbed.contentDocument.head.appendChild(cssLink);
// Scroll to bottom
logsEmbed.contentWindow.scrollTo(0, logsEmbed.contentDocument.body.scrollHeight);
});
// check if the iframe is already loaded (happened with FF Android)
if (logsEmbed.contentDocument.readyState == "complete") {
logsEmbed.dispatchEvent(new Event("load"));
}

View File

@ -17,7 +17,7 @@ async function hideSidebarNoAnim () {
/* The transition needs to be invisible as if it was loaded that way */
content_container.style.transitionDuration = "0s";
sidebar_indicator.style.transitionDuration = "0s";
sidebar_toggle.checked = true;
/* We need to set a sleep because we want to reset the transition duration only once it ended*/

View File

@ -0,0 +1,65 @@
/** Color Schemes */
/* Themes used: Catppuccin Latte & Moccha
* https://github.com/catppuccin/catppuccin */
/* Dark theme: Catpuccin Mocha */
:root {
--color-rosewater: #f5e0dc;
--color-flamingo: #f2cdcd;
--color-pink: #f5c2e7;
--color-mauve: #cba6f7;
--color-red: #f38ba8;
--color-maroon: #eba0ac;
--color-peach: #fab387;
--color-yellow: #f9e2af;
--color-green: #a6e3a1;
--color-teal: #94e2d5;
--color-sky: #89dceb;
--color-sapphire: #74c7ec;
--color-blue: #89b4fa;
--color-lavender: #b4befe;
--color-text: #cdd6f4;
--color-subtext1: #bac2de;
--color-subtext0: #a6adc8;
--color-overlay2: #9399b2;
--color-overlay1: #7f849c;
--color-overlay0: #6c7086;
--color-surface2: #585b70;
--color-surface1: #45475a;
--color-surface0: #313244;
--color-base: #1e1e2e;
--color-mantle: #181825;
--color-crust: #11111b;
}
/* Light theme: Catppuccin Latte */
@media (prefers-color-scheme: light) {
:root {
--color-rosewater: #dc8a78;
--color-flamingo: #dd7878;
--color-pink: #ea76cb;
--color-mauve: #8839ef;
--color-red: #d20f39;
--color-maroon: #e64553;
--color-peach: #fe640b;
--color-yellow: #df8e1d;
--color-green: #40a02b;
--color-teal: #179299;
--color-sky: #04a5e5;
--color-sapphire: #209fb5;
--color-blue: #1e66f5;
--color-lavender: #7287fd;
--color-text: #4c4f69;
--color-subtext1: #5c5f77;
--color-subtext0: #6c6f85;
--color-overlay2: #7c7f93;
--color-overlay1: #8c8fa1;
--color-overlay0: #9ca0b0;
--color-surface2: #acb0be;
--color-surface1: #bcc0cc;
--color-surface0: #ccd0da;
--color-base: #eff1f5;
--color-mantle: #e6e9ef;
--color-crust: #dce0e8;
}
}

View File

@ -0,0 +1,110 @@
@import url('/static/style/colors.css');
* {
font-family: var(--font-family);
}
h2 {
color: var(--color-subtext1);
}
a {
text-decoration: none;
color: var(--color-blue);
}
body {
color: var(--color-text);
background-color: var(--color-base);
}
.no-color-link {
color: var(--color-text);
}
button {
padding: 10px 20px;
margin: 5px;
border-radius: 3px;
cursor: pointer;
font-weight: bold;
font-size: 0.95em;
border: var(--color-subtext0);
border-width: 2px;
border-style: solid;
background-color: var(--color-subtext0);
color: var(--color-base);
}
button.blue {
background-color: var(--color-blue);
border-color: var(--color-blue);
}
button:hover {
background-color: var(--color-crust);
border-color: var(--color-blue);
color: var(--color-blue);
}
header {
display: flex;
}
#login {
position: absolute;
right: 15px;
}
#logo-container {
margin: 25px;
}
main {
margin: 5vw;
text-align: center;
}
#instance-stats {
margin: 35px 0px;
color: var(--color-subtext1);
}
img.preview {
max-width: 85vw;
border-style: solid;
border-radius: 5px;
border-color: var(--color-overlay1);
border-width: 3px;
}
img#light-preview {
display: none;
}
img#dark-preview {
display: initial;
}
@media (prefers-color-scheme: light) {
img#light-preview {
display: initial;
}
img#dark-preview {
display: none;
}
}
footer {
color: var(--color-subtext1);
position: fixed;
bottom: 0;
width: 100%;
text-align: center;
margin-bottom: 10px;
}

View File

@ -0,0 +1,10 @@
@import url('/static/style/colors.css');
body {
background-color: var(--color-crust);
color: var(--color-text);
}
pre {
white-space: pre;
}

View File

@ -19,4 +19,8 @@
input:checked#slide-sidebar~#content-container {
position: initial;
}
input:checked#slide-sidebar~#footer,#content-container {
min-width: unset;
}
}

View File

@ -1,78 +1,10 @@
@import url('/static/style/colors.css');
/** @import url('https://www.augustin64.fr/static/font/iosevka.css'); */
/** Color Schemes */
/* Themes used: Catppuccin Latte & Moccha
* https://github.com/catppuccin/catppuccin */
/* Dark theme: Catpuccin Mocha */
:root {
--color-rosewater: #f5e0dc;
--color-flamingo: #f2cdcd;
--color-pink: #f5c2e7;
--color-mauve: #cba6f7;
--color-red: #f38ba8;
--color-maroon: #eba0ac;
--color-peach: #fab387;
--color-yellow: #f9e2af;
--color-green: #a6e3a1;
--color-teal: #94e2d5;
--color-sky: #89dceb;
--color-sapphire: #74c7ec;
--color-blue: #89b4fa;
--color-lavender: #b4befe;
--color-text: #cdd6f4;
--color-subtext1: #bac2de;
--color-subtext0: #a6adc8;
--color-overlay2: #9399b2;
--color-overlay1: #7f849c;
--color-overlay0: #6c7086;
--color-surface2: #585b70;
--color-surface1: #45475a;
--color-surface0: #313244;
--color-base: #1e1e2e;
--color-mantle: #181825;
--color-crust: #11111b;
/* --font-family: Iosevka Web; /* Specify the font here */
}
/* Light theme: Catppuccin Latte */
@media (prefers-color-scheme: light) {
:root {
--color-rosewater: #dc8a78;
--color-flamingo: #dd7878;
--color-pink: #ea76cb;
--color-mauve: #8839ef;
--color-red: #d20f39;
--color-maroon: #e64553;
--color-peach: #fe640b;
--color-yellow: #df8e1d;
--color-green: #40a02b;
--color-teal: #179299;
--color-sky: #04a5e5;
--color-sapphire: #209fb5;
--color-blue: #1e66f5;
--color-lavender: #7287fd;
--color-text: #4c4f69;
--color-subtext1: #5c5f77;
--color-subtext0: #6c6f85;
--color-overlay2: #7c7f93;
--color-overlay1: #8c8fa1;
--color-overlay0: #9ca0b0;
--color-surface2: #acb0be;
--color-surface1: #bcc0cc;
--color-surface0: #ccd0da;
--color-base: #eff1f5;
--color-mantle: #e6e9ef;
--color-crust: #dce0e8;
}
}
/** Various settings (variables) */
:root {
--sidebar-size: max(10vw, 160px);
--sidebar-size: max(10vw, 250px);
--sidebar-sz-plus10: calc(var(--sidebar-size) + 10px);
--sidebar-sz-moins20: calc(var(--sidebar-size) - 20px);
}
@ -401,6 +333,7 @@ img.partition-thumbnail {
.user {
display: flex;
color: var(--color-text);
}
.username {
@ -455,12 +388,13 @@ a#delete-album {
transform: translateY(-17%);
}
#settings-container>.user {
#settings-container > a > .user {
margin-top: 6px;
border-radius: 3px;
padding: 3px;
}
#settings-container>.user:hover {
#settings-container > a > .user:hover {
background-color: var(--color-mantle);
}
@ -606,6 +540,8 @@ input[type="file"] {
/** Dangerous buttons */
button#logout:hover,
a#delete-album:hover,
.red-confirm,
input[type="submit"].red-confirm,
#delete-partition {
background-color: var(--color-red);
color: var(--color-mantle);
@ -720,7 +656,7 @@ td {
/** Attachment page */
#pdf-embed {
margin: auto;
width: 100%;
width: -moz-available;
width: -webkit-fill-available;
@ -760,6 +696,9 @@ midi-player {
margin: 20px;
margin-top: 50px;
border-radius: 15px;
width: 370px;
height: 370px;
background-color: white;
}
#share-url {
@ -770,3 +709,65 @@ midi-player {
margin-bottom: 100px;
margin-top: 20px;
}
#logs-embed {
margin: auto;
height: 80vh;
width: 95%;
padding: 5px;
border-radius: 5px;
background-color: var(--color-crust);
}
/** Input[file] */
.file-area {
position: relative;
}
.file-area input[type=file] {
position: absolute;
height: 100%;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0;
cursor: pointer;
}
.file-area .inner-file-area {
padding: 30px;
background: var(--color-mantle);
border: 2px dashed var(--color-red);
text-align: center;
transition: background 0.3s ease-in-out;
}
.file-area .inner-file-area .success {
display: none;
}
.file-area:hover > .inner-file-area {
background: var(--color-surface0);
}
.file-area input[type=file]:valid + .inner-file-area {
border-color: var(--color-green);
}
.file-area input[type=file]:not(:required) + .inner-file-area {
border-color: var(--color-blue);
}
.file-area input[type=file]:valid + .inner-file-area .success {
display: inline-block;
}
.file-area input[type=file]:valid + .inner-file-area .default {
display: none;
}
.file-area input[type=file]:not(:required) + .inner-file-area .success {
display: none;
}
.file-area input[type=file]:not(:required) + .inner-file-area .default {
display: inline-block;
}

View File

@ -2,24 +2,27 @@
{% block content %}
<h2>{% block title %}Panneau d'administration{% endblock %}</h2>
<h2>{% block title %}{{ _("Administration Panel") }}{% endblock %}</h2>
<div id="actions-rapides">
<a href="/add-user">
<div class="button">Ajouter un utilisateur</div>
<div class="button">{{ _("New user") }}</div>
</a>
<a href="/partition">
<div class="button">Voir toutes les partitions</div>
<div class="button">{{ _("See scores") }}</div>
</a>
<a href="/admin/logs">
<div class="button">{{ _("See logs") }}</div>
</a>
</div>
<div class="x-scrollable">
<table>
<thead>
<tr>
<th scope="col">Utilisateur</th>
<th scope="col">Albums</th>
<th scope="col">Partitions</th>
<th scope="col">Privilèges</th>
<th scope="col">{{ _("User") }}</th>
<th scope="col">{{ _("Albums") }}</th>
<th scope="col">{{ _("Scores") }}</th>
<th scope="col">{{ _("Admin privileges") }}</th>
</tr>
</thead>
<tbody>
@ -31,7 +34,9 @@
title="{{ user.username }}">
{{ user.username[0] | upper }}
</div>
<div class="table-username">{{ user.username }}</div>
<div class="table-username">
<a href="/admin/user/{{ user.id }}">{{ user.username }}</a>
</div>
</div>
</td>
<td>{{ user.albums | length }}</td>

View File

@ -0,0 +1,10 @@
{% set scripts=["scripts/logs.js"] %}
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}{{ _("Logs") }}{% endblock %}</h1>
{% endblock %}
{% block content %}
<iframe type="text/plain" id="logs-embed" src="/admin/logs.txt" frameborder="0" width="100%" height="100%"></iframe>
{% endblock %}

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Liste des partitions{% endblock %}</h1>
<h1>{% block title %}{{ _("Scores list") }}{% endblock %}</h1>
{% endblock %}
{% block content %}
@ -11,7 +11,7 @@
<div>
<a href="/partition/{{ partition['uuid'] }}">
<div class="partition" id="partition-{{ partition['uuid'] }}">
<img class="partition-thumbnail" src="/thumbnails/{{ partition['uuid'] }}.jpg">
<img class="partition-thumbnail" src="/thumbnails/{{ partition['uuid'] }}.jpg" loading="lazy">
<div class="partition-description">
<div class="partition-name">{{ partition["name"] }}</div>
<div class="partition-author">{{ partition["author"] }}</div>
@ -28,6 +28,6 @@
{% endfor %}
</div>
{% else %}
<div>Aucune partition disponible</div>
<div>{{ _("No available scores") }}</div>
{% endif %}
{% endblock %}

View File

@ -1,15 +1,7 @@
{% extends 'base.html' %}
{% block title %}Ajout de partition{% endblock %}
{% block title %}{{ _("New score") }}{% endblock %}
{% block content %}
<h2>Ajouter une partition à {{ album.name }}</h2>
<form action="/albums/{{ album.uuid }}/add-partition" method="post" enctype="multipart/form-data">
<input name="name" type="text" placeholder="titre" required/><br/>
<input name="author" type="text" placeholder="auteur"/><br/>
<textarea id="paroles" name="body" type="text" placeholder="paroles"></textarea><br/>
<input name="partition-uuid" value="{{ partition_uuid }}" type="hidden">
<input type="submit" value="Ajouter" />
</form>
{% include 'components/add_partition.html' %}
{% endblock %}

View File

@ -5,22 +5,15 @@
{% block dialogs %}
<dialog id="add-partition">
<h2>Ajouter une partition à {{ album.name }}</h2>
<form action="/albums/{{ album.uuid }}/add-partition" method="post" enctype="multipart/form-data">
<input name="name" type="text" required="" placeholder="Titre"><br/>
<input name="author" type="text" placeholder="Auteur"><br/>
<textarea id="paroles" name="body" type="text" placeholder="Paroles"></textarea><br/>
<input name="file" type="file" accept=".pdf" required=""><br/>
<input type="submit" value="Ajouter">
</form>
{% include 'components/add_partition.html' %}
<a href="#!" class="close-dialog">Close</a>
</dialog>
<dialog id="delete">
<h2>Supprimer l'album</h2>
Êtes vous sûr de vouloir supprimer cet album ?
<h2>{{ _("Delete l'album") }}</h2>
{{ _("Do you really want to delete this album?") }}
<br/><br/>
<form method="post" action="/albums/{{ album.uuid }}/delete">
<input type="submit" style="background-color: var(--color-red);" value="Supprimer">
<input type="submit" style="background-color: var(--color-red);" value="{{ _('Delete') }}">
</form>
<a href="#!" class="close-dialog">Close</a>
</dialog>
@ -38,12 +31,12 @@
{% block content %}
<header id="album-header">
<h2 id="album-title">
{% if groupe %}<a href="/groupe/{{ groupe.uuid }}">{{ groupe.name}}</a> /
{% if groupe %}<a href="/groupe/{{ groupe.uuid }}">{{ groupe.name }}</a> /
{% endif %}
{{ album.name }}
</h2>
{% if g.user %}
<div id="header-actions">
<div id="header-actions">
{% if g.user %}
<section id="users">
{% for album_user in album.users %}
<div class="user-profile-picture" style="background-color:{{ album_user.color }};" title="{{ album_user.username }}">
@ -51,25 +44,28 @@
</div>
{% endfor %}
</section>
<div class="dropdown dp1">
+
<div class="dropdown-content dp1">
{% if g.user %}
<a href="#add-partition">Ajouter une partition</a>
{% endif %}
{% if not_participant %}
<a href="/albums/{{ album.uuid }}/join">Rejoindre</a>
{% elif album.users | length > 1 %}
<a href="/albums/{{ album.uuid }}/quit">Quitter</a>
{% endif %}
<a href="#share">Partager</a>
{% if g.user.access_level == 1 or (not not_participant and album.users | length == 1) %}
<a id="delete-album" href="#delete">Supprimer</a>
{% endif %}
</div>
{% endif %}
<div class="dropdown dp1">
+
<div class="dropdown-content dp1">
{% if g.user %}
<a href="#add-partition">{{ _("Add a score") }}</a>
{% endif %}
{% if not_participant %}
<a href="/albums/{{ album.uuid }}/join">{{ _("Join") }}</a>
{% elif g.user and not not_participant %}
<a href="/albums/{{ album.uuid }}/quit">{{ _("Quit") }}</a>
{% endif %}
<a href="#share">{{ _("Share") }}</a>
{% if g.user or not config["ZIP_REQUIRE_LOGIN"] %}
<a href="/albums/{{ album.uuid }}/zip">{{ _("Download as zip") }}</a>
{% endif %}
{% if g.user.access_level == 1 or (g.user and not not_participant and album.users | length == 1) %}
<a id="delete-album" href="#delete">{{ _("Delete") }}</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
</header>
<hr/>
{% if partitions|length != 0 %}
@ -78,7 +74,7 @@
<div>
<a href="/partition/{{ partition['uuid'] }}">
<div class="partition" id="partition-{{ partition['uuid'] }}">
<img class="partition-thumbnail" src="/thumbnails/{{ partition['uuid'] }}.jpg">
<img class="partition-thumbnail" src="/thumbnails/{{ partition['uuid'] }}.jpg" loading="lazy">
<div class="partition-description">
<div class="partition-name">{{ partition["name"] }}</div>
<div class="partition-author">{{ partition["author"] }}</div>
@ -98,6 +94,6 @@
</section>
{% else %}
<br/>
<section id="partitions-grid" style="display: inline;">Aucune partition disponible</section>
<section id="partitions-grid" style="display: inline;">{{ _("No available scores") }}</section>
{% endif %}
{% endblock %}

View File

@ -1,14 +1,14 @@
{% extends 'base.html' %}
{% block title %}Supprimer {{ album.name }}{% endblock %}
{% block title %}{{ _("Delete %(name)s", name=album.name) }}{% endblock %}
{% block content %}
Êtes vous sûr de vouloir supprimer cet album ?
{{ _("Do you really want to delete this album?") }}
<form method="post">
<input type="submit" value="Supprimer">
<input type="submit" value="{{ _('Delete') }}">
</form>
<a class="button-href" href="/albums/{{ album.uuid }}">
<button id="cancel-deletion">Annuler</button>
<button id="cancel-deletion">{{ _("Cancel") }}</button>
</a>
{% endblock %}

View File

@ -1,10 +1,13 @@
{% extends 'base.html' %}
{% block title %}Home{% endblock %}
{% block title %}{{ _("Home") }}{% endblock %}
{% block content %}
<div style="text-align: center;">
Bonjour <i><b>{{ user.username }}</b></i> !<br/>
Aucun album sélectionné
{% set user_name %}
<i><b>{{ user.username }}</b></i>
{% endset %}
{{ _("Hi %(user_name)s !", user_name=user_name) }}<br/>
{{ _("No album selected") }}
</div>
{% endblock %}

View File

@ -2,15 +2,15 @@
{% block content %}
<h2>{% block title %}Résultats de la recherche "{{ query }}"{% endblock %}</h2>
<h2>{% block title %}{{ _('Search results for "%(query)s"', query=query)}}{% endblock %}</h2>
{% if partitions|length != 0 %}
<h3>Résultats dans la bibliothèque locale</h3>
<h3>{{ _("Results in current database") }}</h3>
<div id="partitions-grid">
{% for partition in partitions %}
<div class="partition-container">
<a href="/partition/{{ partition['uuid'] }}">
<div class="partition" id="partition-{{ partition['uuid'] }}">
<img class="partition-thumbnail" src="/thumbnails/{{ partition['uuid'] }}.jpg">
<img class="partition-thumbnail" src="/thumbnails/{{ partition['uuid'] }}.jpg" loading="lazy">
<div class="partition-description">
<div class="partition-name">{{ partition["name"] }}</div>
<div class="partition-author">{{ partition["author"] }}</div>
@ -35,20 +35,20 @@
</select>
<input type="hidden" value="{{ partition['uuid'] }}" name="partition-uuid">
<input type="hidden" value="local_file" name="partition-type">
<input type="submit" value="Ajouter à l'album" class="add-to-album">
<input type="submit" value="{{ _('Add to album') }}" class="add-to-album">
</form>
</div>
{% endfor %}
</div>
{% endif %}
{% if google_results|length != 0 %}
<h3>Résultats de la recherche en ligne</h3>
<h3>{{ _("Online search results") }}</h3>
<div id="partitions-grid">
{% for partition in google_results %}
<div class="partition-container">
<a href="/partition/search/{{ partition['uuid'] }}">
<div class="partition" id="partition-{{ partition['uuid'] }}">
<img class="partition-thumbnail" src="/thumbnails/search/{{ partition['uuid'] }}.jpg">
<img class="partition-thumbnail" src="/thumbnails/search/{{ partition['uuid'] }}.jpg" loading="lazy">
<div class="partition-description">
<div class="partition-name">{{ partition["name"] }}</div>
</div>
@ -67,13 +67,13 @@
</select>
<input type="hidden" value="{{ partition['uuid'] }}" name="partition-uuid">
<input type="hidden" value="online_search" name="partition-type">
<input type="submit" value="Ajouter à l'album">
<input type="submit" value="{{ _('Add to album') }}">
</form>
</div>
{% endfor %}
</div>
{% endif %}
{% if google_results|length == 0 and partitions|length == 0 %}
Aucun résultat. Essayez d'augmenter le nombre de recherches en ligne ou d'affiner votre recherche.
{{ _("No results available. Try to tweak your query or increase the amount of online searches.") }}
{% endif %}
{% endblock %}

View File

@ -2,11 +2,11 @@
{% block content %}
<h2>{% block title %}Connexion{% endblock %}</h2>
<h2>{% block title %}{{ _("Log in") }}{% endblock %}</h2>
<form method="post">
<input type="text" name="username" id="username" placeholder="Nom d'utilisateur" required><br/>
<input type="password" name="password" id="password" placeholder="Mot de passe" required><br/>
<input type="submit" value="Se connecter">
<input type="text" name="username" id="username" placeholder="{{ _('Username') }}" required><br/>
<input type="password" name="password" id="password" placeholder="{{ _('Password') }}" required><br/>
<input type="submit" value="{{ _('Log in') }}">
</form>
{% endblock %}

View File

@ -2,21 +2,21 @@
{% block content %}
<h2>{% block title %}Créer un compte{% endblock %}</h2>
<h2>{% block title %}{{ _("Create account") }}{% endblock %}</h2>
<form method="post" id="add-user-form">
{% if g.user.access_level == 1 %}
<!-- Uniquement pour /add-user -->
<label for="album_uuid">Ajouter à un album: </label><br/>
<label for="album_uuid">{{ _("Add to album:") }}</label><br/>
<select name="album_uuid" id="album_uuid" form="add-user-form" style="margin-bottom:15px;">
<option value="">Aucun</option>
<option value="">{{ _("None") }}</option>
{% for album in albums %}
<option value="{{ album['uuid'] }}">{{ album["name"] }}</option>
{% endfor %}
</select><br/>
{% endif %}
<input type="text" name="username" id="username" placeholder="Nom d'utilisateur" required><br/>
<input type="password" name="password" id="password" placeholder="Mot de passe" required><br/>
<input type="submit" value="Créer un compte">
<input type="text" name="username" id="username" placeholder="{{ _('Username') }}" required><br/>
<input type="password" name="password" id="password" placeholder="{{ _('Password') }}" required><br/>
<input type="submit" value="{{ _('Create account') }}">
</form>
{% endblock %}

View File

@ -1,12 +1,15 @@
<!DOCTYPE html>
<html>
<html lang="{{ lang }}">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="description" content="{{ self.title() }}" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#eff1f5">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1e1e2e">
<title>{% block title %}{% endblock %} - PartitionCloud</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='mobile.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style/mobile.css') }}">
<link rel="icon" type="image/png" href="{{ url_for('static', filename='icons/512.png') }}" />
<link rel="apple-touch-icon" href="{{ url_for('static', filename='icons/512.png') }}">
<link rel="manifest" href="{{ url_for('static', filename='manifest.webmanifest') }}" />
@ -15,26 +18,26 @@
<body>
<div id="dialogs">
<!-- This div contains needed needed dialogs for the page
<!-- This div contains needed needed dialogs for the page
They will only appear if the user clicks a button for one of them -->
{% block dialogs %}{% endblock %}
{% if g.user %}
<dialog id="create-album">
<h2>Créer un nouvel album</h2>
<h2>{{ _("New Album") }}</h2>
<form action="/albums/create-album" method="post">
<input type="text" name="name" id="name" placeholder="Nom" required><br/>
<input type="submit" value="Créer">
<input type="text" name="name" id="name" placeholder="{{ _('Name') }}" required><br/>
<input type="submit" value="{{ _('Create') }}">
</form>
<br/>
<br/>
Je souhaite créer plusieurs albums et pouvoir tous les partager avec un seul lien. <a href="#create-groupe">Créer un groupe</a>.
{{ _("I want to create a collection of albums.") }} <a href="#create-groupe">{{ _("Create group") }}</a>.
<a href="#!" class="close-dialog">Close</a>
</dialog>
<dialog id="create-groupe">
<h2>Créer un nouveau groupe</h2>
<h2>{{ _("Create new group") }}</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">
<input type="text" name="name" id="name" placeholder="{{ _('Name') }}" required><br/>
<input type="submit" value="{{ _('Create') }}">
</form>
<a href="#!" class="close-dialog">Close</a>
</dialog>
@ -58,9 +61,9 @@
<div id="sidebar">
{% if g.user %}
<form id="search-form" action="/albums/search" method="post">
<input type="search" id="search-bar" required="" placeholder="Rechercher" name="query">
<input type="search" id="search-bar" required="" placeholder="{{ _('Search') }}" name="query">
<br>
<select id="nb-queries" name="nb-queries" title="Nombre de recherches en ligne">
<select id="nb-queries" name="nb-queries" title="{{ _('Number of online searches') }}">
{% for i in range(0, user.max_queries+1) %}
<option value="{{ i }}">{{ i }}</option>
{% endfor %}
@ -68,11 +71,11 @@
<input id="search-submit" type="submit" value="Go">
</form>
{% endif %}
<h2>Albums</h2>
<h2>{{ _("Albums") }}</h2>
{% if g.user %}
<a href="#create-album">
<div class="create-button">
Créer un album
{{ _("New album") }}
</div>
</a>
{% endif %}
@ -89,7 +92,7 @@
</summary>
<div class="groupe-albums-cover">
{% if groupe.get_albums() | length == 0 %}
Aucun album
{{ _("No albums") }}
{% else %}
{% for album in groupe.get_albums() %}
<a href="/groupe/{{ groupe.uuid }}/{{ album['uuid'] }}">
@ -108,7 +111,7 @@
<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>{{ _("No album available") }}</i></div>
{% else %}
{% for album in user.albums %}
<a href="/albums/{{ album['uuid'] }}">
@ -122,7 +125,7 @@
</section>
{% else %}
<section id="sidebar-navigation">
<div style="text-align: center;"><i>Connectez vous pour avoir accès à vos albums</i></div>
<div style="text-align: center;"><i>{{ _("Log in to see your albums") }}</i></div>
</section>
{% endif %}
@ -136,7 +139,7 @@
<path d="M9 12h12l-3 -3"></path>
<path d="M18 15l3 -3"></path>
</svg>
Déconnexion
{{ _("Log out") }}
</button>
</a><br/>
{% if g.user.access_level == 1 %}
@ -151,21 +154,21 @@
<path d="M17.27 20l-1.3 .75"></path>
<path d="M15.97 17.25l1.3 .75"></path>
<path d="M20.733 20l1.3 .75"></path>
</svg>Panneau admin
</svg>{{ _("Admin Panel") }}
</button></a><br/>
{% endif %}
<div class="user">
<a href="/settings"><div class="user">
<div class="user-profile-picture" style="background-color:{{ user.color }};"
title="{{ user.username }}">
{{ user.username[0] | upper }}
</div>
<div class="username">{{ user.username }}</div>
</div>
</div></a>
{% else %}
{% if not config.DISABLE_REGISTER %}
<a href="{{ url_for('auth.register') }}"><button>Créer un compte</button></a>
<a href="{{ url_for('auth.register') }}"><button>{{ _("Create account") }}</button></a>
{% endif %}
<a href="{{ url_for('auth.login') }}"><button>Se connecter</button></a>
<a href="{{ url_for('auth.login') }}"><button>{{ _("Log in") }}</button></a>
{% endif %}
</div>
</div>
@ -185,5 +188,8 @@
<div id="footer"><a href="https://github.com/partitioncloud/partitioncloud-server">PartitionCloud</a> {{ version }}</div>
</div>
</body>
<script src="{{ url_for('static', filename='main.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/main.js') }}"></script>
{% for script in scripts %}
<script src="{{ url_for('static', filename=script) }}"></script>
{% endfor %}
</html>

View File

@ -0,0 +1,17 @@
<h2>{{ _("Add a score to %(name)s", name=album.name) }}</h2>
<form action="/albums/{{ album.uuid }}/add-partition" method="post" enctype="multipart/form-data">
<input name="name" type="text" placeholder="{{ _('title') }}" required/><br/>
<input name="author" type="text" placeholder="{{ _('author') }}"/><br/>
<textarea id="lyrics" name="body" type="text" placeholder="{{ _('lyrics') }}"></textarea><br/>
{% if partition_uuid %}
<input name="partition-uuid" value="{{ partition_uuid }}" type="hidden">
{% else %}
{% block input_file %}
{% set required=true %}
{% set filetype=".pdf" %}
{% include 'components/input_file.html' %}
{% endblock %}
{% endif %}
<input type="submit" value="{{ _('Add') }}" />
</form>

View File

@ -0,0 +1,7 @@
<div class="file-area">
<input name="file" type="file" accept="{{ filetype }}" {% if required %}required=""{% endif %}>
<div class="inner-file-area">
<div class="success">{{ _("Your file is selected.") }}</div>
<div class="default">{{ _("Select or drag & drop your file") }} ({{ filetype }}).</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
<dialog id="share">
<center>
<img src="{{ share_qrlink }}" id="share-qrcode"><br/>
<img src="{{ share_qrlink }}" id="share-qrcode" loading="lazy"><br/>
<div id="share-url" onclick='navigator.clipboard.writeText("{{ share_link }}")'>
{{ share_link }}
</div>

View File

@ -5,20 +5,19 @@
{% block dialogs %}
<dialog id="create-groupe-album">
<h2>Créer un nouvel album dans le groupe {{ groupe.name }}</h2>
<h2>{{ _("Add an album to group %(name)s", name=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">
<input type="text" name="name" id="name" placeholder="{{ _('Name') }}" required><br/>
<input type="submit" value="{{ _('Add') }}">
</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).
<h2>{{ _("Delete group") }}</h2>
{{ _("Do you really want to delete this group and the albums it contains?") }}
<br/><br/>
<form method="post" action="/groupe/{{ groupe.uuid }}/delete">
<input type="submit" style="background-color: var(--color-red);" value="Supprimer">
<input type="submit" style="background-color: var(--color-red);" value="{{ _('Delete') }}">
</form>
<a href="#!" class="close-dialog">Close</a>
</dialog>
@ -31,8 +30,8 @@
{% block content %}
<header id="album-header">
<h2 id="groupe-title">{{ groupe.name }}</h2>
{% if g.user %}
<div id="header-actions">
<div id="header-actions">
{% if g.user %}
<section id="users">
{% for groupe_user in groupe.users %}
<div class="user-profile-picture" style="background-color:{{ groupe_user.color }};" title="{{ groupe_user.username }}">
@ -40,23 +39,26 @@
</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 %}
<a href="#share">Partager</a>
{% 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>
{% endif %}
<div class="dropdown dp1">
+
<div class="dropdown-content dp1">
{% if not_participant %}
<a href="/groupe/{{ groupe.uuid }}/join">{{ _("Join") }}</a>
{% elif g.user and not not_participant %}
<a href="/groupe/{{ groupe.uuid }}/quit">{{ _("Quit") }}</a>
{% endif %}
<a href="#share">{{ _("Share") }}</a>
{% if g.user or not config["ZIP_REQUIRE_LOGIN"] %}
<a href="/groupe/{{ groupe.uuid }}/zip">{{ _("Download as zip") }}</a>
{% endif %}
{% if g.user.access_level == 1 or (g.user and user.id in groupe.get_admins()) %}
<a href="#create-groupe-album">{{ _("Add an album") }}</a>
<a id="delete-album" href="#delete">{{ _("Delete") }}</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
</header>
<hr/>
{% if groupe.albums|length != 0 %}
@ -71,6 +73,11 @@
</section>
{% else %}
<br/>
<div id="albums-grid" style="display: inline;">Aucun album disponible. <a href="#create-groupe-album">En créer un</a></div>
{% set create %}
<a href="#create-groupe-album">{{ _("Create one") }}</a>
{% endset %}
<div id="albums-grid" style="display: inline;">
{{ _("No available album. %(create)s", create=create) }}
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="description" content="PartitionCloud launch page" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#eff1f5">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1e1e2e">
<title>PartitionCloud</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style/launch.css') }}">
<link rel="icon" type="image/png" href="{{ url_for('static', filename='icons/512.png') }}" />
<link rel="apple-touch-icon" href="{{ url_for('static', filename='icons/512.png') }}">
<link rel="manifest" href="{{ url_for('static', filename='manifest.webmanifest') }}" />
</head>
<body>
<header>
<a id="logo-container" href="/">
<img src="/static/icons/icon.png" width="60px" height="auto" alt="Logo">
</a>
<div>
<a href="/auth/login">
<button class="blue" id="login">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" class="iconify inline" data-icon="fluent:key-24-filled" style="vertical-align: -0.125em; transform: rotate(360deg);"><path fill="currentColor" d="M8.95 8.6a6.554 6.554 0 0 1 6.55-6.55c3.596 0 6.55 2.819 6.55 6.45a6.554 6.554 0 0 1-6.55 6.55a6.243 6.243 0 0 1-1.552-.204A1.25 1.25 0 0 1 12.7 16.05h-1.75v1.75c0 .69-.56 1.25-1.25 1.25H7.95v1.25a1.75 1.75 0 0 1-1.75 1.75H3.7a1.75 1.75 0 0 1-1.75-1.75v-2.172c0-.73.29-1.429.806-1.944L8.99 9.948a.275.275 0 0 0 .07-.244A6.386 6.386 0 0 1 8.95 8.6Zm9.3-1.6a1.25 1.25 0 1 0-2.5 0a1.25 1.25 0 0 0 2.5 0Z"></path></svg>
&nbsp; {{ _("Log in") }}
</button>
</a>
</div>
</header>
<main>
<h1>{{ _("PartitionCloud is an open-source score library server, to help you in all your musical activities") }}</h1>
<div id="actions">
<a href="/auth/login" class="no-color-link">
<button class="blue">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3" /><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3" /><path d="M15 9m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /></svg>
&nbsp; {{ _("Let's go !") }}
</button>
</a>
<a href="https://github.com/partitioncloud/partitioncloud-server" class="no-color-link">
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M16 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M12 8m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M12 16m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" /><path d="M12 15v-6" /><path d="M15 11l-2 -2" /><path d="M11 7l-1.9 -1.9" /><path d="M13.446 2.6l7.955 7.954a2.045 2.045 0 0 1 0 2.892l-7.955 7.955a2.045 2.045 0 0 1 -2.892 0l-7.955 -7.955a2.045 2.045 0 0 1 0 -2.892l7.955 -7.955a2.045 2.045 0 0 1 2.892 0z" /></svg>
&nbsp; {{ _("Check code") }}
</button>
</a>
</div>
<div id="instance-stats">
{% set user_bold %}
<b>{{ user_count }}</b>
{% endset %}
{% set partition_bold %}
<b>{{ partition_count }}</b>
{% endset %}
{{ _("This instance is used by %(users)s users with a total of %(scores)s scores.", users=user_bold, scores=partition_bold) }}
</div>
<img class="preview" id="dark-preview" src="/static/images/dark-preview.png" loading="lazy">
<img class="preview" id="light-preview" src="/static/images/light-preview.png" loading="lazy">
</main>
<footer>{{ version }}</footer>
</body>
</html>

View File

@ -2,15 +2,19 @@
{% extends 'base.html' %}
{% block title %}Attachments de {{ partition.name }}{% endblock %}
{% block title %}{{ _("Attachments of %(name)s", name=partition.name) }}{% endblock %}
{% block dialogs %}
<dialog id="create-attachment">
<h2>Ajouter un attachment à {{ partition.name }}</h2>
<h2>{{ _("Add an attachment to %(name)s", name=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">
<input type="text" name="name" id="name" placeholder="{{ _('Name') }}"><br/>
{% block input_file %}
{% set required=true %}
{% set filetype=".mp3,.mid" %}
{% include 'components/input_file.html' %}
{% endblock %}
<input type="submit" value="{{ _('Add') }}">
</form>
<a href="#!" class="close-dialog">Close</a>
</dialog>
@ -19,15 +23,15 @@
{% 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.
{{ _("No pdf viewer available in this browser.
You can use Firefox on 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>
@ -38,12 +42,12 @@
<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>
<noscript>{{ _("JavaScript is mandatory to read MIDI files") }}</noscript>
</td>
<td>🎵 {{ attachment.name }}</td>
{% endif %}
@ -55,9 +59,9 @@
{% endif %}
<br/>
{% if user %}
{% if g.user %}
<div class="centered">
<a href="#create-attachment"><button>Ajouter un attachment</button></a>
<a href="#create-attachment"><button>{{ _("Add an attachment") }}</button></a>
</div>
{% endif %}
{% endblock %}

View File

@ -1,16 +1,16 @@
{% extends 'base.html' %}
{% block header %}
<h1>{% block title %}Supprimer {{ partition.name }}{% endblock %}</h1>
<h1>{% block title %}{{ _("Delete %(name)s", name=partition.name) }}{% endblock %}</h1>
{% endblock %}
{% block content %}
Êtes vous sûr de vouloir supprimer cette partition ?
{{ _("Do you really want to delete this score?") }}
<form method="post">
<input type="submit" id="delete-partition" value="Supprimer">
<input type="submit" id="delete-partition" value="{{ _('Delete') }}">
</form>
<a class="button-href" href="/partition/{{ partition.uuid }}/edit">
<button id="cancel-deletion">Annuler</button>
<button id="cancel-deletion">{{ _("Cancel") }}</button>
</a>
{% endblock %}

View File

@ -1,15 +1,15 @@
{% extends 'base.html' %}
{% block content %}
<h2>{% block title %}Détails "{{ partition.name }}"{% endblock %}</h2>
<br />
<h2>{% block title %}{{ _('Details of "%(name)s"', name=partition.name)}}{% endblock %}</h2>
<br/>
<form action="/partition/{{ partition.uuid }}/edit" method="post" enctype="multipart/form-data">
<table>
<tbody>
<tr>
<td>
Responsable de l'ajout
{{ _("Added by") }}
</td>
<td>
{% if user is not none %}
@ -20,13 +20,13 @@
</div>
</div>
{% else %}
inconnu
{{ _("Unknown") }}
{% endif %}
</td>
</tr>
<tr>
<td>
Type d'ajout
{{ _("Type") }}
</td>
<td>
{% if partition.source == "unknown" or partition.source == "upload" %}
@ -38,7 +38,7 @@
</tr>
<tr>
<td>
Albums
{{ _("Albums") }}
</td>
<td class="liste">
<ul>
@ -49,38 +49,47 @@
</td>
</tr>
<tr>
<td>Fichier</td>
<td><a href="/partition/{{ partition.uuid }}"><img class="partition-thumbnail" src="/thumbnails/{{ partition.uuid }}.jpg"></a></td>
<td>{{ _("File") }}</td>
<td><a href="/partition/{{ partition.uuid }}">
<img class="partition-thumbnail" src="/thumbnails/{{ partition.uuid }}.jpg" loading="lazy">
</a><br/>
{% block input_file %}
{% set required=false %}
{% set filetype=".pdf" %}
{% include 'components/input_file.html' %}
{% endblock %}
</td>
</tr>
<tr>
<td>Titre</td>
<td><input name="name" type="text" value="{{ partition.name }}" placeholder="Titre" required /><br/></td>
<td>{{ _("Title") }}</td>
<td><input name="name" type="text" value="{{ partition.name }}" placeholder="{{ _('Title') }}" required /><br/></td>
</tr>
<tr>
<td>Auteur</td>
<td><input name="author" type="text" value="{{ partition.author }}" placeholder="Auteur" /><br/></td>
<td>{{ _("Author") }}</td>
<td><input name="author" type="text" value="{{ partition.author }}" placeholder="{{ _('Author') }}" /><br/></td>
</tr>
<tr>
<td>Paroles</td>
<td><textarea id="paroles" name="body" type="text" placeholder="Paroles">{{ partition.body }}</textarea><br/></td>
<td>{{ _("Lyrics") }}</td>
<td><textarea id="lyrics" name="body" type="text" placeholder="{{ _('Lyrics') }}">{{ partition.body }}</textarea><br/></td>
</tr>
<tr>
<td>Pièces jointes</td>
{% set _ = partition.load_attachments() %}
<td>{{ _("Attachments") }}</td>
{{ partition.load_attachments() }}
<td><a href="/partition/{{ partition.uuid }}/attachments">
{% if partition.attachments %}
Oui, {{ partition.attachments | length }}
{% set number=partition.attachments | length %}
{{ _("Yes, %(number)s", number=number) }}
{% else %}
En rajouter
{{ _("Add one") }}
{% endif %}
</a></td>
</tr>
</tbody>
</table>
<input type="submit" value="Mettre à jour" />
<input type="submit" value="{{ _('Update') }}" />
</form>
<a href="/partition/{{ partition.uuid }}/delete">
<button id="delete-partition">Supprimer</button>
<button id="delete-partition">{{ _("Delete") }}</button>
</a>
{% endblock %}

View File

@ -3,22 +3,28 @@
{% block content %}
<h2>{% block title %}Modifier "{{ partition.name }}"{% endblock %}</h2>
<h2>{% block title %}{{ _("Modify \"%(name)s\"", name=partition.name) }}{% endblock %}</h2>
<br/>
<form action="/partition/{{ partition.uuid }}/edit" method="post" enctype="multipart/form-data">
<table>
<tbody>
<tr>
<td>Fichier</td>
<td>{{ _("File") }}</td>
<td><a href="/partition/{{ partition.uuid }}">
<img class="partition-thumbnail" src="/thumbnails/{{ partition.uuid }}.jpg">
</a></td>
<img class="partition-thumbnail" src="/thumbnails/{{ partition.uuid }}.jpg" loading="lazy">
</a><br/>
{% block input_file %}
{% set required=false %}
{% set filetype=".pdf" %}
{% include 'components/input_file.html' %}
{% endblock %}
</td>
</tr>
{% if partition.source != "unknown" and partition.source != "upload" %}
<tr>
<td>
Source
{{ _("Source") }}
</td>
<td class="partition-source">
<a href="{{ partition.source }}">{{ partition.source.split("/")[2] }}</a>
@ -26,34 +32,35 @@
</tr>
{% endif %}
<tr>
<td>Titre</td>
<td><input name="name" type="text" value="{{ partition.name }}" placeholder="Titre" required /><br/></td>
<td>{{ _("Title") }}</td>
<td><input name="name" type="text" value="{{ partition.name }}" placeholder="{{ _('Title') }}" required /><br/></td>
</tr>
<tr>
<td>Auteur</td>
<td><input name="author" type="text" value="{{ partition.author }}" placeholder="Auteur" /><br/></td>
<td>{{ _("Author") }}</td>
<td><input name="author" type="text" value="{{ partition.author }}" placeholder="{{ _('Author') }}" /><br/></td>
</tr>
<tr>
<td>Paroles</td>
<td><textarea id="paroles" name="body" type="text" placeholder="Paroles">{{ partition.body }}</textarea><br/></td>
<td>{{ _("Lyrics") }}</td>
<td><textarea id="lyrics" name="body" type="text" placeholder="{{ _('Lyrics') }}">{{ partition.body }}</textarea><br/></td>
</tr>
<tr>
<td>Pièces jointes</td>
{% set _ = partition.load_attachments() %}
<td>{{ _("Attachments") }}</td>
{{ partition.load_attachments() }}
<td><a href="/partition/{{ partition.uuid }}/attachments">
{% if partition.attachments %}
Oui, {{ partition.attachments | length }}
{% set number=partition.attachments | length %}
{{ _("Yes, %(number)s", number=number) }}
{% else %}
En rajouter
{{ _("Add one") }}
{% endif %}
</a></td>
</tr>
</tbody>
</table>
<input type="submit" value="Mettre à jour" />
<input type="submit" value="{{ _('Update') }}" />
</form>
<a href="/partition/{{ partition.uuid }}/delete">
<button id="delete-partition">Supprimer</button>
<button id="delete-partition">{{ _("Delete") }}</button>
</a>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends 'base.html' %}
{% block title %}{{ _("Settings") }}{% endblock %}
{% block dialogs %}
<dialog id="delete-account">
<h2>{{ _("Delete account") }}</h2>
{% set username %}
<b>{{ inspected_user.username }}</b>
{% endset %}
{% set irreversible_bold %}
<b>irreversible</b>
{% endset %}
{{ _("Do you really want to delete %(username)s's account ? This action is %(irreversible_bold)s.", username=username, irreversible_bold=irreversible_bold) }}
<br/><br/>
<form method="post" action="/settings/delete-account">
<input type="hidden" id="user_id" name="user_id" value="{{ inspected_user.id }}">
<input type="submit" class="red-confirm" value="{{ _('Delete') }}">
</form>
<a href="#!" class="close-dialog">Close</a>
</dialog>
{% endblock %}
{% block content %}
{{ _("User %(username)s has %(album_count)s albums", username=inspected_user.username, album_count=(inspected_user.get_albums() | length)) }}
<form action="/settings/change-password" method="post">
<h3>{{ _("Change password") }}</h3>
{% if not skip_old_password %}
<input type="password" id="old-password" name="old_password" placeholder="{{ _('old password') }}"/><br/>
{% endif %}
<input type="password" id="new-password" name="new_password" placeholder="{{ _('new password') }}"/><br/>
<input type="password" id="confirm-new-password" name="confirm_new_password" placeholder="{{ _('confirm new password') }}"/><br/>
<input type="hidden" id="user_id" name="user_id" value="{{ inspected_user.id }}">
<input type="Submit" value="{{ _('confirm') }}">
</form>
{% if deletion_allowed %}
<h3>{{ _("Delete account") }}</h3>
<a href="#delete-account"><button class="red-confirm">{{ _("Delete account") }}</button></a>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,715 @@
# English translations for PROJECT.
# Copyright (C) 2024 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-04-10 18:47+0200\n"
"PO-Revision-Date: 2024-01-22 15:38+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: en\n"
"Language-Team: en <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.14.0\n"
#: partitioncloud/__init__.py:148
#, python-format
msgid "Created user %(username)s"
msgstr "Created user %(username)s"
#: partitioncloud/__init__.py:151
#, python-format
msgid "This album does not exists, but user %(username)s has been created"
msgstr "This album does not exists, but user %(username)s has been created"
#: partitioncloud/modules/albums.py:43
msgid "Missing search query"
msgstr "Missing search query"
#: partitioncloud/modules/albums.py:124 partitioncloud/modules/auth.py:27
#: partitioncloud/modules/auth.py:54 partitioncloud/modules/groupe.py:271
msgid "You need to login to access this resource."
msgstr "You need to login to access this resource."
#: partitioncloud/modules/albums.py:155 partitioncloud/modules/groupe.py:72
#: partitioncloud/modules/groupe.py:186
msgid "Missing name."
msgstr "Missing name."
#: partitioncloud/modules/albums.py:192
msgid "This album does not exist."
msgstr "This album does not exist."
#: partitioncloud/modules/albums.py:195
msgid "Album added to collection."
msgstr "Album added to collection."
#: partitioncloud/modules/albums.py:209 partitioncloud/modules/albums.py:272
#: partitioncloud/modules/albums.py:384
msgid "You are not a member of this album"
msgstr "You are not a member of this album"
#: partitioncloud/modules/albums.py:213
msgid "You are alone here, quitting means deleting this album."
msgstr "You are alone here, quitting means deleting this album."
#: partitioncloud/modules/albums.py:217
msgid "Album quitted."
msgstr "Album quitted."
#: partitioncloud/modules/albums.py:236
msgid "You are not alone in this album."
msgstr "You are not alone in this album."
#: partitioncloud/modules/albums.py:238
msgid "You don't own this album."
msgstr "You don't own this album."
#: partitioncloud/modules/albums.py:249
msgid "Album deleted."
msgstr "Album deleted."
#: partitioncloud/modules/albums.py:278 partitioncloud/modules/partition.py:154
#: partitioncloud/modules/partition.py:211
msgid "Missing title"
msgstr "Missing title"
#: partitioncloud/modules/albums.py:280 partitioncloud/modules/partition.py:64
msgid "Missing file"
msgstr "Missing file"
#: partitioncloud/modules/albums.py:292
msgid "Search results expired"
msgstr "Search results expired"
#: partitioncloud/modules/albums.py:302 partitioncloud/modules/partition.py:170
msgid "Invalid PDF file"
msgstr ""
#: partitioncloud/modules/albums.py:364
#, python-format
msgid "Score %(partition_name)s added"
msgstr "Score %(partition_name)s added"
#: partitioncloud/modules/albums.py:378
msgid "Selecting an album is mandatory."
msgstr "Selecting an album is mandatory."
#: partitioncloud/modules/albums.py:380
msgid "Selecting a score is mandatory."
msgstr "Selecting a score is mandatory."
#: partitioncloud/modules/albums.py:382
msgid "Please specify a score type."
msgstr "Please specify a score type."
#: partitioncloud/modules/albums.py:404
msgid "Score added"
msgstr "Score added"
#: partitioncloud/modules/albums.py:406
msgid "Score is already in the album."
msgstr "Score is already in the album."
#: partitioncloud/modules/albums.py:418
msgid "Unknown score type."
msgstr "Unknown score type."
#: partitioncloud/modules/auth.py:59 partitioncloud/modules/settings.py:50
#: partitioncloud/modules/settings.py:82
msgid "Missing rights."
msgstr "Missing rights."
#: partitioncloud/modules/auth.py:85
msgid "Missing username."
msgstr "Missing username."
#: partitioncloud/modules/auth.py:87 partitioncloud/modules/settings.py:96
msgid "Missing password."
msgstr "Missing password."
#: partitioncloud/modules/auth.py:100
#, python-format
msgid "Username %(username)s is not available."
msgstr "Username %(username)s is not available."
#: partitioncloud/modules/auth.py:113
msgid "New users registration is disabled by owner."
msgstr "New users registration is disabled by owner."
#: partitioncloud/modules/auth.py:127
msgid "Successfully created new user. You can log in."
msgstr "Successfully created new user. You can log in."
#: partitioncloud/modules/auth.py:153
msgid "Incorrect username or password"
msgstr "Incorrect username or password"
#: partitioncloud/modules/groupe.py:121
msgid "Unknown group."
msgstr "Unknown group."
#: partitioncloud/modules/groupe.py:124
msgid "Group added to collection."
msgstr "Group added to collection."
#: partitioncloud/modules/groupe.py:135
msgid "You are not a member of this group."
msgstr "You are not a member of this group."
#: partitioncloud/modules/groupe.py:139
msgid "You are alone here, quitting means deleting this group."
msgstr "You are alone here, quitting means deleting this group."
#: partitioncloud/modules/groupe.py:143
msgid "Group quitted."
msgstr "Group quitted."
#: partitioncloud/modules/groupe.py:156
msgid "You are not alone in this group."
msgstr "You are not alone in this group."
#: partitioncloud/modules/groupe.py:167
msgid "Group deleted."
msgstr "Group deleted."
#: partitioncloud/modules/groupe.py:189
msgid "You are not admin of this group."
msgstr "You are not admin of this group."
#: partitioncloud/modules/partition.py:59
msgid "You don't own this score."
msgstr "You don't own this score."
#: partitioncloud/modules/partition.py:72
msgid "Missing filename."
msgstr "Missing filename."
#: partitioncloud/modules/partition.py:77
msgid "Unsupported file type."
msgstr "Unsupported file type."
#: partitioncloud/modules/partition.py:145
msgid "You are not allowed to edit this file."
msgstr "You are not allowed to edit this file."
#: partitioncloud/modules/partition.py:156
#: partitioncloud/modules/partition.py:213
msgid "Missing author in request body (can be null)."
msgstr "Missing author in request body (can be null)."
#: partitioncloud/modules/partition.py:158
#: partitioncloud/modules/partition.py:215
msgid "Missing lyrics (can be null)."
msgstr "Missing lyrics (can be null)."
#: partitioncloud/modules/partition.py:181
#: partitioncloud/modules/partition.py:227
#, python-format
msgid "Successfully modified %(name)s"
msgstr "Successfully modified %(name)s"
#: partitioncloud/modules/partition.py:242
msgid "You are not allowed to delete this score."
msgstr "You are not allowed to delete this score."
#: partitioncloud/modules/partition.py:250
msgid "Score deleted."
msgstr "Score deleted."
#: partitioncloud/modules/settings.py:40 partitioncloud/modules/settings.py:72
msgid "Missing user id."
msgstr "Missing user id."
#: partitioncloud/modules/settings.py:54
msgid "You are not allowed to delete your account."
msgstr "You are not allowed to delete your account."
#: partitioncloud/modules/settings.py:60
msgid "User successfully deleted."
msgstr "User successfully deleted."
#: partitioncloud/modules/settings.py:86
msgid "Missing old password."
msgstr "Missing old password."
#: partitioncloud/modules/settings.py:90
msgid "Incorrect password."
msgstr "Incorrect password."
#: partitioncloud/modules/settings.py:100
msgid "Password and its confirmation differ."
msgstr "Password and its confirmation differ."
#: partitioncloud/modules/settings.py:104
msgid "Successfully updated password."
msgstr "Successfully updated password."
#: partitioncloud/templates/base.html:26
msgid "New Album"
msgstr "New Album"
#: partitioncloud/templates/base.html:28 partitioncloud/templates/base.html:39
#: partitioncloud/templates/groupe/index.html:10
#: partitioncloud/templates/partition/attachments.html:11
msgid "Name"
msgstr "Name"
#: partitioncloud/templates/base.html:29 partitioncloud/templates/base.html:40
msgid "Create"
msgstr "Create"
#: partitioncloud/templates/base.html:33
msgid "I want to create a collection of albums."
msgstr "I want to create a collection of albums."
#: partitioncloud/templates/base.html:33
msgid "Create group"
msgstr "Create group"
#: partitioncloud/templates/base.html:37
msgid "Create new group"
msgstr "Create new group"
#: partitioncloud/templates/base.html:64
msgid "Search"
msgstr "Search"
#: partitioncloud/templates/base.html:66
msgid "Number of online searches"
msgstr "Number of online searches"
#: partitioncloud/templates/admin/index.html:23
#: partitioncloud/templates/base.html:74
#: partitioncloud/templates/partition/details.html:41
msgid "Albums"
msgstr "Albums"
#: partitioncloud/templates/base.html:78
msgid "New album"
msgstr "New album"
#: partitioncloud/templates/base.html:95
msgid "No albums"
msgstr "No albums"
#: partitioncloud/templates/base.html:114
msgid "No album available"
msgstr "No album available"
#: partitioncloud/templates/base.html:128
msgid "Log in to see your albums"
msgstr "Log in to see your albums"
#: partitioncloud/templates/base.html:142
msgid "Log out"
msgstr "Log out"
#: partitioncloud/templates/base.html:157
msgid "Admin Panel"
msgstr "Admin Panel"
#: partitioncloud/templates/auth/register.html:5
#: partitioncloud/templates/auth/register.html:20
#: partitioncloud/templates/base.html:169
msgid "Create account"
msgstr "Create account"
#: partitioncloud/templates/auth/login.html:5
#: partitioncloud/templates/auth/login.html:10
#: partitioncloud/templates/base.html:171
#: partitioncloud/templates/launch.html:26
msgid "Log in"
msgstr "Log in"
#: partitioncloud/templates/launch.html:33
msgid ""
"PartitionCloud is an open-source score library server, to help you in all"
" your musical activities"
msgstr ""
"PartitionCloud is an open-source score library server, to help you in all"
" your musical activities"
#: partitioncloud/templates/launch.html:38
msgid "Let's go !"
msgstr "Let's go !"
#: partitioncloud/templates/launch.html:44
msgid "Check code"
msgstr "Check code"
#: partitioncloud/templates/launch.html:55
#, python-format
msgid ""
"This instance is used by %(users)s users with a total of %(scores)s "
"scores."
msgstr "This instance has %(users)s users with a total of %(scores)s scores."
#: partitioncloud/templates/admin/index.html:5
msgid "Administration Panel"
msgstr "Administration Panel"
#: partitioncloud/templates/admin/index.html:9
msgid "New user"
msgstr "New user"
#: partitioncloud/templates/admin/index.html:12
msgid "See scores"
msgstr "See scores"
#: partitioncloud/templates/admin/index.html:15
msgid "See logs"
msgstr "See logs"
#: partitioncloud/templates/admin/index.html:22
msgid "User"
msgstr "User"
#: partitioncloud/templates/admin/index.html:24
msgid "Scores"
msgstr "Scores"
#: partitioncloud/templates/admin/index.html:25
msgid "Admin privileges"
msgstr "Admin privileges"
#: partitioncloud/templates/admin/logs.html:5
msgid "Logs"
msgstr "Logs"
#: partitioncloud/templates/admin/partitions.html:4
msgid "Scores list"
msgstr "Scores list"
#: partitioncloud/templates/admin/partitions.html:31
#: partitioncloud/templates/albums/album.html:97
msgid "No available scores"
msgstr "No available scores"
#: partitioncloud/templates/albums/add-partition.html:3
msgid "New score"
msgstr "New score"
#: partitioncloud/templates/albums/album.html:12
msgid "Delete l'album"
msgstr "Delete album"
#: partitioncloud/templates/albums/album.html:13
#: partitioncloud/templates/albums/delete-album.html:6
msgid "Do you really want to delete this album?"
msgstr "Do you really want to delete this album?"
#: partitioncloud/templates/albums/album.html:16
#: partitioncloud/templates/albums/album.html:64
#: partitioncloud/templates/albums/delete-album.html:8
#: partitioncloud/templates/groupe/index.html:20
#: partitioncloud/templates/groupe/index.html:57
#: partitioncloud/templates/partition/delete.html:10
#: partitioncloud/templates/partition/details.html:92
#: partitioncloud/templates/partition/edit.html:63
#: partitioncloud/templates/settings/index.html:19
msgid "Delete"
msgstr "Delete"
#: partitioncloud/templates/albums/album.html:52
msgid "Add a score"
msgstr "Add a score"
#: partitioncloud/templates/albums/album.html:55
#: partitioncloud/templates/groupe/index.html:47
msgid "Join"
msgstr "Join"
#: partitioncloud/templates/albums/album.html:57
#: partitioncloud/templates/groupe/index.html:49
msgid "Quit"
msgstr "Quit"
#: partitioncloud/templates/albums/album.html:59
#: partitioncloud/templates/groupe/index.html:51
msgid "Share"
msgstr "Share"
#: partitioncloud/templates/albums/album.html:61
#: partitioncloud/templates/groupe/index.html:53
msgid "Download as zip"
msgstr "Download as zip"
#: partitioncloud/templates/albums/delete-album.html:3
#: partitioncloud/templates/partition/delete.html:4
#, python-format
msgid "Delete %(name)s"
msgstr "Delete %(name)s"
#: partitioncloud/templates/albums/delete-album.html:11
#: partitioncloud/templates/partition/delete.html:13
msgid "Cancel"
msgstr "Cancel"
#: partitioncloud/templates/albums/index.html:3
msgid "Home"
msgstr "Home"
#: partitioncloud/templates/albums/index.html:10
#, python-format
msgid "Hi %(user_name)s !"
msgstr "Hi %(user_name)s !"
#: partitioncloud/templates/albums/index.html:11
msgid "No album selected"
msgstr "No album selected"
#: partitioncloud/templates/albums/search.html:5
#, python-format
msgid "Search results for \"%(query)s\""
msgstr "Search results for \"%(query)s\""
#: partitioncloud/templates/albums/search.html:7
msgid "Results in current database"
msgstr "Results in current database"
#: partitioncloud/templates/albums/search.html:38
#: partitioncloud/templates/albums/search.html:70
msgid "Add to album"
msgstr "Add to album"
#: partitioncloud/templates/albums/search.html:45
msgid "Online search results"
msgstr ""
#: partitioncloud/templates/albums/search.html:77
msgid ""
"No results available. Try to tweak your query or increase the amount of "
"online searches."
msgstr ""
"No results available. Try to tweak your query or increase the amount of "
"online searches."
#: partitioncloud/templates/auth/login.html:8
#: partitioncloud/templates/auth/register.html:18
msgid "Username"
msgstr "Username"
#: partitioncloud/templates/auth/login.html:9
#: partitioncloud/templates/auth/register.html:19
msgid "Password"
msgstr "Password"
#: partitioncloud/templates/auth/register.html:10
msgid "Add to album:"
msgstr "Add to album:"
#: partitioncloud/templates/auth/register.html:12
msgid "None"
msgstr "None"
#: partitioncloud/templates/components/add_partition.html:1
#, python-format
msgid "Add a score to %(name)s"
msgstr "Add a score to %(name)s"
#: partitioncloud/templates/components/add_partition.html:4
msgid "title"
msgstr "title"
#: partitioncloud/templates/components/add_partition.html:5
msgid "author"
msgstr "author"
#: partitioncloud/templates/components/add_partition.html:6
msgid "lyrics"
msgstr "lyrics"
#: partitioncloud/templates/components/add_partition.html:16
#: partitioncloud/templates/groupe/index.html:11
#: partitioncloud/templates/partition/attachments.html:17
msgid "Add"
msgstr "Add"
#: partitioncloud/templates/components/input_file.html:4
msgid "Your file is selected."
msgstr "Your file is selected."
#: partitioncloud/templates/components/input_file.html:5
msgid "Select or drag & drop your file"
msgstr "Select or drag & drop your file"
#: partitioncloud/templates/groupe/index.html:8
#, python-format
msgid "Add an album to group %(name)s"
msgstr "Add an album to group %(name)s"
#: partitioncloud/templates/groupe/index.html:16
msgid "Delete group"
msgstr "Delete group"
#: partitioncloud/templates/groupe/index.html:17
msgid "Do you really want to delete this group and the albums it contains?"
msgstr "Do you really want to delete this group and the albums it contains?"
#: partitioncloud/templates/groupe/index.html:56
msgid "Add an album"
msgstr "Add an album"
#: partitioncloud/templates/groupe/index.html:77
msgid "Create one"
msgstr "Create one"
#: partitioncloud/templates/groupe/index.html:80
#, python-format
msgid "No available album. %(create)s"
msgstr "No available album. %(create)s"
#: partitioncloud/templates/partition/attachments.html:5
#, python-format
msgid "Attachments of %(name)s"
msgstr "Attachments of %(name)s"
#: partitioncloud/templates/partition/attachments.html:9
#, python-format
msgid "Add an attachment to %(name)s"
msgstr "Add an attachment to %(name)s"
#: partitioncloud/templates/partition/attachments.html:26
msgid ""
"No pdf viewer available in this browser.\n"
" You can use Firefox on Android."
msgstr ""
"No pdf viewer available in this browser.\n"
" You can use Firefox on Android."
#: partitioncloud/templates/partition/attachments.html:50
msgid "JavaScript is mandatory to read MIDI files"
msgstr "JavaScript is mandatory to read MIDI files"
#: partitioncloud/templates/partition/attachments.html:64
msgid "Add an attachment"
msgstr "Add an attachment"
#: partitioncloud/templates/partition/delete.html:8
msgid "Do you really want to delete this score?"
msgstr "Do you really want to delete this score?"
#: partitioncloud/templates/partition/details.html:4
#, python-format
msgid "Details of \"%(name)s\""
msgstr "Details of \"%(name)s\""
#: partitioncloud/templates/partition/details.html:12
msgid "Added by"
msgstr "Added by"
#: partitioncloud/templates/partition/details.html:23
msgid "Unknown"
msgstr "Unknown"
#: partitioncloud/templates/partition/details.html:29
msgid "Type"
msgstr "Type"
#: partitioncloud/templates/partition/details.html:52
#: partitioncloud/templates/partition/edit.html:13
msgid "File"
msgstr "File"
#: partitioncloud/templates/partition/details.html:64
#: partitioncloud/templates/partition/details.html:65
#: partitioncloud/templates/partition/edit.html:35
#: partitioncloud/templates/partition/edit.html:36
msgid "Title"
msgstr "Title"
#: partitioncloud/templates/partition/details.html:68
#: partitioncloud/templates/partition/details.html:69
#: partitioncloud/templates/partition/edit.html:39
#: partitioncloud/templates/partition/edit.html:40
msgid "Author"
msgstr "Author"
#: partitioncloud/templates/partition/details.html:72
#: partitioncloud/templates/partition/details.html:73
#: partitioncloud/templates/partition/edit.html:43
#: partitioncloud/templates/partition/edit.html:44
msgid "Lyrics"
msgstr "Lyrics"
#: partitioncloud/templates/partition/details.html:76
#: partitioncloud/templates/partition/edit.html:47
msgid "Attachments"
msgstr "Attachments"
#: partitioncloud/templates/partition/details.html:81
#: partitioncloud/templates/partition/edit.html:52
#, python-format
msgid "Yes, %(number)s"
msgstr "Yes, %(number)s"
#: partitioncloud/templates/partition/details.html:83
#: partitioncloud/templates/partition/edit.html:54
msgid "Add one"
msgstr "Add one"
#: partitioncloud/templates/partition/details.html:89
#: partitioncloud/templates/partition/edit.html:60
msgid "Update"
msgstr "Update"
#: partitioncloud/templates/partition/edit.html:6
#, python-format
msgid "Modify \"%(name)s\""
msgstr "Modify \"%(name)s\""
#: partitioncloud/templates/partition/edit.html:27
msgid "Source"
msgstr "Source"
#: partitioncloud/templates/settings/index.html:3
msgid "Settings"
msgstr "Settings"
#: partitioncloud/templates/settings/index.html:8
#: partitioncloud/templates/settings/index.html:39
#: partitioncloud/templates/settings/index.html:40
msgid "Delete account"
msgstr "Delete account"
#: partitioncloud/templates/settings/index.html:15
#, python-format
msgid ""
"Do you really want to delete %(username)s's account ? This action is "
"%(irreversible_bold)s."
msgstr ""
"Do you really want to delete %(username)s's account ? This action is "
"%(irreversible_bold)s."
#: partitioncloud/templates/settings/index.html:27
#, python-format
msgid "User %(username)s has %(album_count)s albums"
msgstr "User %(username)s has %(album_count)s albums"
#: partitioncloud/templates/settings/index.html:29
msgid "Change password"
msgstr "Change password"
#: partitioncloud/templates/settings/index.html:31
msgid "old password"
msgstr "old password"
#: partitioncloud/templates/settings/index.html:33
msgid "new password"
msgstr "new password"
#: partitioncloud/templates/settings/index.html:34
msgid "confirm new password"
msgstr "confirm new password"
#: partitioncloud/templates/settings/index.html:36
msgid "confirm"
msgstr "confirm"

View File

@ -0,0 +1,724 @@
# French translations for PROJECT.
# Copyright (C) 2024 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-04-10 18:47+0200\n"
"PO-Revision-Date: 2024-01-22 15:24+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: fr\n"
"Language-Team: fr <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.14.0\n"
#: partitioncloud/__init__.py:148
#, python-format
msgid "Created user %(username)s"
msgstr "Utilisateur %(username)s créé"
#: partitioncloud/__init__.py:151
#, python-format
msgid "This album does not exists, but user %(username)s has been created"
msgstr "Cet album n'existe pas. L'utilisateur %(username)s a été créé"
#: partitioncloud/modules/albums.py:43
msgid "Missing search query"
msgstr "Aucun terme de recherche spécifié."
#: partitioncloud/modules/albums.py:124 partitioncloud/modules/auth.py:27
#: partitioncloud/modules/auth.py:54 partitioncloud/modules/groupe.py:271
msgid "You need to login to access this resource."
msgstr "Vous devez être connecté pour accéder à cette page."
#: partitioncloud/modules/albums.py:155 partitioncloud/modules/groupe.py:72
#: partitioncloud/modules/groupe.py:186
msgid "Missing name."
msgstr "Un nom est requis."
#: partitioncloud/modules/albums.py:192
msgid "This album does not exist."
msgstr "Cet album n'existe pas."
#: partitioncloud/modules/albums.py:195
msgid "Album added to collection."
msgstr "Album ajouté à la collection."
#: partitioncloud/modules/albums.py:209 partitioncloud/modules/albums.py:272
#: partitioncloud/modules/albums.py:384
msgid "You are not a member of this album"
msgstr "Vous ne faites pas partie de cet album"
#: partitioncloud/modules/albums.py:213
msgid "You are alone here, quitting means deleting this album."
msgstr "Vous êtes seul dans cet album, le quitter entraînera sa suppression."
#: partitioncloud/modules/albums.py:217
msgid "Album quitted."
msgstr "Album quitté."
#: partitioncloud/modules/albums.py:236
msgid "You are not alone in this album."
msgstr "Vous n'êtes pas seul dans cet album."
#: partitioncloud/modules/albums.py:238
msgid "You don't own this album."
msgstr "Vous ne possédez pas cet album."
#: partitioncloud/modules/albums.py:249
msgid "Album deleted."
msgstr "Album supprimé."
#: partitioncloud/modules/albums.py:278 partitioncloud/modules/partition.py:154
#: partitioncloud/modules/partition.py:211
msgid "Missing title"
msgstr "Un titre est requis."
#: partitioncloud/modules/albums.py:280 partitioncloud/modules/partition.py:64
msgid "Missing file"
msgstr "Aucun fichier n'a été fourni."
#: partitioncloud/modules/albums.py:292
msgid "Search results expired"
msgstr "Les résultats de la recherche ont expiré."
#: partitioncloud/modules/albums.py:302 partitioncloud/modules/partition.py:170
msgid "Invalid PDF file"
msgstr "Fichier PDF invalide"
#: partitioncloud/modules/albums.py:364
#, python-format
msgid "Score %(partition_name)s added"
msgstr "Partition %(partition_name)s ajoutée"
#: partitioncloud/modules/albums.py:378
msgid "Selecting an album is mandatory."
msgstr "Il est nécessaire de sélectionner un album."
#: partitioncloud/modules/albums.py:380
msgid "Selecting a score is mandatory."
msgstr "Il est nécessaire de sélectionner une partition."
#: partitioncloud/modules/albums.py:382
msgid "Please specify a score type."
msgstr "Il est nécessaire de spécifier un type de partition."
#: partitioncloud/modules/albums.py:404
msgid "Score added"
msgstr "Partition ajoutée."
#: partitioncloud/modules/albums.py:406
msgid "Score is already in the album."
msgstr "Partition déjà dans l'album."
#: partitioncloud/modules/albums.py:418
msgid "Unknown score type."
msgstr "Type de partition inconnu."
#: partitioncloud/modules/auth.py:59 partitioncloud/modules/settings.py:50
#: partitioncloud/modules/settings.py:82
msgid "Missing rights."
msgstr "Droits insuffisants."
#: partitioncloud/modules/auth.py:85
msgid "Missing username."
msgstr "Un nom d'utilisateur est requis."
#: partitioncloud/modules/auth.py:87 partitioncloud/modules/settings.py:96
msgid "Missing password."
msgstr "Un mot de passe est requis."
#: partitioncloud/modules/auth.py:100
#, python-format
msgid "Username %(username)s is not available."
msgstr "Le nom d'utilisateur %(username)s est déjà pris."
#: partitioncloud/modules/auth.py:113
msgid "New users registration is disabled by owner."
msgstr ""
"L'enregistrement de nouveaux utilisateurs a été désactivé par "
"l'administrateur."
#: partitioncloud/modules/auth.py:127
msgid "Successfully created new user. You can log in."
msgstr "Utilisateur créé avec succès. Vous pouvez vous connecter."
#: partitioncloud/modules/auth.py:153
msgid "Incorrect username or password"
msgstr "Nom d'utilisateur ou mot de passe incorrect."
#: partitioncloud/modules/groupe.py:121
msgid "Unknown group."
msgstr "Ce groupe n'existe pas."
#: partitioncloud/modules/groupe.py:124
msgid "Group added to collection."
msgstr "Groupe ajouté à la collection."
#: partitioncloud/modules/groupe.py:135
msgid "You are not a member of this group."
msgstr "Vous ne faites pas partie de ce groupe"
#: partitioncloud/modules/groupe.py:139
msgid "You are alone here, quitting means deleting this group."
msgstr "Vous êtes seul dans ce groupe, le quitter entraînera sa suppression."
#: partitioncloud/modules/groupe.py:143
msgid "Group quitted."
msgstr "Groupe quitté."
#: partitioncloud/modules/groupe.py:156
msgid "You are not alone in this group."
msgstr "Vous n'êtes pas seul dans ce groupe."
#: partitioncloud/modules/groupe.py:167
msgid "Group deleted."
msgstr "Groupe supprimé."
#: partitioncloud/modules/groupe.py:189
msgid "You are not admin of this group."
msgstr "Vous n'êtes pas administrateur de ce groupe"
#: partitioncloud/modules/partition.py:59
msgid "You don't own this score."
msgstr "Cette partition ne vous appartient pas"
#: partitioncloud/modules/partition.py:72
msgid "Missing filename."
msgstr "Pas de nom de fichier"
#: partitioncloud/modules/partition.py:77
msgid "Unsupported file type."
msgstr "Extension de fichier non supportée"
#: partitioncloud/modules/partition.py:145
msgid "You are not allowed to edit this file."
msgstr "Vous n'êtes pas autorisé à modifier cette partition."
#: partitioncloud/modules/partition.py:156
#: partitioncloud/modules/partition.py:213
msgid "Missing author in request body (can be null)."
msgstr "Un nom d'auteur est requis (à minima nul)"
#: partitioncloud/modules/partition.py:158
#: partitioncloud/modules/partition.py:215
msgid "Missing lyrics (can be null)."
msgstr "Des paroles sont requises (à minima nulles)"
#: partitioncloud/modules/partition.py:181
#: partitioncloud/modules/partition.py:227
#, python-format
msgid "Successfully modified %(name)s"
msgstr "Partition %(name)s modifiée avec succès."
#: partitioncloud/modules/partition.py:242
msgid "You are not allowed to delete this score."
msgstr "Vous n'êtes pas autorisé à supprimer cette partition."
#: partitioncloud/modules/partition.py:250
msgid "Score deleted."
msgstr "Partition supprimée."
#: partitioncloud/modules/settings.py:40 partitioncloud/modules/settings.py:72
msgid "Missing user id."
msgstr "Identifiant d'utilisateur manquant."
#: partitioncloud/modules/settings.py:54
msgid "You are not allowed to delete your account."
msgstr "Vous n'êtes pas autorisé à supprimer votre compte."
#: partitioncloud/modules/settings.py:60
msgid "User successfully deleted."
msgstr "Utilisateur supprimé."
#: partitioncloud/modules/settings.py:86
msgid "Missing old password."
msgstr "Ancien mot de passe manquant."
#: partitioncloud/modules/settings.py:90
msgid "Incorrect password."
msgstr "Mot de passe incorrect."
#: partitioncloud/modules/settings.py:100
msgid "Password and its confirmation differ."
msgstr "Le mot de passe et sa confirmation diffèrent"
#: partitioncloud/modules/settings.py:104
msgid "Successfully updated password."
msgstr "Mot de passe mis à jour."
#: partitioncloud/templates/base.html:26
msgid "New Album"
msgstr "Créer un nouvel album"
#: partitioncloud/templates/base.html:28 partitioncloud/templates/base.html:39
#: partitioncloud/templates/groupe/index.html:10
#: partitioncloud/templates/partition/attachments.html:11
msgid "Name"
msgstr "Nom"
#: partitioncloud/templates/base.html:29 partitioncloud/templates/base.html:40
msgid "Create"
msgstr "Créer"
#: partitioncloud/templates/base.html:33
msgid "I want to create a collection of albums."
msgstr ""
"Je souhaite créer plusieurs albums et pouvoir tous les partager avec un "
"seul lien."
#: partitioncloud/templates/base.html:33
msgid "Create group"
msgstr "Créer un groupe"
#: partitioncloud/templates/base.html:37
msgid "Create new group"
msgstr "Créer un nouveau groupe"
#: partitioncloud/templates/base.html:64
msgid "Search"
msgstr "Rechercher"
#: partitioncloud/templates/base.html:66
msgid "Number of online searches"
msgstr "Nombre de recherches en ligne"
#: partitioncloud/templates/admin/index.html:23
#: partitioncloud/templates/base.html:74
#: partitioncloud/templates/partition/details.html:41
msgid "Albums"
msgstr "Albums"
#: partitioncloud/templates/base.html:78
msgid "New album"
msgstr "Créer un album"
#: partitioncloud/templates/base.html:95
msgid "No albums"
msgstr "Aucun album disponible"
#: partitioncloud/templates/base.html:114
msgid "No album available"
msgstr "Aucun album disponible"
#: partitioncloud/templates/base.html:128
msgid "Log in to see your albums"
msgstr "Connectez vous pour avoir accès à vos albums"
#: partitioncloud/templates/base.html:142
msgid "Log out"
msgstr "Déconnexion"
#: partitioncloud/templates/base.html:157
msgid "Admin Panel"
msgstr "Panneau admin"
#: partitioncloud/templates/auth/register.html:5
#: partitioncloud/templates/auth/register.html:20
#: partitioncloud/templates/base.html:169
msgid "Create account"
msgstr "Créer un compte"
#: partitioncloud/templates/auth/login.html:5
#: partitioncloud/templates/auth/login.html:10
#: partitioncloud/templates/base.html:171
#: partitioncloud/templates/launch.html:26
msgid "Log in"
msgstr "Se connecter"
#: partitioncloud/templates/launch.html:33
msgid ""
"PartitionCloud is an open-source score library server, to help you in all"
" your musical activities"
msgstr ""
"PartitionCloud est une bibliothèque de partitions open-source, pour vous "
"aider dans toutes vos activités musicales"
#: partitioncloud/templates/launch.html:38
msgid "Let's go !"
msgstr "C'est parti !"
#: partitioncloud/templates/launch.html:44
msgid "Check code"
msgstr "Voir le code"
#: partitioncloud/templates/launch.html:55
#, python-format
msgid ""
"This instance is used by %(users)s users with a total of %(scores)s "
"scores."
msgstr ""
"Cette instance est utilisée par %(users)s personnes avec un total de "
"%(scores)s partitions."
#: partitioncloud/templates/admin/index.html:5
msgid "Administration Panel"
msgstr "Panneau d'administration"
#: partitioncloud/templates/admin/index.html:9
msgid "New user"
msgstr "Nouvel utilisateur"
#: partitioncloud/templates/admin/index.html:12
msgid "See scores"
msgstr "Voir les partitions"
#: partitioncloud/templates/admin/index.html:15
msgid "See logs"
msgstr "Voir les logs"
#: partitioncloud/templates/admin/index.html:22
msgid "User"
msgstr "Utilisateur"
#: partitioncloud/templates/admin/index.html:24
msgid "Scores"
msgstr "Partitions"
#: partitioncloud/templates/admin/index.html:25
msgid "Admin privileges"
msgstr "Privilèges"
#: partitioncloud/templates/admin/logs.html:5
msgid "Logs"
msgstr "Logs"
#: partitioncloud/templates/admin/partitions.html:4
msgid "Scores list"
msgstr "Liste des partitions"
#: partitioncloud/templates/admin/partitions.html:31
#: partitioncloud/templates/albums/album.html:97
msgid "No available scores"
msgstr "Aucune partition disponible"
#: partitioncloud/templates/albums/add-partition.html:3
msgid "New score"
msgstr "Ajout de partition"
#: partitioncloud/templates/albums/album.html:12
msgid "Delete l'album"
msgstr "Supprimer l'album"
#: partitioncloud/templates/albums/album.html:13
#: partitioncloud/templates/albums/delete-album.html:6
msgid "Do you really want to delete this album?"
msgstr "Êtes vous sûr de vouloir supprimer cet album ?"
#: partitioncloud/templates/albums/album.html:16
#: partitioncloud/templates/albums/album.html:64
#: partitioncloud/templates/albums/delete-album.html:8
#: partitioncloud/templates/groupe/index.html:20
#: partitioncloud/templates/groupe/index.html:57
#: partitioncloud/templates/partition/delete.html:10
#: partitioncloud/templates/partition/details.html:92
#: partitioncloud/templates/partition/edit.html:63
#: partitioncloud/templates/settings/index.html:19
msgid "Delete"
msgstr "Supprimer"
#: partitioncloud/templates/albums/album.html:52
msgid "Add a score"
msgstr "Ajouter une partition"
#: partitioncloud/templates/albums/album.html:55
#: partitioncloud/templates/groupe/index.html:47
msgid "Join"
msgstr "Rejoindre"
#: partitioncloud/templates/albums/album.html:57
#: partitioncloud/templates/groupe/index.html:49
msgid "Quit"
msgstr "Quitter"
#: partitioncloud/templates/albums/album.html:59
#: partitioncloud/templates/groupe/index.html:51
msgid "Share"
msgstr "Partager"
#: partitioncloud/templates/albums/album.html:61
#: partitioncloud/templates/groupe/index.html:53
msgid "Download as zip"
msgstr "Télécharger le zip"
#: partitioncloud/templates/albums/delete-album.html:3
#: partitioncloud/templates/partition/delete.html:4
#, python-format
msgid "Delete %(name)s"
msgstr "Supprimer %(name)s"
#: partitioncloud/templates/albums/delete-album.html:11
#: partitioncloud/templates/partition/delete.html:13
msgid "Cancel"
msgstr "Annuler"
#: partitioncloud/templates/albums/index.html:3
msgid "Home"
msgstr "Accueil"
#: partitioncloud/templates/albums/index.html:10
#, python-format
msgid "Hi %(user_name)s !"
msgstr "Bonjour %(user_name)s !"
#: partitioncloud/templates/albums/index.html:11
msgid "No album selected"
msgstr "Aucun album sélectionné"
#: partitioncloud/templates/albums/search.html:5
#, python-format
msgid "Search results for \"%(query)s\""
msgstr "Résultats de la recherche pour \"%(query)s\""
#: partitioncloud/templates/albums/search.html:7
msgid "Results in current database"
msgstr "Résultats dans la recherche locale"
#: partitioncloud/templates/albums/search.html:38
#: partitioncloud/templates/albums/search.html:70
msgid "Add to album"
msgstr "Ajouter à un album"
#: partitioncloud/templates/albums/search.html:45
msgid "Online search results"
msgstr "Résultats de la recherche en ligne"
#: partitioncloud/templates/albums/search.html:77
msgid ""
"No results available. Try to tweak your query or increase the amount of "
"online searches."
msgstr ""
"Aucun résultat disponible. Essayez d'affiner votre recherche ou "
"d'augmenter le nombre de résultats en ligne"
#: partitioncloud/templates/auth/login.html:8
#: partitioncloud/templates/auth/register.html:18
msgid "Username"
msgstr "Nom d'utilisateur"
#: partitioncloud/templates/auth/login.html:9
#: partitioncloud/templates/auth/register.html:19
msgid "Password"
msgstr "Mot de passe"
#: partitioncloud/templates/auth/register.html:10
msgid "Add to album:"
msgstr "Ajouter à un album:"
#: partitioncloud/templates/auth/register.html:12
msgid "None"
msgstr "Aucun"
#: partitioncloud/templates/components/add_partition.html:1
#, python-format
msgid "Add a score to %(name)s"
msgstr "Ajouter une partition à %(name)s"
#: partitioncloud/templates/components/add_partition.html:4
msgid "title"
msgstr "titre"
#: partitioncloud/templates/components/add_partition.html:5
msgid "author"
msgstr "auteur"
#: partitioncloud/templates/components/add_partition.html:6
msgid "lyrics"
msgstr "paroles"
#: partitioncloud/templates/components/add_partition.html:16
#: partitioncloud/templates/groupe/index.html:11
#: partitioncloud/templates/partition/attachments.html:17
msgid "Add"
msgstr "Ajouter"
#: partitioncloud/templates/components/input_file.html:4
msgid "Your file is selected."
msgstr "Fichier sélectionné."
#: partitioncloud/templates/components/input_file.html:5
msgid "Select or drag & drop your file"
msgstr "Sélectionner ou déposer un fichier"
#: partitioncloud/templates/groupe/index.html:8
#, python-format
msgid "Add an album to group %(name)s"
msgstr "Ajouter un album au groupe %(name)s"
#: partitioncloud/templates/groupe/index.html:16
msgid "Delete group"
msgstr "Supprimer le groupe"
#: partitioncloud/templates/groupe/index.html:17
msgid "Do you really want to delete this group and the albums it contains?"
msgstr ""
"Ê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)."
#: partitioncloud/templates/groupe/index.html:56
msgid "Add an album"
msgstr "Ajouter un album"
#: partitioncloud/templates/groupe/index.html:77
msgid "Create one"
msgstr "En créer un"
#: partitioncloud/templates/groupe/index.html:80
#, python-format
msgid "No available album. %(create)s"
msgstr "Aucun album disponible. %(create)s"
#: partitioncloud/templates/partition/attachments.html:5
#, python-format
msgid "Attachments of %(name)s"
msgstr "Attachments de %(name)s"
#: partitioncloud/templates/partition/attachments.html:9
#, python-format
msgid "Add an attachment to %(name)s"
msgstr "Ajouter un attachment à %(name)s"
#: partitioncloud/templates/partition/attachments.html:26
msgid ""
"No pdf viewer available in this browser.\n"
" You can use Firefox on Android."
msgstr ""
"Impossible d'afficher le pdf dans ce navigateur.\n"
" Il est conseillé d'utiliser Firefox sur Android."
#: partitioncloud/templates/partition/attachments.html:50
msgid "JavaScript is mandatory to read MIDI files"
msgstr "JavaScript est nécessaire pour lire les fichiers MIDI"
#: partitioncloud/templates/partition/attachments.html:64
msgid "Add an attachment"
msgstr "Ajouter un attachment"
#: partitioncloud/templates/partition/delete.html:8
msgid "Do you really want to delete this score?"
msgstr "Êtes vous sûr de vouloir supprimer cette partition ?"
#: partitioncloud/templates/partition/details.html:4
#, python-format
msgid "Details of \"%(name)s\""
msgstr "Détails de \"%(name)s\""
#: partitioncloud/templates/partition/details.html:12
msgid "Added by"
msgstr "Responsable de l'ajout"
#: partitioncloud/templates/partition/details.html:23
msgid "Unknown"
msgstr "Inconnu"
#: partitioncloud/templates/partition/details.html:29
msgid "Type"
msgstr "Type d'ajout"
#: partitioncloud/templates/partition/details.html:52
#: partitioncloud/templates/partition/edit.html:13
msgid "File"
msgstr "Fichier"
#: partitioncloud/templates/partition/details.html:64
#: partitioncloud/templates/partition/details.html:65
#: partitioncloud/templates/partition/edit.html:35
#: partitioncloud/templates/partition/edit.html:36
msgid "Title"
msgstr "Titre"
#: partitioncloud/templates/partition/details.html:68
#: partitioncloud/templates/partition/details.html:69
#: partitioncloud/templates/partition/edit.html:39
#: partitioncloud/templates/partition/edit.html:40
msgid "Author"
msgstr "Auteur"
#: partitioncloud/templates/partition/details.html:72
#: partitioncloud/templates/partition/details.html:73
#: partitioncloud/templates/partition/edit.html:43
#: partitioncloud/templates/partition/edit.html:44
msgid "Lyrics"
msgstr "Paroles"
#: partitioncloud/templates/partition/details.html:76
#: partitioncloud/templates/partition/edit.html:47
msgid "Attachments"
msgstr "Pièces jointes"
#: partitioncloud/templates/partition/details.html:81
#: partitioncloud/templates/partition/edit.html:52
#, python-format
msgid "Yes, %(number)s"
msgstr "Oui, %(number)s"
#: partitioncloud/templates/partition/details.html:83
#: partitioncloud/templates/partition/edit.html:54
msgid "Add one"
msgstr "En rajouter"
#: partitioncloud/templates/partition/details.html:89
#: partitioncloud/templates/partition/edit.html:60
msgid "Update"
msgstr "Mettre à jour"
#: partitioncloud/templates/partition/edit.html:6
#, python-format
msgid "Modify \"%(name)s\""
msgstr "Modifier \"%(name)s\""
#: partitioncloud/templates/partition/edit.html:27
msgid "Source"
msgstr "Source"
#: partitioncloud/templates/settings/index.html:3
msgid "Settings"
msgstr "Paramètres"
#: partitioncloud/templates/settings/index.html:8
#: partitioncloud/templates/settings/index.html:39
#: partitioncloud/templates/settings/index.html:40
msgid "Delete account"
msgstr "Supprimer le compte"
#: partitioncloud/templates/settings/index.html:15
#, python-format
msgid ""
"Do you really want to delete %(username)s's account ? This action is "
"%(irreversible_bold)s."
msgstr ""
"Souhaitez-vous vraiment supprimer le compte de %(username)s ? Cette "
"action est %(irreversible_bold)s."
#: partitioncloud/templates/settings/index.html:27
#, python-format
msgid "User %(username)s has %(album_count)s albums"
msgstr "L'utilisateur %(username)s a %(album_count)s albums"
#: partitioncloud/templates/settings/index.html:29
msgid "Change password"
msgstr "Changer de mot de passe"
#: partitioncloud/templates/settings/index.html:31
msgid "old password"
msgstr "ancien mot de passe"
#: partitioncloud/templates/settings/index.html:33
msgid "new password"
msgstr "nouveau mot de passe"
#: partitioncloud/templates/settings/index.html:34
msgid "confirm new password"
msgstr "confirmer le nouveau mot de passe"
#: partitioncloud/templates/settings/index.html:36
msgid "confirm"
msgstr "confirmer"

View File

@ -1,4 +1,7 @@
flask
flask-babel
google
colorama
qrcode
pypdf
qrcode
unidecode

View File

@ -2,4 +2,4 @@
Config parameters, shared between files
"""
instance = "instance"
instance = "instance"

View File

@ -1,6 +1,9 @@
import os
import sys
import random
import string
import sqlite3
from colorama import Fore, Style
from . import config
@ -39,3 +42,29 @@ def new_uuid():
def format_uuid(uuid):
"""Format old uuid4 format"""
return uuid.upper()[:6]
def install_package(package):
print(f"\nThe following python package needs to be installed: {Style.BRIGHT}{Fore.YELLOW}{package}{Style.RESET_ALL}")
print(f"1. Install with {Style.BRIGHT}pip{Style.RESET_ALL} (automatic)")
print(f"2. Install manually (other package manager)")
option = input("Select an option: ")
try:
choice = int(option)
if choice == 1:
return_value = os.system(f"pip install {package} -qq")
if return_value == 0:
return
print(f"{Fore.RED}Installation with pip failed{Style.RESET_ALL}")
sys.exit(return_value)
elif choice == 2:
input("Install via you preferred option, and hit [Enter] when done")
return
except ValueError:
pass
print(f"{Fore.RED}Please enter a valid option{Style.RESET_ALL}")
return install_package(package)

View File

@ -54,7 +54,7 @@ def add_attachments():
def install_colorama():
os.system("pip install colorama -qq")
utils.install_package("colorama")
"""
@ -144,7 +144,7 @@ def base_url_parameter_added():
def install_qrcode():
os.system("pip install qrcode -qq")
utils.install_package("qrcode")
"""
@ -171,3 +171,27 @@ def move_thumbnails():
os.makedirs(os.path.join(config.instance, "cache", "thumbnails"), exist_ok=True)
os.makedirs(os.path.join(config.instance, "cache", "search-thumbnails"), exist_ok=True)
"""
v1.7.*
"""
def install_babel():
utils.install_package("flask-babel")
"""
v1.8.*
"""
def install_pypdf():
utils.install_package("pypdf")
"""
v1.10.*
"""
def install_unidecode():
utils.install_package("unidecode")

View File

@ -34,7 +34,10 @@ hooks = [
),
("v1.4.1", [("Install qrcode", v1_hooks.install_qrcode)]),
("v1.5.0", [("Move to instance directory", v1_hooks.move_instance)]),
("v1.5.1", [("Move thumbnails", v1_hooks.move_thumbnails)])
("v1.5.1", [("Move thumbnails", v1_hooks.move_thumbnails)]),
("v1.7.0", [("Install babel", v1_hooks.install_babel)]),
("v1.8.2", [("Install pypdf", v1_hooks.install_pypdf)]),
("v1.10.3", [("Install unidecode", v1_hooks.install_unidecode)]),
]
@ -199,11 +202,18 @@ if __name__ == "__main__":
"--restore",
help="restore from specific version backup, will not apply any hook (vx.y.z)",
)
parser.add_argument(
"-b",
"--backup",
help="backup current version, without running any hooks",
)
args = parser.parse_args()
config.instance = os.path.abspath(args.instance)
if args.restore is None:
migrate(args.current, args.target, skip_backup=args.skip_backup)
else:
if args.restore is not None:
restore(args.restore)
elif args.backup is not None:
backup_instance(args.backup, verbose=True)
else:
migrate(args.current, args.target, skip_backup=args.skip_backup)