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 # cache
**/__pycache__ **/__pycache__
# translations
**.mo
**.pot
# config # config
.vscode/ .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 - Thème sombre
- dashboard administrateur: gestion de tous les albums, partitions et utilisateurs - 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 - [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 ## 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 - 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
### 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 Installer le serveur
```bash ```bash
# Clone this repo # Clone this repo
@ -27,6 +47,7 @@ git clone https://github.com/partitioncloud/partitioncloud-server.git
cd partitioncloud-server cd partitioncloud-server
# Install dependencies # Install dependencies
pip install -r requirements.txt pip install -r requirements.txt
pybabel compile -d partitioncloud/translations
# Create database and folders # Create database and folders
./make.sh init ./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) ![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 ## TODO
- [ ] Modifier son mot de passe - [x] Modifier son mot de passe
- [ ] Supprimer un utilisateur - [x] Supprimer un utilisateur
- [ ] Ajouter config:DISABLE_DARK_MODE - [ ] Ajouter config:DISABLE_DARK_MODE
- [x] Ajouter config:DISABLE_REGISTER - [x] Ajouter config:DISABLE_REGISTER
- [ ] Ajouter config:ONLINE_SEARCH_BASE_QUERY pour la recherche google, actuellement 'filetype:pdf partition' - [ ] 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 registration of new users via /auth/register (they can still be added by root)
DISABLE_REGISTER=False 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) # Front URL of the application (for QRCodes generation)
BASE_URL="http://localhost:5000" 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, # Keep in mind that this config option can only be loaded from default_config.py,
# as the custom config is stored in $INSTANCE_PATH/ # as the custom config is stored in $INSTANCE_PATH/
INSTANCE_PATH="instance" 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

35
make.sh
View File

