From 5a2057560a5703a59408009e40b51de5a0c18600 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 18 Jul 2022 18:46:10 +0200 Subject: implement image upload --- .gitignore | 1 + development.ini | 3 +- fietsboek/__init__.py | 11 +++ fietsboek/data.py | 118 +++++++++++++++++++++++++++++++ fietsboek/routes.py | 2 + fietsboek/static/fietsboek.js | 53 ++++++++++++++ fietsboek/static/theme.css | 36 ++++++++++ fietsboek/templates/details.jinja2 | 21 ++++++ fietsboek/templates/edit.jinja2 | 4 +- fietsboek/templates/edit_form.jinja2 | 22 +++++- fietsboek/templates/finish_upload.jinja2 | 4 +- fietsboek/util.py | 69 ++++++++++++++++++ fietsboek/views/detail.py | 30 +++++++- fietsboek/views/edit.py | 17 +++++ fietsboek/views/upload.py | 6 ++ 15 files changed, 389 insertions(+), 8 deletions(-) create mode 100644 fietsboek/data.py diff --git a/.gitignore b/.gitignore index 3afaa13..a833b62 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ coverage test *.sqlite .venv/ +/data diff --git a/development.ini b/development.ini index 47e0231..eb33586 100644 --- a/development.ini +++ b/development.ini @@ -18,11 +18,12 @@ pyramid.includes = pyramid_debugtoolbar sqlalchemy.url = sqlite:///%(here)s/fietsboek.sqlite +fietsboek.data_dir = %(here)s/data retry.attempts = 3 email.from = fietsboek@kingdread.de -email.smtp_url = debug://localhost:1025 +email.smtp_url = debug:// available_locales = en de enable_account_registration = true diff --git a/fietsboek/__init__.py b/fietsboek/__init__.py index 4a28dd4..bd2d705 100644 --- a/fietsboek/__init__.py +++ b/fietsboek/__init__.py @@ -2,6 +2,8 @@ For more information, see the README or the included documentation. """ +from pathlib import Path + from pyramid.config import Configurator from pyramid.session import SignedCookieSessionFactory from pyramid.csrf import CookieCSRFStoragePolicy @@ -9,6 +11,7 @@ from pyramid.settings import asbool, aslist from pyramid.i18n import default_locale_negotiator from .security import SecurityPolicy +from .data import DataManager from . import jinja2 as fiets_jinja2 @@ -46,6 +49,13 @@ def main(global_config, **settings): if settings.get('session_key', '') == '': raise ValueError("Please set a session signing key (session_key) in your settings!") + if 'fietsboek.data_dir' not in settings: + raise ValueError("Please set a data directory (fietsboek.data_dir) in your settings!") + + def data_manager(request): + data_dir = request.registry.settings["fietsboek.data_dir"] + return DataManager(Path(data_dir)) + settings['enable_account_registration'] = asbool( settings.get('enable_account_registration', 'false')) settings['available_locales'] = aslist( @@ -63,6 +73,7 @@ def main(global_config, **settings): config.set_csrf_storage_policy(CookieCSRFStoragePolicy()) config.set_default_csrf_options(require_csrf=True) config.set_locale_negotiator(locale_negotiator) + config.add_request_method(data_manager, reify=True) jinja2_env = config.get_jinja2_environment() jinja2_env.filters['format_decimal'] = fiets_jinja2.filter_format_decimal diff --git a/fietsboek/data.py b/fietsboek/data.py new file mode 100644 index 0000000..992ddf7 --- /dev/null +++ b/fietsboek/data.py @@ -0,0 +1,118 @@ +"""Data manager for fietsboek. + +Data are objects that belong to a track (such as images), but are not stored in +the database itself. This module makes access to such data objects easier. +""" +import random +import string +import shutil +import uuid + +from .util import secure_filename + + +def generate_filename(filename): + """Generates a safe-to-use filename for uploads. + + If possible, tries to keep parts of the original filename intact, such as + the extension. + + :param filename: The original filename. + :type filename: str + :return: The generated filename. + :rtype: str + """ + if filename: + good_name = secure_filename(filename) + if good_name: + random_prefix = "".join(random.choice(string.ascii_lowercase) for _ in range(5)) + return f"{random_prefix}_{good_name}" + + return str(uuid.uuid4()) + + +class DataManager: + """Data manager. + + The data manager is usually provided as ``request.data_manager`` and can be + used to access track's images and other on-disk data. + + :ivar data_dir: Path to the data folder. + :vartype data_dir: pathlib.Path + """ + + def __init__(self, data_dir): + self.data_dir = data_dir + + def _track_data_dir(self, track_id): + return self.data_dir / "tracks" / str(track_id) + + def images(self, track_id): + """Returns a list of images that belong to a track. + + :param track_id: Numerical ID of the track. + :type track_id: int + :return: A list of image IDs. + :rtype: list[str] + """ + image_dir = self._track_data_dir(track_id) / "images" + if not image_dir.exists(): + return [] + images = [] + for image in image_dir.iterdir(): + images.append(image.name) + return images + + def image_path(self, track_id, image_id): + """Returns a path to a saved image. + + :raises FileNotFoundError: If the given image could not be found. + :param track_id: ID of the track. + :type track_id: int + :param image_id: ID of the image. + :type image_id: str + :return: A path pointing to the requested image. + :rtype: pathlib.Path + """ + image = self._track_data_dir(track_id) / "images" / secure_filename(image_id) + if not image.exists(): + raise FileNotFoundError("The requested image does not exist") + return image + + def add_image(self, track_id, image, filename=None): + """Saves an image to a track. + + :param track_id: ID of the track. + :type track_id: int + :param image: The image, as a file-like object to read from. + :type image: file + :param filename: The image's original filename. + :type filename: str + :return: The ID of the saved image. + :rtype: str + """ + image_dir = self._track_data_dir(track_id) / "images" + image_dir.mkdir(parents=True, exist_ok=True) + + filename = generate_filename(filename) + path = image_dir / filename + with open(path, "wb") as fobj: + shutil.copyfileobj(image, fobj) + + return filename + + def delete_image(self, track_id, image_id): + """Deletes an image from a track. + + :raises FileNotFoundError: If the given image could not be found. + :param track_id: ID of the track. + :type track_id: int + :param image_id: ID of the image. + :type image_id: str + """ + # Be sure to not delete anything else than the image file + image_id = secure_filename(image_id) + if '/' in image_id or '\\' in image_id: + return + path = self.image_path(track_id, image_id) + path.unlink() diff --git a/fietsboek/routes.py b/fietsboek/routes.py index a28f926..9d705c1 100644 --- a/fietsboek/routes.py +++ b/fietsboek/routes.py @@ -35,6 +35,8 @@ def includeme(config): factory='fietsboek.models.Track.factory') config.add_route('add-comment', '/track/{track_id}/comment', factory='fietsboek.models.Track.factory') + config.add_route('image', '/track/{track_id}/images/{image_name}', + factory='fietsboek.models.Track.factory') config.add_route('badge', '/badge/{badge_id}', factory='fietsboek.models.Badge.factory') diff --git a/fietsboek/static/fietsboek.js b/fietsboek/static/fietsboek.js index d27e21e..e403628 100644 --- a/fietsboek/static/fietsboek.js +++ b/fietsboek/static/fietsboek.js @@ -69,6 +69,48 @@ function removeFriendClicked(event) { button.parentNode.parentNode.removeChild(button.parentNode); } +function imageSelectorChanged(event) { + console.log(event.target.files); + + for (var file of event.target.files) { + const input = document.createElement("input"); + input.type = "file"; + input.hidden = true; + input.name = "image[]"; + + const transfer = new DataTransfer(); + transfer.items.add(file); + input.files = transfer.files; + + let preview = document.querySelector("#trackImagePreviewBlueprint").cloneNode(true); + preview.removeAttribute("id"); + preview.querySelector("img").src = URL.createObjectURL(file); + preview.querySelector("button.delete-image").addEventListener("click", deleteImageButtonClicked); + preview.appendChild(input); + + document.querySelector("#trackImageList").appendChild(preview); + } + + event.target.value = ""; +} + +function deleteImageButtonClicked(event) { + let preview = event.target.closest("div.track-image-preview"); + /* If this was a image yet to be uploaded, simply remove it */ + let input = preview.querySelector("input[type=file]"); + if (input) { + preview.parentNode.removeChild(preview); + return; + } + + /* Otherwise, we need to remove it but also insert a "delete-image" input */ + let deleter = preview.querySelector("input[type=hidden]"); + deleter.removeAttribute("disabled"); + preview.removeChild(deleter); + preview.parentNode.appendChild(deleter); + preview.parentNode.removeChild(preview); +} + document.addEventListener('DOMContentLoaded', function(event) { /* Enable the "Add tag" button in the track edit page */ let $ = (selector) => document.querySelector(selector); @@ -150,4 +192,15 @@ document.addEventListener('DOMContentLoaded', function(event) { $("#archiveDownloadButton").disabled = (checked.length == 0); }); }); + + /* Enable the image selector */ + var button = $("#imageSelector"); + if (button) { + button.addEventListener("change", imageSelectorChanged); + } + + /* Enable the "delete image" buttons for the already existing images */ + document.querySelectorAll("button.delete-image").forEach((b) => { + b.addEventListener("click", deleteImageButtonClicked); + }); }); diff --git a/fietsboek/static/theme.css b/fietsboek/static/theme.css index 831b1f6..076b519 100644 --- a/fietsboek/static/theme.css +++ b/fietsboek/static/theme.css @@ -18,6 +18,42 @@ strong { align-items: center; } +.carousel-item img { + max-height: 700px; + margin: auto; +} + +#trackImageList { + display: flex; + flex-wrap: wrap; +} + +.track-image-preview button { + position: absolute; + z-index: 5; + background-color: white; + right: 0px; +} + +.track-image-preview img { + max-width: 100%; + max-height: 100%; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.track-image-preview { + position: relative; + width: min(317px, 100%); + height: calc(9 / 16 * 317px); + border-radius: 5px; + border: 1px solid grey; + margin-right: 5px; + margin-bottom: 5px; +} + .track-description img { max-width: 100%; } diff --git a/fietsboek/templates/details.jinja2 b/fietsboek/templates/details.jinja2 index 4d88224..2ec62ec 100644 --- a/fietsboek/templates/details.jinja2 +++ b/fietsboek/templates/details.jinja2 @@ -137,6 +137,27 @@
{% endif %} + {% if images %} + + {% endif %}

