post edit and delete. beginning of notifications

This commit is contained in:
rimu 2023-11-30 20:57:51 +13:00
parent 38a00d3617
commit f0a4e01fe9
15 changed files with 398 additions and 104 deletions

View file

@ -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')))

View file

@ -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:

View file

@ -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)

View file

@ -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()

View file

@ -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')

View file

@ -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))

View file

@ -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):
...

View file

@ -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;

View file

@ -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;

View file

@ -49,7 +49,7 @@
</div>
</div>
{{ render_field(form.notify) }}
{{ render_field(form.notify_author) }}
{{ render_field(form.submit) }}
</form>
</div>

View 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 %}

View file

@ -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>

View file

@ -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">

View file

@ -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)

View 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 ###