aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2022-07-21 23:51:51 +0200
committerDaniel Schadt <kingdread@gmx.de>2022-07-21 23:51:51 +0200
commit85a3ab7c64d9c8f6b758cad6f265903af3838009 (patch)
treea1b0d1b68c42c772befdc9ddca0cc5bcf5a486e8
parentd53483c918965ccd510b17a2bf6cfd49d6974aed (diff)
downloadfietsboek-85a3ab7c64d9c8f6b758cad6f265903af3838009.tar.gz
fietsboek-85a3ab7c64d9c8f6b758cad6f265903af3838009.tar.bz2
fietsboek-85a3ab7c64d9c8f6b758cad6f265903af3838009.zip
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.
-rw-r--r--fietsboek/alembic/versions/20220721_091ce24409fe.py30
-rw-r--r--fietsboek/locale/de/LC_MESSAGES/messages.mobin10063 -> 10209 bytes
-rw-r--r--fietsboek/locale/de/LC_MESSAGES/messages.po42
-rw-r--r--fietsboek/locale/en/LC_MESSAGES/messages.mobin9471 -> 9612 bytes
-rw-r--r--fietsboek/locale/en/LC_MESSAGES/messages.po42
-rw-r--r--fietsboek/locale/fietslog.pot42
-rw-r--r--fietsboek/models/__init__.py1
-rw-r--r--fietsboek/models/image.py62
-rw-r--r--fietsboek/models/track.py3
-rw-r--r--fietsboek/static/fietsboek.js40
-rw-r--r--fietsboek/static/theme.css26
-rw-r--r--fietsboek/templates/details.jinja27
-rw-r--r--fietsboek/templates/edit_form.jinja225
-rw-r--r--fietsboek/views/detail.py17
-rw-r--r--fietsboek/views/edit.py78
15 files changed, 344 insertions, 71 deletions
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
--- a/fietsboek/locale/de/LC_MESSAGES/messages.mo
+++ b/fietsboek/locale/de/LC_MESSAGES/messages.mo
Binary files 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 <EMAIL@ADDRESS>\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
--- a/fietsboek/locale/en/LC_MESSAGES/messages.mo
+++ b/fietsboek/locale/en/LC_MESSAGES/messages.mo
Binary files 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 %}
<div id="trackImageShowcase" class="carousel carousel-dark slide" data-bs-ride="carousel">
<div class="carousel-inner">
- {% for image in images %}
+ {% for image, description in images %}
<div class="carousel-item{% if loop.first %} active{% endif %}">
<a href="{{ image }}" target="_blank">
<img src="{{ image }}" class="d-block">
</a>
+ {% if description %}
+ <div class="track-image-caption">
+ <p>{{ description }}</p>
+ </div>
+ {% endif %}
</div>
{% endfor %}
</div>
diff --git a/fietsboek/templates/edit_form.jinja2 b/fietsboek/templates/edit_form.jinja2
index 0ba933e..c4a478c 100644
--- a/fietsboek/templates/edit_form.jinja2
+++ b/fietsboek/templates/edit_form.jinja2
@@ -92,8 +92,10 @@
{% for image in images %}
<div class="track-image-preview">
<button type="button" class="btn-close delete-image" aria-label="{{ _("page.track.form.remove_image") }}"></button>
- <input type="hidden" name="delete-image[]" value="{{ image.name }}" disabled>
- <img src="{{ image.url }}">
+ <button type="button" class="edit-image-description"><i class="bi bi-cursor-text"></i></button>
+ <input type="hidden" class="image-description-input" name="image-description[{{ image.name }}]" value="{{ image.description }}">
+ <input type="hidden" class="image-deleter-input" name="delete-image[]" value="{{ image.name }}" disabled>
+ <img src="{{ image.url }}" title="{{ image.description }}">
</div>
{% endfor %}
</div>
@@ -104,7 +106,26 @@
<div style="display:none;">
<div id="trackImagePreviewBlueprint" class="track-image-preview">
<button type="button" class="btn-close delete-image" aria-label="{{ _("page.track.form.remove_image") }}"></button>
+ <button type="button" class="edit-image-description"><i class="bi bi-cursor-text"></i></button>
+ <input type="hidden" class="image-description-input" name="image-description[]" value="">
<img>
</div>
</div>
+<!-- Edit image description modal -->
+<div class="modal" tabindex="-1" id="imageDescriptionModal">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">{{ _("page.track.form.image_description_modal") }}</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <textarea class="form-control"></textarea>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success">{{ _("page.track.form.image_description_modal.save") }}</button>
+ </div>
+ </div>
+ </div>
+</div>
{% 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)