From 79b3797b5fc348aa11400455db85ba192c2bfc89 Mon Sep 17 00:00:00 2001 From: mtyton Date: Sat, 14 Dec 2024 23:30:45 +0100 Subject: [PATCH 1/2] Added possibility to edit images in Image posts --- app/community/forms.py | 6 +- app/community/routes.py | 9 ++- app/community/util.py | 106 ++++++++++++++++-------------- app/models.py | 10 +-- app/post/routes.py | 9 +++ app/templates/post/post_edit.html | 1 + 6 files changed, 82 insertions(+), 59 deletions(-) diff --git a/app/community/forms.py b/app/community/forms.py index e537cef7..6eb5c362 100644 --- a/app/community/forms.py +++ b/app/community/forms.py @@ -178,9 +178,9 @@ class CreateImageForm(CreatePostForm): return True -class EditImageForm(CreatePostForm): - image_alt_text = StringField(_l('Alt text'), validators=[Optional(), Length(min=3, max=1500)]) - +class EditImageForm(CreateImageForm): + image_file = FileField(_l('Replace Image'), validators=[DataRequired()], render_kw={'accept': 'image/*'}) + def validate(self, extra_validators=None) -> bool: if self.communities: community = Community.query.get(self.communities.data) diff --git a/app/community/routes.py b/app/community/routes.py index 9a6cfa87..0743e30c 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -688,8 +688,13 @@ def add_post(actor, type): img.thumbnail((2000, 2000)) img.save(final_place) - request_json['object']['attachment'] = [{'type': 'Image', 'url': f'https://{current_app.config["SERVER_NAME"]}/{final_place.replace("app/", "")}', - 'name': form.image_alt_text.data}] + request_json['object']['attachment'] = [{ + 'type': 'Image', + 'url': f'https://{current_app.config["SERVER_NAME"]}/{final_place.replace("app/", "")}', + 'name': form.image_alt_text.data, + 'file_path': final_place + }] + elif type == 'video': request_json['object']['attachment'] = [{'type': 'Document', 'url': form.video_url.data}] elif type == 'poll': diff --git a/app/community/util.py b/app/community/util.py index 236e6aeb..560529d1 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -304,66 +304,72 @@ def save_post(form, post: Post, type: int): elif type == POST_TYPE_IMAGE: post.type = POST_TYPE_IMAGE alt_text = form.image_alt_text.data if form.image_alt_text.data else form.title.data - if post.image_id is not None: - # editing an existing image post, dont try an upload - pass - else: - uploaded_file = request.files['image_file'] - if uploaded_file and uploaded_file.filename != '': - if post.image_id: - remove_old_file(post.image_id) - post.image_id = None + uploaded_file = request.files['image_file'] + # If we are uploading new file in the place of existing one just remove the old one + if post.image_id is not None and uploaded_file: + post.image.delete_from_disk() + image_id = post.image_id + post.image_id = None + db.session.add(post) + db.session.commit() + File.query.filter_by(id=image_id).delete() + + if uploaded_file and uploaded_file.filename != '': + if post.image_id: + remove_old_file(post.image_id) + post.image_id = None - # check if this is an allowed type of file - file_ext = os.path.splitext(uploaded_file.filename)[1] - if file_ext.lower() not in allowed_extensions: - abort(400) - new_filename = gibberish(15) + # check if this is an allowed type of file + file_ext = os.path.splitext(uploaded_file.filename)[1] + if file_ext.lower() not in allowed_extensions: + abort(400) + new_filename = gibberish(15) - # set up the storage directory - directory = 'app/static/media/posts/' + new_filename[0:2] + '/' + new_filename[2:4] - ensure_directory_exists(directory) + # set up the storage directory + directory = 'app/static/media/posts/' + new_filename[0:2] + '/' + new_filename[2:4] + ensure_directory_exists(directory) - # save the file - final_place = os.path.join(directory, new_filename + file_ext) - final_place_medium = os.path.join(directory, new_filename + '_medium.webp') - final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp') - uploaded_file.seek(0) - uploaded_file.save(final_place) + # save the file + final_place = os.path.join(directory, new_filename + file_ext) + final_place_medium = os.path.join(directory, new_filename + '_medium.webp') + final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp') + uploaded_file.seek(0) + uploaded_file.save(final_place) - if file_ext.lower() == '.heic': - register_heif_opener() + if file_ext.lower() == '.heic': + register_heif_opener() - Image.MAX_IMAGE_PIXELS = 89478485 + Image.MAX_IMAGE_PIXELS = 89478485 - # resize if necessary - img = Image.open(final_place) - if '.' + img.format.lower() in allowed_extensions: - img = ImageOps.exif_transpose(img) + # resize if necessary + img = Image.open(final_place) + if '.' + img.format.lower() in allowed_extensions: + img = ImageOps.exif_transpose(img) - # limit full sized version to 2000px - img_width = img.width - img_height = img.height - img.thumbnail((2000, 2000)) - img.save(final_place) + # limit full sized version to 2000px + img_width = img.width + img_height = img.height + img.thumbnail((2000, 2000)) + img.save(final_place) - # medium sized version - img.thumbnail((512, 512)) - img.save(final_place_medium, format="WebP", quality=93) + # medium sized version + img.thumbnail((512, 512)) + img.save(final_place_medium, format="WebP", quality=93) - # save a third, smaller, version as a thumbnail - img.thumbnail((170, 170)) - img.save(final_place_thumbnail, format="WebP", quality=93) - thumbnail_width = img.width - thumbnail_height = img.height + # save a third, smaller, version as a thumbnail + img.thumbnail((170, 170)) + img.save(final_place_thumbnail, format="WebP", quality=93) + thumbnail_width = img.width + thumbnail_height = img.height + + file = File(file_path=final_place_medium, file_name=new_filename + file_ext, alt_text=alt_text, + width=img_width, height=img_height, thumbnail_width=thumbnail_width, + thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail, + source_url=final_place.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/")) + db.session.add(file) + db.session.commit() + post.image_id = file.id - file = File(file_path=final_place_medium, file_name=new_filename + file_ext, alt_text=alt_text, - width=img_width, height=img_height, thumbnail_width=thumbnail_width, - thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail, - source_url=final_place.replace('app/static/', f"https://{current_app.config['SERVER_NAME']}/static/")) - db.session.add(file) - db.session.commit() - post.image_id = file.id elif type == POST_TYPE_VIDEO: form.video_url.data = form.video_url.data.strip() url_changed = post.id is None or form.video_url.data != post.url diff --git a/app/models.py b/app/models.py index ceae4a4a..7c35cee9 100644 --- a/app/models.py +++ b/app/models.py @@ -323,7 +323,6 @@ class File(db.Model): if purge_from_cache: flush_cdn_cache(purge_from_cache) - def filesize(self): size = 0 if self.file_path and os.path.exists(self.file_path): @@ -1242,9 +1241,10 @@ class Post(db.Model): if 'name' in request_json['object']['attachment'][0]: alt_text = request_json['object']['attachment'][0]['name'] if request_json['object']['attachment'][0]['type'] == 'Image': - post.url = request_json['object']['attachment'][0]['url'] # PixelFed, PieFed, Lemmy >= 0.19.4 - if 'name' in request_json['object']['attachment'][0]: - alt_text = request_json['object']['attachment'][0]['name'] + attachment = request_json['object']['attachment'][0] + post.url = attachment['url'] # PixelFed, PieFed, Lemmy >= 0.19.4 + alt_text = attachment.get("name") + file_path = attachment.get("file_path") if 'attachment' in request_json['object'] and isinstance(request_json['object']['attachment'], dict): # a.gup.pe (Mastodon) alt_text = None @@ -1256,6 +1256,8 @@ class Post(db.Model): image = File(source_url=post.url) if alt_text: image.alt_text = alt_text + if file_path: + image.file_path = file_path db.session.add(image) post.image = image elif is_video_url(post.url): # youtube is detected later diff --git a/app/post/routes.py b/app/post/routes.py index 4b94f67b..4e2f6ffa 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -817,6 +817,15 @@ def post_edit(post_id: int): elif post.type == POST_TYPE_IMAGE: # existing_image = True form.image_alt_text.data = post.image.alt_text + path = post.image.file_path + # This is fallback for existing entries + if not path: + path = "app/" + post.image.source_url.replace( + f"https://{current_app.config['SERVER_NAME']}/", "" + ) + with open(path, "rb")as file: + form.image_file.data = file.read() + elif post.type == POST_TYPE_VIDEO: form.video_url.data = post.url elif post.type == POST_TYPE_POLL: diff --git a/app/templates/post/post_edit.html b/app/templates/post/post_edit.html index 861ac374..390ad57a 100644 --- a/app/templates/post/post_edit.html +++ b/app/templates/post/post_edit.html @@ -35,6 +35,7 @@ {% else %} {{ render_field(form.image_file) }} {% endif %} + {{ render_field(form.image_file) }} {{ render_field(form.image_alt_text) }} {{ _('Describe the image, to help visually impaired people.') }} {% elif post_type == POST_TYPE_VIDEO %} From 22e2eacc8c7388a0553252f2bb1728eb5349f412 Mon Sep 17 00:00:00 2001 From: mtyton Date: Sat, 21 Dec 2024 13:05:14 +0100 Subject: [PATCH 2/2] Fixed issues with Image post editing. Added utility to check if image is local or remote. --- app/community/forms.py | 1 + app/models.py | 1 + app/post/routes.py | 27 ++++++++++++++++++--------- app/templates/post/post_edit.html | 2 -- app/utils.py | 8 ++++++++ requirements.txt | 1 + 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/app/community/forms.py b/app/community/forms.py index 6eb5c362..970dacda 100644 --- a/app/community/forms.py +++ b/app/community/forms.py @@ -180,6 +180,7 @@ class CreateImageForm(CreatePostForm): class EditImageForm(CreateImageForm): image_file = FileField(_l('Replace Image'), validators=[DataRequired()], render_kw={'accept': 'image/*'}) + image_file = FileField(_l('Image'), validators=[Optional()], render_kw={'accept': 'image/*'}) def validate(self, extra_validators=None) -> bool: if self.communities: diff --git a/app/models.py b/app/models.py index 7c35cee9..7457f7c6 100644 --- a/app/models.py +++ b/app/models.py @@ -1229,6 +1229,7 @@ class Post(db.Model): if blocked_phrase in post.body: return None + file_path = None if ('attachment' in request_json['object'] and isinstance(request_json['object']['attachment'], list) and len(request_json['object']['attachment']) > 0 and diff --git a/app/post/routes.py b/app/post/routes.py index 4e2f6ffa..61f43881 100644 --- a/app/post/routes.py +++ b/app/post/routes.py @@ -32,7 +32,7 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_ blocked_instances, blocked_domains, community_moderators, blocked_phrases, show_ban_message, recently_upvoted_posts, \ recently_downvoted_posts, recently_upvoted_post_replies, recently_downvoted_post_replies, reply_is_stupid, \ languages_for_form, menu_topics, add_to_modlog, blocked_communities, piefed_markdown_to_lemmy_markdown, \ - permission_required, blocked_users, get_request + permission_required, blocked_users, get_request, is_local_image_url, is_video_url def show_post(post_id: int): @@ -730,14 +730,23 @@ def post_reply_options(post_id: int, comment_id: int): @login_required def post_edit(post_id: int): post = Post.query.get_or_404(post_id) + post_type = post.type if post.type == POST_TYPE_ARTICLE: form = CreateDiscussionForm() elif post.type == POST_TYPE_LINK: form = CreateLinkForm() elif post.type == POST_TYPE_IMAGE: - form = EditImageForm() + if post.image and post.image.source_url and is_local_image_url(post.image.source_url): + form = EditImageForm() + else: + form = CreateLinkForm() + post_type = POST_TYPE_LINK elif post.type == POST_TYPE_VIDEO: - form = CreateVideoForm() + if is_video_url(post.url): + form = CreateVideoForm() + else: + form = CreateLinkForm() + post_type = POST_TYPE_LINK elif post.type == POST_TYPE_POLL: form = CreatePollForm() poll = Poll.query.filter_by(post_id=post_id).first() @@ -769,7 +778,7 @@ def post_edit(post_id: int): form.language_id.choices = languages_for_form() if form.validate_on_submit(): - save_post(form, post, post.type) + save_post(form, post, post_type) post.community.last_active = utcnow() post.edited_at = utcnow() @@ -812,9 +821,9 @@ def post_edit(post_id: int): form.sticky.data = post.sticky form.language_id.data = post.language_id form.tags.data = tags_to_string(post) - if post.type == POST_TYPE_LINK: + if post_type == POST_TYPE_LINK: form.link_url.data = post.url - elif post.type == POST_TYPE_IMAGE: + elif post_type == POST_TYPE_IMAGE: # existing_image = True form.image_alt_text.data = post.image.alt_text path = post.image.file_path @@ -826,9 +835,9 @@ def post_edit(post_id: int): with open(path, "rb")as file: form.image_file.data = file.read() - elif post.type == POST_TYPE_VIDEO: + elif post_type == POST_TYPE_VIDEO: form.video_url.data = post.url - elif post.type == POST_TYPE_POLL: + elif post_type == POST_TYPE_POLL: poll = Poll.query.filter_by(post_id=post.id).first() form.mode.data = poll.mode form.local_only.data = poll.local_only @@ -841,7 +850,7 @@ def post_edit(post_id: int): if not (post.community.is_moderator() or post.community.is_owner() or current_user.is_admin()): form.sticky.render_kw = {'disabled': True} return render_template('post/post_edit.html', title=_('Edit post'), form=form, - post_type=post.type, community=post.community, post=post, + post_type=post_type, community=post.community, post=post, markdown_editor=current_user.markdown_editor, mods=mod_list, moderating_communities=moderating_communities(current_user.get_id()), joined_communities=joined_communities(current_user.get_id()), diff --git a/app/templates/post/post_edit.html b/app/templates/post/post_edit.html index 390ad57a..ec0fdc0d 100644 --- a/app/templates/post/post_edit.html +++ b/app/templates/post/post_edit.html @@ -32,8 +32,6 @@ {% endif -%} - {% else %} - {{ render_field(form.image_file) }} {% endif %} {{ render_field(form.image_file) }} {{ render_field(form.image_alt_text) }} diff --git a/app/utils.py b/app/utils.py index 14bfdc06..999c4115 100644 --- a/app/utils.py +++ b/app/utils.py @@ -25,6 +25,7 @@ import jwt warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning) import os +from furl import furl from flask import current_app, json, redirect, url_for, request, make_response, Response, g, flash from flask_babel import _ from flask_login import current_user, logout_user @@ -199,6 +200,13 @@ def is_image_url(url): return any(path.endswith(extension) for extension in common_image_extensions) +def is_local_image_url(url): + if not is_image_url(url): + return False + f = furl(url) + return f.host in ["127.0.0.1", current_app.config["SERVER_NAME"]] + + def is_video_url(url: str) -> bool: common_video_extensions = ['.mp4', '.webm'] mime_type = mime_type_using_head(url) diff --git a/requirements.txt b/requirements.txt index 3f0cbefd..5a88b76b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,3 +33,4 @@ pytesseract==0.3.10 sentry-sdk==1.40.6 python-slugify==8.0.4 moviepy==1.0.3 +furl==2.1.3