diff options
-rw-r--r-- | fietsboek/alembic/versions/20220721_091ce24409fe.py | 30 | ||||
-rw-r--r-- | fietsboek/locale/de/LC_MESSAGES/messages.mo | bin | 10063 -> 10209 bytes | |||
-rw-r--r-- | fietsboek/locale/de/LC_MESSAGES/messages.po | 42 | ||||
-rw-r--r-- | fietsboek/locale/en/LC_MESSAGES/messages.mo | bin | 9471 -> 9612 bytes | |||
-rw-r--r-- | fietsboek/locale/en/LC_MESSAGES/messages.po | 42 | ||||
-rw-r--r-- | fietsboek/locale/fietslog.pot | 42 | ||||
-rw-r--r-- | fietsboek/models/__init__.py | 1 | ||||
-rw-r--r-- | fietsboek/models/image.py | 62 | ||||
-rw-r--r-- | fietsboek/models/track.py | 3 | ||||
-rw-r--r-- | fietsboek/static/fietsboek.js | 40 | ||||
-rw-r--r-- | fietsboek/static/theme.css | 26 | ||||
-rw-r--r-- | fietsboek/templates/details.jinja2 | 7 | ||||
-rw-r--r-- | fietsboek/templates/edit_form.jinja2 | 25 | ||||
-rw-r--r-- | fietsboek/views/detail.py | 17 | ||||
-rw-r--r-- | fietsboek/views/edit.py | 78 |
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 Binary files differindex b6375fa..7d7af8d 100644 --- a/fietsboek/locale/de/LC_MESSAGES/messages.mo +++ b/fietsboek/locale/de/LC_MESSAGES/messages.mo 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 Binary files differindex a9986ee..8261149 100644 --- a/fietsboek/locale/en/LC_MESSAGES/messages.mo +++ b/fietsboek/locale/en/LC_MESSAGES/messages.mo 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) |