{{ _("page.details.comments") }}

{% for comment in track.comments %}
diff --git a/fietsboek/templates/edit.jinja2 b/fietsboek/templates/edit.jinja2 index 6053509..4e64f69 100644 --- a/fietsboek/templates/edit.jinja2 +++ b/fietsboek/templates/edit.jinja2 @@ -9,8 +9,8 @@
-
- {{ edit_form.edit_track(track.title, track.date_raw, track.date_tz or 0, track.visibility, track.description, track.text_tags(), badges, track.tagged_people) }} + + {{ edit_form.edit_track(track.title, track.date_raw, track.date_tz or 0, track.visibility, track.description, track.text_tags(), badges, track.tagged_people, images) }} {{ util.hidden_csrf_input() }}
diff --git a/fietsboek/templates/edit_form.jinja2 b/fietsboek/templates/edit_form.jinja2 index 3779d5d..0ba933e 100644 --- a/fietsboek/templates/edit_form.jinja2 +++ b/fietsboek/templates/edit_form.jinja2 @@ -1,4 +1,4 @@ -{% macro edit_track(title, date, date_tz, visibility, description, tags, badges, friends) %} +{% macro edit_track(title, date, date_tz, visibility, description, tags, badges, friends, images) %}
@@ -87,4 +87,24 @@
+
+
+ {% for image in images %} +
+ + + +
+ {% endfor %} +
+ + +
+ +
+
+ + +
+
{% endmacro %} diff --git a/fietsboek/templates/finish_upload.jinja2 b/fietsboek/templates/finish_upload.jinja2 index 58c67d4..d6f9f00 100644 --- a/fietsboek/templates/finish_upload.jinja2 +++ b/fietsboek/templates/finish_upload.jinja2 @@ -9,8 +9,8 @@
- - {{ edit_form.edit_track(upload_title, upload_date, upload_date_tz, upload_visibility, upload_description, upload_tags, badges, upload_tagged_people) }} + + {{ edit_form.edit_track(upload_title, upload_date, upload_date_tz, upload_visibility, upload_description, upload_tags, badges, upload_tagged_people, []) }} {{ util.hidden_csrf_input() }}
diff --git a/fietsboek/util.py b/fietsboek/util.py index 1014c87..bab2074 100644 --- a/fietsboek/util.py +++ b/fietsboek/util.py @@ -2,6 +2,9 @@ import random import string import datetime +import re +import os +import unicodedata # Compat for Python < 3.9 import importlib_resources @@ -25,6 +28,22 @@ ALLOWED_ATTRIBUTES = dict(bleach.sanitizer.ALLOWED_ATTRIBUTES) ALLOWED_ATTRIBUTES['img'] = ['alt', 'src'] +_filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9_.-]") +_windows_device_files = ( + "CON", + "AUX", + "COM1", + "COM2", + "COM3", + "COM4", + "LPT1", + "LPT2", + "LPT3", + "PRN", + "NUL", +) + + def safe_markdown(md_source): """Transform a markdown document into a safe HTML document. @@ -289,3 +308,53 @@ def read_localized_resource(locale_name, path, raise_on_error=False): if raise_on_error: raise FileNotFoundError(f"Resource {path!r} not found") return f"{locale_name}:{path}" + + +def secure_filename(filename): + r"""Pass it a filename and it will return a secure version of it. This + filename can then safely be stored on a regular file system and passed + to :func:`os.path.join`. The filename returned is an ASCII only string + for maximum portability. + On windows systems the function also makes sure that the file is not + named after one of the special device files. + + >>> secure_filename("My cool movie.mov") + 'My_cool_movie.mov' + >>> secure_filename("../../../etc/passwd") + 'etc_passwd' + >>> secure_filename('i contain cool \xfcml\xe4uts.txt') + 'i_contain_cool_umlauts.txt' + + The function might return an empty filename. It's your responsibility + to ensure that the filename is unique and that you abort or + generate a random filename if the function returned an empty one. + + :param filename: the filename to secure + :type filename: str + :return: The secure filename. + :rtype: str + """ + # Taken from + # https://github.com/pallets/werkzeug/blob/main/src/werkzeug/utils.py + + filename = unicodedata.normalize("NFKD", filename) + filename = filename.encode("ascii", "ignore").decode("ascii") + + for sep in os.path.sep, os.path.altsep: + if sep: + filename = filename.replace(sep, " ") + filename = str(_filename_ascii_strip_re.sub("", "_".join(filename.split()))).strip( + "._" + ) + + # on nt a couple of special files are present in each folder. We + # have to ensure that the target file is not such a filename. In + # this case we prepend an underline + if ( + os.name == "nt" + and filename + and filename.split(".", maxsplit=1)[0].upper() in _windows_device_files + ): + filename = f"_{filename}" + + return filename diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py index a15b29d..3953f47 100644 --- a/fietsboek/views/detail.py +++ b/fietsboek/views/detail.py @@ -2,9 +2,9 @@ import datetime from pyramid.view import view_config -from pyramid.response import Response +from pyramid.response import Response, FileResponse from pyramid.i18n import TranslationString as _ -from pyramid.httpexceptions import HTTPFound +from pyramid.httpexceptions import HTTPFound, HTTPNotFound from .. import models, util @@ -22,12 +22,17 @@ def details(request): track = request.context description = util.safe_markdown(track.description) show_edit_link = (track.owner == request.identity) + images = [ + request.route_url("image", track_id=track.id, image_name=image) + for image in request.data_manager.images(track.id) + ] return { 'track': track, 'show_edit_link': show_edit_link, 'mps_to_kph': util.mps_to_kph, 'comment_md_to_html': util.safe_markdown, 'description': description, + 'images': images, } @@ -85,6 +90,27 @@ def badge(request): return Response(request.context.image) +@view_config(route_name='image', http_cache=3600, permission='track.view') +def image(request): + """Returns the image data for the requested image. + + This ensures that the image is sent efficiently, by delegating to the WSGI + file wrapper if possible. + + :param request: The Pyramid request. + :type request: pyramid.request.Request + :return: The HTTP response. + :rtype: pyramid.response.Response + """ + track = request.context + try: + image_path = request.data_manager.image_path(track.id, request.matchdict['image_name']) + except FileNotFoundError: + return HTTPNotFound() + else: + return FileResponse(image_path, request) + + @view_config(route_name="add-comment", request_method="POST", permission="track.comment") def add_comment(request): """Endpoint to add a comment to a track. diff --git a/fietsboek/views/edit.py b/fietsboek/views/edit.py index aabbcbc..04ed000 100644 --- a/fietsboek/views/edit.py +++ b/fietsboek/views/edit.py @@ -1,5 +1,6 @@ """Views for editing a track.""" import datetime +from collections import namedtuple from pyramid.view import view_config from pyramid.httpexceptions import HTTPFound, HTTPBadRequest @@ -10,6 +11,9 @@ from .. import models, util from ..models.track import Visibility +_Image = namedtuple("_Image", "name url") + + @view_config(route_name='edit', renderer='fietsboek:templates/edit.jinja2', permission='track.edit', request_method='GET') def edit(request): @@ -23,9 +27,14 @@ def edit(request): track = request.context badges = request.dbsession.execute(select(models.Badge)).scalars() badges = [(badge in track.badges, badge) for badge in badges] + images = [ + _Image(image, request.route_url("image", track_id=track.id, image_name=image)) + for image in request.data_manager.images(track.id) + ] return { 'track': track, 'badges': badges, + 'images': images, } @@ -61,4 +70,12 @@ def do_edit(request): tags = request.params.getall("tag[]") track.sync_tags(tags) + for image in request.params.getall("image[]"): + if image == b"": + continue + request.data_manager.add_image(track.id, image.file, image.filename) + + for image in request.params.getall("delete-image[]"): + request.data_manager.delete_image(track.id, image) + return HTTPFound(request.route_url('details', track_id=track.id)) diff --git a/fietsboek/views/upload.py b/fietsboek/views/upload.py index 979291e..4f42b80 100644 --- a/fietsboek/views/upload.py +++ b/fietsboek/views/upload.py @@ -169,6 +169,12 @@ def do_finish_upload(request): track.ensure_cache() request.dbsession.add(track.cache) + # Don't forget to add the images + for image in request.params.getall("image[]"): + if image == b"": + continue + request.data_manager.add_image(track.id, image.file, image.filename) + request.session.flash(request.localizer.translate(_("flash.upload_success"))) return HTTPFound(request.route_url('details', track_id=track.id)) -- cgit v1.2.3 From f8c198709120cf94d9e7263d5979c4cf0dc95b67 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 18 Jul 2022 18:55:40 +0200 Subject: update documentation with new data dir --- doc/administration/backup.rst | 28 +++++++++++++++++++++------- doc/administration/configuration.rst | 10 ++++++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/doc/administration/backup.rst b/doc/administration/backup.rst index 9566e38..3973298 100644 --- a/doc/administration/backup.rst +++ b/doc/administration/backup.rst @@ -15,10 +15,20 @@ case data is lost due unforseen reasons. Backup ------ -All of the Fietsboek data is contained in the database. You should refer to the -manual of your DBMS to see what the procedure for a backup is. Using the -preferred way for your DBMS ensures that your backup represents a consistent -state of the database. +The Fietsboek data lives in two places: Most of it is saved in the database, +while track images and other "big files" are saved in the data directory. + +You should refer to the manual of your DBMS to see what the procedure for a +backup is. Using the preferred way for your DBMS ensures that your backup +represents a consistent state of the database. + +After backing up the database, you should back up the data directory with a +tool of your choice, for example by using ``tar``, ``rsync`` or a proper backup +tool like ``borg``: + +.. code:: bash + + tar -czf backup.tar.gz path/to/data/dir In addition to the actual data, you should also note down the Fietsboek version and the database schema version, as backups can only be restored to the same @@ -42,7 +52,7 @@ Note those value in addition to your backup. Restore ------- -The restoration process works in three steps: +The restoration process works in four steps: First, we ensure that we are on the correct Fietsboek version. If you are using ``git``, this can be done with ``git checkout``: @@ -57,5 +67,9 @@ Then, we get the database schema to the right version: .venv/bin/alembic -c production.ini upgrade NOTED_ALEMBIC_VERSION_HERE -Finally, we can restore the data. This step is dependent on the DBMS that you, -therefore you should consult its documentation. +Now, we can restore the data in the database. This step is dependent on the +DBMS that you, therefore you should consult its documentation. + +Finally, we can restore the data directory. This step depends on how you chose +to back up the data directory earlier. In the case of ``tar``, you can simply +extract the archive to the right location. diff --git a/doc/administration/configuration.rst b/doc/administration/configuration.rst index 48afde1..3927540 100644 --- a/doc/administration/configuration.rst +++ b/doc/administration/configuration.rst @@ -35,6 +35,7 @@ Most of the configuration is in the ``[app:main]`` category and looks like this: session_key = sqlalchemy.url = sqlite:///%(here)s/fietsboek.sqlite + fietsboek.data_dir = %(here)s/data retry.attempts = 3 @@ -51,6 +52,15 @@ Most of the configuration is in the ``[app:main]`` category and looks like this: creation of new accounts via the web interface, for example if you want to have a private instance. New accounts can always be created using the CLI management tool. +* ``session_key`` should be set to a random string of characters. This is the + key used to sign session data, so it should not get into wrong hands! +* ``sqlalchemy.url`` is the URL to the database. See the `SQLAlchemy + documentation + `__ for + more information. +* ``fietsboek.data_dir`` sets the directory for data uploads. This directory + must be writable by the Fietsboek process, as Fietsboek will save track + images in there. * ``email.from`` sets the sender of emails, for example for account verifications. * ``email.smtp_url`` sets the URL of the SMTP server. The following formats are accepted: -- cgit v1.2.3 From c1c81247e8c8a451589d788412bfffdd6a82e238 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 18 Jul 2022 18:55:57 +0200 Subject: give tests a temporary data dir --- tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 79f0245..a91d2f4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,7 +47,8 @@ def dbengine(app_settings, ini_file): alembic.command.stamp(alembic_cfg, None, purge=True) @pytest.fixture(scope='session') -def app(app_settings, dbengine): +def app(app_settings, dbengine, tmp_path_factory): + app_settings["fietsboek.data_dir"] = str(tmp_path_factory.mktemp("data")) return main({}, dbengine=dbengine, **app_settings) @pytest.fixture -- cgit v1.2.3 From 687058a0f37b1c5694fa5f863ab810842da9c5fc Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 18 Jul 2022 19:10:37 +0200 Subject: update message catalogs --- fietsboek/locale/de/LC_MESSAGES/messages.mo | Bin 9866 -> 10063 bytes fietsboek/locale/de/LC_MESSAGES/messages.po | 87 ++++++++++++++++------------ fietsboek/locale/en/LC_MESSAGES/messages.mo | Bin 9289 -> 9471 bytes fietsboek/locale/en/LC_MESSAGES/messages.po | 87 ++++++++++++++++------------ fietsboek/locale/fietslog.pot | 87 ++++++++++++++++------------ 5 files changed, 150 insertions(+), 111 deletions(-) diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.mo b/fietsboek/locale/de/LC_MESSAGES/messages.mo index 34bd05a..b6375fa 100644 Binary files a/fietsboek/locale/de/LC_MESSAGES/messages.mo and b/fietsboek/locale/de/LC_MESSAGES/messages.mo differ diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.po b/fietsboek/locale/de/LC_MESSAGES/messages.po index 0912079..27c840f 100644 --- a/fietsboek/locale/de/LC_MESSAGES/messages.po +++ b/fietsboek/locale/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-07-11 14:34+0200\n" +"POT-Creation-Date: 2022-07-18 19:07+0200\n" "PO-Revision-Date: 2022-07-02 17:35+0200\n" "Last-Translator: FULL NAME \n" "Language: de\n" @@ -18,39 +18,39 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.10.3\n" -#: fietsboek/util.py:262 +#: fietsboek/util.py:273 msgid "password_constraint.mismatch" msgstr "Passwörter stimmen nicht überein" -#: fietsboek/util.py:264 +#: fietsboek/util.py:275 msgid "password_constraint.length" msgstr "Passwort zu kurz" -#: fietsboek/models/track.py:494 +#: fietsboek/models/track.py:495 msgid "tooltip.table.length" msgstr "Länge" -#: fietsboek/models/track.py:495 +#: fietsboek/models/track.py:496 msgid "tooltip.table.uphill" msgstr "Bergauf" -#: fietsboek/models/track.py:496 +#: fietsboek/models/track.py:497 msgid "tooltip.table.downhill" msgstr "Bergab" -#: fietsboek/models/track.py:497 +#: fietsboek/models/track.py:498 msgid "tooltip.table.moving_time" msgstr "Fahrzeit" -#: fietsboek/models/track.py:498 +#: fietsboek/models/track.py:499 msgid "tooltip.table.stopped_time" msgstr "Haltezeit" -#: fietsboek/models/track.py:499 +#: fietsboek/models/track.py:500 msgid "tooltip.table.max_speed" msgstr "Maximalgeschwindigkeit" -#: fietsboek/models/track.py:501 +#: fietsboek/models/track.py:502 msgid "tooltip.table.avg_speed" msgstr "Durchschnittsgeschwindigkeit" @@ -86,47 +86,51 @@ msgstr "Hinzufügen" msgid "page.browse.title" msgstr "Stöbern" -#: fietsboek/templates/browse.jinja2:17 fietsboek/templates/details.jinja2:86 +#: fietsboek/templates/browse.jinja2:19 fietsboek/templates/details.jinja2:86 msgid "page.details.date" msgstr "Datum" -#: fietsboek/templates/browse.jinja2:19 fietsboek/templates/details.jinja2:98 +#: fietsboek/templates/browse.jinja2:21 fietsboek/templates/details.jinja2:98 msgid "page.details.length" msgstr "Länge" -#: fietsboek/templates/browse.jinja2:23 fietsboek/templates/details.jinja2:90 +#: fietsboek/templates/browse.jinja2:25 fietsboek/templates/details.jinja2:90 msgid "page.details.start_time" msgstr "Startzeit" -#: fietsboek/templates/browse.jinja2:25 fietsboek/templates/details.jinja2:94 +#: fietsboek/templates/browse.jinja2:27 fietsboek/templates/details.jinja2:94 msgid "page.details.end_time" msgstr "Endzeit" -#: fietsboek/templates/browse.jinja2:29 fietsboek/templates/details.jinja2:102 +#: fietsboek/templates/browse.jinja2:31 fietsboek/templates/details.jinja2:102 msgid "page.details.uphill" msgstr "Bergauf" -#: fietsboek/templates/browse.jinja2:31 fietsboek/templates/details.jinja2:106 +#: fietsboek/templates/browse.jinja2:33 fietsboek/templates/details.jinja2:106 msgid "page.details.downhill" msgstr "Bergab" -#: fietsboek/templates/browse.jinja2:35 fietsboek/templates/details.jinja2:110 +#: fietsboek/templates/browse.jinja2:37 fietsboek/templates/details.jinja2:110 msgid "page.details.moving_time" msgstr "Fahrzeit" -#: fietsboek/templates/browse.jinja2:37 fietsboek/templates/details.jinja2:114 +#: fietsboek/templates/browse.jinja2:39 fietsboek/templates/details.jinja2:114 msgid "page.details.stopped_time" msgstr "Haltezeit" -#: fietsboek/templates/browse.jinja2:41 fietsboek/templates/details.jinja2:118 +#: fietsboek/templates/browse.jinja2:43 fietsboek/templates/details.jinja2:118 msgid "page.details.max_speed" msgstr "maximale Geschwindigkeit" -#: fietsboek/templates/browse.jinja2:43 fietsboek/templates/details.jinja2:122 +#: fietsboek/templates/browse.jinja2:45 fietsboek/templates/details.jinja2:122 msgid "page.details.avg_speed" msgstr "durchschnittliche Geschwindigkeit" -#: fietsboek/templates/browse.jinja2:59 +#: fietsboek/templates/browse.jinja2:60 +msgid "page.browse.download_multiple" +msgstr "ausgewählte Herunterladen" + +#: fietsboek/templates/browse.jinja2:62 msgid "page.browse.no_tracks" msgstr "" "Es wurden keine Strecken gefunden, auf die Du Zugriff hast. Versuche, " @@ -235,27 +239,27 @@ msgstr "" msgid "page.details.download" msgstr "Herunterladen" -#: fietsboek/templates/details.jinja2:140 +#: fietsboek/templates/details.jinja2:161 msgid "page.details.comments" msgstr "Kommentare" -#: fietsboek/templates/details.jinja2:144 +#: fietsboek/templates/details.jinja2:165 msgid "page.details.comments.author" msgstr "Kommentar von {}" -#: fietsboek/templates/details.jinja2:161 +#: fietsboek/templates/details.jinja2:182 msgid "page.details.comments.new.title" msgstr "Kommentar erstellen" -#: fietsboek/templates/details.jinja2:164 +#: fietsboek/templates/details.jinja2:185 msgid "page.details.comments.new.input_title" msgstr "Titel" -#: fietsboek/templates/details.jinja2:165 +#: fietsboek/templates/details.jinja2:186 msgid "page.details.comments.new.input_comment" msgstr "Kommentar" -#: fietsboek/templates/details.jinja2:168 +#: fietsboek/templates/details.jinja2:189 msgid "page.details.comments.new.submit" msgstr "Absenden" @@ -333,6 +337,15 @@ msgstr "Wappen" msgid "page.track.form.description" msgstr "Beschreibung" +#: fietsboek/templates/edit_form.jinja2:94 +#: fietsboek/templates/edit_form.jinja2:106 +msgid "page.track.form.remove_image" +msgstr "Bild entfernen" + +#: fietsboek/templates/edit_form.jinja2:101 +msgid "page.track.form.select_images" +msgstr "Bilder auswählen" + #: fietsboek/templates/finish_upload.jinja2:8 #: fietsboek/templates/upload.jinja2:6 msgid "page.upload.title" @@ -580,7 +593,7 @@ msgstr "Ein Link zum Zurücksetzen des Passworts wurde versandt" msgid "page.password_reset.email.subject" msgstr "Fietsboek Passwortzurücksetzung" -#: fietsboek/views/default.py:139 +#: fietsboek/views/default.py:138 msgid "page.password_reset.email.body" msgstr "" "Du kannst Dein Fietsboek-Passwort hier zurücksetzen: {}\n" @@ -588,15 +601,15 @@ msgstr "" "Falls Du keine Passwortzurücksetzung beantragt hast, dann ignoriere diese" " E-Mail." -#: fietsboek/views/default.py:166 +#: fietsboek/views/default.py:165 msgid "flash.email_verified" msgstr "E-Mail-Adresse bestätigt" -#: fietsboek/views/default.py:180 +#: fietsboek/views/default.py:179 msgid "flash.password_updated" msgstr "Passwort aktualisiert" -#: fietsboek/views/detail.py:71 +#: fietsboek/views/detail.py:77 msgid "flash.track_deleted" msgstr "Strecke gelöscht" @@ -604,19 +617,19 @@ msgstr "Strecke gelöscht" msgid "flash.personal_data_updated" msgstr "Persönliche Daten wurden gespeichert" -#: fietsboek/views/profile.py:78 +#: fietsboek/views/profile.py:77 msgid "flash.friend_not_found" msgstr "Das angegebene Konto wurde nicht gefunden" -#: fietsboek/views/profile.py:83 +#: fietsboek/views/profile.py:82 msgid "flash.friend_already_exists" msgstr "Dieser Freund existiert bereits" -#: fietsboek/views/profile.py:91 +#: fietsboek/views/profile.py:90 msgid "flash.friend_added" msgstr "Freund hinzugefügt" -#: fietsboek/views/profile.py:101 +#: fietsboek/views/profile.py:100 msgid "flash.friend_request_sent" msgstr "Freundschaftsanfrage gesendet" @@ -628,11 +641,11 @@ msgstr "Keine Datei ausgewählt" msgid "flash.invalid_file" msgstr "Ungültige GPX-Datei gesendet" -#: fietsboek/views/upload.py:172 +#: fietsboek/views/upload.py:178 msgid "flash.upload_success" msgstr "Hochladen erfolgreich" -#: fietsboek/views/upload.py:187 +#: fietsboek/views/upload.py:194 msgid "flash.upload_cancelled" msgstr "Hochladen abgebrochen" diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.mo b/fietsboek/locale/en/LC_MESSAGES/messages.mo index 127794c..a9986ee 100644 Binary files a/fietsboek/locale/en/LC_MESSAGES/messages.mo and b/fietsboek/locale/en/LC_MESSAGES/messages.mo differ diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.po b/fietsboek/locale/en/LC_MESSAGES/messages.po index 723c12c..021baa4 100644 --- a/fietsboek/locale/en/LC_MESSAGES/messages.po +++ b/fietsboek/locale/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-07-11 14:34+0200\n" +"POT-Creation-Date: 2022-07-18 19:07+0200\n" "PO-Revision-Date: 2022-06-28 13:11+0200\n" "Last-Translator: \n" "Language: en\n" @@ -18,39 +18,39 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.10.3\n" -#: fietsboek/util.py:262 +#: fietsboek/util.py:273 msgid "password_constraint.mismatch" msgstr "Passwords don't match" -#: fietsboek/util.py:264 +#: fietsboek/util.py:275 msgid "password_constraint.length" msgstr "Password not long enough" -#: fietsboek/models/track.py:494 +#: fietsboek/models/track.py:495 msgid "tooltip.table.length" msgstr "Length" -#: fietsboek/models/track.py:495 +#: fietsboek/models/track.py:496 msgid "tooltip.table.uphill" msgstr "Uphill" -#: fietsboek/models/track.py:496 +#: fietsboek/models/track.py:497 msgid "tooltip.table.downhill" msgstr "Downhill" -#: fietsboek/models/track.py:497 +#: fietsboek/models/track.py:498 msgid "tooltip.table.moving_time" msgstr "Moving Time" -#: fietsboek/models/track.py:498 +#: fietsboek/models/track.py:499 msgid "tooltip.table.stopped_time" msgstr "Stopped Time" -#: fietsboek/models/track.py:499 +#: fietsboek/models/track.py:500 msgid "tooltip.table.max_speed" msgstr "Max Speed" -#: fietsboek/models/track.py:501 +#: fietsboek/models/track.py:502 msgid "tooltip.table.avg_speed" msgstr "Average Speed" @@ -86,47 +86,51 @@ msgstr "Add Badge" msgid "page.browse.title" msgstr "Browse" -#: fietsboek/templates/browse.jinja2:17 fietsboek/templates/details.jinja2:86 +#: fietsboek/templates/browse.jinja2:19 fietsboek/templates/details.jinja2:86 msgid "page.details.date" msgstr "Date" -#: fietsboek/templates/browse.jinja2:19 fietsboek/templates/details.jinja2:98 +#: fietsboek/templates/browse.jinja2:21 fietsboek/templates/details.jinja2:98 msgid "page.details.length" msgstr "Length" -#: fietsboek/templates/browse.jinja2:23 fietsboek/templates/details.jinja2:90 +#: fietsboek/templates/browse.jinja2:25 fietsboek/templates/details.jinja2:90 msgid "page.details.start_time" msgstr "Record Start" -#: fietsboek/templates/browse.jinja2:25 fietsboek/templates/details.jinja2:94 +#: fietsboek/templates/browse.jinja2:27 fietsboek/templates/details.jinja2:94 msgid "page.details.end_time" msgstr "Record End" -#: fietsboek/templates/browse.jinja2:29 fietsboek/templates/details.jinja2:102 +#: fietsboek/templates/browse.jinja2:31 fietsboek/templates/details.jinja2:102 msgid "page.details.uphill" msgstr "Uphill" -#: fietsboek/templates/browse.jinja2:31 fietsboek/templates/details.jinja2:106 +#: fietsboek/templates/browse.jinja2:33 fietsboek/templates/details.jinja2:106 msgid "page.details.downhill" msgstr "Downhill" -#: fietsboek/templates/browse.jinja2:35 fietsboek/templates/details.jinja2:110 +#: fietsboek/templates/browse.jinja2:37 fietsboek/templates/details.jinja2:110 msgid "page.details.moving_time" msgstr "Moving Time" -#: fietsboek/templates/browse.jinja2:37 fietsboek/templates/details.jinja2:114 +#: fietsboek/templates/browse.jinja2:39 fietsboek/templates/details.jinja2:114 msgid "page.details.stopped_time" msgstr "Stopped Time" -#: fietsboek/templates/browse.jinja2:41 fietsboek/templates/details.jinja2:118 +#: fietsboek/templates/browse.jinja2:43 fietsboek/templates/details.jinja2:118 msgid "page.details.max_speed" msgstr "Max Speed" -#: fietsboek/templates/browse.jinja2:43 fietsboek/templates/details.jinja2:122 +#: fietsboek/templates/browse.jinja2:45 fietsboek/templates/details.jinja2:122 msgid "page.details.avg_speed" msgstr "Average Speed" -#: fietsboek/templates/browse.jinja2:59 +#: fietsboek/templates/browse.jinja2:60 +msgid "page.browse.download_multiple" +msgstr "Download selected" + +#: fietsboek/templates/browse.jinja2:62 msgid "page.browse.no_tracks" msgstr "You currently do not have access to any tracks. Try logging in." @@ -231,27 +235,27 @@ msgstr "JavaScript is disabled, please enable JavaScript" msgid "page.details.download" msgstr "Download Tour" -#: fietsboek/templates/details.jinja2:140 +#: fietsboek/templates/details.jinja2:161 msgid "page.details.comments" msgstr "Comments" -#: fietsboek/templates/details.jinja2:144 +#: fietsboek/templates/details.jinja2:165 msgid "page.details.comments.author" msgstr "Comment by {}" -#: fietsboek/templates/details.jinja2:161 +#: fietsboek/templates/details.jinja2:182 msgid "page.details.comments.new.title" msgstr "Create a new comment" -#: fietsboek/templates/details.jinja2:164 +#: fietsboek/templates/details.jinja2:185 msgid "page.details.comments.new.input_title" msgstr "Title" -#: fietsboek/templates/details.jinja2:165 +#: fietsboek/templates/details.jinja2:186 msgid "page.details.comments.new.input_comment" msgstr "Comment" -#: fietsboek/templates/details.jinja2:168 +#: fietsboek/templates/details.jinja2:189 msgid "page.details.comments.new.submit" msgstr "Submit" @@ -329,6 +333,15 @@ msgstr "Badges" msgid "page.track.form.description" msgstr "Description" +#: fietsboek/templates/edit_form.jinja2:94 +#: fietsboek/templates/edit_form.jinja2:106 +msgid "page.track.form.remove_image" +msgstr "Remove image" + +#: fietsboek/templates/edit_form.jinja2:101 +msgid "page.track.form.select_images" +msgstr "Select images" + #: fietsboek/templates/finish_upload.jinja2:8 #: fietsboek/templates/upload.jinja2:6 msgid "page.upload.title" @@ -576,22 +589,22 @@ msgstr "A password reset email has been sent" msgid "page.password_reset.email.subject" msgstr "Fietsboek Password Reset" -#: fietsboek/views/default.py:139 +#: fietsboek/views/default.py:138 msgid "page.password_reset.email.body" msgstr "" "You can reset your Fietsboek password here: {}\n" "\n" "If you did not request a password reset, ignore this email." -#: fietsboek/views/default.py:166 +#: fietsboek/views/default.py:165 msgid "flash.email_verified" msgstr "Your email address has been verified" -#: fietsboek/views/default.py:180 +#: fietsboek/views/default.py:179 msgid "flash.password_updated" msgstr "Password has been updated" -#: fietsboek/views/detail.py:71 +#: fietsboek/views/detail.py:77 msgid "flash.track_deleted" msgstr "Track has been deleted" @@ -599,19 +612,19 @@ msgstr "Track has been deleted" msgid "flash.personal_data_updated" msgstr "Personal data has been updated" -#: fietsboek/views/profile.py:78 +#: fietsboek/views/profile.py:77 msgid "flash.friend_not_found" msgstr "The friend was not found" -#: fietsboek/views/profile.py:83 +#: fietsboek/views/profile.py:82 msgid "flash.friend_already_exists" msgstr "Friend already exists" -#: fietsboek/views/profile.py:91 +#: fietsboek/views/profile.py:90 msgid "flash.friend_added" msgstr "Friend has been added" -#: fietsboek/views/profile.py:101 +#: fietsboek/views/profile.py:100 msgid "flash.friend_request_sent" msgstr "Friend request sent" @@ -623,11 +636,11 @@ msgstr "No file selected" msgid "flash.invalid_file" msgstr "Invalid GPX file selected" -#: fietsboek/views/upload.py:172 +#: fietsboek/views/upload.py:178 msgid "flash.upload_success" msgstr "Upload successful" -#: fietsboek/views/upload.py:187 +#: fietsboek/views/upload.py:194 msgid "flash.upload_cancelled" msgstr "Upload cancelled" diff --git a/fietsboek/locale/fietslog.pot b/fietsboek/locale/fietslog.pot index b3a5837..f89f0c9 100644 --- a/fietsboek/locale/fietslog.pot +++ b/fietsboek/locale/fietslog.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-07-11 14:34+0200\n" +"POT-Creation-Date: 2022-07-18 19:07+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,39 +17,39 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.10.3\n" -#: fietsboek/util.py:262 +#: fietsboek/util.py:273 msgid "password_constraint.mismatch" msgstr "" -#: fietsboek/util.py:264 +#: fietsboek/util.py:275 msgid "password_constraint.length" msgstr "" -#: fietsboek/models/track.py:494 +#: fietsboek/models/track.py:495 msgid "tooltip.table.length" msgstr "" -#: fietsboek/models/track.py:495 +#: fietsboek/models/track.py:496 msgid "tooltip.table.uphill" msgstr "" -#: fietsboek/models/track.py:496 +#: fietsboek/models/track.py:497 msgid "tooltip.table.downhill" msgstr "" -#: fietsboek/models/track.py:497 +#: fietsboek/models/track.py:498 msgid "tooltip.table.moving_time" msgstr "" -#: fietsboek/models/track.py:498 +#: fietsboek/models/track.py:499 msgid "tooltip.table.stopped_time" msgstr "" -#: fietsboek/models/track.py:499 +#: fietsboek/models/track.py:500 msgid "tooltip.table.max_speed" msgstr "" -#: fietsboek/models/track.py:501 +#: fietsboek/models/track.py:502 msgid "tooltip.table.avg_speed" msgstr "" @@ -85,47 +85,51 @@ msgstr "" msgid "page.browse.title" msgstr "" -#: fietsboek/templates/browse.jinja2:17 fietsboek/templates/details.jinja2:86 +#: fietsboek/templates/browse.jinja2:19 fietsboek/templates/details.jinja2:86 msgid "page.details.date" msgstr "" -#: fietsboek/templates/browse.jinja2:19 fietsboek/templates/details.jinja2:98 +#: fietsboek/templates/browse.jinja2:21 fietsboek/templates/details.jinja2:98 msgid "page.details.length" msgstr "" -#: fietsboek/templates/browse.jinja2:23 fietsboek/templates/details.jinja2:90 +#: fietsboek/templates/browse.jinja2:25 fietsboek/templates/details.jinja2:90 msgid "page.details.start_time" msgstr "" -#: fietsboek/templates/browse.jinja2:25 fietsboek/templates/details.jinja2:94 +#: fietsboek/templates/browse.jinja2:27 fietsboek/templates/details.jinja2:94 msgid "page.details.end_time" msgstr "" -#: fietsboek/templates/browse.jinja2:29 fietsboek/templates/details.jinja2:102 +#: fietsboek/templates/browse.jinja2:31 fietsboek/templates/details.jinja2:102 msgid "page.details.uphill" msgstr "" -#: fietsboek/templates/browse.jinja2:31 fietsboek/templates/details.jinja2:106 +#: fietsboek/templates/browse.jinja2:33 fietsboek/templates/details.jinja2:106 msgid "page.details.downhill" msgstr "" -#: fietsboek/templates/browse.jinja2:35 fietsboek/templates/details.jinja2:110 +#: fietsboek/templates/browse.jinja2:37 fietsboek/templates/details.jinja2:110 msgid "page.details.moving_time" msgstr "" -#: fietsboek/templates/browse.jinja2:37 fietsboek/templates/details.jinja2:114 +#: fietsboek/templates/browse.jinja2:39 fietsboek/templates/details.jinja2:114 msgid "page.details.stopped_time" msgstr "" -#: fietsboek/templates/browse.jinja2:41 fietsboek/templates/details.jinja2:118 +#: fietsboek/templates/browse.jinja2:43 fietsboek/templates/details.jinja2:118 msgid "page.details.max_speed" msgstr "" -#: fietsboek/templates/browse.jinja2:43 fietsboek/templates/details.jinja2:122 +#: fietsboek/templates/browse.jinja2:45 fietsboek/templates/details.jinja2:122 msgid "page.details.avg_speed" msgstr "" -#: fietsboek/templates/browse.jinja2:59 +#: fietsboek/templates/browse.jinja2:60 +msgid "page.browse.download_multiple" +msgstr "" + +#: fietsboek/templates/browse.jinja2:62 msgid "page.browse.no_tracks" msgstr "" @@ -230,27 +234,27 @@ msgstr "" msgid "page.details.download" msgstr "" -#: fietsboek/templates/details.jinja2:140 +#: fietsboek/templates/details.jinja2:161 msgid "page.details.comments" msgstr "" -#: fietsboek/templates/details.jinja2:144 +#: fietsboek/templates/details.jinja2:165 msgid "page.details.comments.author" msgstr "" -#: fietsboek/templates/details.jinja2:161 +#: fietsboek/templates/details.jinja2:182 msgid "page.details.comments.new.title" msgstr "" -#: fietsboek/templates/details.jinja2:164 +#: fietsboek/templates/details.jinja2:185 msgid "page.details.comments.new.input_title" msgstr "" -#: fietsboek/templates/details.jinja2:165 +#: fietsboek/templates/details.jinja2:186 msgid "page.details.comments.new.input_comment" msgstr "" -#: fietsboek/templates/details.jinja2:168 +#: fietsboek/templates/details.jinja2:189 msgid "page.details.comments.new.submit" msgstr "" @@ -326,6 +330,15 @@ msgstr "" msgid "page.track.form.description" msgstr "" +#: fietsboek/templates/edit_form.jinja2:94 +#: fietsboek/templates/edit_form.jinja2:106 +msgid "page.track.form.remove_image" +msgstr "" + +#: fietsboek/templates/edit_form.jinja2:101 +msgid "page.track.form.select_images" +msgstr "" + #: fietsboek/templates/finish_upload.jinja2:8 #: fietsboek/templates/upload.jinja2:6 msgid "page.upload.title" @@ -568,19 +581,19 @@ msgstr "" msgid "page.password_reset.email.subject" msgstr "" -#: fietsboek/views/default.py:139 +#: fietsboek/views/default.py:138 msgid "page.password_reset.email.body" msgstr "" -#: fietsboek/views/default.py:166 +#: fietsboek/views/default.py:165 msgid "flash.email_verified" msgstr "" -#: fietsboek/views/default.py:180 +#: fietsboek/views/default.py:179 msgid "flash.password_updated" msgstr "" -#: fietsboek/views/detail.py:71 +#: fietsboek/views/detail.py:77 msgid "flash.track_deleted" msgstr "" @@ -588,19 +601,19 @@ msgstr "" msgid "flash.personal_data_updated" msgstr "" -#: fietsboek/views/profile.py:78 +#: fietsboek/views/profile.py:77 msgid "flash.friend_not_found" msgstr "" -#: fietsboek/views/profile.py:83 +#: fietsboek/views/profile.py:82 msgid "flash.friend_already_exists" msgstr "" -#: fietsboek/views/profile.py:91 +#: fietsboek/views/profile.py:90 msgid "flash.friend_added" msgstr "" -#: fietsboek/views/profile.py:101 +#: fietsboek/views/profile.py:100 msgid "flash.friend_request_sent" msgstr "" @@ -612,11 +625,11 @@ msgstr "" msgid "flash.invalid_file" msgstr "" -#: fietsboek/views/upload.py:172 +#: fietsboek/views/upload.py:178 msgid "flash.upload_success" msgstr "" -#: fietsboek/views/upload.py:187 +#: fietsboek/views/upload.py:194 msgid "flash.upload_cancelled" msgstr "" -- cgit v1.2.3 From 22514b18f3f01287b88c235c91bdd8cacf8ac085 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 18 Jul 2022 19:14:53 +0200 Subject: update changelog --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 766e322..951e015 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +Unreleased +---------- + +**Added** +- A button to delete tracks. +- A small "user menu" in the top right corner. +- A button to download multiple tracks in the "Browse" view. +- Image uploading. + +**Changed** +- The position of the login/logout buttons. +- Style of the "Tag friend" button. + +**Fixed** +- The track editing overwriting the timezone information. +- The manifest file for inclusion of the README and CHANGELOG. + 0.1.0 - 2022-07-09 ------------------ -- cgit v1.2.3 From 7b9e8b1ee61b9211dc2c9dac59e29f4b7a5a7f15 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 18 Jul 2022 20:37:56 +0200 Subject: remove images when removing a track --- fietsboek/data.py | 20 ++++++++++++++++++++ fietsboek/views/detail.py | 2 ++ 2 files changed, 22 insertions(+) diff --git a/fietsboek/data.py b/fietsboek/data.py index 992ddf7..c1cd214 100644 --- a/fietsboek/data.py +++ b/fietsboek/data.py @@ -7,10 +7,14 @@ import random import string import shutil import uuid +import logging from .util import secure_filename +LOGGER = logging.getLogger(__name__) + + def generate_filename(filename): """Generates a safe-to-use filename for uploads. @@ -63,6 +67,22 @@ class DataManager: images.append(image.name) return images + def purge(self, track_id): + """Purge all data pertaining to the given track. + + This function logs errors but raises no exception, as such it can + always be used to clean up after a track. + + :param track_id: The ID of the track. + :type track_id: int + """ + def log_error(_, path, exc_info): + LOGGER.warning("Failed to remove %s", path, exc_info=exc_info) + + path = self._track_data_dir(track_id) + if path.is_dir(): + shutil.rmtree(path, ignore_errors=False, onerror=log_error) + def image_path(self, track_id, image_id): """Returns a path to a saved image. diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py index 3953f47..0c72d64 100644 --- a/fietsboek/views/detail.py +++ b/fietsboek/views/detail.py @@ -73,7 +73,9 @@ def delete_track(request): :rtype: pyramid.response.Response """ track = request.context + track_id = track.id request.dbsession.delete(track) + request.data_manager.purge(track_id) request.session.flash(request.localizer.translate(_("flash.track_deleted"))) return HTTPFound(request.route_url('home')) -- cgit v1.2.3 From d53483c918965ccd510b17a2bf6cfd49d6974aed Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Tue, 19 Jul 2022 19:42:10 +0200 Subject: no round borders in image preview --- fietsboek/static/theme.css | 1 - 1 file changed, 1 deletion(-) diff --git a/fietsboek/static/theme.css b/fietsboek/static/theme.css index 076b519..3cda0d7 100644 --- a/fietsboek/static/theme.css +++ b/fietsboek/static/theme.css @@ -48,7 +48,6 @@ strong { position: relative; width: min(317px, 100%); height: calc(9 / 16 * 317px); - border-radius: 5px; border: 1px solid grey; margin-right: 5px; margin-bottom: 5px; -- cgit v1.2.3 From 85a3ab7c64d9c8f6b758cad6f265903af3838009 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 21 Jul 2022 23:51:51 +0200 Subject: allow descriptions to be added to images Even though the change is "relatively simple", it does add quite a bit of stuff since now we do need metadata in the database. --- .../alembic/versions/20220721_091ce24409fe.py | 30 ++++++++ fietsboek/locale/de/LC_MESSAGES/messages.mo | Bin 10063 -> 10209 bytes fietsboek/locale/de/LC_MESSAGES/messages.po | 42 ++++++----- fietsboek/locale/en/LC_MESSAGES/messages.mo | Bin 9471 -> 9612 bytes fietsboek/locale/en/LC_MESSAGES/messages.po | 42 ++++++----- fietsboek/locale/fietslog.pot | 42 ++++++----- fietsboek/models/__init__.py | 1 + fietsboek/models/image.py | 62 ++++++++++++++++ fietsboek/models/track.py | 3 + fietsboek/static/fietsboek.js | 40 ++++++++++- fietsboek/static/theme.css | 26 ++++++- fietsboek/templates/details.jinja2 | 7 +- fietsboek/templates/edit_form.jinja2 | 25 ++++++- fietsboek/views/detail.py | 17 +++-- fietsboek/views/edit.py | 78 ++++++++++++++++++--- 15 files changed, 344 insertions(+), 71 deletions(-) create mode 100644 fietsboek/alembic/versions/20220721_091ce24409fe.py create mode 100644 fietsboek/models/image.py diff --git a/fietsboek/alembic/versions/20220721_091ce24409fe.py b/fietsboek/alembic/versions/20220721_091ce24409fe.py new file mode 100644 index 0000000..88044b6 --- /dev/null +++ b/fietsboek/alembic/versions/20220721_091ce24409fe.py @@ -0,0 +1,30 @@ +"""add image metadata to uploads + +Revision ID: 091ce24409fe +Revises: c89d9bdbfa68 +Create Date: 2022-07-21 23:24:54.241170 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '091ce24409fe' +down_revision = 'c89d9bdbfa68' +branch_labels = None +depends_on = None + +def upgrade(): + op.create_table('image_metadata', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('track_id', sa.Integer(), nullable=False), + sa.Column('image_name', sa.Text(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['track_id'], ['tracks.id'], name=op.f('fk_image_metadata_track_id_tracks')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_image_metadata')), + sa.UniqueConstraint('track_id', 'image_name', name=op.f('uq_image_metadata_track_id')) + ) + +def downgrade(): + op.drop_table('image_metadata') diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.mo b/fietsboek/locale/de/LC_MESSAGES/messages.mo index b6375fa..7d7af8d 100644 Binary files a/fietsboek/locale/de/LC_MESSAGES/messages.mo and b/fietsboek/locale/de/LC_MESSAGES/messages.mo differ diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.po b/fietsboek/locale/de/LC_MESSAGES/messages.po index 27c840f..35a6bd4 100644 --- a/fietsboek/locale/de/LC_MESSAGES/messages.po +++ b/fietsboek/locale/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-07-18 19:07+0200\n" +"POT-Creation-Date: 2022-07-21 23:49+0200\n" "PO-Revision-Date: 2022-07-02 17:35+0200\n" "Last-Translator: FULL NAME \n" "Language: de\n" @@ -26,31 +26,31 @@ msgstr "Passwörter stimmen nicht überein" msgid "password_constraint.length" msgstr "Passwort zu kurz" -#: fietsboek/models/track.py:495 +#: fietsboek/models/track.py:498 msgid "tooltip.table.length" msgstr "Länge" -#: fietsboek/models/track.py:496 +#: fietsboek/models/track.py:499 msgid "tooltip.table.uphill" msgstr "Bergauf" -#: fietsboek/models/track.py:497 +#: fietsboek/models/track.py:500 msgid "tooltip.table.downhill" msgstr "Bergab" -#: fietsboek/models/track.py:498 +#: fietsboek/models/track.py:501 msgid "tooltip.table.moving_time" msgstr "Fahrzeit" -#: fietsboek/models/track.py:499 +#: fietsboek/models/track.py:502 msgid "tooltip.table.stopped_time" msgstr "Haltezeit" -#: fietsboek/models/track.py:500 +#: fietsboek/models/track.py:503 msgid "tooltip.table.max_speed" msgstr "Maximalgeschwindigkeit" -#: fietsboek/models/track.py:502 +#: fietsboek/models/track.py:505 msgid "tooltip.table.avg_speed" msgstr "Durchschnittsgeschwindigkeit" @@ -239,27 +239,27 @@ msgstr "" msgid "page.details.download" msgstr "Herunterladen" -#: fietsboek/templates/details.jinja2:161 +#: fietsboek/templates/details.jinja2:166 msgid "page.details.comments" msgstr "Kommentare" -#: fietsboek/templates/details.jinja2:165 +#: fietsboek/templates/details.jinja2:170 msgid "page.details.comments.author" msgstr "Kommentar von {}" -#: fietsboek/templates/details.jinja2:182 +#: fietsboek/templates/details.jinja2:187 msgid "page.details.comments.new.title" msgstr "Kommentar erstellen" -#: fietsboek/templates/details.jinja2:185 +#: fietsboek/templates/details.jinja2:190 msgid "page.details.comments.new.input_title" msgstr "Titel" -#: fietsboek/templates/details.jinja2:186 +#: fietsboek/templates/details.jinja2:191 msgid "page.details.comments.new.input_comment" msgstr "Kommentar" -#: fietsboek/templates/details.jinja2:189 +#: fietsboek/templates/details.jinja2:194 msgid "page.details.comments.new.submit" msgstr "Absenden" @@ -338,14 +338,22 @@ msgid "page.track.form.description" msgstr "Beschreibung" #: fietsboek/templates/edit_form.jinja2:94 -#: fietsboek/templates/edit_form.jinja2:106 +#: fietsboek/templates/edit_form.jinja2:108 msgid "page.track.form.remove_image" msgstr "Bild entfernen" -#: fietsboek/templates/edit_form.jinja2:101 +#: fietsboek/templates/edit_form.jinja2:103 msgid "page.track.form.select_images" msgstr "Bilder auswählen" +#: fietsboek/templates/edit_form.jinja2:119 +msgid "page.track.form.image_description_modal" +msgstr "Bildbeschreibung" + +#: fietsboek/templates/edit_form.jinja2:126 +msgid "page.track.form.image_description_modal.save" +msgstr "Übernehmen" + #: fietsboek/templates/finish_upload.jinja2:8 #: fietsboek/templates/upload.jinja2:6 msgid "page.upload.title" @@ -609,7 +617,7 @@ msgstr "E-Mail-Adresse bestätigt" msgid "flash.password_updated" msgstr "Passwort aktualisiert" -#: fietsboek/views/detail.py:77 +#: fietsboek/views/detail.py:88 msgid "flash.track_deleted" msgstr "Strecke gelöscht" diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.mo b/fietsboek/locale/en/LC_MESSAGES/messages.mo index a9986ee..8261149 100644 Binary files a/fietsboek/locale/en/LC_MESSAGES/messages.mo and b/fietsboek/locale/en/LC_MESSAGES/messages.mo differ diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.po b/fietsboek/locale/en/LC_MESSAGES/messages.po index 021baa4..c19184c 100644 --- a/fietsboek/locale/en/LC_MESSAGES/messages.po +++ b/fietsboek/locale/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-07-18 19:07+0200\n" +"POT-Creation-Date: 2022-07-21 23:49+0200\n" "PO-Revision-Date: 2022-06-28 13:11+0200\n" "Last-Translator: \n" "Language: en\n" @@ -26,31 +26,31 @@ msgstr "Passwords don't match" msgid "password_constraint.length" msgstr "Password not long enough" -#: fietsboek/models/track.py:495 +#: fietsboek/models/track.py:498 msgid "tooltip.table.length" msgstr "Length" -#: fietsboek/models/track.py:496 +#: fietsboek/models/track.py:499 msgid "tooltip.table.uphill" msgstr "Uphill" -#: fietsboek/models/track.py:497 +#: fietsboek/models/track.py:500 msgid "tooltip.table.downhill" msgstr "Downhill" -#: fietsboek/models/track.py:498 +#: fietsboek/models/track.py:501 msgid "tooltip.table.moving_time" msgstr "Moving Time" -#: fietsboek/models/track.py:499 +#: fietsboek/models/track.py:502 msgid "tooltip.table.stopped_time" msgstr "Stopped Time" -#: fietsboek/models/track.py:500 +#: fietsboek/models/track.py:503 msgid "tooltip.table.max_speed" msgstr "Max Speed" -#: fietsboek/models/track.py:502 +#: fietsboek/models/track.py:505 msgid "tooltip.table.avg_speed" msgstr "Average Speed" @@ -235,27 +235,27 @@ msgstr "JavaScript is disabled, please enable JavaScript" msgid "page.details.download" msgstr "Download Tour" -#: fietsboek/templates/details.jinja2:161 +#: fietsboek/templates/details.jinja2:166 msgid "page.details.comments" msgstr "Comments" -#: fietsboek/templates/details.jinja2:165 +#: fietsboek/templates/details.jinja2:170 msgid "page.details.comments.author" msgstr "Comment by {}" -#: fietsboek/templates/details.jinja2:182 +#: fietsboek/templates/details.jinja2:187 msgid "page.details.comments.new.title" msgstr "Create a new comment" -#: fietsboek/templates/details.jinja2:185 +#: fietsboek/templates/details.jinja2:190 msgid "page.details.comments.new.input_title" msgstr "Title" -#: fietsboek/templates/details.jinja2:186 +#: fietsboek/templates/details.jinja2:191 msgid "page.details.comments.new.input_comment" msgstr "Comment" -#: fietsboek/templates/details.jinja2:189 +#: fietsboek/templates/details.jinja2:194 msgid "page.details.comments.new.submit" msgstr "Submit" @@ -334,14 +334,22 @@ msgid "page.track.form.description" msgstr "Description" #: fietsboek/templates/edit_form.jinja2:94 -#: fietsboek/templates/edit_form.jinja2:106 +#: fietsboek/templates/edit_form.jinja2:108 msgid "page.track.form.remove_image" msgstr "Remove image" -#: fietsboek/templates/edit_form.jinja2:101 +#: fietsboek/templates/edit_form.jinja2:103 msgid "page.track.form.select_images" msgstr "Select images" +#: fietsboek/templates/edit_form.jinja2:119 +msgid "page.track.form.image_description_modal" +msgstr "Image description" + +#: fietsboek/templates/edit_form.jinja2:126 +msgid "page.track.form.image_description_modal.save" +msgstr "Apply" + #: fietsboek/templates/finish_upload.jinja2:8 #: fietsboek/templates/upload.jinja2:6 msgid "page.upload.title" @@ -604,7 +612,7 @@ msgstr "Your email address has been verified" msgid "flash.password_updated" msgstr "Password has been updated" -#: fietsboek/views/detail.py:77 +#: fietsboek/views/detail.py:88 msgid "flash.track_deleted" msgstr "Track has been deleted" diff --git a/fietsboek/locale/fietslog.pot b/fietsboek/locale/fietslog.pot index f89f0c9..a30c79c 100644 --- a/fietsboek/locale/fietslog.pot +++ b/fietsboek/locale/fietslog.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2022-07-18 19:07+0200\n" +"POT-Creation-Date: 2022-07-21 23:49+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -25,31 +25,31 @@ msgstr "" msgid "password_constraint.length" msgstr "" -#: fietsboek/models/track.py:495 +#: fietsboek/models/track.py:498 msgid "tooltip.table.length" msgstr "" -#: fietsboek/models/track.py:496 +#: fietsboek/models/track.py:499 msgid "tooltip.table.uphill" msgstr "" -#: fietsboek/models/track.py:497 +#: fietsboek/models/track.py:500 msgid "tooltip.table.downhill" msgstr "" -#: fietsboek/models/track.py:498 +#: fietsboek/models/track.py:501 msgid "tooltip.table.moving_time" msgstr "" -#: fietsboek/models/track.py:499 +#: fietsboek/models/track.py:502 msgid "tooltip.table.stopped_time" msgstr "" -#: fietsboek/models/track.py:500 +#: fietsboek/models/track.py:503 msgid "tooltip.table.max_speed" msgstr "" -#: fietsboek/models/track.py:502 +#: fietsboek/models/track.py:505 msgid "tooltip.table.avg_speed" msgstr "" @@ -234,27 +234,27 @@ msgstr "" msgid "page.details.download" msgstr "" -#: fietsboek/templates/details.jinja2:161 +#: fietsboek/templates/details.jinja2:166 msgid "page.details.comments" msgstr "" -#: fietsboek/templates/details.jinja2:165 +#: fietsboek/templates/details.jinja2:170 msgid "page.details.comments.author" msgstr "" -#: fietsboek/templates/details.jinja2:182 +#: fietsboek/templates/details.jinja2:187 msgid "page.details.comments.new.title" msgstr "" -#: fietsboek/templates/details.jinja2:185 +#: fietsboek/templates/details.jinja2:190 msgid "page.details.comments.new.input_title" msgstr "" -#: fietsboek/templates/details.jinja2:186 +#: fietsboek/templates/details.jinja2:191 msgid "page.details.comments.new.input_comment" msgstr "" -#: fietsboek/templates/details.jinja2:189 +#: fietsboek/templates/details.jinja2:194 msgid "page.details.comments.new.submit" msgstr "" @@ -331,14 +331,22 @@ msgid "page.track.form.description" msgstr "" #: fietsboek/templates/edit_form.jinja2:94 -#: fietsboek/templates/edit_form.jinja2:106 +#: fietsboek/templates/edit_form.jinja2:108 msgid "page.track.form.remove_image" msgstr "" -#: fietsboek/templates/edit_form.jinja2:101 +#: fietsboek/templates/edit_form.jinja2:103 msgid "page.track.form.select_images" msgstr "" +#: fietsboek/templates/edit_form.jinja2:119 +msgid "page.track.form.image_description_modal" +msgstr "" + +#: fietsboek/templates/edit_form.jinja2:126 +msgid "page.track.form.image_description_modal.save" +msgstr "" + #: fietsboek/templates/finish_upload.jinja2:8 #: fietsboek/templates/upload.jinja2:6 msgid "page.upload.title" @@ -593,7 +601,7 @@ msgstr "" msgid "flash.password_updated" msgstr "" -#: fietsboek/views/detail.py:77 +#: fietsboek/views/detail.py:88 msgid "flash.track_deleted" msgstr "" diff --git a/fietsboek/models/__init__.py b/fietsboek/models/__init__.py index 02d6257..53feb22 100644 --- a/fietsboek/models/__init__.py +++ b/fietsboek/models/__init__.py @@ -14,6 +14,7 @@ from .user import User, FriendRequest, Token # flake8: noqa from .badge import Badge # flake8: noqa from .track import Tag, Track, TrackCache, Upload # flake8: noqa from .comment import Comment # flake8: noqa +from .image import ImageMetadata # flake8: noqa # Run ``configure_mappers`` after defining all of the models to ensure # all relationships can be setup. diff --git a/fietsboek/models/image.py b/fietsboek/models/image.py new file mode 100644 index 0000000..4037619 --- /dev/null +++ b/fietsboek/models/image.py @@ -0,0 +1,62 @@ +"""Image metadata. + +The actual image data is saved on disk, we only store the metadata such as an +image description here. +""" +from sqlalchemy import ( + Column, + Integer, + ForeignKey, + Text, + UniqueConstraint, + select, +) +from sqlalchemy.orm import relationship + +from .meta import Base + + +class ImageMetadata(Base): + """Represents metadata for an uploaded image. + + :ivar id: Database ID. + :vartype id: int + :ivar track_id: ID of the track that this image belongs to. + :vartype track_id: int + :ivar image_name: Name of the image file. + :vartype image_name: str + :ivar description: The description that should be shown. + :vartype description: str + :ivar track: The track that this image belongs to. + :vartype track: fietsboek.models.track.Track + """ + # pylint: disable=too-few-public-methods + __tablename__ = "image_metadata" + id = Column(Integer, primary_key=True) + track_id = Column(Integer, ForeignKey("tracks.id"), nullable=False) + image_name = Column(Text, nullable=False) + description = Column(Text) + + track = relationship('Track', back_populates='images') + + __table_args__ = (UniqueConstraint('track_id', 'image_name'),) + + @classmethod + def get_or_create(cls, dbsession, track, image_name): + """Retrieves the image metadata for the given image, or creates a new + one if it doesn't exist. + + :param dbsession: The database session. + :type dbsession: sqlalchemy.orm.session.Session + :param track: The track. + :type track: fietsboek.models.track.Track + :param image_name: The name of the image. + :type image_name: str + :return: The created/retrieved object. + :rtype: ImageMetadata + """ + query = select(cls).filter_by(track=track, image_name=image_name) + result = dbsession.execute(query).scalar_one_or_none() + if result: + return result + return cls(track=track, image_name=image_name) diff --git a/fietsboek/models/track.py b/fietsboek/models/track.py index 95344d9..83c725f 100644 --- a/fietsboek/models/track.py +++ b/fietsboek/models/track.py @@ -160,6 +160,8 @@ class Track(Base): :vartype badges: list[fietsboek.models.badge.Badge] :ivar comments: Comments left on this track. :vartype comments: list[fietsboek.models.comment.Comment] + :ivar images: Metadata of the images saved for this track. + :vartype images: list[fietsboek.models.image.ImageMetadata] """ __tablename__ = 'tracks' id = Column(Integer, primary_key=True) @@ -180,6 +182,7 @@ class Track(Base): badges = relationship('Badge', secondary=track_badge_assoc, back_populates='tracks') tags = relationship('Tag', back_populates='track', cascade="all, delete-orphan") comments = relationship('Comment', back_populates='track', cascade="all, delete-orphan") + images = relationship('ImageMetadata', back_populates='track', cascade="all, delete-orphan") @classmethod def factory(cls, request): diff --git a/fietsboek/static/fietsboek.js b/fietsboek/static/fietsboek.js index e403628..3a38a76 100644 --- a/fietsboek/static/fietsboek.js +++ b/fietsboek/static/fietsboek.js @@ -73,10 +73,12 @@ function imageSelectorChanged(event) { console.log(event.target.files); for (var file of event.target.files) { + window.fietsboek_image_index++; + const input = document.createElement("input"); input.type = "file"; input.hidden = true; - input.name = "image[]"; + input.name = `image[${window.fietsboek_image_index}]`; const transfer = new DataTransfer(); transfer.items.add(file); @@ -86,6 +88,8 @@ function imageSelectorChanged(event) { preview.removeAttribute("id"); preview.querySelector("img").src = URL.createObjectURL(file); preview.querySelector("button.delete-image").addEventListener("click", deleteImageButtonClicked); + preview.querySelector("button.edit-image-description").addEventListener("click", editImageDescriptionClicked); + preview.querySelector("input.image-description-input").name = `image-description[${window.fietsboek_image_index}]`; preview.appendChild(input); document.querySelector("#trackImageList").appendChild(preview); @@ -104,14 +108,38 @@ function deleteImageButtonClicked(event) { } /* Otherwise, we need to remove it but also insert a "delete-image" input */ - let deleter = preview.querySelector("input[type=hidden]"); + let deleter = preview.querySelector("input.image-deleter-input"); deleter.removeAttribute("disabled"); preview.removeChild(deleter); preview.parentNode.appendChild(deleter); preview.parentNode.removeChild(preview); } +function editImageDescriptionClicked(event) { + window.fietsboekCurrentImage = event.target.closest("div"); + + let currentDescription = event.target.closest("div").querySelector("input.image-description-input").value; + let modalDom = document.getElementById("imageDescriptionModal"); + modalDom.querySelector("textarea").value = currentDescription; + + let modal = bootstrap.Modal.getOrCreateInstance(modalDom, {}); + modal.show(); +} + +function saveImageDescriptionClicked(event) { + let modalDom = document.getElementById("imageDescriptionModal"); + let wantedDescription = modalDom.querySelector("textarea").value; + window.fietsboekCurrentImage.querySelector("input.image-description-input").value = wantedDescription; + window.fietsboekCurrentImage.querySelector("img").title = wantedDescription; + + let modal = bootstrap.Modal.getOrCreateInstance(modalDom, {}); + modal.hide(); + + window.fietsboekCurrentImage = undefined; +} + document.addEventListener('DOMContentLoaded', function(event) { + window.fietsboek_image_index = 0; /* Enable the "Add tag" button in the track edit page */ let $ = (selector) => document.querySelector(selector); var button = $("#add-tag-btn"); @@ -203,4 +231,12 @@ document.addEventListener('DOMContentLoaded', function(event) { document.querySelectorAll("button.delete-image").forEach((b) => { b.addEventListener("click", deleteImageButtonClicked); }); + + /* Enable the "edit image description" buttons */ + document.querySelectorAll("button.edit-image-description").forEach((b) => { + b.addEventListener("click", editImageDescriptionClicked); + }); + document.querySelectorAll("#imageDescriptionModal button.btn-success").forEach((b) => { + b.addEventListener("click", saveImageDescriptionClicked); + }); }); diff --git a/fietsboek/static/theme.css b/fietsboek/static/theme.css index 3cda0d7..01ee06e 100644 --- a/fietsboek/static/theme.css +++ b/fietsboek/static/theme.css @@ -23,18 +23,42 @@ strong { margin: auto; } +.track-image-caption { + text-align: center; +} + #trackImageList { display: flex; flex-wrap: wrap; } -.track-image-preview button { +.track-image-preview .delete-image { position: absolute; z-index: 5; background-color: white; right: 0px; } +.track-image-preview .edit-image-description { + position: absolute; + z-index: 5; + right: 0px; + top: 2em; + width: 1em; + height: 1em; + border-radius: 0.375em; + border: 0; + box-sizing: content-box; + color: #000; + opacity: 0.5; + padding: 0.25em 0.25em; +} + +.track-image-preview .edit-image-description i { + position: relative; + top: -0.25em; +} + .track-image-preview img { max-width: 100%; max-height: 100%; diff --git a/fietsboek/templates/details.jinja2 b/fietsboek/templates/details.jinja2 index 2ec62ec..4dcd5ee 100644 --- a/fietsboek/templates/details.jinja2 +++ b/fietsboek/templates/details.jinja2 @@ -140,11 +140,16 @@ {% if images %} @@ -104,7 +106,26 @@
+ +
+ + {% endmacro %} diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py index 0c72d64..e20df44 100644 --- a/fietsboek/views/detail.py +++ b/fietsboek/views/detail.py @@ -6,6 +6,8 @@ from pyramid.response import Response, FileResponse from pyramid.i18n import TranslationString as _ from pyramid.httpexceptions import HTTPFound, HTTPNotFound +from sqlalchemy import select + from .. import models, util @@ -22,10 +24,17 @@ def details(request): track = request.context description = util.safe_markdown(track.description) show_edit_link = (track.owner == request.identity) - images = [ - request.route_url("image", track_id=track.id, image_name=image) - for image in request.data_manager.images(track.id) - ] + + images = [] + for image_name in request.data_manager.images(track.id): + img_src = request.route_url("image", track_id=track.id, image_name=image_name) + query = select(models.ImageMetadata).filter_by(track=track, image_name=image_name) + image_metadata = request.dbsession.execute(query).scalar_one_or_none() + if image_metadata: + images.append((img_src, image_metadata.description)) + else: + images.append((img_src, "")) + return { 'track': track, 'show_edit_link': show_edit_link, diff --git a/fietsboek/views/edit.py b/fietsboek/views/edit.py index 04ed000..2c46527 100644 --- a/fietsboek/views/edit.py +++ b/fietsboek/views/edit.py @@ -1,4 +1,6 @@ """Views for editing a track.""" +import re +import logging import datetime from collections import namedtuple @@ -11,7 +13,9 @@ from .. import models, util from ..models.track import Visibility -_Image = namedtuple("_Image", "name url") +ImageEmbed = namedtuple("ImageEmbed", "name url description") + +LOGGER = logging.getLogger(__name__) @view_config(route_name='edit', renderer='fietsboek:templates/edit.jinja2', @@ -27,10 +31,19 @@ def edit(request): track = request.context badges = request.dbsession.execute(select(models.Badge)).scalars() badges = [(badge in track.badges, badge) for badge in badges] - images = [ - _Image(image, request.route_url("image", track_id=track.id, image_name=image)) - for image in request.data_manager.images(track.id) - ] + + images = [] + for image in request.data_manager.images(track.id): + metadata = request.dbsession.execute( + select(models.ImageMetadata).filter_by(track=track, image_name=image) + ).scalar_one_or_none() + if metadata: + description = metadata.description + else: + description = "" + img_src = request.route_url("image", track_id=track.id, image_name=image) + images.append(ImageEmbed(image, img_src, description)) + return { 'track': track, 'badges': badges, @@ -70,12 +83,57 @@ def do_edit(request): tags = request.params.getall("tag[]") track.sync_tags(tags) - for image in request.params.getall("image[]"): - if image == b"": - continue - request.data_manager.add_image(track.id, image.file, image.filename) + _edit_images(request) + return HTTPFound(request.route_url('details', track_id=track.id)) + + +def _edit_images(request): + track = request.context + + # Delete requested images for image in request.params.getall("delete-image[]"): request.data_manager.delete_image(track.id, image) + image_meta = request.dbsession.execute( + select(models.ImageMetadata).filter_by(track_id=track.id, image_name=image) + ).scalar_one_or_none() + LOGGER.debug("Deleted image %s %s (metadata: %s)", track.id, image, image_meta) + if image_meta: + request.dbsession.delete(image_meta) + + # Add new images + set_descriptions = set() + for param_name, image in request.params.items(): + match = re.match("image\\[(\\d+)\\]$", param_name) + if not match: + continue + # Sent for the multi input + if image == b"": + continue - return HTTPFound(request.route_url('details', track_id=track.id)) + upload_id = match.group(1) + image_name = request.data_manager.add_image(track.id, image.file, image.filename) + image_meta = models.ImageMetadata(track=track, image_name=image_name) + image_meta.description = request.params.get(f"image-description[{upload_id}]", "") + request.dbsession.add(image_meta) + LOGGER.debug("Uploaded image %s %s", track.id, image_name) + set_descriptions.add(upload_id) + + images = request.data_manager.images(track.id) + # Set image descriptions + for param_name, description in request.params.items(): + match = re.match("image-description\\[(.+)\\]$", param_name) + if not match: + continue + image_id = match.group(1) + # Descriptions that we already set while adding new images can be + # ignored + if image_id in set_descriptions: + continue + # Did someone give us a wrong ID?! + if image_id not in images: + LOGGER.info("Got a ghost image description for track %s: %s", track.id, image_id) + continue + image_meta = models.ImageMetadata.get_or_create(request.dbsession, track, image_id) + image_meta.description = description + request.dbsession.add(image_meta) -- cgit v1.2.3 From d2a8a75667e7e2ce5c7bdd7fd351136b70ec7913 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Thu, 21 Jul 2022 23:53:23 +0200 Subject: remove leftover console.log --- fietsboek/static/fietsboek.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/fietsboek/static/fietsboek.js b/fietsboek/static/fietsboek.js index 3a38a76..15cb08c 100644 --- a/fietsboek/static/fietsboek.js +++ b/fietsboek/static/fietsboek.js @@ -70,8 +70,6 @@ function removeFriendClicked(event) { } function imageSelectorChanged(event) { - console.log(event.target.files); - for (var file of event.target.files) { window.fietsboek_image_index++; -- cgit v1.2.3