mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
create and view image posts
This commit is contained in:
parent
e2b86f3caf
commit
a17b8785d3
15 changed files with 178 additions and 25 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -160,3 +160,4 @@ cython_debug/
|
|||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
app/static/*.css.map
|
||||
/app/static/media/
|
||||
|
|
|
@ -3,19 +3,22 @@ from datetime import date, datetime, timedelta
|
|||
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort
|
||||
from flask_login import login_user, logout_user, current_user, login_required
|
||||
from flask_babel import _
|
||||
from pillow_heif import register_heif_opener
|
||||
from sqlalchemy import or_, desc
|
||||
|
||||
from app import db, constants
|
||||
from app.activitypub.signature import RsaKeys, HttpSignature
|
||||
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePost, NewReplyForm
|
||||
from app.community.util import search_for_community, community_url_exists, actor_to_community, post_replies, \
|
||||
get_comment_branch, post_reply_count
|
||||
get_comment_branch, post_reply_count, ensure_directory_exists
|
||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE
|
||||
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, PostReply, \
|
||||
PostReplyVote, PostVote
|
||||
PostReplyVote, PostVote, File
|
||||
from app.community import bp
|
||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||
shorten_string, markdown_to_text, domain_from_url
|
||||
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish
|
||||
import os
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
|
||||
@bp.route('/add_local', methods=['GET', 'POST'])
|
||||
|
@ -95,7 +98,7 @@ def show_community(community: Community):
|
|||
|
||||
return render_template('community/community.html', community=community, title=community.title,
|
||||
is_moderator=is_moderator, is_owner=is_owner, mods=mod_list, posts=posts, description=description,
|
||||
og_image=og_image)
|
||||
og_image=og_image, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK)
|
||||
|
||||
|
||||
@bp.route('/<actor>/subscribe', methods=['GET'])
|
||||
|
@ -209,9 +212,48 @@ def add_post(actor):
|
|||
domain.post_count += 1
|
||||
post.domain = domain
|
||||
elif form.type.data == 'image':
|
||||
allowed_extensions = ['.gif', '.jpg', '.jpeg', '.png', '.webp', '.heic']
|
||||
post.title = form.image_title.data
|
||||
post.type = POST_TYPE_IMAGE
|
||||
# todo: handle file upload
|
||||
uploaded_file = request.files['image_file']
|
||||
if uploaded_file.filename != '':
|
||||
file_ext = os.path.splitext(uploaded_file.filename)[1]
|
||||
if file_ext.lower() not in allowed_extensions or file_ext != validate_image(
|
||||
uploaded_file.stream):
|
||||
abort(400)
|
||||
new_filename = gibberish(15)
|
||||
|
||||
directory = 'app/static/media/posts/' + new_filename[0:2] + '/' + new_filename[2:4]
|
||||
ensure_directory_exists(directory)
|
||||
|
||||
final_place = os.path.join(directory, new_filename + file_ext)
|
||||
final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp')
|
||||
uploaded_file.save(final_place)
|
||||
|
||||
if file_ext.lower() == '.heic':
|
||||
register_heif_opener()
|
||||
|
||||
# resize if necessary
|
||||
img = Image.open(final_place)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
img = ImageOps.exif_transpose(img)
|
||||
if img.width > 2000 or img.height > 2000:
|
||||
img.thumbnail((2000, 2000))
|
||||
img.save(final_place)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
img.thumbnail((256, 256))
|
||||
img.save(final_place_thumbnail, format="WebP", quality=93)
|
||||
thumbnail_width = img.width
|
||||
thumbnail_height = img.height
|
||||
|
||||
file = File(file_path=final_place, file_name=new_filename + file_ext, alt_text=form.image_title.data,
|
||||
width=img_width, height=img_height, thumbnail_width=thumbnail_width,
|
||||
thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail)
|
||||
post.image = file
|
||||
db.session.add(file)
|
||||
|
||||
elif form.type.data == 'poll':
|
||||
...
|
||||
else:
|
||||
|
@ -262,7 +304,7 @@ def show_post(post_id: int):
|
|||
|
||||
return render_template('community/post.html', title=post.title, post=post, is_moderator=is_moderator,
|
||||
canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH,
|
||||
description=description, og_image=og_image)
|
||||
description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE)
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/<vote_direction>', methods=['GET', 'POST'])
|
||||
|
|
|
@ -5,6 +5,7 @@ from app import db
|
|||
from app.models import Community, File, BannedInstances, PostReply
|
||||
from app.utils import get_request
|
||||
from sqlalchemy import desc, text
|
||||
import os
|
||||
|
||||
|
||||
def search_for_community(address: str):
|
||||
|
@ -132,3 +133,13 @@ def get_comment_branch(post_id: int, comment_id: int, sort_by: str) -> List[Post
|
|||
def post_reply_count(post_id) -> int:
|
||||
return db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE post_id = :post_id'),
|
||||
{'post_id': post_id}).scalar()
|
||||
|
||||
|
||||
def ensure_directory_exists(directory):
|
||||
parts = directory.split('/')
|
||||
rebuild_directory = ''
|
||||
for part in parts:
|
||||
rebuild_directory += part
|
||||
if not os.path.isdir(rebuild_directory):
|
||||
os.mkdir(rebuild_directory)
|
||||
rebuild_directory += '/'
|
||||
|
|
|
@ -31,6 +31,22 @@ class File(db.Model):
|
|||
height = db.Column(db.Integer)
|
||||
alt_text = db.Column(db.String(256))
|
||||
source_url = db.Column(db.String(256))
|
||||
thumbnail_path = db.Column(db.String(255))
|
||||
thumbnail_width = db.Column(db.Integer)
|
||||
thumbnail_height = db.Column(db.Integer)
|
||||
|
||||
def view_url(self):
|
||||
if self.source_url:
|
||||
return self.source_url
|
||||
elif self.file_path:
|
||||
file_path = self.file_path[4:] if self.file_path.startswith('app/') else self.file_path
|
||||
return f"https://{current_app.config['SERVER_NAME']}/{file_path}"
|
||||
else:
|
||||
return ''
|
||||
|
||||
def thumbnail_url(self):
|
||||
thumbnail_path = self.thumbnail_path[4:] if self.thumbnail_path.startswith('app/') else self.thumbnail_path
|
||||
return f"https://{current_app.config['SERVER_NAME']}/{thumbnail_path}"
|
||||
|
||||
|
||||
class Community(db.Model):
|
||||
|
|
|
@ -166,6 +166,10 @@
|
|||
content: "\e935";
|
||||
}
|
||||
|
||||
.fe-camera::before {
|
||||
content: "\e928";
|
||||
}
|
||||
|
||||
a.no-underline {
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
|
|
|
@ -169,6 +169,10 @@ nav, etc which are used site-wide */
|
|||
content: "\e935";
|
||||
}
|
||||
|
||||
.fe-camera::before {
|
||||
content: "\e928";
|
||||
}
|
||||
|
||||
a.no-underline {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@ -385,6 +389,11 @@ fieldset legend {
|
|||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.post_type_image .post_image img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.voting_buttons {
|
||||
float: right;
|
||||
display: block;
|
||||
|
|
|
@ -156,6 +156,15 @@ nav, etc which are used site-wide */
|
|||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.post_type_image {
|
||||
.post_image {
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.voting_buttons {
|
||||
float: right;
|
||||
display: block;
|
||||
|
|
|
@ -168,6 +168,10 @@
|
|||
content: "\e935";
|
||||
}
|
||||
|
||||
.fe-camera::before {
|
||||
content: "\e928";
|
||||
}
|
||||
|
||||
a.no-underline {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@ -399,6 +403,10 @@ nav.navbar {
|
|||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #777;
|
||||
|
|
|
@ -174,6 +174,10 @@ nav.navbar {
|
|||
}
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: $dark-grey;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="row">
|
||||
{% if post.image_id %}
|
||||
<div class="col-8">
|
||||
{% if post.type == POST_TYPE_IMAGE %}
|
||||
<div class="col post_type_image">
|
||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||
|
@ -21,15 +21,10 @@
|
|||
<p><small>submitted {{ moment(post.posted_at).fromNow() }} by {{ render_username(post.author) }}
|
||||
{% if post.edited_at %} edited {{ moment(post.edited_at).fromNow() }}{% endif %}
|
||||
</small></p>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
{% if post.url %}
|
||||
<a href="post.url" rel="nofollow ugc"><img src="{{ post.image.source_url }}" alt="{{ post.image.alt_text }}"
|
||||
width="100" /></a>
|
||||
{% else %}
|
||||
<a href="post.image.source_url"><img src="{{ post.image.source_url }}" alt="{{ post.image.alt_text }}"
|
||||
width="100" /></a>
|
||||
{% endif %}
|
||||
<div class="post_image">
|
||||
<a href="{{ post.image.view_url() }}"><img src="{{ post.image.view_url() }}" alt="{{ post.image.alt_text }}"
|
||||
width="{{ post.image.width }}" height="{{ post.image.height }}" /></a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col">
|
||||
|
@ -54,6 +49,12 @@
|
|||
{{ render_username(post.author) }}
|
||||
{% if post.edited_at %} edited {{ moment(post.edited_at).fromNow() }}{% endif %}
|
||||
</p>
|
||||
{% if post.image_id %}
|
||||
<div class="post_image">
|
||||
<a href="{{ post.image.view_url() }}"><img src="{{ post.image.thumbnail_url() }}" alt="{{ post.image.alt_text }}"
|
||||
width="{{ post.image.thumbnail_width }}" height="{{ post.image.thumbnail_height }}" /></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
<div class="row main_row">
|
||||
<div class="col{% if post.image_id %}-8{% endif %}">
|
||||
<h3>
|
||||
<a href="{{ url_for('community.show_post', post_id=post.id) }}">{{ post.title }}</a>
|
||||
{% if post.type == post_type_link and post.domain_id %}
|
||||
<a href="{{ post.url }}" rel="nofollow ugc">
|
||||
<a href="{{ url_for('community.show_post', post_id=post.id) }}">{{ post.title }}
|
||||
{% if post.type == POST_TYPE_IMAGE %}<span class="fe fe-camera"> </span>{% endif %}</a>
|
||||
{% if post.type == POST_TYPE_LINK and post.domain_id %}
|
||||
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank">
|
||||
<img src="/static/images/external_link_black.svg" class="external_link_icon" alt="External link" />
|
||||
</a>
|
||||
<span class="domain_link">(<a href="/d/{{ post.domain_id }}">{{ post.domain.name }}</a>)</span>
|
||||
|
@ -15,9 +16,9 @@
|
|||
<span class="small">{{ render_username(post.author) }} · {{ moment(post.posted_at).fromNow() }}</span>
|
||||
</div>
|
||||
{% if post.image_id %}
|
||||
<div class="col-4">
|
||||
<img src="{{ post.image.source_url}}" alt="{{ post.image.alt_text }}"
|
||||
width="100" />
|
||||
<div class="col-4 text-right">
|
||||
<a href="{{ url_for('community.show_post', post_id=post.id) }}"><img src="{{ post.image.thumbnail_url() }}" alt="{{ post.image.alt_text }}"
|
||||
width="100" /></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div class="row">
|
||||
<div class="col-8 position-relative main_pane">
|
||||
<h1>{{ _('Create post') }}</h1>
|
||||
<form method="post">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.communities) }}
|
||||
<nav id="post_type_chooser">
|
||||
|
|
11
app/utils.py
11
app/utils.py
|
@ -4,11 +4,11 @@ import math
|
|||
from urllib.parse import urlparse
|
||||
import requests
|
||||
from functools import wraps
|
||||
|
||||
import flask
|
||||
from bs4 import BeautifulSoup
|
||||
import requests
|
||||
import os
|
||||
import imghdr
|
||||
from flask import current_app, json, redirect, url_for
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import text
|
||||
|
@ -207,6 +207,15 @@ def retrieve_block_list():
|
|||
return response.text
|
||||
|
||||
|
||||
def validate_image(stream):
|
||||
header = stream.read(512)
|
||||
stream.seek(0)
|
||||
format = imghdr.what(None, header)
|
||||
if not format:
|
||||
return None
|
||||
return '.' + (format if format != 'jpeg' else 'jpg')
|
||||
|
||||
|
||||
def validation_required(func):
|
||||
@wraps(func)
|
||||
def decorated_view(*args, **kwargs):
|
||||
|
|
36
migrations/versions/ee45b9ea4a0c_image_thumbnails.py
Normal file
36
migrations/versions/ee45b9ea4a0c_image_thumbnails.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
"""image-thumbnails
|
||||
|
||||
Revision ID: ee45b9ea4a0c
|
||||
Revises: 4a3ca1701711
|
||||
Create Date: 2023-11-27 20:53:29.624833
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'ee45b9ea4a0c'
|
||||
down_revision = '4a3ca1701711'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('file', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('thumbnail_path', sa.String(length=255), nullable=True))
|
||||
batch_op.add_column(sa.Column('thumbnail_width', sa.Integer(), nullable=True))
|
||||
batch_op.add_column(sa.Column('thumbnail_height', sa.Integer(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('file', schema=None) as batch_op:
|
||||
batch_op.drop_column('thumbnail_height')
|
||||
batch_op.drop_column('thumbnail_width')
|
||||
batch_op.drop_column('thumbnail_path')
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -22,3 +22,5 @@ boto3==1.28.35
|
|||
markdown2==2.4.10
|
||||
beautifulsoup4==4.12.2
|
||||
flask-caching==2.0.2
|
||||
Pillow
|
||||
pillow-heif
|
||||
|
|
Loading…
Reference in a new issue