mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-02-02 16:21:32 -08:00
post edit and delete. beginning of notifications
This commit is contained in:
parent
38a00d3617
commit
f0a4e01fe9
15 changed files with 398 additions and 104 deletions
|
@ -66,6 +66,7 @@ def register(app):
|
|||
db.session.add(BannedInstances(domain='exploding-heads.com'))
|
||||
db.session.add(BannedInstances(domain='hexbear.net'))
|
||||
db.session.add(BannedInstances(domain='threads.net'))
|
||||
db.session.add(BannedInstances(domain='pieville.net'))
|
||||
interests = file_get_contents('interests.txt')
|
||||
db.session.add(Interest(name='🕊 Chilling', communities=parse_communities(interests, 'chilling')))
|
||||
db.session.add(Interest(name='💭 Interesting stuff', communities=parse_communities(interests, 'interesting stuff')))
|
||||
|
|
|
@ -32,7 +32,7 @@ class SearchRemoteCommunity(FlaskForm):
|
|||
submit = SubmitField(_l('Search'))
|
||||
|
||||
|
||||
class CreatePost(FlaskForm):
|
||||
class CreatePostForm(FlaskForm):
|
||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int)
|
||||
type = HiddenField() # https://getbootstrap.com/docs/4.6/components/navs/#tabs
|
||||
discussion_title = StringField(_l('Title'), validators={Optional(), Length(min=3, max=255)})
|
||||
|
@ -44,7 +44,7 @@ class CreatePost(FlaskForm):
|
|||
# flair = SelectField(_l('Flair'), coerce=int)
|
||||
nsfw = BooleanField(_l('NSFW'))
|
||||
nsfl = BooleanField(_l('NSFL'))
|
||||
notify = BooleanField(_l('Send me post reply notifications'))
|
||||
notify_author = BooleanField(_l('Send me post reply notifications'))
|
||||
submit = SubmitField(_l('Post'))
|
||||
|
||||
def validate(self, extra_validators=None) -> bool:
|
||||
|
|
|
@ -6,17 +6,18 @@ 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
|
||||
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm
|
||||
from app.community.util import search_for_community, community_url_exists, actor_to_community, \
|
||||
ensure_directory_exists, opengraph_parse, url_to_thumbnail_file
|
||||
ensure_directory_exists, opengraph_parse, url_to_thumbnail_file, save_post
|
||||
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, \
|
||||
File
|
||||
File, PostVote
|
||||
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, validate_image, gibberish
|
||||
import os
|
||||
from PIL import Image, ImageOps
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
@bp.route('/add_local', methods=['GET', 'POST'])
|
||||
|
@ -185,7 +186,7 @@ def unsubscribe(actor):
|
|||
@validation_required
|
||||
def add_post(actor):
|
||||
community = actor_to_community(actor)
|
||||
form = CreatePost()
|
||||
form = CreatePostForm()
|
||||
if get_setting('allow_nsfw', False) is False:
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if get_setting('allow_nsfl', False) is False:
|
||||
|
@ -195,100 +196,23 @@ def add_post(actor):
|
|||
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
||||
|
||||
if form.validate_on_submit():
|
||||
post = Post(user_id=current_user.id, community_id=form.communities.data, nsfw=form.nsfw.data,
|
||||
nsfl=form.nsfl.data)
|
||||
if form.type.data == '' or form.type.data == 'discussion':
|
||||
post.title = form.discussion_title.data
|
||||
post.body = form.discussion_body.data
|
||||
post.body_html = markdown_to_html(post.body)
|
||||
post.type = POST_TYPE_ARTICLE
|
||||
elif form.type.data == 'link':
|
||||
post.title = form.link_title.data
|
||||
post.url = form.link_url.data
|
||||
post.type = POST_TYPE_LINK
|
||||
domain = domain_from_url(form.link_url.data)
|
||||
domain.post_count += 1
|
||||
post.domain = domain
|
||||
valid_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}
|
||||
unused, file_extension = os.path.splitext(form.link_url.data) # do not use _ here instead of 'unused'
|
||||
# this url is a link to an image - generate a thumbnail of it
|
||||
if file_extension in valid_extensions:
|
||||
file = url_to_thumbnail_file(form.link_url.data)
|
||||
if file:
|
||||
post.image = file
|
||||
db.session.add(file)
|
||||
else:
|
||||
# check opengraph tags on the page and make a thumbnail if an image is available in the og:image meta tag
|
||||
opengraph = opengraph_parse(form.link_url.data)
|
||||
if opengraph and opengraph.get('og:image', '') != '':
|
||||
filename = opengraph.get('og:image')
|
||||
valid_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}
|
||||
unused, file_extension = os.path.splitext(filename)
|
||||
if file_extension.lower() in valid_extensions:
|
||||
file = url_to_thumbnail_file(filename)
|
||||
if file:
|
||||
file.alt_text = opengraph.get('og:title')
|
||||
post.image = file
|
||||
db.session.add(file)
|
||||
|
||||
elif form.type.data == 'image':
|
||||
allowed_extensions = ['.gif', '.jpg', '.jpeg', '.png', '.webp', '.heic']
|
||||
post.title = form.image_title.data
|
||||
post.type = POST_TYPE_IMAGE
|
||||
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:
|
||||
raise Exception('invalid post type')
|
||||
db.session.add(post)
|
||||
post = Post(user_id=current_user.id, community_id=form.communities.data)
|
||||
save_post(form, post)
|
||||
community.post_count += 1
|
||||
community.last_active = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
# todo: federate post creation out to followers
|
||||
|
||||
flash('Post has been added')
|
||||
return redirect(f"/c/{community.link()}")
|
||||
else:
|
||||
form.communities.data = community.id
|
||||
form.notify.data = True
|
||||
form.notify_author.data = True
|
||||
|
||||
return render_template('community/add_post.html', title=_('Add post to community'), form=form, community=community,
|
||||
images_disabled=images_disabled)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -3,10 +3,14 @@ from typing import List
|
|||
|
||||
import requests
|
||||
from PIL import Image, ImageOps
|
||||
from flask import request, abort
|
||||
from flask_login import current_user
|
||||
from pillow_heif import register_heif_opener
|
||||
|
||||
from app import db, cache
|
||||
from app.models import Community, File, BannedInstances, PostReply
|
||||
from app.utils import get_request, gibberish
|
||||
from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE
|
||||
from app.models import Community, File, BannedInstances, PostReply, PostVote
|
||||
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, validate_image
|
||||
from sqlalchemy import desc, text
|
||||
import os
|
||||
from opengraph_parse import parse_page
|
||||
|
@ -124,3 +128,111 @@ def url_to_thumbnail_file(filename) -> File:
|
|||
return File(file_name=new_filename + file_extension, thumbnail_width=thumbnail_width,
|
||||
thumbnail_height=thumbnail_height, thumbnail_path=final_place,
|
||||
source_url=filename)
|
||||
|
||||
|
||||
def save_post(form, post):
|
||||
post.nsfw = form.nsfw.data
|
||||
post.nsfl = form.nsfl.data
|
||||
post.notify_author = form.notify_author.data
|
||||
if form.type.data == '' or form.type.data == 'discussion':
|
||||
post.title = form.discussion_title.data
|
||||
post.body = form.discussion_body.data
|
||||
post.body_html = markdown_to_html(post.body)
|
||||
post.type = POST_TYPE_ARTICLE
|
||||
elif form.type.data == 'link':
|
||||
post.title = form.link_title.data
|
||||
url_changed = post.id is None or form.link_url.data != post.url
|
||||
post.url = form.link_url.data
|
||||
post.type = POST_TYPE_LINK
|
||||
domain = domain_from_url(form.link_url.data)
|
||||
domain.post_count += 1
|
||||
post.domain = domain
|
||||
|
||||
if url_changed:
|
||||
if post.image_id:
|
||||
remove_old_file(post.image_id)
|
||||
post.image_id = None
|
||||
valid_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}
|
||||
unused, file_extension = os.path.splitext(form.link_url.data) # do not use _ here instead of 'unused'
|
||||
# this url is a link to an image - generate a thumbnail of it
|
||||
if file_extension in valid_extensions:
|
||||
file = url_to_thumbnail_file(form.link_url.data)
|
||||
if file:
|
||||
post.image = file
|
||||
db.session.add(file)
|
||||
else:
|
||||
# check opengraph tags on the page and make a thumbnail if an image is available in the og:image meta tag
|
||||
opengraph = opengraph_parse(form.link_url.data)
|
||||
if opengraph and opengraph.get('og:image', '') != '':
|
||||
filename = opengraph.get('og:image')
|
||||
valid_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}
|
||||
unused, file_extension = os.path.splitext(filename)
|
||||
if file_extension.lower() in valid_extensions:
|
||||
file = url_to_thumbnail_file(filename)
|
||||
if file:
|
||||
file.alt_text = opengraph.get('og:title')
|
||||
post.image = file
|
||||
db.session.add(file)
|
||||
|
||||
elif form.type.data == 'image':
|
||||
allowed_extensions = ['.gif', '.jpg', '.jpeg', '.png', '.webp', '.heic']
|
||||
post.title = form.image_title.data
|
||||
post.type = POST_TYPE_IMAGE
|
||||
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
|
||||
|
||||
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:
|
||||
raise Exception('invalid post type')
|
||||
if post.id is None:
|
||||
postvote = PostVote(user_id=current_user.id, author_id=current_user.id, post=post, effect=1.0)
|
||||
post.up_votes = 1
|
||||
post.score = 1
|
||||
db.session.add(postvote)
|
||||
db.session.add(post)
|
||||
|
||||
|
||||
def remove_old_file(file_id):
|
||||
remove_file = File.query.get(file_id)
|
||||
remove_file.delete_from_disk()
|
||||
|
|
|
@ -15,7 +15,6 @@ from app.models import Community, CommunityMember
|
|||
@bp.route('/', methods=['GET', 'POST'])
|
||||
@bp.route('/index', methods=['GET', 'POST'])
|
||||
def index():
|
||||
raise Exception('cowbell')
|
||||
verification_warning()
|
||||
return render_template('index.html')
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ from flask_sqlalchemy import BaseQuery
|
|||
from sqlalchemy_searchable import SearchQueryMixin
|
||||
from app import db, login, cache
|
||||
import jwt
|
||||
import os
|
||||
|
||||
from app.constants import SUBSCRIPTION_NONMEMBER, SUBSCRIPTION_MEMBER, SUBSCRIPTION_MODERATOR, SUBSCRIPTION_OWNER, \
|
||||
SUBSCRIPTION_BANNED
|
||||
|
@ -50,6 +51,12 @@ class File(db.Model):
|
|||
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}"
|
||||
|
||||
def delete_from_disk(self):
|
||||
if self.file_path and os.path.isfile(self.file_path):
|
||||
os.unlink(self.file_path)
|
||||
if self.thumbnail_path and os.path.isfile(self.thumbnail_path):
|
||||
os.unlink(self.thumbnail_path)
|
||||
|
||||
|
||||
class Community(db.Model):
|
||||
query_class = FullTextSearchQuery
|
||||
|
@ -356,6 +363,7 @@ class Post(db.Model):
|
|||
nsfw = db.Column(db.Boolean, default=False)
|
||||
nsfl = db.Column(db.Boolean, default=False)
|
||||
sticky = db.Column(db.Boolean, default=False)
|
||||
notify_author = db.Column(db.Boolean, default=True)
|
||||
indexable = db.Column(db.Boolean, default=False)
|
||||
from_bot = db.Column(db.Boolean, default=False)
|
||||
created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow) # this is when the content arrived here
|
||||
|
@ -387,6 +395,9 @@ class Post(db.Model):
|
|||
{'post_id': self.id})
|
||||
db.session.execute(text('DELETE FROM post_reply WHERE post_id = :post_id'), {'post_id': self.id})
|
||||
db.session.execute(text('DELETE FROM post_vote WHERE post_id = :post_id'), {'post_id': self.id})
|
||||
if self.image_id:
|
||||
file = File.query.get(self.image_id)
|
||||
file.delete_from_disk()
|
||||
|
||||
def youtube_embed(self):
|
||||
if self.url:
|
||||
|
@ -412,6 +423,7 @@ class PostReply(db.Model):
|
|||
score = db.Column(db.Integer, default=0, index=True)
|
||||
nsfw = db.Column(db.Boolean, default=False)
|
||||
nsfl = db.Column(db.Boolean, default=False)
|
||||
notify_author = db.Column(db.Boolean, default=True)
|
||||
created_at = db.Column(db.DateTime, index=True, default=datetime.utcnow)
|
||||
posted_at = db.Column(db.DateTime, index=True, default=datetime.utcnow)
|
||||
ip = db.Column(db.String(50))
|
||||
|
@ -543,6 +555,7 @@ class PostVote(db.Model):
|
|||
post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
|
||||
effect = db.Column(db.Float)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
post = db.relationship('Post', foreign_keys=[post_id])
|
||||
|
||||
|
||||
class PostReplyVote(db.Model):
|
||||
|
@ -594,6 +607,16 @@ class RolePermission(db.Model):
|
|||
permission = db.Column(db.String, primary_key=True, index=True)
|
||||
|
||||
|
||||
class Notification(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(50))
|
||||
url = db.Column(db.String(512))
|
||||
read = db.Column(db.Boolean, default=False)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
@login.user_loader
|
||||
def load_user(id):
|
||||
return User.query.get(int(id))
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
from datetime import datetime
|
||||
|
||||
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 sqlalchemy import or_, desc
|
||||
|
||||
from app import db, constants
|
||||
from app.community.util import save_post
|
||||
from app.post.forms import NewReplyForm
|
||||
from app.community.forms import CreatePostForm
|
||||
from app.post.util import post_replies, get_comment_branch, post_reply_count
|
||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE
|
||||
from app.models import Post, PostReply, \
|
||||
|
@ -195,8 +199,65 @@ def add_reply(post_id: int, comment_id: int):
|
|||
is_moderator=is_moderator, form=form, comment=comment)
|
||||
|
||||
|
||||
@bp.route('/post/<int:post_id>/options', methods=['GET', 'POST'])
|
||||
@bp.route('/post/<int:post_id>/options', methods=['GET'])
|
||||
def post_options(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
return render_template('post/post_options.html', post=post)
|
||||
|
||||
@login_required
|
||||
@bp.route('/post/<int:post_id>/edit', methods=['GET', 'POST'])
|
||||
def post_edit(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
form = CreatePostForm()
|
||||
if post.user_id == current_user.id or post.community.is_moderator():
|
||||
if get_setting('allow_nsfw', False) is False:
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if get_setting('allow_nsfl', False) is False:
|
||||
form.nsfl.render_kw = {'disabled': True}
|
||||
images_disabled = 'disabled' if not get_setting('allow_local_image_posts', True) else ''
|
||||
|
||||
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
||||
|
||||
if form.validate_on_submit():
|
||||
save_post(form, post)
|
||||
post.community.last_active = datetime.utcnow()
|
||||
post.edited_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
flash(_('Your changes have been saved.'), 'success')
|
||||
return redirect(url_for('post.show_post', post_id=post.id))
|
||||
else:
|
||||
if post.type == constants.POST_TYPE_ARTICLE:
|
||||
form.type.data = 'discussion'
|
||||
form.discussion_title.data = post.title
|
||||
form.discussion_body.data = post.body
|
||||
elif post.type == constants.POST_TYPE_LINK:
|
||||
form.type.data = 'link'
|
||||
form.link_title.data = post.title
|
||||
form.link_url.data = post.url
|
||||
elif post.type == constants.POST_TYPE_IMAGE:
|
||||
form.type.data = 'image'
|
||||
form.image_title.data = post.title
|
||||
form.notify_author.data = post.notify_author
|
||||
return render_template('post/post_edit.html', title=_('Edit post'), form=form, post=post, images_disabled=images_disabled)
|
||||
else:
|
||||
abort(401)
|
||||
|
||||
|
||||
@login_required
|
||||
@bp.route('/post/<int:post_id>/delete', methods=['GET', 'POST'])
|
||||
def post_delete(post_id: int):
|
||||
post = Post.query.get_or_404(post_id)
|
||||
community = post.community
|
||||
if post.user_id == current_user.id or community.is_moderator():
|
||||
post.delete_dependencies()
|
||||
db.session.delete(post)
|
||||
db.session.commit()
|
||||
flash('Post deleted.')
|
||||
return redirect(url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name))
|
||||
|
||||
|
||||
@login_required
|
||||
@bp.route('/post/<int:post_id>/report', methods=['GET', 'POST'])
|
||||
def post_report(post_id: int):
|
||||
...
|
||||
|
||||
|
|
|
@ -458,6 +458,14 @@ nav.navbar {
|
|||
cursor: wait;
|
||||
}
|
||||
|
||||
.option_list {
|
||||
list-style-type: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.option_list li {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #777;
|
||||
|
|
|
@ -182,6 +182,15 @@ nav.navbar {
|
|||
cursor: wait;
|
||||
}
|
||||
|
||||
.option_list {
|
||||
list-style-type: none;
|
||||
margin-bottom: 0;
|
||||
|
||||
li {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: $dark-grey;
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
|
||||
</div>
|
||||
</div>
|
||||
{{ render_field(form.notify) }}
|
||||
{{ render_field(form.notify_author) }}
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
|
93
app/templates/post/post_edit.html
Normal file
93
app/templates/post/post_edit.html
Normal file
|
@ -0,0 +1,93 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<script nonce="{{ session['nonce'] }}" type="text/javascript">
|
||||
window.addEventListener("load", function () {
|
||||
var type = document.forms[0].elements['type'].value;
|
||||
var toClick = undefined;
|
||||
switch(type) {
|
||||
case '':
|
||||
case 'discussion':
|
||||
toClick = document.getElementById('discussion-tab');
|
||||
break;
|
||||
case 'link':
|
||||
toClick = document.getElementById('link-tab');
|
||||
break;
|
||||
case 'image':
|
||||
toClick = document.getElementById('image-tab');
|
||||
break;
|
||||
case 'poll':
|
||||
toClick = document.getElementById('poll-tab');
|
||||
break;
|
||||
}
|
||||
if(toClick) {
|
||||
toClick.click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<div class="row">
|
||||
<div class="col-8 position-relative main_pane">
|
||||
<h1>{{ _('Edit post') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.communities) }}
|
||||
<nav id="post_type_chooser">
|
||||
<div class="nav nav-tabs nav-justified" id="typeTab" role="tablist">
|
||||
<button class="nav-link active" id="discussion-tab" data-bs-toggle="tab" data-bs-target="#discussion-tab-pane"
|
||||
type="button" role="tab" aria-controls="discussion-tab-pane" aria-selected="true">Discussion</button>
|
||||
<button class="nav-link" id="link-tab" data-bs-toggle="tab" data-bs-target="#link-tab-pane"
|
||||
type="button" role="tab" aria-controls="link-tab-pane" aria-selected="false">Link</button>
|
||||
<button class="nav-link" id="image-tab" data-bs-toggle="tab" data-bs-target="#image-tab-pane"
|
||||
type="button" role="tab" aria-controls="image-tab-pane" aria-selected="false" {{ images_disabled }}>Image</button>
|
||||
<button class="nav-link" id="poll-tab" data-bs-toggle="tab" data-bs-target="#poll-tab-pane"
|
||||
type="button" role="tab" aria-controls="poll-tab-pane" aria-selected="false" disabled>Poll</button>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<div class="tab-pane fade show active" id="discussion-tab-pane" role="tabpanel" aria-labelledby="home-tab" tabindex="0">
|
||||
{{ render_field(form.discussion_title) }}
|
||||
{{ render_field(form.discussion_body) }}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="link-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">
|
||||
{{ render_field(form.link_title) }}
|
||||
{{ render_field(form.link_url) }}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="image-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">
|
||||
{{ render_field(form.image_title) }}
|
||||
{{ render_field(form.image_file) }}
|
||||
</div>
|
||||
<div class="tab-pane fade" id="poll-tab-pane" role="tabpanel" aria-labelledby="disabled-tab" tabindex="0">
|
||||
Poll
|
||||
</div>
|
||||
</div>
|
||||
{{ render_field(form.type) }}
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfw) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfl) }}
|
||||
</div>
|
||||
<div class="col">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{{ render_field(form.notify_author) }}
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ post.community.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ post.community.description|safe }}</p>
|
||||
<p>{{ post.community.rules|safe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -7,13 +7,13 @@
|
|||
<div class="card mt-5">
|
||||
<div class="card-body p-6">
|
||||
<div class="card-title">{{ _('Options for "%(post_title)s"', post_title=post.title) }}</div>
|
||||
<ul>
|
||||
{% if post.user_id == current_user.id or post.community.is_moderator() %}
|
||||
<li><a href="#" class="no-underline"><span class="fe fe-edit"></span> Edit</a></li>
|
||||
<li><a href="#" class="no-underline confirm_first"><span class="fe fe-delete"></span> Delete</a></li>
|
||||
<ul class="option_list">
|
||||
{% if current_user.is_authenticated and (post.user_id == current_user.id or post.community.is_moderator()) %}
|
||||
<li><a href="{{ url_for('post.post_edit', post_id=post.id) }}" class="no-underline" rel="nofollow"><span class="fe fe-edit"></span> Edit</a></li>
|
||||
<li><a href="{{ url_for('post.post_delete', post_id=post.id) }}" class="no-underline confirm_first" rel="nofollow"><span class="fe fe-delete"></span> Delete</a></li>
|
||||
{% endif %}
|
||||
{% if post.user_id != current_user.id %}
|
||||
<li><a href="#" class="no-underline"><span class="fe fe-report"></span> Report</a></li>
|
||||
<li><a href="{{ url_for('post.post_report', post_id=post.id) }}" class="no-underline"><span class="fe fe-report"></span> Report</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-8 position-relative main_pane">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
{% if user.cover_image() != '' %}
|
||||
<div class="community_header" style="height: 240px; background-image: url({{ user.cover_image() }});">
|
||||
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||
|
@ -57,7 +57,7 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-4">
|
||||
<div class="col-12 col-md-4">
|
||||
{% if current_user.is_authenticated and current_user.id == user.id %}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
|
|
15
app/utils.py
15
app/utils.py
|
@ -9,7 +9,7 @@ from bs4 import BeautifulSoup
|
|||
import requests
|
||||
import os
|
||||
import imghdr
|
||||
from flask import current_app, json, redirect, url_for
|
||||
from flask import current_app, json, redirect, url_for, request
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import text
|
||||
|
||||
|
@ -239,3 +239,16 @@ def permission_required(permission):
|
|||
return decorated_view
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# sends the user back to where they came from
|
||||
def back(default_url):
|
||||
# Get the referrer from the request headers
|
||||
referrer = request.referrer
|
||||
|
||||
# If the referrer exists and is not the same as the current request URL, redirect to the referrer
|
||||
if referrer and referrer != request.url:
|
||||
return redirect(referrer)
|
||||
|
||||
# If referrer is not available or is the same as the current request URL, redirect to the default URL
|
||||
return redirect(default_url)
|
||||
|
|
51
migrations/versions/f5706aa6b94c_notifications.py
Normal file
51
migrations/versions/f5706aa6b94c_notifications.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
"""notifications
|
||||
|
||||
Revision ID: f5706aa6b94c
|
||||
Revises: ee45b9ea4a0c
|
||||
Create Date: 2023-11-30 19:21:04.549875
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'f5706aa6b94c'
|
||||
down_revision = 'ee45b9ea4a0c'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('notification',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('title', sa.String(length=50), nullable=True),
|
||||
sa.Column('url', sa.String(length=512), nullable=True),
|
||||
sa.Column('read', sa.Boolean(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('author_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('notify_author', sa.Boolean(), nullable=True))
|
||||
|
||||
with op.batch_alter_table('post_reply', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('notify_author', sa.Boolean(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('post_reply', schema=None) as batch_op:
|
||||
batch_op.drop_column('notify_author')
|
||||
|
||||
with op.batch_alter_table('post', schema=None) as batch_op:
|
||||
batch_op.drop_column('notify_author')
|
||||
|
||||
op.drop_table('notification')
|
||||
# ### end Alembic commands ###
|
Loading…
Add table
Reference in a new issue