@ -18,6 +18,7 @@ init () {
printf "Souhaitez vous supprimer la base de données existante ? [y/n] " printf "Souhaitez vous supprimer la base de données existante ? [y/n] "
read -r CONFIRMATION read -r CONFIRMATION
[[ $CONFIRMATION == y ]] || exit 1 [[ $CONFIRMATION == y ]] || exit 1
rm "$INSTANCE_PATH/partitioncloud.sqlite"
fi fi
sqlite3 "$INSTANCE_PATH/partitioncloud.sqlite" '.read partitioncloud/schema.sql' sqlite3 "$INSTANCE_PATH/partitioncloud.sqlite" '.read partitioncloud/schema.sql'
echo "Base de données créé" echo "Base de données créé"
@ -25,30 +26,54 @@ init () {
echo "Utilisateur root:root ajouté" 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 () { start () {
pybabel compile -d partitioncloud/translations/
flask run --port=$PORT flask run --port=$PORT
} }
production () { production () {
FLASK_APP=partitioncloud /usr/bin/gunicorn \ pybabel compile -d partitioncloud/translations/
FLASK_APP=partitioncloud gunicorn \
wsgi:app \ wsgi:app \
--bind 0.0.0.0:$PORT --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 () { usage () {
echo "Usage:" echo "Usage:"
echo -e "\t$0 init" echo -e "\t$0 init"
echo -e "\t$0 start" 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 # Import config
source "default_config.py" load_config "default_config.py"
[[ ! -x "$INSTANCE_PATH/config.py" ]] && source "$INSTANCE_PATH/config.py"
if test -f "instance/config.py"; then
load_config "instance/config.py"
fi
$1 ${*:2} # Call the function $1 ${*:2} # Call the function
else else
usage usage
echo $(type "$1") echo $RESULT
exit 1 exit 1
fi fi

View File

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

View File

@ -2,7 +2,8 @@
""" """
Admin Panel 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 .db import get_db
from .auth import admin_required from .auth import admin_required
@ -18,7 +19,6 @@ def index():
Admin panel home page Admin panel home page
""" """
current_user = User(user_id=session.get("user_id")) 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() db = get_db()
users_id = db.execute( users_id = db.execute(
""" """
@ -35,3 +35,46 @@ def index():
users=users, users=users,
user=current_user 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 Albums module
""" """
import os import os
import pypdf
import shutil import shutil
from uuid import uuid4 from uuid import uuid4
from typing import TypeVar from typing import TypeVar
from flask import (Blueprint, abort, flash, redirect, render_template, 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 .auth import login_required
from .db import get_db from .db import get_db
from .utils import User, Album from .utils import User, Album
from . import search, utils from . import search, utils, logging
bp = Blueprint("albums", __name__, url_prefix="/albums") bp = Blueprint("albums", __name__, url_prefix="/albums")
@ -37,15 +40,21 @@ def search_page():
Résultats de recherche Résultats de recherche
""" """
if "query" not in request.form or request.form["query"] == "": if "query" not in request.form or request.form["query"] == "":
flash("Aucun terme de recherche spécifié.") flash(_("Missing search query"))
return redirect("/albums") return redirect("/albums")
user = User(user_id=session.get("user_id"))
query = request.form["query"] query = request.form["query"]
nb_queries = abs(int(request.form["nb-queries"])) nb_queries = abs(int(request.form["nb-queries"]))
search.flush_cache(current_app.instance_path) 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 nb_queries > 0:
if user.access_level != 1: if user.access_level != 1:
@ -80,7 +89,7 @@ def get_album(uuid):
except LookupError: except LookupError:
return abort(404) 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")) user = User(user_id=session.get("user_id"))
partitions = album.get_partitions() partitions = album.get_partitions()
if user.id is None: if user.id is None:
@ -106,6 +115,30 @@ def qr_code(uuid):
return utils.get_qrcode(f"/albums/{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"]) @bp.route("/create-album", methods=["POST"])
@login_required @login_required
def create_album_req(): def create_album_req():
@ -116,8 +149,10 @@ def create_album_req():
db = get_db() db = get_db()
error = None error = None
user = User(user_id=session["user_id"])
if not name or name.strip() == "": 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: if error is None:
uuid = utils.create_album(name) uuid = utils.create_album(name)
@ -131,6 +166,8 @@ def create_album_req():
) )
db.commit() db.commit()
logging.log([album.name, album.uuid, user.username], logging.LogEntry.NEW_ALBUM)
if "response" in request.args and request.args["response"] == "json": if "response" in request.args and request.args["response"] == "json":
return { return {
"status": "ok", "status": "ok",
@ -152,10 +189,10 @@ def join_album(uuid):
try: try:
user.join_album(uuid) user.join_album(uuid)
except LookupError: except LookupError:
flash("Cet album n'existe pas.") flash(_("This album does not exist."))
return redirect(request.referrer) return redirect(request.referrer)
flash("Album ajouté à la collection.") flash(_("Album added to collection."))
return redirect(request.referrer) return redirect(request.referrer)
@ -167,17 +204,18 @@ def quit_album(uuid):
""" """
user = User(user_id=session.get("user_id")) user = User(user_id=session.get("user_id"))
album = Album(uuid=uuid) album = Album(uuid=uuid)
users = album.get_users() users = album.get_users()
if user.id not in [u["id"] for u in users]: if user.id not in users:
flash("Vous ne faites pas partie de cet album") flash(_("You are not a member of this album"))
return redirect(request.referrer) return redirect(request.referrer)
if len(users) == 1: 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") return redirect(f"/albums/{uuid}#delete")
user.quit_album(uuid) user.quit_album(uuid)
flash("Album quitté.") flash(_("Album quitted."))
return redirect("/albums") return redirect("/albums")
@ -196,9 +234,9 @@ def delete_album(uuid):
error = None error = None
users = album.get_users() users = album.get_users()
if len(users) > 1: if len(users) > 1:
error = "Vous n'êtes pas seul dans cet album." error = _("You are not alone in this album.")
elif len(users) == 1 and users[0]["id"] != user.id: elif len(users) == 1 and users[0] != user.id:
error = "Vous ne possédez pas cet album." error = _("You don't own this album.")
if user.access_level == 1: if user.access_level == 1:
error = None error = None
@ -209,7 +247,7 @@ def delete_album(uuid):
album.delete(current_app.instance_path) album.delete(current_app.instance_path)
flash("Album supprimé.") flash(_("Album deleted."))
return redirect("/albums") return redirect("/albums")
@ -217,7 +255,7 @@ def delete_album(uuid):
@login_required @login_required
def add_partition(album_uuid): def add_partition(album_uuid):
""" """
Ajouter une partition à un album (par upload) Ajouter une partition à un album (nouveau fichier)
""" """
T = TypeVar("T") T = TypeVar("T")
def get_opt_string(dictionary: dict[T, str], key: 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 source = "upload" # source type: upload, unknown or url
if (not user.is_participant(album.uuid)) and (user.access_level != 1): 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) return redirect(request.referrer)
error = None error = None
if "name" not in request.form: 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: 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: elif "file" not in request.files:
partition_type = "uuid" partition_type = "uuid"
search_uuid = request.form["partition-uuid"] search_uuid = request.form["partition-uuid"]
@ -252,12 +290,18 @@ def add_partition(album_uuid):
(search_uuid,) (search_uuid,)
).fetchone() ).fetchone()
if data is None: if data is None:
error = "Les résultats de la recherche ont expiré." error = _("Search results expired")
else: else:
source = data["url"] source = data["url"]
else: else:
partition_type = "file" 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: if error is not None:
flash(error) flash(error)
return redirect(request.referrer) return redirect(request.referrer)
@ -265,6 +309,7 @@ def add_partition(album_uuid):
author = get_opt_string(request.form, "author") author = get_opt_string(request.form, "author")
body = get_opt_string(request.form, "body") body = get_opt_string(request.form, "body")
partition_uuid: str
while True: while True:
try: try:
partition_uuid = str(uuid4()) partition_uuid = str(uuid4())
@ -307,12 +352,17 @@ def add_partition(album_uuid):
except db.IntegrityError: except db.IntegrityError:
pass pass
logging.log(
[request.form["name"], partition_uuid, user.username],
logging.LogEntry.NEW_PARTITION
)
if "response" in request.args and request.args["response"] == "json": if "response" in request.args and request.args["response"] == "json":
return { return {
"status": "ok", "status": "ok",
"uuid": partition_uuid "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}") return redirect(f"/albums/{album.uuid}")
@ -320,19 +370,19 @@ def add_partition(album_uuid):
@login_required @login_required
def add_partition_from_search(): 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")) user = User(user_id=session.get("user_id"))
error = None error = None
if "album-uuid" not in request.form: 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: 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: 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): 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: if error is not None:
flash(error) flash(error)
@ -352,9 +402,9 @@ def add_partition_from_search():
if data is None: if data is None:
album.add_partition(request.form["partition-uuid"]) album.add_partition(request.form["partition-uuid"])
flash("Partition ajoutée.") flash(_("Score added"))
else: else:
flash("Partition déjà dans l'album.") flash(_("Score is already in the album."))
return redirect(f"/albums/{album.uuid}") return redirect(f"/albums/{album.uuid}")
@ -366,5 +416,5 @@ def add_partition_from_search():
user=user user=user
) )
flash("Type de partition inconnu.") flash(_("Unknown score type."))
return redirect("/albums") return redirect("/albums")

View File

@ -7,11 +7,13 @@ from typing import Optional
from flask import (Blueprint, flash, g, redirect, render_template, from flask import (Blueprint, flash, g, redirect, render_template,
request, session, url_for, current_app) request, session, url_for, current_app)
from flask_babel import _
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
from .db import get_db from .db import get_db
from .utils import User from .utils import User
from . import logging
bp = Blueprint("auth", __name__, url_prefix="/auth") bp = Blueprint("auth", __name__, url_prefix="/auth")
@ -22,7 +24,7 @@ def login_required(view):
@functools.wraps(view) @functools.wraps(view)
def wrapped_view(**kwargs): def wrapped_view(**kwargs):
if g.user is None: 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 redirect(url_for("auth.login"))
return view(**kwargs) return view(**kwargs)
@ -49,12 +51,12 @@ def admin_required(view):
@functools.wraps(view) @functools.wraps(view)
def wrapped_view(**kwargs): def wrapped_view(**kwargs):
if g.user is None: 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 redirect(url_for("auth.login"))
user = User(user_id=session.get("user_id")) user = User(user_id=session.get("user_id"))
if user.access_level != 1: if user.access_level != 1:
flash("Droits insuffisants.") flash(_("Missing rights."))
return redirect("/albums") return redirect("/albums")
return view(**kwargs) return view(**kwargs)
@ -80,9 +82,9 @@ def create_user(username: str, password: str) -> Optional[str]:
"""Adds a new user to the database""" """Adds a new user to the database"""
error = None error = None
if not username: if not username:
error = "Un nom d'utilisateur est requis." error = _("Missing username.")
elif not password: elif not password:
error = "Un mot de passe est requis." error = _("Missing password.")
try: try:
db = get_db() db = get_db()
@ -95,7 +97,7 @@ def create_user(username: str, password: str) -> Optional[str]:
except db.IntegrityError: except db.IntegrityError:
# The username was already taken, which caused the # The username was already taken, which caused the
# commit to fail. Show a validation error. # 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 return error # may be None
@ -108,7 +110,7 @@ def register():
password for security. password for security.
""" """
if current_app.config["DISABLE_REGISTER"]: 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")) return redirect(url_for("auth.login"))
if request.method == "POST": if request.method == "POST":
@ -120,7 +122,15 @@ def register():
if error is not None: if error is not None:
flash(error) flash(error)
else: 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") return render_template("auth/register.html")
@ -139,12 +149,16 @@ def login():
).fetchone() ).fetchone()
if (user is None) or not check_password_hash(user["password"], password): 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: if error is None:
# store the user id in a new session and return to the index # store the user id in a new session and return to the index
session.clear() session.clear()
session["user_id"] = user["id"] session["user_id"] = user["id"]
logging.log([username], logging.LogEntry.LOGIN)
return redirect(url_for("albums.index")) return redirect(url_for("albums.index"))
flash(error) flash(error)
@ -156,4 +170,4 @@ def login():
def logout(): def logout():
"""Clear the current session, including the stored user id.""" """Clear the current session, including the stored user id."""
session.clear() session.clear()
return redirect(url_for("auth.login")) return redirect("/")

View File

@ -2,6 +2,10 @@
Classe Album Classe Album
""" """
import os import os
import io
import zipfile
from werkzeug.utils import secure_filename
from ..db import get_db from ..db import get_db
from ..utils import new_uuid from ..utils import new_uuid
@ -47,11 +51,11 @@ class Album():
def get_users(self, force_reload=False): 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: if self.users is None or force_reload:
db = get_db() db = get_db()
self.users = db.execute( data = db.execute(
""" """
SELECT * FROM user SELECT * FROM user
JOIN contient_user ON user_id = user.id JOIN contient_user ON user_id = user.id
@ -60,6 +64,7 @@ class Album():
""", """,
(self.uuid,) (self.uuid,)
).fetchall() ).fetchall()
self.users = [i["id"] for i in data]
return self.users return self.users
def get_partitions(self): def get_partitions(self):
@ -166,6 +171,23 @@ class Album():
db.commit() 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: def create(name: str) -> str:
"""Créer un nouvel album""" """Créer un nouvel album"""
db = get_db() db = get_db()

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 ..db import get_db
from .album import Album from .album import Album
@ -66,13 +75,13 @@ class Groupe():
album.delete(instance_path) 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 Renvoie les data["id"] des utilisateurs liés au groupe
TODO: uniformiser le tout
""" """
if self.users is None or force_reload:
db = get_db() db = get_db()
return db.execute( data = db.execute(
""" """
SELECT * FROM user SELECT * FROM user
JOIN groupe_contient_user ON user_id = user.id JOIN groupe_contient_user ON user_id = user.id
@ -81,6 +90,8 @@ class Groupe():
""", """,
(self.id,) (self.id,)
).fetchall() ).fetchall()
self.users = [i["id"] for i in data]
return self.users
def get_albums(self, force_reload=False): def get_albums(self, force_reload=False):
""" """
@ -116,3 +127,36 @@ class Groupe():
(self.id,) (self.id,)
).fetchall() ).fetchall()
return [i["id"] for i in data] 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() 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): def get_user(self):
db = get_db() db = get_db()
user = db.execute( user = db.execute(

View File

@ -1,4 +1,5 @@
from flask import current_app from flask import current_app
from werkzeug.security import generate_password_hash
from ..db import get_db from ..db import get_db
from .album import Album from .album import Album
@ -28,9 +29,11 @@ class User():
def __init__(self, user_id=None, name=None): def __init__(self, user_id=None, name=None):
self.id = user_id self.id = user_id
self.username = name self.username = name
self.password = None
self.albums = None self.albums = None
self.groupes = None self.groupes = None
self.partitions = None self.partitions = None
self.accessible_partitions = None
self.max_queries = 0 self.max_queries = 0
db = get_db() db = get_db()
@ -58,6 +61,7 @@ class User():
self.id = data["id"] self.id = data["id"]
self.username = data["username"] self.username = data["username"]
self.password = data["password"]
self.access_level = data["access_level"] self.access_level = data["access_level"]
self.color = self.get_color() self.color = self.get_color()
if self.access_level == 1: if self.access_level == 1:
@ -166,6 +170,44 @@ class User():
).fetchall() ).fetchall()
return self.partitions 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): def join_album(self, album_uuid):
db = get_db() db = get_db()
album = Album(uuid=album_uuid) album = Album(uuid=album_uuid)
@ -198,12 +240,10 @@ class User():
db.execute( db.execute(
""" """
DELETE FROM contient_user DELETE FROM contient_user
JOIN album WHERE album_id IN (SELECT id FROM album WHERE uuid = ?)
ON album.id = album_id AND user_id = ?
WHERE user_id = ?
AND album.uuid = ?
""", """,
(self.id, album_uuid) (album_uuid, self.id)
) )
db.commit() db.commit()
@ -221,6 +261,49 @@ class User():
) )
db.commit() 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): def get_color(self):
if len(colors) == 0: if len(colors) == 0:

