mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
communities that are local-only, w access control for posting and voting
This commit is contained in:
parent
520db4a924
commit
74cc2d17c0
13 changed files with 202 additions and 63 deletions
|
@ -18,7 +18,8 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti
|
|||
user_removed_from_remote_server, create_post, create_post_reply, update_post_reply_from_activity, \
|
||||
update_post_from_activity, undo_vote, undo_downvote
|
||||
from app.utils import gibberish, get_setting, is_image_url, allowlist_html, html_to_markdown, render_template, \
|
||||
domain_from_url, markdown_to_html, community_membership, ap_datetime, markdown_to_text, ip_address
|
||||
domain_from_url, markdown_to_html, community_membership, ap_datetime, markdown_to_text, ip_address, can_downvote, \
|
||||
can_upvote, can_create
|
||||
import werkzeug.exceptions
|
||||
|
||||
|
||||
|
@ -247,7 +248,9 @@ def community_profile(actor):
|
|||
"moderators": f"https://{server}/c/{actor}/moderators",
|
||||
"featured": f"https://{server}/c/{actor}/featured",
|
||||
"attributedTo": f"https://{server}/c/{actor}/moderators",
|
||||
"postingRestrictedToMods": community.restricted_to_mods,
|
||||
"postingRestrictedToMods": community.restricted_to_mods or community.local_only,
|
||||
"newModsWanted": community.new_mods_wanted,
|
||||
"privateMods": community.private_mods,
|
||||
"url": f"https://{server}/c/{actor}",
|
||||
"publicKey": {
|
||||
"id": f"https://{server}/c/{actor}#main-key",
|
||||
|
@ -403,10 +406,11 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
if object_type in new_content_types: # create a new post
|
||||
in_reply_to = request_json['object']['object']['inReplyTo'] if 'inReplyTo' in \
|
||||
request_json['object']['object'] else None
|
||||
if not in_reply_to:
|
||||
post = create_post(activity_log, community, request_json['object'], user, announce_id=request_json['id'])
|
||||
else:
|
||||
post = create_post_reply(activity_log, community, in_reply_to, request_json['object'], user, announce_id=request_json['id'])
|
||||
if can_create(user, community):
|
||||
if not in_reply_to:
|
||||
post = create_post(activity_log, community, request_json['object'], user, announce_id=request_json['id'])
|
||||
else:
|
||||
post = create_post_reply(activity_log, community, in_reply_to, request_json['object'], user, announce_id=request_json['id'])
|
||||
else:
|
||||
activity_log.exception_message = 'Unacceptable type: ' + object_type
|
||||
else:
|
||||
|
@ -421,8 +425,14 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
user_ap_id = request_json['object']['actor']
|
||||
liked_ap_id = request_json['object']['object']
|
||||
user = find_actor_or_create(user_ap_id)
|
||||
if user and not user.is_local():
|
||||
liked = find_liked_object(liked_ap_id)
|
||||
liked = find_liked_object(liked_ap_id)
|
||||
|
||||
if user is None:
|
||||
activity_log.exception_message = 'Blocked or unfound user'
|
||||
elif user.is_local():
|
||||
activity_log.exception_message = 'Activity about local content which is already present'
|
||||
activity_log.result = 'ignored'
|
||||
elif can_upvote(user, liked):
|
||||
# insert into voted table
|
||||
if liked is None:
|
||||
activity_log.exception_message = 'Liked object not found'
|
||||
|
@ -439,11 +449,8 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
# todo: recalculate 'hotness' of liked post/reply
|
||||
# todo: if vote was on content in local community, federate the vote out to followers
|
||||
else:
|
||||
if user is None:
|
||||
activity_log.exception_message = 'Blocked or unfound user'
|
||||
if user and user.is_local():
|
||||
activity_log.exception_message = 'Activity about local content which is already present'
|
||||
activity_log.result = 'ignored'
|
||||
activity_log.exception_message = 'Cannot upvote this'
|
||||
activity_log.result = 'ignored'
|
||||
|
||||
elif request_json['object']['type'] == 'Dislike':
|
||||
activity_log.activity_type = request_json['object']['type']
|
||||
|
@ -453,28 +460,29 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
user_ap_id = request_json['object']['actor']
|
||||
liked_ap_id = request_json['object']['object']
|
||||
user = find_actor_or_create(user_ap_id)
|
||||
if user and not user.is_local():
|
||||
disliked = find_liked_object(liked_ap_id)
|
||||
disliked = find_liked_object(liked_ap_id)
|
||||
if user is None:
|
||||
activity_log.exception_message = 'Blocked or unfound user'
|
||||
elif user.is_local():
|
||||
activity_log.exception_message = 'Activity about local content which is already present'
|
||||
activity_log.result = 'ignored'
|
||||
elif can_downvote(user, disliked, site):
|
||||
# insert into voted table
|
||||
if disliked is None:
|
||||
activity_log.exception_message = 'Liked object not found'
|
||||
elif disliked is not None and isinstance(disliked, Post):
|
||||
downvote_post(disliked, user)
|
||||
activity_log.result = 'success'
|
||||
elif disliked is not None and isinstance(disliked, PostReply):
|
||||
downvote_post_reply(disliked, user)
|
||||
elif isinstance(disliked, (Post, PostReply)):
|
||||
if isinstance(disliked, Post):
|
||||
downvote_post(disliked, user)
|
||||
elif isinstance(disliked, PostReply):
|
||||
downvote_post_reply(disliked, user)
|
||||
activity_log.result = 'success'
|
||||
# todo: recalculate 'hotness' of liked post/reply
|
||||
# todo: if vote was on content in the local community, federate the vote out to followers
|
||||
else:
|
||||
activity_log.exception_message = 'Could not detect type of like'
|
||||
if activity_log.result == 'success':
|
||||
... # todo: recalculate 'hotness' of liked post/reply
|
||||
# todo: if vote was on content in local community, federate the vote out to followers
|
||||
else:
|
||||
if user is None:
|
||||
activity_log.exception_message = 'Blocked or unfound user'
|
||||
if user and user.is_local():
|
||||
activity_log.exception_message = 'Activity about local content which is already present'
|
||||
activity_log.result = 'ignored'
|
||||
activity_log.exception_message = 'Cannot downvote this'
|
||||
activity_log.result = 'ignored'
|
||||
elif request_json['object']['type'] == 'Delete':
|
||||
activity_log.activity_type = request_json['object']['type']
|
||||
user_ap_id = request_json['object']['actor']
|
||||
|
@ -634,12 +642,14 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
comment = PostReply.query.filter_by(ap_id=target_ap_id).first()
|
||||
if '/post/' in target_ap_id:
|
||||
post = Post.query.filter_by(ap_id=target_ap_id).first()
|
||||
if (user and not user.is_local()) and post:
|
||||
if (user and not user.is_local()) and post and can_upvote(user, post):
|
||||
upvote_post(post, user)
|
||||
activity_log.result = 'success'
|
||||
elif (user and not user.is_local()) and comment:
|
||||
elif (user and not user.is_local()) and comment and can_upvote(user, comment):
|
||||
upvote_post_reply(comment, user)
|
||||
activity_log.result = 'success'
|
||||
else:
|
||||
activity_log.exception_message = 'Could not find user or content for vote'
|
||||
|
||||
elif request_json['type'] == 'Dislike': # Downvote
|
||||
if get_setting('allow_dislike', True) is False:
|
||||
|
@ -655,10 +665,10 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
|||
comment = PostReply.query.filter_by(ap_id=target_ap_id).first()
|
||||
if '/post/' in target_ap_id:
|
||||
post = Post.query.filter_by(ap_id=target_ap_id).first()
|
||||
if (user and not user.is_local()) and comment:
|
||||
if (user and not user.is_local()) and comment and can_downvote(user, comment, site):
|
||||
downvote_post_reply(comment, user)
|
||||
activity_log.result = 'success'
|
||||
elif (user and not user.is_local()) and post:
|
||||
elif (user and not user.is_local()) and post and can_downvote(user, post, site):
|
||||
downvote_post(post, user)
|
||||
activity_log.result = 'success'
|
||||
else:
|
||||
|
|
|
@ -371,6 +371,8 @@ def actor_json_to_model(activity_json, address, server):
|
|||
rules_html=markdown_to_html(activity_json['rules'] if 'rules' in activity_json else ''),
|
||||
nsfw=activity_json['sensitive'],
|
||||
restricted_to_mods=activity_json['postingRestrictedToMods'],
|
||||
new_mods_wanted=activity_json['newModsWanted'] if 'newModsWanted' in activity_json else False,
|
||||
private_mods=activity_json['privateMods'] if 'privateMods' in activity_json else False,
|
||||
created_at=activity_json['published'] if 'published' in activity_json else utcnow(),
|
||||
last_active=activity_json['updated'] if 'updated' in activity_json else utcnow(),
|
||||
ap_id=f"{address[1:]}",
|
||||
|
|
|
@ -47,6 +47,9 @@ class EditCommunityForm(FlaskForm):
|
|||
banner_file = FileField(_('Banner image'))
|
||||
rules = TextAreaField(_l('Rules'))
|
||||
nsfw = BooleanField('Porn community')
|
||||
local_only = BooleanField('Only accept posts from current instance')
|
||||
restricted_to_mods = BooleanField('Only moderators can post')
|
||||
new_mods_wanted = BooleanField('New moderators wanted')
|
||||
show_home = BooleanField('Posts show on home page')
|
||||
show_popular = BooleanField('Posts can be popular')
|
||||
show_all = BooleanField('Posts show in All list')
|
||||
|
|
|
@ -194,6 +194,9 @@ def admin_community_edit(community_id):
|
|||
community.description = form.description.data
|
||||
community.rules = form.rules.data
|
||||
community.nsfw = form.nsfw.data
|
||||
community.local_only = form.local_only.data
|
||||
community.restricted_to_mods = form.restricted_to_mods.data
|
||||
community.new_mods_wanted = form.new_mods_wanted.data
|
||||
community.show_home = form.show_home.data
|
||||
community.show_popular = form.show_popular.data
|
||||
community.show_all = form.show_all.data
|
||||
|
@ -224,6 +227,9 @@ def admin_community_edit(community_id):
|
|||
form.description.data = community.description
|
||||
form.rules.data = community.rules
|
||||
form.nsfw.data = community.nsfw
|
||||
form.local_only.data = community.local_only
|
||||
form.new_mods_wanted.data = community.new_mods_wanted
|
||||
form.restricted_to_mods.data = community.restricted_to_mods
|
||||
form.show_home.data = community.show_home
|
||||
form.show_popular.data = community.show_popular
|
||||
form.show_all.data = community.show_all
|
||||
|
|
|
@ -17,7 +17,7 @@ from app.models import User, Community, CommunityMember, CommunityJoinRequest, C
|
|||
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, community_membership, ap_datetime, \
|
||||
request_etag_matches, return_304, instance_banned
|
||||
request_etag_matches, return_304, instance_banned, can_create
|
||||
from feedgen.feed import FeedGenerator
|
||||
from datetime import timezone
|
||||
|
||||
|
@ -313,8 +313,13 @@ def add_post(actor):
|
|||
|
||||
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
||||
|
||||
if not can_create(current_user, community):
|
||||
abort(401)
|
||||
|
||||
if form.validate_on_submit():
|
||||
community = Community.query.get_or_404(form.communities.data)
|
||||
if not can_create(current_user, community):
|
||||
abort(401)
|
||||
post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1)
|
||||
save_post(form, post)
|
||||
community.post_count += 1
|
||||
|
|
|
@ -105,6 +105,7 @@ class Community(db.Model):
|
|||
|
||||
banned = db.Column(db.Boolean, default=False)
|
||||
restricted_to_mods = db.Column(db.Boolean, default=False)
|
||||
local_only = db.Column(db.Boolean, default=False) # only users on this instance can post
|
||||
new_mods_wanted = db.Column(db.Boolean, default=False)
|
||||
searchable = db.Column(db.Boolean, default=True)
|
||||
private_mods = db.Column(db.Boolean, default=False)
|
||||
|
@ -116,8 +117,8 @@ class Community(db.Model):
|
|||
|
||||
search_vector = db.Column(TSVectorType('name', 'title', 'description', 'rules'))
|
||||
|
||||
posts = db.relationship('Post', backref='community', lazy='dynamic', cascade="all, delete-orphan")
|
||||
replies = db.relationship('PostReply', backref='community', lazy='dynamic', cascade="all, delete-orphan")
|
||||
posts = db.relationship('Post', lazy='dynamic', cascade="all, delete-orphan")
|
||||
replies = db.relationship('PostReply', lazy='dynamic', cascade="all, delete-orphan")
|
||||
icon = db.relationship('File', foreign_keys=[icon_id], single_parent=True, backref='community', cascade="all, delete-orphan")
|
||||
image = db.relationship('File', foreign_keys=[image_id], single_parent=True, cascade="all, delete-orphan")
|
||||
|
||||
|
@ -574,6 +575,7 @@ class Post(db.Model):
|
|||
image = db.relationship(File, lazy='joined', foreign_keys=[image_id], cascade="all, delete")
|
||||
domain = db.relationship('Domain', lazy='joined', foreign_keys=[domain_id])
|
||||
author = db.relationship('User', lazy='joined', overlaps='posts', foreign_keys=[user_id])
|
||||
community = db.relationship('Community', lazy='joined', overlaps='posts', foreign_keys=[community_id])
|
||||
replies = db.relationship('PostReply', lazy='dynamic', backref='post')
|
||||
|
||||
def is_local(self):
|
||||
|
@ -647,6 +649,7 @@ class PostReply(db.Model):
|
|||
search_vector = db.Column(TSVectorType('body'))
|
||||
|
||||
author = db.relationship('User', lazy='joined', foreign_keys=[user_id], single_parent=True, overlaps="post_replies")
|
||||
community = db.relationship('Community', lazy='joined', overlaps='replies', foreign_keys=[community_id])
|
||||
|
||||
def is_local(self):
|
||||
return self.ap_id is None or self.ap_id.startswith('https://' + current_app.config['SERVER_NAME'])
|
||||
|
|
|
@ -33,10 +33,13 @@
|
|||
<small class="field_hint">Provide a wide image - letterbox orientation.</small>
|
||||
{{ render_field(form.rules) }}
|
||||
{{ render_field(form.nsfw) }}
|
||||
{{ render_field(form.restricted_to_mods) }}
|
||||
{% if not community.is_local() %}
|
||||
<fieldset class="border pl-2 pt-2 mb-4">
|
||||
<legend>{{ _('Will not be overwritten by remote server') }}</legend>
|
||||
{% endif %}
|
||||
{{ render_field(form.local_only) }}
|
||||
{{ render_field(form.new_mods_wanted) }}
|
||||
{{ render_field(form.show_home) }}
|
||||
{{ render_field(form.show_popular) }}
|
||||
{{ render_field(form.show_all) }}
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
{% if current_user.is_authenticated and current_user.verified %}
|
||||
<div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }}"
|
||||
hx-post="/post/{{ post.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||
<span class="fe fe-arrow-up"></span>
|
||||
{{ post.up_votes }}
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
</div>
|
||||
<div class="downvote_button digits_{{ digits(post.down_votes) }} {{ downvoted_class }}"
|
||||
hx-post="/post/{{ post.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||
<span class="fe fe-arrow-down"></span>
|
||||
{{ post.down_votes }}
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
</div>
|
||||
{% if can_upvote(current_user, post) %}
|
||||
<div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }}"
|
||||
hx-post="/post/{{ post.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||
<span class="fe fe-arrow-up"></span>
|
||||
{{ post.up_votes }}
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if can_downvote(current_user, post) %}
|
||||
<div class="downvote_button digits_{{ digits(post.down_votes) }} {{ downvoted_class }}"
|
||||
hx-post="/post/{{ post.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||
<span class="fe fe-arrow-down"></span>
|
||||
{{ post.down_votes }}
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }}">
|
||||
<span class="fe fe-arrow-up"></span>
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
{% if current_user.is_authenticated and current_user.verified %}
|
||||
<div class="upvote_button digits_{{ digits(comment.up_votes) }} {{ upvoted_class }}"
|
||||
hx-post="/comment/{{ comment.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||
<span class="fe fe-arrow-up"></span>
|
||||
{{ comment.up_votes }}
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
</div>
|
||||
<div class="downvote_button digits_{{ digits(comment.down_votes) }} {{ downvoted_class }}"
|
||||
hx-post="/comment/{{ comment.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||
<span class="fe fe-arrow-down"></span>
|
||||
{{ comment.down_votes }}
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
</div>
|
||||
{% if can_upvote(current_user, comment) %}
|
||||
<div class="upvote_button digits_{{ digits(comment.up_votes) }} {{ upvoted_class }}"
|
||||
hx-post="/comment/{{ comment.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||
<span class="fe fe-arrow-up"></span>
|
||||
{{ comment.up_votes }}
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if can_downvote(current_user, comment) %}
|
||||
<div class="downvote_button digits_{{ digits(comment.down_votes) }} {{ downvoted_class }}"
|
||||
hx-post="/comment/{{ comment.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||
<span class="fe fe-arrow-down"></span>
|
||||
{{ comment.down_votes }}
|
||||
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="upvote_button digits_{{ digits(comment.up_votes) }} {{ upvoted_class }}">
|
||||
<span class="fe fe-arrow-up"></span>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<div class="card-body">
|
||||
<p>{{ _('If you wish to de-escalate the discussion on your post and now feel like it was a mistake, click the button below.') }}</p>
|
||||
<p>{{ _('No further comments will be posted and a message saying you made a mistake in this post will be displayed.') }}</p>
|
||||
<!-- <p>{{ _('The effect of downvotes on your reputation score will be removed.') }}</p> -->
|
||||
<p><a href="https://nickpunt.com/blog/deescalating-social-media/" target="_blank">More about this</a></p>
|
||||
{{ render_form(form) }}
|
||||
</div>
|
||||
|
|
68
app/utils.py
68
app/utils.py
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
|
||||
import random
|
||||
from datetime import datetime
|
||||
from typing import List, Literal
|
||||
from typing import List, Literal, Union
|
||||
|
||||
import markdown2
|
||||
import math
|
||||
|
@ -14,14 +14,15 @@ from bs4 import BeautifulSoup
|
|||
import requests
|
||||
import os
|
||||
import imghdr
|
||||
from flask import current_app, json, redirect, url_for, request, make_response, Response
|
||||
from flask import current_app, json, redirect, url_for, request, make_response, Response, g
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import text
|
||||
from wtforms.fields import SelectField, SelectMultipleField
|
||||
from wtforms.widgets import Select, html_params, ListWidget, CheckboxInput
|
||||
from app import db, cache
|
||||
|
||||
from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan
|
||||
from app.models import Settings, Domain, Instance, BannedInstances, User, Community, DomainBlock, ActivityPubLog, IpBan, \
|
||||
Site, Post, PostReply
|
||||
|
||||
|
||||
# Flask's render_template function, with support for themes added
|
||||
|
@ -374,3 +375,64 @@ def user_cookie_banned() -> bool:
|
|||
def banned_ip_addresses() -> List[str]:
|
||||
ips = IpBan.query.all()
|
||||
return [ip.ip_address for ip in ips]
|
||||
|
||||
|
||||
def can_downvote(user, content: Union[Post, PostReply], site=None) -> bool:
|
||||
if user is None or content is None or user.banned:
|
||||
return False
|
||||
|
||||
if site is None:
|
||||
try:
|
||||
site = g.site
|
||||
except:
|
||||
site = Site.query.get(1)
|
||||
|
||||
if not site.enable_downvotes and content.community.is_local():
|
||||
return False
|
||||
|
||||
if content.community.is_moderator(user) or user.is_admin():
|
||||
return True
|
||||
|
||||
if content.community.local_only and not user.is_local():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def can_upvote(user, content: Union[Post, PostReply]) -> bool:
|
||||
if user is None or content is None or user.banned:
|
||||
return False
|
||||
|
||||
if content.community.is_moderator(user) or user.is_admin():
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def can_create(user, content: Union[Community, Post, PostReply]) -> bool:
|
||||
if user is None or content is None or user.banned:
|
||||
return False
|
||||
|
||||
if isinstance(content, Community):
|
||||
if content.is_moderator(user) or user.is_admin():
|
||||
return True
|
||||
|
||||
if content.restricted_to_mods:
|
||||
return False
|
||||
|
||||
if content.local_only and not user.is_local():
|
||||
return False
|
||||
else:
|
||||
if content.community.is_moderator(user) or user.is_admin():
|
||||
return True
|
||||
|
||||
if content.community.restricted_to_mods and isinstance(content, Post):
|
||||
return False
|
||||
|
||||
if content.community.local_only and not user.is_local():
|
||||
return False
|
||||
|
||||
if isinstance(content, PostReply) and content.post.comments_enabled is False:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
32
migrations/versions/328c9990e53e_local_only_communities.py
Normal file
32
migrations/versions/328c9990e53e_local_only_communities.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
"""local only communities
|
||||
|
||||
Revision ID: 328c9990e53e
|
||||
Revises: b18ea0b841fe
|
||||
Create Date: 2024-01-02 16:10:04.201183
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '328c9990e53e'
|
||||
down_revision = 'b18ea0b841fe'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('community', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('local_only', sa.Boolean(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('community', schema=None) as batch_op:
|
||||
batch_op.drop_column('local_only')
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -7,7 +7,8 @@ import os, click
|
|||
from flask import session, g, json
|
||||
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
|
||||
from app.models import Site
|
||||
from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership
|
||||
from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership, \
|
||||
can_create, can_upvote, can_downvote
|
||||
|
||||
app = create_app()
|
||||
cli.register(app)
|
||||
|
@ -33,6 +34,9 @@ with app.app_context():
|
|||
app.jinja_env.globals['community_membership'] = community_membership
|
||||
app.jinja_env.globals['json_loads'] = json.loads
|
||||
app.jinja_env.globals['user_access'] = user_access
|
||||
app.jinja_env.globals['can_create'] = can_create
|
||||
app.jinja_env.globals['can_upvote'] = can_upvote
|
||||
app.jinja_env.globals['can_downvote'] = can_downvote
|
||||
app.jinja_env.filters['shorten'] = shorten_string
|
||||
app.jinja_env.filters['shorten_url'] = shorten_url
|
||||
|
||||
|
|
Loading…
Reference in a new issue