View File

@ -3,12 +3,15 @@
Groupe module Groupe module
""" """
from flask import (Blueprint, abort, flash, redirect, render_template, 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 .auth import login_required
from .db import get_db from .db import get_db
from .utils import User, Album, Groupe from .utils import User, Album, Groupe
from . import utils from . import utils
from . import logging
bp = Blueprint("groupe", __name__, url_prefix="/groupe") bp = Blueprint("groupe", __name__, url_prefix="/groupe")
@ -32,7 +35,7 @@ def get_groupe(uuid):
except LookupError: except LookupError:
return abort(404) 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() groupe.get_albums()
user = User(user_id=session.get("user_id")) user = User(user_id=session.get("user_id"))
@ -63,8 +66,10 @@ def create_groupe():
db = get_db() db = get_db()
error = None error = None
user = User(user_id=session["user_id"])
if not name or name.strip() == "": 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: if error is None:
while True: while True:
@ -93,6 +98,8 @@ def create_groupe():
except db.IntegrityError: except db.IntegrityError:
pass pass
logging.log([name, uuid, user.username], logging.LogEntry.NEW_GROUPE)
if "response" in request.args and request.args["response"] == "json": if "response" in request.args and request.args["response"] == "json":
return { return {
"status": "ok", "status": "ok",
@ -111,10 +118,10 @@ def join_groupe(uuid):
try: try:
user.join_groupe(uuid) user.join_groupe(uuid)
except LookupError: except LookupError:
flash("Ce groupe n'existe pas.") flash(_("Unknown group."))
return redirect(f"/groupe/{uuid}") return redirect(f"/groupe/{uuid}")
flash("Groupe ajouté à la collection.") flash(_("Group added to collection."))
return redirect(f"/groupe/{uuid}") return redirect(f"/groupe/{uuid}")
@ -124,16 +131,21 @@ def quit_groupe(uuid):
user = User(user_id=session.get("user_id")) user = User(user_id=session.get("user_id"))
groupe = Groupe(uuid=uuid) groupe = Groupe(uuid=uuid)
users = groupe.get_users() users = groupe.get_users()
if user.id not in [u["id"] for u in users]: if user.id not in users:
flash("Vous ne faites pas partie de ce groupe") flash(_("You are not a member of this group."))
return redirect(f"/groupe/{uuid}") return redirect(f"/groupe/{uuid}")
if len(users) == 1: 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") return redirect(f"/groupe/{uuid}#delete")
user.quit_groupe(groupe.uuid) 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") return redirect("/albums")
@ -144,9 +156,8 @@ def delete_groupe(uuid):
user = User(user_id=session.get("user_id")) user = User(user_id=session.get("user_id"))
error = None error = None
users = groupe.get_users() if len(groupe.get_users()) > 1:
if len(users) > 1: error = _("You are not alone in this group.")
error = "Vous n'êtes pas seul dans ce groupe."
if user.access_level == 1 or user.id not in groupe.get_admins(): if user.access_level == 1 or user.id not in groupe.get_admins():
error = None error = None
@ -157,7 +168,7 @@ def delete_groupe(uuid):
groupe.delete(current_app.instance_path) groupe.delete(current_app.instance_path)
flash("Groupe supprimé.") flash(_("Group deleted."))
return redirect("/albums") return redirect("/albums")
@ -176,10 +187,10 @@ def create_album_req(groupe_uuid):
error = None error = None
if not name or name.strip() == "": 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(): if user.id not in groupe.get_admins() and user.access_level != 1:
error ="Vous n'êtes pas administrateur de ce groupe" error = _("You are not admin of this group.")
if error is None: if error is None:
uuid = utils.create_album(name) uuid = utils.create_album(name)
@ -194,6 +205,8 @@ def create_album_req(groupe_uuid):
) )
db.commit() db.commit()
logging.log([album.name, album.uuid, user.username], logging.LogEntry.NEW_ALBUM)
if "response" in request.args and request.args["response"] == "json": if "response" in request.args and request.args["response"] == "json":
return { return {
"status": "ok", "status": "ok",
@ -232,7 +245,7 @@ def get_album(groupe_uuid, album_uuid):
user = User(user_id=session.get("user_id")) user = User(user_id=session.get("user_id"))
# List of users without duplicate # 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] album.users = [User(user_id=id) for id in users_id]
partitions = album.get_partitions() 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") @bp.route("/<groupe_uuid>/<album_uuid>/qr")
def groupe_qr_code(groupe_uuid, album_uuid): def groupe_qr_code(groupe_uuid, album_uuid):
return utils.get_qrcode(f"/groupe/{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 Partition module
""" """
import os import os
import pypdf
from uuid import uuid4 from uuid import uuid4
from flask import (Blueprint, abort, send_file, render_template, from flask import (Blueprint, abort, send_file, render_template,
request, redirect, flash, session, current_app) request, redirect, flash, session, current_app)
from flask_babel import _
from .db import get_db from .db import get_db
from .auth import login_required, admin_required from .auth import login_required, admin_required
@ -54,12 +56,12 @@ def add_attachment(uuid):
user = User(user_id=session.get("user_id")) user = User(user_id=session.get("user_id"))
if user.id != partition.user_id and user.access_level != 1: 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) return redirect(request.referrer)
error = None # À mettre au propre error = None # À mettre au propre
if "file" not in request.files: if "file" not in request.files:
error = "Aucun fichier n'a été fourni." error = _("Missing file")
else: else:
if "name" not in request.form or request.form["name"] == "": if "name" not in request.form or request.form["name"] == "":
name = ".".join(request.files["file"].filename.split(".")[:-1]) name = ".".join(request.files["file"].filename.split(".")[:-1])
@ -67,12 +69,12 @@ def add_attachment(uuid):
name = request.form["name"] name = request.form["name"]
if name == "": if name == "":
error = "Pas de nom de fichier" error = _("Missing filename.")
else: else:
filename = request.files["file"].filename filename = request.files["file"].filename
ext = filename.split(".")[-1] ext = filename.split(".")[-1]
if ext not in ["mid", "mp3"]: if ext not in ["mid", "mp3"]:
error = "Extension de fichier non supportée" error = _("Unsupported file type.")
if error is not None: if error is not None:
flash(error) flash(error)
@ -140,7 +142,7 @@ def edit(uuid):
user = User(user_id=session.get("user_id")) user = User(user_id=session.get("user_id"))
if user.access_level != 1 and partition.user_id != 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") return redirect("/albums")
if request.method == "GET": if request.method == "GET":
@ -149,23 +151,34 @@ def edit(uuid):
error = None error = None
if "name" not in request.form or request.form["name"].strip() == "": 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: 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: elif "body" not in request.form:
error = "Des paroles sont requises (à minima nulles)" error = _("Missing lyrics (can be null).")
if error is not None: if error is not None:
flash(error) flash(error)
return redirect(f"/partition/{ uuid }/edit") 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( partition.update(
name=request.form["name"], name=request.form["name"],
author=request.form["author"], author=request.form["author"],
body=request.form["body"] 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") return redirect("/albums")
@ -195,11 +208,11 @@ def details(uuid):
error = None error = None
if "name" not in request.form or request.form["name"].strip() == "": 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: 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: elif "body" not in request.form:
error = "Des paroles sont requises (à minima nulles)" error = _("Missing lyrics (can be null).")
if error is not None: if error is not None:
flash(error) flash(error)
@ -211,7 +224,7 @@ def details(uuid):
body=request.form["body"] 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") return redirect("/albums")
@ -226,7 +239,7 @@ def delete(uuid):
user = User(user_id=session.get("user_id")) user = User(user_id=session.get("user_id"))
if user.access_level != 1 and partition.user_id != 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") return redirect("/albums")
if request.method == "GET": if request.method == "GET":
@ -234,7 +247,7 @@ def delete(uuid):
partition.delete(current_app.instance_path) partition.delete(current_app.instance_path)
flash("Partition supprimée.") flash(_("Score deleted."))
return redirect("/albums") return redirect("/albums")

View File

@ -8,7 +8,9 @@ import threading
import socket import socket
import os import os
import pypdf
import googlesearch import googlesearch
from unidecode import unidecode
from .db import get_db 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 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): def score_attribution(partition):
score = 0 score = 0
for word in query_words: for word in query_words:
if word != "": if word != "":
if word in partition["name"].lower(): if word in unidecode(partition["name"]).lower():
score += 6 score += 6
elif word in partition["author"].lower(): elif word in unidecode(partition["author"]).lower():
score += 4 score += 4
elif word in partition["body"].lower(): elif word in unidecode(partition["body"]).lower():
score += 2 score += 2
else: else:
score -= 1 score -= 6
for word in partition["name"].split(" "): for word in unidecode(partition["name"]).split():
if word != "" and word.lower() not in query_words: if word != "" and word.lower() not in query_words:
score -= 1 score -= 1
return score return score
@ -52,12 +54,17 @@ def local_search(query, partitions):
def download_search_result(element, instance_path): def download_search_result(element, instance_path):
uuid = element["uuid"] uuid = element["uuid"]
url = element["url"] url = element["url"]
filename = f"{instance_path}/search-partitions/{uuid}.pdf"
try: 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): except (urllib.error.HTTPError, urllib.error.URLError,
with open(f"{instance_path}/search-partitions/{uuid}.pdf", 'a', encoding="utf8") as _: 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 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 Thumbnails
""" """
import os import os
import pypdf
from flask import current_app, abort, Blueprint, send_file from flask import current_app, abort, Blueprint, send_file
from .db import get_db
from .auth import login_required from .auth import login_required
bp = Blueprint("thumbnails", __name__, url_prefix="/thumbnails") bp = Blueprint("thumbnails", __name__, url_prefix="/thumbnails")
@ -15,13 +15,18 @@ def generate_thumbnail(source, dest):
""" """
Generates a thumbnail with 'convert' (ImageMagick) Generates a thumbnail with 'convert' (ImageMagick)
""" """
os.system( try:
f'/usr/bin/convert -thumbnail\ pypdf.PdfReader(source) # Check if file is really a PDF
"178^>" -background white -alpha \ except (pypdf.errors.PdfReadError, pypdf.errors.PdfStreamError):
remove -crop 178x178+0+0 \ return
{source}[0] \
{dest}' 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): 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): if not os.path.exists(thumbnail_file):
generate_thumbnail(partition_file, thumbnail_file) generate_thumbnail(partition_file, thumbnail_file)
if not os.path.exists(thumbnail_file):
abort(404)
return send_file(thumbnail_file) return send_file(thumbnail_file)

View File

@ -72,3 +72,25 @@ def get_all_albums():
"uuid": a["uuid"] "uuid": a["uuid"]
} for a in albums } 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", "description": "Partitioncloud",
"display": "fullscreen", "display": "fullscreen",
"icons": [ "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

@ -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 { input:checked#slide-sidebar~#content-container {
position: initial; 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'); */ /** @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) */ /** Various settings (variables) */
:root { :root {
--sidebar-size: max(10vw, 160px); --sidebar-size: max(10vw, 250px);
--sidebar-sz-plus10: calc(var(--sidebar-size) + 10px); --sidebar-sz-plus10: calc(var(--sidebar-size) + 10px);
--sidebar-sz-moins20: calc(var(--sidebar-size) - 20px); --sidebar-sz-moins20: calc(var(--sidebar-size) - 20px);
} }
@ -401,6 +333,7 @@ img.partition-thumbnail {
.user { .user {
display: flex; display: flex;
color: var(--color-text);
} }
.username { .username {
@ -455,12 +388,13 @@ a#delete-album {
transform: translateY(-17%); transform: translateY(-17%);
} }
#settings-container>.user { #settings-container > a > .user {
margin-top: 6px; margin-top: 6px;
border-radius: 3px; border-radius: 3px;
padding: 3px;
} }
#settings-container>.user:hover { #settings-container > a > .user:hover {
background-color: var(--color-mantle); background-color: var(--color-mantle);
} }
@ -606,6 +540,8 @@ input[type="file"] {
/** Dangerous buttons */ /** Dangerous buttons */
button#logout:hover, button#logout:hover,
a#delete-album:hover, a#delete-album:hover,
.red-confirm,
input[type="submit"].red-confirm,
#delete-partition { #delete-partition {
background-color: var(--color-red); background-color: var(--color-red);
color: var(--color-mantle); color: var(--color-mantle);
@ -760,6 +696,9 @@ midi-player {
margin: 20px; margin: 20px;
margin-top: 50px; margin-top: 50px;
border-radius: 15px; border-radius: 15px;
width: 370px;
height: 370px;
background-color: white;
} }
#share-url { #share-url {
@ -770,3 +709,65 @@ midi-player {
margin-bottom: 100px; margin-bottom: 100px;
margin-top: 20px; 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 %} {% block content %}
<h2>{% block title %}Panneau d'administration{% endblock %}</h2> <h2>{% block title %}{{ _("Administration Panel") }}{% endblock %}</h2>
<div id="actions-rapides"> <div id="actions-rapides">
<a href="/add-user"> <a href="/add-user">
<div class="button">Ajouter un utilisateur</div> <div class="button">{{ _("New user") }}</div>
</a> </a>
<a href="/partition"> <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> </a>
</div> </div>
<div class="x-scrollable"> <div class="x-scrollable">
<table> <table>
<thead> <thead>
<tr> <tr>
<th scope="col">Utilisateur</th> <th scope="col">{{ _("User") }}</th>
<th scope="col">Albums</th> <th scope="col">{{ _("Albums") }}</th>
<th scope="col">Partitions</th> <th scope="col">{{ _("Scores") }}</th>
<th scope="col">Privilèges</th> <th scope="col">{{ _("Admin privileges") }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -31,7 +34,9 @@
title="{{ user.username }}"> title="{{ user.username }}">
{{ user.username[0] | upper }} {{ user.username[0] | upper }}
</div> </div>
<div class="table-username">{{ user.username }}</div> <div class="table-username">
<a href="/admin/user/{{ user.id }}">{{ user.username }}</a>
</div>
</div> </div>
</td> </td>
<td>{{ user.albums | length }}</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' %} {% extends 'base.html' %}
{% block header %} {% block header %}
<h1>{% block title %}Liste des partitions{% endblock %}</h1> <h1>{% block title %}{{ _("Scores list") }}{% endblock %}</h1>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -11,7 +11,7 @@
<div> <div>
<a href="/partition/{{ partition['uuid'] }}"> <a href="/partition/{{ partition['uuid'] }}">
<div class="partition" id="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-description">
<div class="partition-name">{{ partition["name"] }}</div> <div class="partition-name">{{ partition["name"] }}</div>
<div class="partition-author">{{ partition["author"] }}</div> <div class="partition-author">{{ partition["author"] }}</div>
@ -28,6 +28,6 @@
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
<div>Aucune partition disponible</div> <div>{{ _("No available scores") }}</div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -1,15 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block title %}Ajout de partition{% endblock %} {% block title %}{{ _("New score") }}{% endblock %}
{% block content %} {% block content %}
<h2>Ajouter une partition à {{ album.name }}</h2> {% include 'components/add_partition.html' %}
<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>
{% endblock %} {% endblock %}

View File

@ -5,22 +5,15 @@
{% block dialogs %} {% block dialogs %}
<dialog id="add-partition"> <dialog id="add-partition">
<h2>Ajouter une partition à {{ album.name }}</h2> {% include 'components/add_partition.html' %}
<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>
<a href="#!" class="close-dialog">Close</a> <a href="#!" class="close-dialog">Close</a>
</dialog> </dialog>
<dialog id="delete"> <dialog id="delete">
<h2>Supprimer l'album</h2> <h2>{{ _("Delete l'album") }}</h2>
Êtes vous sûr de vouloir supprimer cet album ? {{ _("Do you really want to delete this album?") }}
<br/><br/> <br/><br/>
<form method="post" action="/albums/{{ album.uuid }}/delete"> <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> </form>
<a href="#!" class="close-dialog">Close</a> <a href="#!" class="close-dialog">Close</a>
</dialog> </dialog>
@ -42,8 +35,8 @@
{% endif %} {% endif %}
{{ album.name }} {{ album.name }}
</h2> </h2>
{% if g.user %}
<div id="header-actions"> <div id="header-actions">
{% if g.user %}
<section id="users"> <section id="users">
{% for album_user in album.users %} {% for album_user in album.users %}
<div class="user-profile-picture" style="background-color:{{ album_user.color }};" title="{{ album_user.username }}"> <div class="user-profile-picture" style="background-color:{{ album_user.color }};" title="{{ album_user.username }}">
@ -51,25 +44,28 @@
</div> </div>
{% endfor %} {% endfor %}
</section> </section>
{% endif %}
<div class="dropdown dp1"> <div class="dropdown dp1">
+ +
<div class="dropdown-content dp1"> <div class="dropdown-content dp1">
{% if g.user %} {% if g.user %}
<a href="#add-partition">Ajouter une partition</a> <a href="#add-partition">{{ _("Add a score") }}</a>
{% endif %} {% endif %}
{% if not_participant %} {% if not_participant %}
<a href="/albums/{{ album.uuid }}/join">Rejoindre</a> <a href="/albums/{{ album.uuid }}/join">{{ _("Join") }}</a>
{% elif album.users | length > 1 %} {% elif g.user and not not_participant %}
<a href="/albums/{{ album.uuid }}/quit">Quitter</a> <a href="/albums/{{ album.uuid }}/quit">{{ _("Quit") }}</a>
{% endif %} {% endif %}
<a href="#share">Partager</a> <a href="#share">{{ _("Share") }}</a>
{% if g.user.access_level == 1 or (not not_participant and album.users | length == 1) %} {% if g.user or not config["ZIP_REQUIRE_LOGIN"] %}
<a id="delete-album" href="#delete">Supprimer</a> <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 %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endif %}
</header> </header>
<hr/> <hr/>
{% if partitions|length != 0 %} {% if partitions|length != 0 %}
@ -78,7 +74,7 @@
<div> <div>
<a href="/partition/{{ partition['uuid'] }}"> <a href="/partition/{{ partition['uuid'] }}">
<div class="partition" id="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-description">
<div class="partition-name">{{ partition["name"] }}</div> <div class="partition-name">{{ partition["name"] }}</div>
<div class="partition-author">{{ partition["author"] }}</div> <div class="partition-author">{{ partition["author"] }}</div>
@ -98,6 +94,6 @@
</section> </section>
{% else %} {% else %}
<br/> <br/>
<section id="partitions-grid" style="display: inline;">Aucune partition disponible</section> <section id="partitions-grid" style="display: inline;">{{ _("No available scores") }}</section>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

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

View File

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

View File

@ -2,15 +2,15 @@
{% block content %} {% 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 %} {% if partitions|length != 0 %}
<h3>Résultats dans la bibliothèque locale</h3> <h3>{{ _("Results in current database") }}</h3>
<div id="partitions-grid"> <div id="partitions-grid">
{% for partition in partitions %} {% for partition in partitions %}
<div class="partition-container"> <div class="partition-container">
<a href="/partition/{{ partition['uuid'] }}"> <a href="/partition/{{ partition['uuid'] }}">
<div class="partition" id="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-description">
<div class="partition-name">{{ partition["name"] }}</div> <div class="partition-name">{{ partition["name"] }}</div>
<div class="partition-author">{{ partition["author"] }}</div> <div class="partition-author">{{ partition["author"] }}</div>
@ -35,20 +35,20 @@
</select> </select>
<input type="hidden" value="{{ partition['uuid'] }}" name="partition-uuid"> <input type="hidden" value="{{ partition['uuid'] }}" name="partition-uuid">
<input type="hidden" value="local_file" name="partition-type"> <input type="hidden" value="local_file" name="partition-type">
<input type="submit" value="Ajouter à l'album" class="add-to-album"> <input type="submit" value="{{ _('Add to album') }}" class="add-to-album">
</form> </form>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
{% if google_results|length != 0 %} {% if google_results|length != 0 %}
<h3>Résultats de la recherche en ligne</h3> <h3>{{ _("Online search results") }}</h3>
<div id="partitions-grid"> <div id="partitions-grid">
{% for partition in google_results %} {% for partition in google_results %}
<div class="partition-container"> <div class="partition-container">
<a href="/partition/search/{{ partition['uuid'] }}"> <a href="/partition/search/{{ partition['uuid'] }}">
<div class="partition" id="partition-{{ 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-description">
<div class="partition-name">{{ partition["name"] }}</div> <div class="partition-name">{{ partition["name"] }}</div>
</div> </div>
@ -67,13 +67,13 @@
</select> </select>
<input type="hidden" value="{{ partition['uuid'] }}" name="partition-uuid"> <input type="hidden" value="{{ partition['uuid'] }}" name="partition-uuid">
<input type="hidden" value="online_search" name="partition-type"> <input type="hidden" value="online_search" name="partition-type">
<input type="submit" value="Ajouter à l'album"> <input type="submit" value="{{ _('Add to album') }}">
</form> </form>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
{% if google_results|length == 0 and partitions|length == 0 %} {% 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 %} {% endif %}
{% endblock %} {% endblock %}

View File

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

View File

@ -2,21 +2,21 @@
{% block content %} {% 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"> <form method="post" id="add-user-form">
{% if g.user.access_level == 1 %} {% if g.user.access_level == 1 %}
<!-- Uniquement pour /add-user --> <!-- 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;"> <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 %} {% for album in albums %}
<option value="{{ album['uuid'] }}">{{ album["name"] }}</option> <option value="{{ album['uuid'] }}">{{ album["name"] }}</option>
{% endfor %} {% endfor %}
</select><br/> </select><br/>
{% endif %} {% endif %}
<input type="text" name="username" id="username" placeholder="Nom d'utilisateur" required><br/> <input type="text" name="username" id="username" placeholder="{{ _('Username') }}" required><br/>
<input type="password" name="password" id="password" placeholder="Mot de passe" required><br/> <input type="password" name="password" id="password" placeholder="{{ _('Password') }}" required><br/>
<input type="submit" value="Créer un compte"> <input type="submit" value="{{ _('Create account') }}">
</form> </form>
{% endblock %} {% endblock %}

View File

@ -1,12 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="{{ lang }}">
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <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 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> <title>{% block title %}{% endblock %} - PartitionCloud</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='mobile.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="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="apple-touch-icon" href="{{ url_for('static', filename='icons/512.png') }}">
<link rel="manifest" href="{{ url_for('static', filename='manifest.webmanifest') }}" /> <link rel="manifest" href="{{ url_for('static', filename='manifest.webmanifest') }}" />
@ -20,21 +23,21 @@
{% block dialogs %}{% endblock %} {% block dialogs %}{% endblock %}
{% if g.user %} {% if g.user %}
<dialog id="create-album"> <dialog id="create-album">
<h2>Créer un nouvel album</h2> <h2>{{ _("New Album") }}</h2>
<form action="/albums/create-album" method="post"> <form action="/albums/create-album" method="post">
<input type="text" name="name" id="name" placeholder="Nom" required><br/> <input type="text" name="name" id="name" placeholder="{{ _('Name') }}" required><br/>
<input type="submit" value="Créer"> <input type="submit" value="{{ _('Create') }}">
</form> </form>
<br/> <br/>
<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> <a href="#!" class="close-dialog">Close</a>
</dialog> </dialog>
<dialog id="create-groupe"> <dialog id="create-groupe">
<h2>Créer un nouveau groupe</h2> <h2>{{ _("Create new group") }}</h2>
<form action="/groupe/create-groupe" method="post"> <form action="/groupe/create-groupe" method="post">
<input type="text" name="name" id="name" placeholder="Nom" required><br/> <input type="text" name="name" id="name" placeholder="{{ _('Name') }}" required><br/>
<input type="submit" value="Créer"> <input type="submit" value="{{ _('Create') }}">
</form> </form>
<a href="#!" class="close-dialog">Close</a> <a href="#!" class="close-dialog">Close</a>
</dialog> </dialog>
@ -58,9 +61,9 @@
<div id="sidebar"> <div id="sidebar">
{% if g.user %} {% if g.user %}
<form id="search-form" action="/albums/search" method="post"> <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> <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) %} {% for i in range(0, user.max_queries+1) %}
<option value="{{ i }}">{{ i }}</option> <option value="{{ i }}">{{ i }}</option>
{% endfor %} {% endfor %}
@ -68,11 +71,11 @@
<input id="search-submit" type="submit" value="Go"> <input id="search-submit" type="submit" value="Go">
</form> </form>
{% endif %} {% endif %}
<h2>Albums</h2> <h2>{{ _("Albums") }}</h2>
{% if g.user %} {% if g.user %}
<a href="#create-album"> <a href="#create-album">
<div class="create-button"> <div class="create-button">
Créer un album {{ _("New album") }}
</div> </div>
</a> </a>
{% endif %} {% endif %}
@ -89,7 +92,7 @@
</summary> </summary>
<div class="groupe-albums-cover"> <div class="groupe-albums-cover">
{% if groupe.get_albums() | length == 0 %} {% if groupe.get_albums() | length == 0 %}
Aucun album {{ _("No albums") }}
{% else %} {% else %}
{% for album in groupe.get_albums() %} {% for album in groupe.get_albums() %}
<a href="/groupe/{{ groupe.uuid }}/{{ album['uuid'] }}"> <a href="/groupe/{{ groupe.uuid }}/{{ album['uuid'] }}">
@ -108,7 +111,7 @@
<section id="albums"> <section id="albums">
{% if user.get_albums() | length == 0 %} {% 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 %} {% else %}
{% for album in user.albums %} {% for album in user.albums %}
<a href="/albums/{{ album['uuid'] }}"> <a href="/albums/{{ album['uuid'] }}">
@ -122,7 +125,7 @@
</section> </section>
{% else %} {% else %}
<section id="sidebar-navigation"> <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> </section>
{% endif %} {% endif %}
@ -136,7 +139,7 @@
<path d="M9 12h12l-3 -3"></path> <path d="M9 12h12l-3 -3"></path>
<path d="M18 15l3 -3"></path> <path d="M18 15l3 -3"></path>
</svg> </svg>
Déconnexion {{ _("Log out") }}
</button> </button>
</a><br/> </a><br/>
{% if g.user.access_level == 1 %} {% if g.user.access_level == 1 %}
@ -151,21 +154,21 @@
<path d="M17.27 20l-1.3 .75"></path> <path d="M17.27 20l-1.3 .75"></path>
<path d="M15.97 17.25l1.3 .75"></path> <path d="M15.97 17.25l1.3 .75"></path>
<path d="M20.733 20l1.3 .75"></path> <path d="M20.733 20l1.3 .75"></path>
</svg>Panneau admin </svg>{{ _("Admin Panel") }}
</button></a><br/> </button></a><br/>
{% endif %} {% endif %}
<div class="user"> <a href="/settings"><div class="user">
<div class="user-profile-picture" style="background-color:{{ user.color }};" <div class="user-profile-picture" style="background-color:{{ user.color }};"
title="{{ user.username }}"> title="{{ user.username }}">
{{ user.username[0] | upper }} {{ user.username[0] | upper }}
</div> </div>
<div class="username">{{ user.username }}</div> <div class="username">{{ user.username }}</div>
</div> </div></a>
{% else %} {% else %}
{% if not config.DISABLE_REGISTER %} {% 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 %} {% endif %}
<a href="{{ url_for('auth.login') }}"><button>Se connecter</button></a> <a href="{{ url_for('auth.login') }}"><button>{{ _("Log in") }}</button></a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -185,5 +188,8 @@
<div id="footer"><a href="https://github.com/partitioncloud/partitioncloud-server">PartitionCloud</a> {{ version }}</div> <div id="footer"><a href="https://github.com/partitioncloud/partitioncloud-server">PartitionCloud</a> {{ version }}</div>
</div> </div>
</body> </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> </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"> <dialog id="share">
<center> <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 }}")'> <div id="share-url" onclick='navigator.clipboard.writeText("{{ share_link }}")'>
{{ share_link }} {{ share_link }}
</div> </div>

View File

@ -5,20 +5,19 @@
{% block dialogs %} {% block dialogs %}
<dialog id="create-groupe-album"> <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"> <form action="/groupe/{{ groupe.uuid }}/create-album" method="post">
<input type="text" name="name" id="name" placeholder="Nom" required><br/> <input type="text" name="name" id="name" placeholder="{{ _('Name') }}" required><br/>
<input type="submit" value="Ajouter"> <input type="submit" value="{{ _('Add') }}">
</form> </form>
<a href="#!" class="close-dialog">Close</a> <a href="#!" class="close-dialog">Close</a>
</dialog> </dialog>
<dialog id="delete"> <dialog id="delete">
<h2>Supprimer le groupe</h2> <h2>{{ _("Delete group") }}</h2>
Êtes vous sûr de vouloir supprimer ce groupe ? Cela supprimera les albums {{ _("Do you really want to delete this group and the albums it contains?") }}
sous-jacents et leurs partitions si personne ne les a rejoints (indépendamment du groupe).
<br/><br/> <br/><br/>
<form method="post" action="/groupe/{{ groupe.uuid }}/delete"> <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> </form>
<a href="#!" class="close-dialog">Close</a> <a href="#!" class="close-dialog">Close</a>
</dialog> </dialog>
@ -31,8 +30,8 @@
{% block content %} {% block content %}
<header id="album-header"> <header id="album-header">
<h2 id="groupe-title">{{ groupe.name }}</h2> <h2 id="groupe-title">{{ groupe.name }}</h2>
{% if g.user %}
<div id="header-actions"> <div id="header-actions">
{% if g.user %}
<section id="users"> <section id="users">
{% for groupe_user in groupe.users %} {% for groupe_user in groupe.users %}
<div class="user-profile-picture" style="background-color:{{ groupe_user.color }};" title="{{ groupe_user.username }}"> <div class="user-profile-picture" style="background-color:{{ groupe_user.color }};" title="{{ groupe_user.username }}">
@ -40,23 +39,26 @@
</div> </div>
{% endfor %} {% endfor %}
</section> </section>
{% endif %}
<div class="dropdown dp1"> <div class="dropdown dp1">
+ +
<div class="dropdown-content dp1"> <div class="dropdown-content dp1">
{% if not_participant %} {% if not_participant %}
<a href="/groupe/{{ groupe.uuid }}/join">Rejoindre</a> <a href="/groupe/{{ groupe.uuid }}/join">{{ _("Join") }}</a>
{% elif groupe.users | length > 1 %} {% elif g.user and not not_participant %}
<a href="/groupe/{{ groupe.uuid }}/quit">Quitter</a> <a href="/groupe/{{ groupe.uuid }}/quit">{{ _("Quit") }}</a>
{% endif %} {% endif %}
<a href="#share">Partager</a> <a href="#share">{{ _("Share") }}</a>
{% if g.user.access_level == 1 or user.id in groupe.get_admins() %} {% if g.user or not config["ZIP_REQUIRE_LOGIN"] %}
<a href="#create-groupe-album">Ajouter un album</a> <a href="/groupe/{{ groupe.uuid }}/zip">{{ _("Download as zip") }}</a>
<a id="delete-album" href="#delete">Supprimer</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 %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
{% endif %}
</header> </header>
<hr/> <hr/>
{% if groupe.albums|length != 0 %} {% if groupe.albums|length != 0 %}
@ -71,6 +73,11 @@
</section> </section>
{% else %} {% else %}
<br/> <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 %} {% endif %}
{% endblock %} {% 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' %} {% extends 'base.html' %}
{% block title %}Attachments de {{ partition.name }}{% endblock %} {% block title %}{{ _("Attachments of %(name)s", name=partition.name) }}{% endblock %}
{% block dialogs %} {% block dialogs %}
<dialog id="create-attachment"> <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"> <form action="/partition/{{ partition.uuid }}/add-attachment" method="post" enctype="multipart/form-data">
<input type="text" name="name" id="name" placeholder="Nom"><br/> <input type="text" name="name" id="name" placeholder="{{ _('Name') }}"><br/>
<input name="file" type="file" accept=".mp3,.mid" required=""><br/> {% block input_file %}
<input type="submit" value="Ajouter"> {% set required=true %}
{% set filetype=".mp3,.mid" %}
{% include 'components/input_file.html' %}
{% endblock %}
<input type="submit" value="{{ _('Add') }}">
</form> </form>
<a href="#!" class="close-dialog">Close</a> <a href="#!" class="close-dialog">Close</a>
</dialog> </dialog>
@ -19,8 +23,8 @@
{% block content %} {% block content %}
<object id="pdf-embed" width="400" height="500" type="application/pdf" data="/partition/{{ partition.uuid }}"> <object id="pdf-embed" width="400" height="500" type="application/pdf" data="/partition/{{ partition.uuid }}">
<p> <p>
Impossible d'afficher le pdf dans ce navigateur. {{ _("No pdf viewer available in this browser.
Il est conseillé d'utiliser Firefox sur Android. You can use Firefox on Android.") }}
</p> </p>
</object> </object>
@ -43,7 +47,7 @@
src="/partition/attachment/{{ attachment.uuid }}.mid" src="/partition/attachment/{{ attachment.uuid }}.mid"
sound-font visualizer="#midi-visualizer" data-js-focus-visible> sound-font visualizer="#midi-visualizer" data-js-focus-visible>
</midi-player> </midi-player>
<noscript>MIDI support needs JavaScript</noscript> <noscript>{{ _("JavaScript is mandatory to read MIDI files") }}</noscript>
</td> </td>
<td>🎵 {{ attachment.name }}</td> <td>🎵 {{ attachment.name }}</td>
{% endif %} {% endif %}
@ -55,9 +59,9 @@
{% endif %} {% endif %}
<br/> <br/>
{% if user %} {% if g.user %}
<div class="centered"> <div class="centered">
<a href="#create-attachment"><button>Ajouter un attachment</button></a> <a href="#create-attachment"><button>{{ _("Add an attachment") }}</button></a>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,9 @@
import os
import sys
import random import random
import string import string
import sqlite3 import sqlite3
from colorama import Fore, Style
from . import config from . import config
@ -39,3 +42,29 @@ def new_uuid():
def format_uuid(uuid): def format_uuid(uuid):
"""Format old uuid4 format""" """Format old uuid4 format"""
return uuid.upper()[:6] 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(): 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(): 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", "thumbnails"), exist_ok=True)
os.makedirs(os.path.join(config.instance, "cache", "search-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.4.1", [("Install qrcode", v1_hooks.install_qrcode)]),
("v1.5.0", [("Move to instance directory", v1_hooks.move_instance)]), ("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", "--restore",
help="restore from specific version backup, will not apply any hook (vx.y.z)", 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() args = parser.parse_args()
config.instance = os.path.abspath(args.instance) config.instance = os.path.abspath(args.instance)
if args.restore is None: if args.restore is not None:
migrate(args.current, args.target, skip_backup=args.skip_backup)
else:
restore(args.restore) 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)