mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-27 05:11:25 -08:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
67660d3ee5
26 changed files with 12028 additions and 38 deletions
|
@ -19,6 +19,13 @@ from sqlalchemy_searchable import make_searchable
|
||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
|
|
||||||
|
def get_locale():
|
||||||
|
try:
|
||||||
|
return request.accept_languages.best_match(current_app.config['LANGUAGES'])
|
||||||
|
except:
|
||||||
|
return 'en'
|
||||||
|
|
||||||
|
|
||||||
db = SQLAlchemy() # engine_options={'pool_size': 5, 'max_overflow': 10} # session_options={"autoflush": False}
|
db = SQLAlchemy() # engine_options={'pool_size': 5, 'max_overflow': 10} # session_options={"autoflush": False}
|
||||||
migrate = Migrate()
|
migrate = Migrate()
|
||||||
login = LoginManager()
|
login = LoginManager()
|
||||||
|
@ -27,7 +34,7 @@ login.login_message = _l('Please log in to access this page.')
|
||||||
mail = Mail()
|
mail = Mail()
|
||||||
bootstrap = Bootstrap5()
|
bootstrap = Bootstrap5()
|
||||||
moment = Moment()
|
moment = Moment()
|
||||||
babel = Babel()
|
babel = Babel(locale_selector=get_locale)
|
||||||
cache = Cache()
|
cache = Cache()
|
||||||
celery = Celery(__name__, broker=Config.CELERY_BROKER_URL)
|
celery = Celery(__name__, broker=Config.CELERY_BROKER_URL)
|
||||||
|
|
||||||
|
@ -130,11 +137,4 @@ def create_app(config_class=Config):
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
def get_locale():
|
|
||||||
try:
|
|
||||||
return request.accept_languages.best_match(current_app.config['LANGUAGES'])
|
|
||||||
except:
|
|
||||||
return 'en_US'
|
|
||||||
|
|
||||||
|
|
||||||
from app import models
|
from app import models
|
||||||
|
|
|
@ -125,24 +125,24 @@ def post_to_activity(post: Post, community: Community):
|
||||||
],
|
],
|
||||||
"object": {
|
"object": {
|
||||||
"id": create_id,
|
"id": create_id,
|
||||||
"actor": f"https://{current_app.config['SERVER_NAME']}/u/{post.author.user_name}",
|
"actor": post.author.ap_public_url,
|
||||||
"to": [
|
"to": [
|
||||||
"https://www.w3.org/ns/activitystreams#Public"
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
],
|
],
|
||||||
"object": {
|
"object": {
|
||||||
"type": "Page",
|
"type": "Page",
|
||||||
"id": f"https://{current_app.config['SERVER_NAME']}/post/{post.id}",
|
"id": post.ap_id,
|
||||||
"attributedTo": f"https://{current_app.config['SERVER_NAME']}/u/{post.author.user_name}",
|
"attributedTo": post.author.ap_public_url,
|
||||||
"to": [
|
"to": [
|
||||||
f"https://{current_app.config['SERVER_NAME']}/c/{community.name}",
|
f"https://{current_app.config['SERVER_NAME']}/c/{community.name}",
|
||||||
"https://www.w3.org/ns/activitystreams#Public"
|
"https://www.w3.org/ns/activitystreams#Public"
|
||||||
],
|
],
|
||||||
"name": post.title,
|
"name": post.title,
|
||||||
"cc": [],
|
"cc": [],
|
||||||
"content": post.body_html,
|
"content": post.body_html if post.body_html else '',
|
||||||
"mediaType": "text/html",
|
"mediaType": "text/html",
|
||||||
"source": {
|
"source": {
|
||||||
"content": post.body,
|
"content": post.body if post.body else '',
|
||||||
"mediaType": "text/markdown"
|
"mediaType": "text/markdown"
|
||||||
},
|
},
|
||||||
"attachment": [],
|
"attachment": [],
|
||||||
|
@ -200,6 +200,7 @@ def instance_allowed(host: str) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def find_actor_or_create(actor: str, create_if_not_found=True, community_only=False) -> Union[User, Community, None]:
|
def find_actor_or_create(actor: str, create_if_not_found=True, community_only=False) -> Union[User, Community, None]:
|
||||||
|
actor_url = actor.strip()
|
||||||
actor = actor.strip().lower()
|
actor = actor.strip().lower()
|
||||||
user = None
|
user = None
|
||||||
# actor parameter must be formatted as https://server/u/actor or https://server/c/actor
|
# actor parameter must be formatted as https://server/u/actor or https://server/c/actor
|
||||||
|
@ -244,10 +245,15 @@ def find_actor_or_create(actor: str, create_if_not_found=True, community_only=Fa
|
||||||
if create_if_not_found:
|
if create_if_not_found:
|
||||||
if actor.startswith('https://'):
|
if actor.startswith('https://'):
|
||||||
try:
|
try:
|
||||||
actor_data = get_request(actor, headers={'Accept': 'application/activity+json'})
|
actor_data = get_request(actor_url, headers={'Accept': 'application/activity+json'})
|
||||||
except requests.exceptions.ReadTimeout:
|
except requests.exceptions.ReadTimeout:
|
||||||
time.sleep(randint(3, 10))
|
time.sleep(randint(3, 10))
|
||||||
actor_data = get_request(actor, headers={'Accept': 'application/activity+json'})
|
try:
|
||||||
|
actor_data = get_request(actor_url, headers={'Accept': 'application/activity+json'})
|
||||||
|
except requests.exceptions.ReadTimeout:
|
||||||
|
return None
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
return None
|
||||||
if actor_data.status_code == 200:
|
if actor_data.status_code == 200:
|
||||||
actor_json = actor_data.json()
|
actor_json = actor_data.json()
|
||||||
actor_data.close()
|
actor_data.close()
|
||||||
|
@ -698,6 +704,9 @@ def make_image_sizes_async(file_id, thumbnail_width, medium_width, directory):
|
||||||
content_type_parts = content_type.split('/')
|
content_type_parts = content_type.split('/')
|
||||||
if content_type_parts:
|
if content_type_parts:
|
||||||
file_ext = '.' + content_type_parts[-1]
|
file_ext = '.' + content_type_parts[-1]
|
||||||
|
else:
|
||||||
|
if '?' in file_ext:
|
||||||
|
file_ext = file_ext.split('?')[0]
|
||||||
|
|
||||||
new_filename = gibberish(15)
|
new_filename = gibberish(15)
|
||||||
|
|
||||||
|
@ -868,7 +877,7 @@ def refresh_instance_profile(instance_id: int):
|
||||||
@celery.task
|
@celery.task
|
||||||
def refresh_instance_profile_task(instance_id: int):
|
def refresh_instance_profile_task(instance_id: int):
|
||||||
instance = Instance.query.get(instance_id)
|
instance = Instance.query.get(instance_id)
|
||||||
if instance.updated_at < utcnow() - timedelta(days=7):
|
if instance.inbox is None or instance.updated_at < utcnow() - timedelta(days=7):
|
||||||
try:
|
try:
|
||||||
instance_data = get_request(f"https://{instance.domain}", headers={'Accept': 'application/activity+json'})
|
instance_data = get_request(f"https://{instance.domain}", headers={'Accept': 'application/activity+json'})
|
||||||
except:
|
except:
|
||||||
|
@ -930,6 +939,11 @@ def refresh_instance_profile_task(instance_id: int):
|
||||||
InstanceRole.instance_id == instance.id,
|
InstanceRole.instance_id == instance.id,
|
||||||
InstanceRole.role == 'admin').delete()
|
InstanceRole.role == 'admin').delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
elif instance_data.status_code == 406: # Mastodon does this
|
||||||
|
instance.software = 'Mastodon'
|
||||||
|
instance.inbox = f"https://{instance.domain}/inbox"
|
||||||
|
instance.updated_at = utcnow()
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
# alter the effect of upvotes based on their instance. Default to 1.0
|
# alter the effect of upvotes based on their instance. Default to 1.0
|
||||||
|
|
|
@ -86,7 +86,11 @@ def register(app):
|
||||||
db.session.add(Settings(name='registration_open', value=json.dumps(True)))
|
db.session.add(Settings(name='registration_open', value=json.dumps(True)))
|
||||||
db.session.add(Settings(name='approve_registrations', value=json.dumps(False)))
|
db.session.add(Settings(name='approve_registrations', value=json.dumps(False)))
|
||||||
db.session.add(Settings(name='federation', value=json.dumps(True)))
|
db.session.add(Settings(name='federation', value=json.dumps(True)))
|
||||||
banned_instances = ['anonib.al','lemmygrad.ml', 'gab.com', 'rqd2.net', 'exploding-heads.com', 'hexbear.net', 'threads.net', 'pieville.net', 'noauthority.social', 'pieville.net', 'links.hackliberty.org']
|
banned_instances = ['anonib.al','lemmygrad.ml', 'gab.com', 'rqd2.net', 'exploding-heads.com', 'hexbear.net',
|
||||||
|
'threads.net', 'noauthority.social', 'pieville.net', 'links.hackliberty.org',
|
||||||
|
'poa.st', 'freespeechextremist.com', 'bae.st', 'nicecrew.digital', 'detroitriotcity.com',
|
||||||
|
'pawoo.net', 'shitposter.club', 'spinster.xyz', 'catgirl.life', 'gameliberty.club',
|
||||||
|
'yggdrasil.social', 'beefyboys.win', 'brighteon.social', 'cum.salon']
|
||||||
for bi in banned_instances:
|
for bi in banned_instances:
|
||||||
db.session.add(BannedInstances(domain=bi))
|
db.session.add(BannedInstances(domain=bi))
|
||||||
print("Added banned instance", bi)
|
print("Added banned instance", bi)
|
||||||
|
|
|
@ -121,7 +121,13 @@ def show_community(community: Community):
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
sort = request.args.get('sort', '' if current_user.is_anonymous else current_user.default_sort)
|
sort = request.args.get('sort', '' if current_user.is_anonymous else current_user.default_sort)
|
||||||
low_bandwidth = request.cookies.get('low_bandwidth', '0') == '1'
|
low_bandwidth = request.cookies.get('low_bandwidth', '0') == '1'
|
||||||
post_layout = request.args.get('layout', community.default_layout if not low_bandwidth else None)
|
if low_bandwidth:
|
||||||
|
post_layout = None
|
||||||
|
else:
|
||||||
|
if community.default_layout is not None:
|
||||||
|
post_layout = request.args.get('layout', community.default_layout)
|
||||||
|
else:
|
||||||
|
post_layout = request.args.get('layout', 'list')
|
||||||
|
|
||||||
# If nothing has changed since their last visit, return HTTP 304
|
# If nothing has changed since their last visit, return HTTP 304
|
||||||
current_etag = f"{community.id}{sort}{post_layout}_{hash(community.last_active)}"
|
current_etag = f"{community.id}{sort}{post_layout}_{hash(community.last_active)}"
|
||||||
|
@ -846,3 +852,59 @@ def community_notification(community_id: int):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return render_template('community/_notification_toggle.html', community=community)
|
return render_template('community/_notification_toggle.html', community=community)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<actor>/moderate', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def community_moderate(actor):
|
||||||
|
community = actor_to_community(actor)
|
||||||
|
|
||||||
|
if community is not None:
|
||||||
|
if community.is_moderator() or current_user.is_admin():
|
||||||
|
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
search = request.args.get('search', '')
|
||||||
|
local_remote = request.args.get('local_remote', '')
|
||||||
|
|
||||||
|
reports = Report.query.filter_by(status=0, in_community_id=community.id)
|
||||||
|
if local_remote == 'local':
|
||||||
|
reports = reports.filter_by(ap_id=None)
|
||||||
|
if local_remote == 'remote':
|
||||||
|
reports = reports.filter(Report.ap_id != None)
|
||||||
|
reports = reports.order_by(desc(Report.created_at)).paginate(page=page, per_page=1000, error_out=False)
|
||||||
|
|
||||||
|
next_url = url_for('admin.admin_reports', page=reports.next_num) if reports.has_next else None
|
||||||
|
prev_url = url_for('admin.admin_reports', page=reports.prev_num) if reports.has_prev and page != 1 else None
|
||||||
|
|
||||||
|
return render_template('community/community_moderate.html', title=_('Moderation of %(community)s', community=community.display_name()),
|
||||||
|
community=community, reports=reports, current='reports',
|
||||||
|
next_url=next_url, prev_url=prev_url,
|
||||||
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
|
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
abort(401)
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/<actor>/moderate/banned', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def community_moderate_banned(actor):
|
||||||
|
community = actor_to_community(actor)
|
||||||
|
|
||||||
|
if community is not None:
|
||||||
|
if community.is_moderator() or current_user.is_admin():
|
||||||
|
banned_people = User.query.join(CommunityBan, CommunityBan.user_id == User.id).filter(CommunityBan.community_id == community.id).all()
|
||||||
|
return render_template('community/community_moderate_banned.html',
|
||||||
|
title=_('People banned from of %(community)s', community=community.display_name()),
|
||||||
|
community=community, banned_people=banned_people, current='banned',
|
||||||
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
|
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
abort(401)
|
||||||
|
else:
|
||||||
|
abort(404)
|
|
@ -211,7 +211,7 @@ def save_post(form, post: Post):
|
||||||
filename = opengraph.get('og:image') or opengraph.get('og:image:url')
|
filename = opengraph.get('og:image') or opengraph.get('og:image:url')
|
||||||
filename_for_extension = filename.split('?')[0] if '?' in filename else filename
|
filename_for_extension = filename.split('?')[0] if '?' in filename else filename
|
||||||
unused, file_extension = os.path.splitext(filename_for_extension)
|
unused, file_extension = os.path.splitext(filename_for_extension)
|
||||||
if file_extension.lower() in allowed_extensions:
|
if file_extension.lower() in allowed_extensions and not filename.startswith('/'):
|
||||||
file = url_to_thumbnail_file(filename)
|
file = url_to_thumbnail_file(filename)
|
||||||
if file:
|
if file:
|
||||||
file.alt_text = opengraph.get('og:title')
|
file.alt_text = opengraph.get('og:title')
|
||||||
|
|
|
@ -1120,11 +1120,12 @@ class Report(db.Model):
|
||||||
status = db.Column(db.Integer, default=0)
|
status = db.Column(db.Integer, default=0)
|
||||||
type = db.Column(db.Integer, default=0) # 0 = user, 1 = post, 2 = reply, 3 = community, 4 = conversation
|
type = db.Column(db.Integer, default=0) # 0 = user, 1 = post, 2 = reply, 3 = community, 4 = conversation
|
||||||
reporter_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
reporter_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
suspect_community_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
suspect_community_id = db.Column(db.Integer, db.ForeignKey('community.id'))
|
||||||
suspect_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
suspect_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||||
suspect_post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
|
suspect_post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
|
||||||
suspect_post_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id'))
|
suspect_post_reply_id = db.Column(db.Integer, db.ForeignKey('post_reply.id'))
|
||||||
suspect_conversation_id = db.Column(db.Integer, db.ForeignKey('conversation.id'))
|
suspect_conversation_id = db.Column(db.Integer, db.ForeignKey('conversation.id'))
|
||||||
|
in_community_id = db.Column(db.Integer, db.ForeignKey('community.id'))
|
||||||
created_at = db.Column(db.DateTime, default=utcnow)
|
created_at = db.Column(db.DateTime, default=utcnow)
|
||||||
updated = db.Column(db.DateTime, default=utcnow)
|
updated = db.Column(db.DateTime, default=utcnow)
|
||||||
|
|
||||||
|
|
|
@ -224,7 +224,7 @@ def show_post(post_id: int):
|
||||||
joined_communities=joined_communities(current_user.get_id()),
|
joined_communities=joined_communities(current_user.get_id()),
|
||||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||||
)
|
)
|
||||||
response.headers.set('Vary', 'Accept, Cookie')
|
response.headers.set('Vary', 'Accept, Cookie, Accept-Language')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -416,7 +416,7 @@ def continue_discussion(post_id, comment_id):
|
||||||
moderating_communities=moderating_communities(current_user.get_id()),
|
moderating_communities=moderating_communities(current_user.get_id()),
|
||||||
joined_communities=joined_communities(current_user.get_id()), community=post.community,
|
joined_communities=joined_communities(current_user.get_id()), community=post.community,
|
||||||
inoculation=inoculation[randint(0, len(inoculation) - 1)])
|
inoculation=inoculation[randint(0, len(inoculation) - 1)])
|
||||||
response.headers.set('Vary', 'Accept, Cookie')
|
response.headers.set('Vary', 'Accept, Cookie, Accept-Language')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -795,7 +795,7 @@ def post_report(post_id: int):
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
report = Report(reasons=form.reasons_to_string(form.reasons.data), description=form.description.data,
|
||||||
type=1, reporter_id=current_user.id, suspect_user_id=post.author.id, suspect_post_id=post.id,
|
type=1, reporter_id=current_user.id, suspect_user_id=post.author.id, suspect_post_id=post.id,
|
||||||
suspect_community_id=post.community.id)
|
suspect_community_id=post.community.id, in_community_id=post.community.id)
|
||||||
db.session.add(report)
|
db.session.add(report)
|
||||||
|
|
||||||
# Notify moderators
|
# Notify moderators
|
||||||
|
|
198
app/static/js/lightbox/baguetteBox.css
Normal file
198
app/static/js/lightbox/baguetteBox.css
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
/*!
|
||||||
|
* baguetteBox.js
|
||||||
|
* @author feimosi
|
||||||
|
* @version 1.11.1
|
||||||
|
* @url https://github.com/feimosi/baguetteBox.js
|
||||||
|
*/
|
||||||
|
#baguetteBox-overlay {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
position: fixed;
|
||||||
|
overflow: hidden;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1000000;
|
||||||
|
background-color: #222;
|
||||||
|
background-color: rgba(0, 0, 0, 0.8);
|
||||||
|
-webkit-transition: opacity .5s ease;
|
||||||
|
transition: opacity .5s ease; }
|
||||||
|
#baguetteBox-overlay.visible {
|
||||||
|
opacity: 1; }
|
||||||
|
#baguetteBox-overlay .full-image {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
text-align: center; }
|
||||||
|
#baguetteBox-overlay .full-image figure {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
height: 100%; }
|
||||||
|
#baguetteBox-overlay .full-image img {
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
-webkit-box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
|
||||||
|
-moz-box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6); }
|
||||||
|
#baguetteBox-overlay .full-image figcaption {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.8;
|
||||||
|
white-space: normal;
|
||||||
|
color: #ccc;
|
||||||
|
background-color: #000;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
font-family: sans-serif; }
|
||||||
|
#baguetteBox-overlay .full-image:before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
height: 50%;
|
||||||
|
width: 1px;
|
||||||
|
margin-right: -1px; }
|
||||||
|
|
||||||
|
#baguetteBox-slider {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-transition: left .4s ease, -webkit-transform .4s ease;
|
||||||
|
transition: left .4s ease, -webkit-transform .4s ease;
|
||||||
|
transition: left .4s ease, transform .4s ease;
|
||||||
|
transition: left .4s ease, transform .4s ease, -webkit-transform .4s ease, -moz-transform .4s ease; }
|
||||||
|
#baguetteBox-slider.bounce-from-right {
|
||||||
|
-webkit-animation: bounceFromRight .4s ease-out;
|
||||||
|
animation: bounceFromRight .4s ease-out; }
|
||||||
|
#baguetteBox-slider.bounce-from-left {
|
||||||
|
-webkit-animation: bounceFromLeft .4s ease-out;
|
||||||
|
animation: bounceFromLeft .4s ease-out; }
|
||||||
|
|
||||||
|
@-webkit-keyframes bounceFromRight {
|
||||||
|
0% {
|
||||||
|
margin-left: 0; }
|
||||||
|
50% {
|
||||||
|
margin-left: -30px; }
|
||||||
|
100% {
|
||||||
|
margin-left: 0; } }
|
||||||
|
|
||||||
|
@keyframes bounceFromRight {
|
||||||
|
0% {
|
||||||
|
margin-left: 0; }
|
||||||
|
50% {
|
||||||
|
margin-left: -30px; }
|
||||||
|
100% {
|
||||||
|
margin-left: 0; } }
|
||||||
|
|
||||||
|
@-webkit-keyframes bounceFromLeft {
|
||||||
|
0% {
|
||||||
|
margin-left: 0; }
|
||||||
|
50% {
|
||||||
|
margin-left: 30px; }
|
||||||
|
100% {
|
||||||
|
margin-left: 0; } }
|
||||||
|
|
||||||
|
@keyframes bounceFromLeft {
|
||||||
|
0% {
|
||||||
|
margin-left: 0; }
|
||||||
|
50% {
|
||||||
|
margin-left: 30px; }
|
||||||
|
100% {
|
||||||
|
margin-left: 0; } }
|
||||||
|
|
||||||
|
.baguetteBox-button#next-button, .baguetteBox-button#previous-button {
|
||||||
|
top: 50%;
|
||||||
|
top: calc(50% - 30px);
|
||||||
|
width: 44px;
|
||||||
|
height: 60px; }
|
||||||
|
|
||||||
|
.baguetteBox-button {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
-moz-border-radius: 15%;
|
||||||
|
border-radius: 15%;
|
||||||
|
background-color: #323232;
|
||||||
|
background-color: rgba(50, 50, 50, 0.5);
|
||||||
|
color: #ddd;
|
||||||
|
font: 1.6em sans-serif;
|
||||||
|
-webkit-transition: background-color .4s ease;
|
||||||
|
transition: background-color .4s ease; }
|
||||||
|
.baguetteBox-button:focus, .baguetteBox-button:hover {
|
||||||
|
background-color: rgba(50, 50, 50, 0.9); }
|
||||||
|
.baguetteBox-button#next-button {
|
||||||
|
right: 2%; }
|
||||||
|
.baguetteBox-button#previous-button {
|
||||||
|
left: 2%; }
|
||||||
|
.baguetteBox-button#close-button {
|
||||||
|
top: 20px;
|
||||||
|
right: 2%;
|
||||||
|
right: calc(2% + 6px);
|
||||||
|
width: 30px;
|
||||||
|
height: 30px; }
|
||||||
|
.baguetteBox-button svg {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0; }
|
||||||
|
|
||||||
|
/*
|
||||||
|
Preloader
|
||||||
|
Borrowed from http://tobiasahlin.com/spinkit/
|
||||||
|
*/
|
||||||
|
.baguetteBox-spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -20px;
|
||||||
|
margin-left: -20px; }
|
||||||
|
|
||||||
|
.baguetteBox-double-bounce1,
|
||||||
|
.baguetteBox-double-bounce2 {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
-moz-border-radius: 50%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
opacity: .6;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
-webkit-animation: bounce 2s infinite ease-in-out;
|
||||||
|
animation: bounce 2s infinite ease-in-out; }
|
||||||
|
|
||||||
|
.baguetteBox-double-bounce2 {
|
||||||
|
-webkit-animation-delay: -1s;
|
||||||
|
animation-delay: -1s; }
|
||||||
|
|
||||||
|
@-webkit-keyframes bounce {
|
||||||
|
0%, 100% {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
transform: scale(0); }
|
||||||
|
50% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1); } }
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-moz-transform: scale(0);
|
||||||
|
transform: scale(0); }
|
||||||
|
50% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
transform: scale(1); } }
|
793
app/static/js/lightbox/baguetteBox.js
Normal file
793
app/static/js/lightbox/baguetteBox.js
Normal file
|
@ -0,0 +1,793 @@
|
||||||
|
/*!
|
||||||
|
* baguetteBox.js
|
||||||
|
* @author feimosi
|
||||||
|
* @version 1.11.1
|
||||||
|
* @url https://github.com/feimosi/baguetteBox.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* global define, module */
|
||||||
|
|
||||||
|
(function (root, factory) {
|
||||||
|
'use strict';
|
||||||
|
if (typeof define === 'function' && define.amd) {
|
||||||
|
define(factory);
|
||||||
|
} else if (typeof exports === 'object') {
|
||||||
|
module.exports = factory();
|
||||||
|
} else {
|
||||||
|
root.baguetteBox = factory();
|
||||||
|
}
|
||||||
|
}(this, function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// SVG shapes used on the buttons
|
||||||
|
var leftArrow = '<svg width="44" height="60">' +
|
||||||
|
'<polyline points="30 10 10 30 30 50" stroke="rgba(255,255,255,0.5)" stroke-width="4"' +
|
||||||
|
'stroke-linecap="butt" fill="none" stroke-linejoin="round"/>' +
|
||||||
|
'</svg>',
|
||||||
|
rightArrow = '<svg width="44" height="60">' +
|
||||||
|
'<polyline points="14 10 34 30 14 50" stroke="rgba(255,255,255,0.5)" stroke-width="4"' +
|
||||||
|
'stroke-linecap="butt" fill="none" stroke-linejoin="round"/>' +
|
||||||
|
'</svg>',
|
||||||
|
closeX = '<svg width="30" height="30">' +
|
||||||
|
'<g stroke="rgb(160,160,160)" stroke-width="4">' +
|
||||||
|
'<line x1="5" y1="5" x2="25" y2="25"/>' +
|
||||||
|
'<line x1="5" y1="25" x2="25" y2="5"/>' +
|
||||||
|
'</g></svg>';
|
||||||
|
// Global options and their defaults
|
||||||
|
var options = {},
|
||||||
|
defaults = {
|
||||||
|
captions: true,
|
||||||
|
buttons: 'auto',
|
||||||
|
fullScreen: false,
|
||||||
|
noScrollbars: false,
|
||||||
|
bodyClass: 'baguetteBox-open',
|
||||||
|
titleTag: false,
|
||||||
|
async: false,
|
||||||
|
preload: 2,
|
||||||
|
animation: 'slideIn',
|
||||||
|
afterShow: null,
|
||||||
|
afterHide: null,
|
||||||
|
onChange: null,
|
||||||
|
overlayBackgroundColor: 'rgba(0,0,0,.8)'
|
||||||
|
};
|
||||||
|
// Object containing information about features compatibility
|
||||||
|
var supports = {};
|
||||||
|
// DOM Elements references
|
||||||
|
var overlay, slider, previousButton, nextButton, closeButton;
|
||||||
|
// An array with all images in the current gallery
|
||||||
|
var currentGallery = [];
|
||||||
|
// Current image index inside the slider
|
||||||
|
var currentIndex = 0;
|
||||||
|
// Visibility of the overlay
|
||||||
|
var isOverlayVisible = false;
|
||||||
|
// Touch event start position (for slide gesture)
|
||||||
|
var touch = {};
|
||||||
|
// If set to true ignore touch events because animation was already fired
|
||||||
|
var touchFlag = false;
|
||||||
|
// Regex pattern to match image files
|
||||||
|
var regex = /.+\.(gif|jpe?g|png|webp)/i;
|
||||||
|
// Object of all used galleries
|
||||||
|
var data = {};
|
||||||
|
// Array containing temporary images DOM elements
|
||||||
|
var imagesElements = [];
|
||||||
|
// The last focused element before opening the overlay
|
||||||
|
var documentLastFocus = null;
|
||||||
|
var overlayClickHandler = function(event) {
|
||||||
|
// Close the overlay when user clicks directly on the background
|
||||||
|
if (event.target.id.indexOf('baguette-img') !== -1) {
|
||||||
|
hideOverlay();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var previousButtonClickHandler = function(event) {
|
||||||
|
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; // eslint-disable-line no-unused-expressions
|
||||||
|
showPreviousImage();
|
||||||
|
};
|
||||||
|
var nextButtonClickHandler = function(event) {
|
||||||
|
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; // eslint-disable-line no-unused-expressions
|
||||||
|
showNextImage();
|
||||||
|
};
|
||||||
|
var closeButtonClickHandler = function(event) {
|
||||||
|
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; // eslint-disable-line no-unused-expressions
|
||||||
|
hideOverlay();
|
||||||
|
};
|
||||||
|
var touchstartHandler = function(event) {
|
||||||
|
touch.count++;
|
||||||
|
if (touch.count > 1) {
|
||||||
|
touch.multitouch = true;
|
||||||
|
}
|
||||||
|
// Save x and y axis position
|
||||||
|
touch.startX = event.changedTouches[0].pageX;
|
||||||
|
touch.startY = event.changedTouches[0].pageY;
|
||||||
|
};
|
||||||
|
var touchmoveHandler = function(event) {
|
||||||
|
// If action was already triggered or multitouch return
|
||||||
|
if (touchFlag || touch.multitouch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault ? event.preventDefault() : event.returnValue = false; // eslint-disable-line no-unused-expressions
|
||||||
|
var touchEvent = event.touches[0] || event.changedTouches[0];
|
||||||
|
// Move at least 40 pixels to trigger the action
|
||||||
|
if (touchEvent.pageX - touch.startX > 40) {
|
||||||
|
touchFlag = true;
|
||||||
|
showPreviousImage();
|
||||||
|
} else if (touchEvent.pageX - touch.startX < -40) {
|
||||||
|
touchFlag = true;
|
||||||
|
showNextImage();
|
||||||
|
// Move 100 pixels up to close the overlay
|
||||||
|
} else if (touch.startY - touchEvent.pageY > 100) {
|
||||||
|
hideOverlay();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var touchendHandler = function() {
|
||||||
|
touch.count--;
|
||||||
|
if (touch.count <= 0) {
|
||||||
|
touch.multitouch = false;
|
||||||
|
}
|
||||||
|
touchFlag = false;
|
||||||
|
};
|
||||||
|
var contextmenuHandler = function() {
|
||||||
|
touchendHandler();
|
||||||
|
};
|
||||||
|
|
||||||
|
var trapFocusInsideOverlay = function(event) {
|
||||||
|
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(event.target))) {
|
||||||
|
event.stopPropagation();
|
||||||
|
initFocus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// forEach polyfill for IE8
|
||||||
|
// http://stackoverflow.com/a/14827443/1077846
|
||||||
|
/* eslint-disable */
|
||||||
|
if (![].forEach) {
|
||||||
|
Array.prototype.forEach = function(callback, thisArg) {
|
||||||
|
for (var i = 0; i < this.length; i++) {
|
||||||
|
callback.call(thisArg, this[i], i, this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter polyfill for IE8
|
||||||
|
// https://gist.github.com/eliperelman/1031656
|
||||||
|
if (![].filter) {
|
||||||
|
Array.prototype.filter = function(a, b, c, d, e) {
|
||||||
|
c = this;
|
||||||
|
d = [];
|
||||||
|
for (e = 0; e < c.length; e++)
|
||||||
|
a.call(b, c[e], e, c) && d.push(c[e]);
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
|
// Script entry point
|
||||||
|
function run(selector, userOptions) {
|
||||||
|
// Fill supports object
|
||||||
|
supports.transforms = testTransformsSupport();
|
||||||
|
supports.svg = testSvgSupport();
|
||||||
|
supports.passiveEvents = testPassiveEventsSupport();
|
||||||
|
|
||||||
|
buildOverlay();
|
||||||
|
removeFromCache(selector);
|
||||||
|
return bindImageClickListeners(selector, userOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindImageClickListeners(selector, userOptions) {
|
||||||
|
// For each gallery bind a click event to every image inside it
|
||||||
|
var galleryNodeList = document.querySelectorAll(selector);
|
||||||
|
var selectorData = {
|
||||||
|
galleries: [],
|
||||||
|
nodeList: galleryNodeList
|
||||||
|
};
|
||||||
|
data[selector] = selectorData;
|
||||||
|
|
||||||
|
[].forEach.call(galleryNodeList, function(galleryElement) {
|
||||||
|
if (userOptions && userOptions.filter) {
|
||||||
|
regex = userOptions.filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get nodes from gallery elements or single-element galleries
|
||||||
|
var tagsNodeList = [];
|
||||||
|
if (galleryElement.tagName === 'A') {
|
||||||
|
tagsNodeList = [galleryElement];
|
||||||
|
} else {
|
||||||
|
tagsNodeList = galleryElement.getElementsByTagName('a');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter 'a' elements from those not linking to images
|
||||||
|
tagsNodeList = [].filter.call(tagsNodeList, function(element) {
|
||||||
|
if (element.className.indexOf(userOptions && userOptions.ignoreClass) === -1) {
|
||||||
|
return regex.test(element.href);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (tagsNodeList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gallery = [];
|
||||||
|
[].forEach.call(tagsNodeList, function(imageElement, imageIndex) {
|
||||||
|
var imageElementClickHandler = function(event) {
|
||||||
|
event.preventDefault ? event.preventDefault() : event.returnValue = false; // eslint-disable-line no-unused-expressions
|
||||||
|
prepareOverlay(gallery, userOptions);
|
||||||
|
showOverlay(imageIndex);
|
||||||
|
};
|
||||||
|
var imageItem = {
|
||||||
|
eventHandler: imageElementClickHandler,
|
||||||
|
imageElement: imageElement
|
||||||
|
};
|
||||||
|
bind(imageElement, 'click', imageElementClickHandler);
|
||||||
|
gallery.push(imageItem);
|
||||||
|
});
|
||||||
|
selectorData.galleries.push(gallery);
|
||||||
|
});
|
||||||
|
|
||||||
|
return selectorData.galleries;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCachedData() {
|
||||||
|
for (var selector in data) {
|
||||||
|
if (data.hasOwnProperty(selector)) {
|
||||||
|
removeFromCache(selector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFromCache(selector) {
|
||||||
|
if (!data.hasOwnProperty(selector)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var galleries = data[selector].galleries;
|
||||||
|
[].forEach.call(galleries, function(gallery) {
|
||||||
|
[].forEach.call(gallery, function(imageItem) {
|
||||||
|
unbind(imageItem.imageElement, 'click', imageItem.eventHandler);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentGallery === gallery) {
|
||||||
|
currentGallery = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
delete data[selector];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildOverlay() {
|
||||||
|
overlay = getByID('baguetteBox-overlay');
|
||||||
|
// Check if the overlay already exists
|
||||||
|
if (overlay) {
|
||||||
|
slider = getByID('baguetteBox-slider');
|
||||||
|
previousButton = getByID('previous-button');
|
||||||
|
nextButton = getByID('next-button');
|
||||||
|
closeButton = getByID('close-button');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Create overlay element
|
||||||
|
overlay = create('div');
|
||||||
|
overlay.setAttribute('role', 'dialog');
|
||||||
|
overlay.id = 'baguetteBox-overlay';
|
||||||
|
document.getElementsByTagName('body')[0].appendChild(overlay);
|
||||||
|
// Create gallery slider element
|
||||||
|
slider = create('div');
|
||||||
|
slider.id = 'baguetteBox-slider';
|
||||||
|
overlay.appendChild(slider);
|
||||||
|
// Create all necessary buttons
|
||||||
|
previousButton = create('button');
|
||||||
|
previousButton.setAttribute('type', 'button');
|
||||||
|
previousButton.id = 'previous-button';
|
||||||
|
previousButton.setAttribute('aria-label', 'Previous');
|
||||||
|
previousButton.innerHTML = supports.svg ? leftArrow : '<';
|
||||||
|
overlay.appendChild(previousButton);
|
||||||
|
|
||||||
|
nextButton = create('button');
|
||||||
|
nextButton.setAttribute('type', 'button');
|
||||||
|
nextButton.id = 'next-button';
|
||||||
|
nextButton.setAttribute('aria-label', 'Next');
|
||||||
|
nextButton.innerHTML = supports.svg ? rightArrow : '>';
|
||||||
|
overlay.appendChild(nextButton);
|
||||||
|
|
||||||
|
closeButton = create('button');
|
||||||
|
closeButton.setAttribute('type', 'button');
|
||||||
|
closeButton.id = 'close-button';
|
||||||
|
closeButton.setAttribute('aria-label', 'Close');
|
||||||
|
closeButton.innerHTML = supports.svg ? closeX : '×';
|
||||||
|
overlay.appendChild(closeButton);
|
||||||
|
|
||||||
|
previousButton.className = nextButton.className = closeButton.className = 'baguetteBox-button';
|
||||||
|
|
||||||
|
bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyDownHandler(event) {
|
||||||
|
switch (event.keyCode) {
|
||||||
|
case 37: // Left arrow
|
||||||
|
showPreviousImage();
|
||||||
|
break;
|
||||||
|
case 39: // Right arrow
|
||||||
|
showNextImage();
|
||||||
|
break;
|
||||||
|
case 27: // Esc
|
||||||
|
hideOverlay();
|
||||||
|
break;
|
||||||
|
case 36: // Home
|
||||||
|
showFirstImage(event);
|
||||||
|
break;
|
||||||
|
case 35: // End
|
||||||
|
showLastImage(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents() {
|
||||||
|
var passiveEvent = supports.passiveEvents ? { passive: false } : null;
|
||||||
|
var nonPassiveEvent = supports.passiveEvents ? { passive: true } : null;
|
||||||
|
|
||||||
|
bind(overlay, 'click', overlayClickHandler);
|
||||||
|
bind(previousButton, 'click', previousButtonClickHandler);
|
||||||
|
bind(nextButton, 'click', nextButtonClickHandler);
|
||||||
|
bind(closeButton, 'click', closeButtonClickHandler);
|
||||||
|
bind(slider, 'contextmenu', contextmenuHandler);
|
||||||
|
bind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||||
|
bind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||||
|
bind(overlay, 'touchend', touchendHandler);
|
||||||
|
bind(document, 'focus', trapFocusInsideOverlay, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbindEvents() {
|
||||||
|
var passiveEvent = supports.passiveEvents ? { passive: false } : null;
|
||||||
|
var nonPassiveEvent = supports.passiveEvents ? { passive: true } : null;
|
||||||
|
|
||||||
|
unbind(overlay, 'click', overlayClickHandler);
|
||||||
|
unbind(previousButton, 'click', previousButtonClickHandler);
|
||||||
|
unbind(nextButton, 'click', nextButtonClickHandler);
|
||||||
|
unbind(closeButton, 'click', closeButtonClickHandler);
|
||||||
|
unbind(slider, 'contextmenu', contextmenuHandler);
|
||||||
|
unbind(overlay, 'touchstart', touchstartHandler, nonPassiveEvent);
|
||||||
|
unbind(overlay, 'touchmove', touchmoveHandler, passiveEvent);
|
||||||
|
unbind(overlay, 'touchend', touchendHandler);
|
||||||
|
unbind(document, 'focus', trapFocusInsideOverlay, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareOverlay(gallery, userOptions) {
|
||||||
|
// If the same gallery is being opened prevent from loading it once again
|
||||||
|
if (currentGallery === gallery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentGallery = gallery;
|
||||||
|
// Update gallery specific options
|
||||||
|
setOptions(userOptions);
|
||||||
|
// Empty slider of previous contents (more effective than .innerHTML = "")
|
||||||
|
while (slider.firstChild) {
|
||||||
|
slider.removeChild(slider.firstChild);
|
||||||
|
}
|
||||||
|
imagesElements.length = 0;
|
||||||
|
|
||||||
|
var imagesFiguresIds = [];
|
||||||
|
var imagesCaptionsIds = [];
|
||||||
|
// Prepare and append images containers and populate figure and captions IDs arrays
|
||||||
|
for (var i = 0, fullImage; i < gallery.length; i++) {
|
||||||
|
fullImage = create('div');
|
||||||
|
fullImage.className = 'full-image';
|
||||||
|
fullImage.id = 'baguette-img-' + i;
|
||||||
|
imagesElements.push(fullImage);
|
||||||
|
|
||||||
|
imagesFiguresIds.push('baguetteBox-figure-' + i);
|
||||||
|
imagesCaptionsIds.push('baguetteBox-figcaption-' + i);
|
||||||
|
slider.appendChild(imagesElements[i]);
|
||||||
|
}
|
||||||
|
overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));
|
||||||
|
overlay.setAttribute('aria-describedby', imagesCaptionsIds.join(' '));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOptions(newOptions) {
|
||||||
|
if (!newOptions) {
|
||||||
|
newOptions = {};
|
||||||
|
}
|
||||||
|
// Fill options object
|
||||||
|
for (var item in defaults) {
|
||||||
|
options[item] = defaults[item];
|
||||||
|
if (typeof newOptions[item] !== 'undefined') {
|
||||||
|
options[item] = newOptions[item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Apply new options */
|
||||||
|
// Change transition for proper animation
|
||||||
|
slider.style.transition = slider.style.webkitTransition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
|
||||||
|
options.animation === 'slideIn' ? '' : 'none');
|
||||||
|
// Hide buttons if necessary
|
||||||
|
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) {
|
||||||
|
options.buttons = false;
|
||||||
|
}
|
||||||
|
// Set buttons style to hide or display them
|
||||||
|
previousButton.style.display = nextButton.style.display = (options.buttons ? '' : 'none');
|
||||||
|
// Set overlay color
|
||||||
|
try {
|
||||||
|
overlay.style.backgroundColor = options.overlayBackgroundColor;
|
||||||
|
} catch (e) {
|
||||||
|
// Silence the error and continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showOverlay(chosenImageIndex) {
|
||||||
|
if (options.noScrollbars) {
|
||||||
|
document.documentElement.style.overflowY = 'hidden';
|
||||||
|
document.body.style.overflowY = 'scroll';
|
||||||
|
}
|
||||||
|
if (overlay.style.display === 'block') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bind(document, 'keydown', keyDownHandler);
|
||||||
|
currentIndex = chosenImageIndex;
|
||||||
|
touch = {
|
||||||
|
count: 0,
|
||||||
|
startX: null,
|
||||||
|
startY: null
|
||||||
|
};
|
||||||
|
loadImage(currentIndex, function() {
|
||||||
|
preloadNext(currentIndex);
|
||||||
|
preloadPrev(currentIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
updateOffset();
|
||||||
|
overlay.style.display = 'block';
|
||||||
|
if (options.fullScreen) {
|
||||||
|
enterFullScreen();
|
||||||
|
}
|
||||||
|
// Fade in overlay
|
||||||
|
setTimeout(function() {
|
||||||
|
overlay.className = 'visible';
|
||||||
|
if (options.bodyClass && document.body.classList) {
|
||||||
|
document.body.classList.add(options.bodyClass);
|
||||||
|
}
|
||||||
|
if (options.afterShow) {
|
||||||
|
options.afterShow();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
if (options.onChange) {
|
||||||
|
options.onChange(currentIndex, imagesElements.length);
|
||||||
|
}
|
||||||
|
documentLastFocus = document.activeElement;
|
||||||
|
initFocus();
|
||||||
|
isOverlayVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFocus() {
|
||||||
|
if (options.buttons) {
|
||||||
|
previousButton.focus();
|
||||||
|
} else {
|
||||||
|
closeButton.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterFullScreen() {
|
||||||
|
if (overlay.requestFullscreen) {
|
||||||
|
overlay.requestFullscreen();
|
||||||
|
} else if (overlay.webkitRequestFullscreen) {
|
||||||
|
overlay.webkitRequestFullscreen();
|
||||||
|
} else if (overlay.mozRequestFullScreen) {
|
||||||
|
overlay.mozRequestFullScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitFullscreen() {
|
||||||
|
if (document.exitFullscreen) {
|
||||||
|
document.exitFullscreen();
|
||||||
|
} else if (document.mozCancelFullScreen) {
|
||||||
|
document.mozCancelFullScreen();
|
||||||
|
} else if (document.webkitExitFullscreen) {
|
||||||
|
document.webkitExitFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideOverlay() {
|
||||||
|
if (options.noScrollbars) {
|
||||||
|
document.documentElement.style.overflowY = 'auto';
|
||||||
|
document.body.style.overflowY = 'auto';
|
||||||
|
}
|
||||||
|
if (overlay.style.display === 'none') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unbind(document, 'keydown', keyDownHandler);
|
||||||
|
// Fade out and hide the overlay
|
||||||
|
overlay.className = '';
|
||||||
|
setTimeout(function() {
|
||||||
|
overlay.style.display = 'none';
|
||||||
|
if (document.fullscreen) {
|
||||||
|
exitFullscreen();
|
||||||
|
}
|
||||||
|
if (options.bodyClass && document.body.classList) {
|
||||||
|
document.body.classList.remove(options.bodyClass);
|
||||||
|
}
|
||||||
|
if (options.afterHide) {
|
||||||
|
options.afterHide();
|
||||||
|
}
|
||||||
|
documentLastFocus && documentLastFocus.focus();
|
||||||
|
isOverlayVisible = false;
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadImage(index, callback) {
|
||||||
|
var imageContainer = imagesElements[index];
|
||||||
|
var galleryItem = currentGallery[index];
|
||||||
|
|
||||||
|
// Return if the index exceeds prepared images in the overlay
|
||||||
|
// or if the current gallery has been changed / closed
|
||||||
|
if (typeof imageContainer === 'undefined' || typeof galleryItem === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If image is already loaded run callback and return
|
||||||
|
if (imageContainer.getElementsByTagName('img')[0]) {
|
||||||
|
if (callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get element reference, optional caption and source path
|
||||||
|
var imageElement = galleryItem.imageElement;
|
||||||
|
var thumbnailElement = imageElement.getElementsByTagName('img')[0];
|
||||||
|
var imageCaption = typeof options.captions === 'function' ?
|
||||||
|
options.captions.call(currentGallery, imageElement) :
|
||||||
|
imageElement.getAttribute('data-caption') || imageElement.title;
|
||||||
|
var imageSrc = getImageSrc(imageElement);
|
||||||
|
|
||||||
|
// Prepare figure element
|
||||||
|
var figure = create('figure');
|
||||||
|
figure.id = 'baguetteBox-figure-' + index;
|
||||||
|
figure.innerHTML = '<div class="baguetteBox-spinner">' +
|
||||||
|
'<div class="baguetteBox-double-bounce1"></div>' +
|
||||||
|
'<div class="baguetteBox-double-bounce2"></div>' +
|
||||||
|
'</div>';
|
||||||
|
// Insert caption if available
|
||||||
|
if (options.captions && imageCaption) {
|
||||||
|
var figcaption = create('figcaption');
|
||||||
|
figcaption.id = 'baguetteBox-figcaption-' + index;
|
||||||
|
figcaption.innerHTML = imageCaption;
|
||||||
|
figure.appendChild(figcaption);
|
||||||
|
}
|
||||||
|
imageContainer.appendChild(figure);
|
||||||
|
|
||||||
|
// Prepare gallery img element
|
||||||
|
var image = create('img');
|
||||||
|
image.onload = function() {
|
||||||
|
// Remove loader element
|
||||||
|
var spinner = document.querySelector('#baguette-img-' + index + ' .baguetteBox-spinner');
|
||||||
|
figure.removeChild(spinner);
|
||||||
|
if (!options.async && callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
image.setAttribute('src', imageSrc);
|
||||||
|
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
|
||||||
|
if (options.titleTag && imageCaption) {
|
||||||
|
image.title = imageCaption;
|
||||||
|
}
|
||||||
|
figure.appendChild(image);
|
||||||
|
|
||||||
|
// Run callback
|
||||||
|
if (options.async && callback) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get image source location, mostly used for responsive images
|
||||||
|
function getImageSrc(image) {
|
||||||
|
// Set default image path from href
|
||||||
|
var result = image.href;
|
||||||
|
// If dataset is supported find the most suitable image
|
||||||
|
if (image.dataset) {
|
||||||
|
var srcs = [];
|
||||||
|
// Get all possible image versions depending on the resolution
|
||||||
|
for (var item in image.dataset) {
|
||||||
|
if (item.substring(0, 3) === 'at-' && !isNaN(item.substring(3))) {
|
||||||
|
srcs[item.replace('at-', '')] = image.dataset[item];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sort resolutions ascending
|
||||||
|
var keys = Object.keys(srcs).sort(function(a, b) {
|
||||||
|
return parseInt(a, 10) < parseInt(b, 10) ? -1 : 1;
|
||||||
|
});
|
||||||
|
// Get real screen resolution
|
||||||
|
var width = window.innerWidth * window.devicePixelRatio;
|
||||||
|
// Find the first image bigger than or equal to the current width
|
||||||
|
var i = 0;
|
||||||
|
while (i < keys.length - 1 && keys[i] < width) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
result = srcs[keys[i]] || result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false at the right end of the gallery
|
||||||
|
function showNextImage() {
|
||||||
|
return show(currentIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false at the left end of the gallery
|
||||||
|
function showPreviousImage() {
|
||||||
|
return show(currentIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false at the left end of the gallery
|
||||||
|
function showFirstImage(event) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
return show(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return false at the right end of the gallery
|
||||||
|
function showLastImage(event) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
return show(currentGallery.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the gallery to a specific index
|
||||||
|
* @param `index` {number} - the position of the image
|
||||||
|
* @param `gallery` {array} - gallery which should be opened, if omitted assumes the currently opened one
|
||||||
|
* @return {boolean} - true on success or false if the index is invalid
|
||||||
|
*/
|
||||||
|
function show(index, gallery) {
|
||||||
|
if (!isOverlayVisible && index >= 0 && index < gallery.length) {
|
||||||
|
prepareOverlay(gallery, options);
|
||||||
|
showOverlay(index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (index < 0) {
|
||||||
|
if (options.animation) {
|
||||||
|
bounceAnimation('left');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (index >= imagesElements.length) {
|
||||||
|
if (options.animation) {
|
||||||
|
bounceAnimation('right');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentIndex = index;
|
||||||
|
loadImage(currentIndex, function() {
|
||||||
|
preloadNext(currentIndex);
|
||||||
|
preloadPrev(currentIndex);
|
||||||
|
});
|
||||||
|
updateOffset();
|
||||||
|
|
||||||
|
if (options.onChange) {
|
||||||
|
options.onChange(currentIndex, imagesElements.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers the bounce animation
|
||||||
|
* @param {('left'|'right')} direction - Direction of the movement
|
||||||
|
*/
|
||||||
|
function bounceAnimation(direction) {
|
||||||
|
slider.className = 'bounce-from-' + direction;
|
||||||
|
setTimeout(function() {
|
||||||
|
slider.className = '';
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOffset() {
|
||||||
|
var offset = -currentIndex * 100 + '%';
|
||||||
|
if (options.animation === 'fadeIn') {
|
||||||
|
slider.style.opacity = 0;
|
||||||
|
setTimeout(function() {
|
||||||
|
supports.transforms ?
|
||||||
|
slider.style.transform = slider.style.webkitTransform = 'translate3d(' + offset + ',0,0)'
|
||||||
|
: slider.style.left = offset;
|
||||||
|
slider.style.opacity = 1;
|
||||||
|
}, 400);
|
||||||
|
} else {
|
||||||
|
supports.transforms ?
|
||||||
|
slider.style.transform = slider.style.webkitTransform = 'translate3d(' + offset + ',0,0)'
|
||||||
|
: slider.style.left = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CSS 3D Transforms test
|
||||||
|
function testTransformsSupport() {
|
||||||
|
var div = create('div');
|
||||||
|
return typeof div.style.perspective !== 'undefined' || typeof div.style.webkitPerspective !== 'undefined';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inline SVG test
|
||||||
|
function testSvgSupport() {
|
||||||
|
var div = create('div');
|
||||||
|
div.innerHTML = '<svg/>';
|
||||||
|
return (div.firstChild && div.firstChild.namespaceURI) === 'http://www.w3.org/2000/svg';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Borrowed from https://github.com/seiyria/bootstrap-slider/pull/680/files
|
||||||
|
/* eslint-disable getter-return */
|
||||||
|
function testPassiveEventsSupport() {
|
||||||
|
var passiveEvents = false;
|
||||||
|
try {
|
||||||
|
var opts = Object.defineProperty({}, 'passive', {
|
||||||
|
get: function() {
|
||||||
|
passiveEvents = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('test', null, opts);
|
||||||
|
} catch (e) { /* Silence the error and continue */ }
|
||||||
|
|
||||||
|
return passiveEvents;
|
||||||
|
}
|
||||||
|
/* eslint-enable getter-return */
|
||||||
|
|
||||||
|
function preloadNext(index) {
|
||||||
|
if (index - currentIndex >= options.preload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadImage(index + 1, function() {
|
||||||
|
preloadNext(index + 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function preloadPrev(index) {
|
||||||
|
if (currentIndex - index >= options.preload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadImage(index - 1, function() {
|
||||||
|
preloadPrev(index - 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind(element, event, callback, options) {
|
||||||
|
if (element.addEventListener) {
|
||||||
|
element.addEventListener(event, callback, options);
|
||||||
|
} else {
|
||||||
|
// IE8 fallback
|
||||||
|
element.attachEvent('on' + event, function(event) {
|
||||||
|
// `event` and `event.target` are not provided in IE8
|
||||||
|
event = event || window.event;
|
||||||
|
event.target = event.target || event.srcElement;
|
||||||
|
callback(event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unbind(element, event, callback, options) {
|
||||||
|
if (element.removeEventListener) {
|
||||||
|
element.removeEventListener(event, callback, options);
|
||||||
|
} else {
|
||||||
|
// IE8 fallback
|
||||||
|
element.detachEvent('on' + event, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getByID(id) {
|
||||||
|
return document.getElementById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(element) {
|
||||||
|
return document.createElement(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyPlugin() {
|
||||||
|
unbindEvents();
|
||||||
|
clearCachedData();
|
||||||
|
unbind(document, 'keydown', keyDownHandler);
|
||||||
|
document.getElementsByTagName('body')[0].removeChild(document.getElementById('baguetteBox-overlay'));
|
||||||
|
data = {};
|
||||||
|
currentGallery = [];
|
||||||
|
currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
run: run,
|
||||||
|
show: show,
|
||||||
|
showNext: showNextImage,
|
||||||
|
showPrevious: showPreviousImage,
|
||||||
|
hide: hideOverlay,
|
||||||
|
destroy: destroyPlugin
|
||||||
|
};
|
||||||
|
}));
|
|
@ -12,8 +12,37 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||||
setupTopicChooser();
|
setupTopicChooser();
|
||||||
setupConversationChooser();
|
setupConversationChooser();
|
||||||
setupMarkdownEditorEnabler();
|
setupMarkdownEditorEnabler();
|
||||||
|
setupLightboxGallery();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function setupLightboxGallery() {
|
||||||
|
// Check if there are elements with either "post_list_masonry_wide" or "post_list_masonry" class
|
||||||
|
var widePosts = document.querySelectorAll('.post_list_masonry_wide');
|
||||||
|
var regularPosts = document.querySelectorAll('.post_list_masonry');
|
||||||
|
|
||||||
|
// Enable lightbox on masonry images
|
||||||
|
if (widePosts.length > 0) {
|
||||||
|
baguetteBox.run('.post_list_masonry_wide', {
|
||||||
|
fullScreen: false,
|
||||||
|
titleTag: true,
|
||||||
|
preload: 5,
|
||||||
|
captions: function(element) {
|
||||||
|
return element.getElementsByTagName('img')[0].title;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (regularPosts.length > 0) {
|
||||||
|
baguetteBox.run('.post_list_masonry', {
|
||||||
|
fullScreen: false,
|
||||||
|
titleTag: true,
|
||||||
|
preload: 5,
|
||||||
|
captions: function(element) {
|
||||||
|
return element.getElementsByTagName('img')[0].title;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// fires after all resources have loaded, including stylesheets and js files
|
// fires after all resources have loaded, including stylesheets and js files
|
||||||
window.addEventListener("load", function () {
|
window.addEventListener("load", function () {
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
<link href="{{ url_for('static', filename='themes/high_contrast/styles.css') }}" type="text/css" rel="alternate stylesheet" title="High contrast" />
|
<link href="{{ url_for('static', filename='themes/high_contrast/styles.css') }}" type="text/css" rel="alternate stylesheet" title="High contrast" />
|
||||||
{% if not low_bandwidth %}
|
{% if not low_bandwidth %}
|
||||||
<link href="{{ url_for('static', filename='js/markdown/downarea.css') }}" type="text/css" rel="stylesheet" />
|
<link href="{{ url_for('static', filename='js/markdown/downarea.css') }}" type="text/css" rel="stylesheet" />
|
||||||
|
<link href="{{ url_for('static', filename='js/lightbox/baguetteBox.css') }}" type="text/css" rel="stylesheet" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/styles.css') %}
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/styles.css') %}
|
||||||
<link href="{{ url_for('static', filename='themes/' + theme() + '/styles.css') }}" type="text/css" rel="stylesheet" />
|
<link href="{{ url_for('static', filename='themes/' + theme() + '/styles.css') }}" type="text/css" rel="stylesheet" />
|
||||||
|
@ -234,10 +235,14 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% if not low_bandwidth %}
|
{% if not low_bandwidth %}
|
||||||
{{ str(bootstrap.load_js()).replace('<script ', '<script nonce="' + session['nonce'] + '" ')|safe }}
|
{{ str(bootstrap.load_js()).replace('<script ', '<script nonce="' + session['nonce'] + '" ')|safe }}
|
||||||
|
<script src="{{ url_for('static', filename='js/lightbox/baguetteBox.js') }}" nonce="{{ session['nonce'] }}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/htmx.min.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='js/htmx.min.js') }}"></script>
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/scripts.js', changed=getmtime('js/scripts.js')) }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='js/scripts.js', changed=getmtime('js/scripts.js')) }}"></script>
|
||||||
{% if not low_bandwidth %}
|
{% if not low_bandwidth %}
|
||||||
|
{% if post_layout == 'masonry' or post_layout == 'masonry_wide' %}
|
||||||
|
<!-- -->
|
||||||
|
{% endif %}
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/markdown/downarea.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='js/markdown/downarea.js') }}"></script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/scripts.js') %}
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/scripts.js') %}
|
||||||
|
|
14
app/templates/community/_community_moderation_nav.html
Normal file
14
app/templates/community/_community_moderation_nav.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<div class="btn-group mt-1 mb-2">
|
||||||
|
<a href="/community/{{ community.link() }}/moderate" aria-label="{{ _('Sort by hot') }}" class="btn {{ 'btn-primary' if current == '' or current == 'reports' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
||||||
|
{{ _('Reports') }}
|
||||||
|
</a>
|
||||||
|
<a href="/community/{{ community.link() }}/moderate/banned" class="btn {{ 'btn-primary' if current == 'banned' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
||||||
|
{{ _('Banned people') }}
|
||||||
|
</a>
|
||||||
|
<a href="/community/{{ community.link() }}/moderate/appeals" class="btn {{ 'btn-primary' if current == 'appeals' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
||||||
|
{{ _('Appeals') }}
|
||||||
|
</a>
|
||||||
|
<a href="/community/{{ community.link() }}/moderate/modlog" class="btn {{ 'btn-primary' if current == 'modlog' else 'btn-outline-secondary' }}" rel="nofollow noindex">
|
||||||
|
{{ _('Mod log') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
|
@ -175,7 +175,9 @@
|
||||||
<h2>{{ _('Community Settings') }}</h2>
|
<h2>{{ _('Community Settings') }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p><a href="#" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
{% if is_moderator or is_owner or is_admin %}
|
||||||
|
<p><a href="/community/{{ community.link() }}/moderate" class="btn btn-primary">{{ _('Moderate') }}</a></p>
|
||||||
|
{% endif %}
|
||||||
{% if is_owner or is_admin %}
|
{% if is_owner or is_admin %}
|
||||||
<p><a href="{{ url_for('community.community_edit', community_id=community.id) }}" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
<p><a href="{{ url_for('community.community_edit', community_id=community.id) }}" class="btn btn-primary">{{ _('Settings') }}</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
84
app/templates/community/community_moderate.html
Normal file
84
app/templates/community/community_moderate.html
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||||
|
{% extends 'themes/' + theme() + '/base.html' %}
|
||||||
|
{% else %}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% endif %} %}
|
||||||
|
{% from 'bootstrap/form.html' import render_field %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8 position-relative main_pane">
|
||||||
|
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not none else community.name) }}">{{ (community.title + '@' + community.ap_domain)|shorten }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ _('Moderation') }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-10">
|
||||||
|
<h1 class="mt-2">{{ _('Moderation of %(community)s', community=community.display_name()) }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-2 text-right">
|
||||||
|
<!-- <a class="btn btn-primary" href="{{ url_for('community.community_add_moderator', community_id=community.id) }}">{{ _('Add moderator') }}</a> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include "community/_community_moderation_nav.html" %}
|
||||||
|
<h2>{{ _('Reports') }}</h2>
|
||||||
|
{% if reports.items %}
|
||||||
|
<form method="get">
|
||||||
|
<input type="search" name="search" value="{{ search }}">
|
||||||
|
<input type="radio" name="local_remote" value="local" id="local_remote_local" {{ 'checked' if local_remote == 'local' }}><label for="local_remote_local"> Local</label>
|
||||||
|
<input type="radio" name="local_remote" value="remote" id="local_remote_remote" {{ 'checked' if local_remote == 'remote' }}><label for="local_remote_remote"> Remote</label>
|
||||||
|
<input type="submit" name="submit" value="Search" class="btn btn-primary">
|
||||||
|
</form>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<th>Local/Remote</th>
|
||||||
|
<th>Reasons</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
{% for report in reports.items %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ 'Local' if report.is_local() else 'Remote' }}</td>
|
||||||
|
<td>{{ report.reasons }}</td>
|
||||||
|
<td>{{ report.description }}</td>
|
||||||
|
<td>{{ report.type_text() }}</td>
|
||||||
|
<td>{{ moment(report.created_at).fromNow() }}</td>
|
||||||
|
<td>
|
||||||
|
{% if report.suspect_conversation_id %}
|
||||||
|
<a href="/chat/{{ report.suspect_conversation_id }}#message">View</a>
|
||||||
|
{% elif report.suspect_post_reply_id %}
|
||||||
|
<a href="/post/{{ report.suspect_post_id }}#comment_{{ report.suspect_post_reply_id }}">View</a>
|
||||||
|
{% elif report.suspect_post_id %}
|
||||||
|
<a href="/post/{{ report.suspect_post_id }}">View</a>
|
||||||
|
{% elif report.suspect_user_id %}
|
||||||
|
<a href="/user/{{ report.suspect_user_id }}">View</a>
|
||||||
|
{% elif report.suspect_community_id %}
|
||||||
|
<a href="/user/{{ report.suspect_community_id }}">View</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
<nav aria-label="Pagination" class="mt-4" role="navigation">
|
||||||
|
{% if prev_url %}
|
||||||
|
<a href="{{ prev_url }}" class="btn btn-primary">
|
||||||
|
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if next_url %}
|
||||||
|
<a href="{{ next_url }}" class="btn btn-primary">
|
||||||
|
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ _('No reports yet') }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
65
app/templates/community/community_moderate_banned.html
Normal file
65
app/templates/community/community_moderate_banned.html
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||||
|
{% extends 'themes/' + theme() + '/base.html' %}
|
||||||
|
{% else %}
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% endif %} %}
|
||||||
|
{% from 'bootstrap/form.html' import render_field %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8 position-relative main_pane">
|
||||||
|
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="{{ url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not none else community.name) }}">{{ (community.title + '@' + community.ap_domain)|shorten }}</a></li>
|
||||||
|
<li class="breadcrumb-item active">{{ _('Moderation') }}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-10">
|
||||||
|
<h1 class="mt-2">{{ _('Moderation of %(community)s', community=community.display_name()) }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-2 text-right">
|
||||||
|
<!-- <a class="btn btn-primary" href="{{ url_for('community.community_add_moderator', community_id=community.id) }}">{{ _('Add moderator') }}</a> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include "community/_community_moderation_nav.html" %}
|
||||||
|
<h2>{{ _('Banned people') }}</h2>
|
||||||
|
{% if banned_people %}
|
||||||
|
<form method="get">
|
||||||
|
<input type="search" name="search" value="{{ search }}">
|
||||||
|
<input type="radio" name="local_remote" value="local" id="local_remote_local" {{ 'checked' if local_remote == 'local' }}><label for="local_remote_local"> Local</label>
|
||||||
|
<input type="radio" name="local_remote" value="remote" id="local_remote_remote" {{ 'checked' if local_remote == 'remote' }}><label for="local_remote_remote"> Remote</label>
|
||||||
|
<input type="submit" name="submit" value="Search" class="btn btn-primary">
|
||||||
|
</form>
|
||||||
|
<table class="table table-striped mt-1">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Local/Remote</th>
|
||||||
|
<th>Reports</th>
|
||||||
|
<th>IP</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
{% for user in banned_people %}
|
||||||
|
<tr>
|
||||||
|
<td><img src="{{ user.avatar_thumbnail() }}" class="community_icon rounded-circle" loading="lazy" />
|
||||||
|
{{ user.display_name() }}</td>
|
||||||
|
<td>{{ 'Local' if user.is_local() else 'Remote' }}</td>
|
||||||
|
<td>{{ user.reports if user.reports > 0 }} </td>
|
||||||
|
<td>{{ user.ip_address if user.ip_address }} </td>
|
||||||
|
<td>{% if user.is_local() %}
|
||||||
|
<a href="/u/{{ user.link() }}">View local</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ user.ap_profile_id }}">View remote</a>
|
||||||
|
{% endif %}
|
||||||
|
| <a href="#" class="confirm_first">{{ _('Un ban') }}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ _('No banned people yet') }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -14,20 +14,23 @@
|
||||||
{% if post.type == POST_TYPE_LINK %}
|
{% if post.type == POST_TYPE_LINK %}
|
||||||
{% if post.image.medium_url() %}
|
{% if post.image.medium_url() %}
|
||||||
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('View image') }}"><img src="{{ post.image.medium_url() }}"
|
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('View image') }}"><img src="{{ post.image.medium_url() }}"
|
||||||
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" loading="lazy" width="{{ post.image.width }}" height="{{ post.image.height }}" /></a>
|
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" title="{{ post.title }}"
|
||||||
|
loading="lazy" width="{{ post.image.width }}" height="{{ post.image.height }}" /></a>
|
||||||
{% elif post.image.source_url %}
|
{% elif post.image.source_url %}
|
||||||
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('View image') }}"><img src="{{ post.image.source_url }}"
|
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('View image') }}"><img src="{{ post.image.source_url }}"
|
||||||
alt="{{ post.title }}" loading="lazy" /></a>
|
alt="{{ post.title }}" title="{{ post.title }}" loading="{{ 'lazy' if low_bandwidth else 'eager' }}" /></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('View image') }}"><img src="{{ post.url }}"
|
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('View image') }}"><img src="{{ post.url }}"
|
||||||
alt="{{ post.title }}" loading="{{ 'lazy' if low_bandwidth else 'eager' }}" /></a>
|
alt="{{ post.title }}" title="{{ post.title }}"
|
||||||
|
loading="{{ 'lazy' if low_bandwidth else 'eager' }}" /></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% elif post.type == POST_TYPE_IMAGE %}
|
{% elif post.type == POST_TYPE_IMAGE %}
|
||||||
<a href="{{ post.image.view_url() }}" rel="nofollow ugc" target="_blank"><img src="{{ post.image.medium_url() }}"
|
<a href="{{ post.image.view_url() }}" rel="nofollow ugc" target="_blank"><img src="{{ post.image.medium_url() }}"
|
||||||
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" loading="lazy" width="{{ post.image.width }}" height="{{ post.image.height }}" /></a>
|
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" title="{{ post.title }}"
|
||||||
|
loading="lazy" width="{{ post.image.width }}" height="{{ post.image.height }}" /></a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('activitypub.post_ap', post_id=post.id) }}"><img src="{{ post.image.thumbnail_url() }}"
|
<a href="{{ url_for('activitypub.post_ap', post_id=post.id) }}"><img src="{{ post.image.thumbnail_url() }}"
|
||||||
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" loading="lazy" /></a>
|
alt="{{ post.image.alt_text if post.image.alt_text else '' }}" loading="{{ 'lazy' if low_bandwidth else 'eager' }}" /></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="masonry_info">
|
<div class="masonry_info">
|
||||||
|
@ -49,8 +52,9 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if post.url and (post.url.endswith('.jpg') or post.url.endswith('.webp') or post.url.endswith('.png') or post.url.endswith('.gif') or post.url.endswith('.avif') or post.url.endswith('.jpeg')) %}
|
{% if post.url and (post.url.endswith('.jpg') or post.url.endswith('.webp') or post.url.endswith('.png') or post.url.endswith('.gif') or post.url.endswith('.avif') or post.url.endswith('.jpeg')) %}
|
||||||
<div class="masonry_thumb" title="{{ post.title }}">
|
<div class="masonry_thumb" title="{{ post.title }}">
|
||||||
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('See image') }}"><img src="{{ post.url }}"
|
<a href="{{ post.url }}" rel="nofollow ugc" target="_blank" aria-label="{{ _('View image') }}"><img src="{{ post.url }}"
|
||||||
alt="{{ post.title }}" loading="{{ 'lazy' if low_bandwidth else 'eager' }}" /></a>
|
alt="{{ post.title }}" title="{{ post.title }}"
|
||||||
|
loading="{{ 'lazy' if low_bandwidth else 'eager' }}" /></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="masonry_info">
|
<div class="masonry_info">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
2607
app/translations/de/LC_MESSAGES/messages.po
Normal file
2607
app/translations/de/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load diff
2683
app/translations/fr/LC_MESSAGES/messages.po
Normal file
2683
app/translations/fr/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load diff
2683
app/translations/ja/LC_MESSAGES/messages.po
Normal file
2683
app/translations/ja/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load diff
2683
app/translations/pt/LC_MESSAGES/messages.po
Normal file
2683
app/translations/pt/LC_MESSAGES/messages.po
Normal file
File diff suppressed because it is too large
Load diff
13
app/utils.py
13
app/utils.py
|
@ -13,7 +13,9 @@ import math
|
||||||
from urllib.parse import urlparse, parse_qs, urlencode
|
from urllib.parse import urlparse, parse_qs, urlencode
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import flask
|
import flask
|
||||||
from bs4 import BeautifulSoup, NavigableString
|
from bs4 import BeautifulSoup, NavigableString, MarkupResemblesLocatorWarning
|
||||||
|
import warnings
|
||||||
|
warnings.filterwarnings("ignore", category=MarkupResemblesLocatorWarning)
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
from flask import current_app, json, redirect, url_for, request, make_response, Response, g
|
from flask import current_app, json, redirect, url_for, request, make_response, Response, g
|
||||||
|
@ -57,7 +59,7 @@ def return_304(etag, content_type=None):
|
||||||
resp = make_response('', 304)
|
resp = make_response('', 304)
|
||||||
resp.headers.add_header('ETag', request.headers['If-None-Match'])
|
resp.headers.add_header('ETag', request.headers['If-None-Match'])
|
||||||
resp.headers.add_header('Cache-Control', 'no-cache, max-age=600, must-revalidate')
|
resp.headers.add_header('Cache-Control', 'no-cache, max-age=600, must-revalidate')
|
||||||
resp.headers.add_header('Vary', 'Accept, Cookie')
|
resp.headers.add_header('Vary', 'Accept, Cookie, Accept-Language')
|
||||||
if content_type:
|
if content_type:
|
||||||
resp.headers.set('Content-Type', content_type)
|
resp.headers.set('Content-Type', content_type)
|
||||||
return resp
|
return resp
|
||||||
|
@ -91,6 +93,9 @@ def get_request(uri, params=None, headers=None) -> requests.Response:
|
||||||
except requests.exceptions.ReadTimeout as read_timeout:
|
except requests.exceptions.ReadTimeout as read_timeout:
|
||||||
current_app.logger.info(f"{uri} {read_timeout}")
|
current_app.logger.info(f"{uri} {read_timeout}")
|
||||||
raise requests.exceptions.ReadTimeout from read_timeout
|
raise requests.exceptions.ReadTimeout from read_timeout
|
||||||
|
except requests.exceptions.ConnectionError as connection_error:
|
||||||
|
current_app.logger.info(f"{uri} {connection_error}")
|
||||||
|
raise requests.exceptions.ConnectionError from connection_error
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -194,7 +199,7 @@ def allowlist_html(html: str) -> str:
|
||||||
else:
|
else:
|
||||||
# Filter and sanitize attributes
|
# Filter and sanitize attributes
|
||||||
for attr in list(tag.attrs):
|
for attr in list(tag.attrs):
|
||||||
if attr not in ['href', 'src', 'alt']:
|
if attr not in ['href', 'src', 'alt', 'class']:
|
||||||
del tag[attr]
|
del tag[attr]
|
||||||
# Add nofollow and target=_blank to anchors
|
# Add nofollow and target=_blank to anchors
|
||||||
if tag.name == 'a':
|
if tag.name == 'a':
|
||||||
|
@ -246,7 +251,7 @@ def html_to_markdown_worker(element, indent_level=0):
|
||||||
|
|
||||||
def markdown_to_html(markdown_text) -> str:
|
def markdown_to_html(markdown_text) -> str:
|
||||||
if markdown_text:
|
if markdown_text:
|
||||||
return allowlist_html(markdown2.markdown(markdown_text, safe_mode=True, extras={'middle-word-em': False, 'tables': True}))
|
return allowlist_html(markdown2.markdown(markdown_text, safe_mode=True, extras={'middle-word-em': False, 'tables': True, 'fenced-code-blocks': True, 'spoiler': True}))
|
||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
[python: app/**.py]
|
[python: app/**.py]
|
||||||
[jinja2: app/templates/**.html]
|
[jinja2: app/templates/**.html]
|
||||||
extensions=jinja2.ext.autoescape,jinja2.ext.with_
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ class Config(object):
|
||||||
RECAPTCHA_PUBLIC_KEY = os.environ.get("RECAPTCHA_PUBLIC_KEY")
|
RECAPTCHA_PUBLIC_KEY = os.environ.get("RECAPTCHA_PUBLIC_KEY")
|
||||||
RECAPTCHA_PRIVATE_KEY = os.environ.get("RECAPTCHA_PRIVATE_KEY")
|
RECAPTCHA_PRIVATE_KEY = os.environ.get("RECAPTCHA_PRIVATE_KEY")
|
||||||
MODE = os.environ.get('MODE') or 'development'
|
MODE = os.environ.get('MODE') or 'development'
|
||||||
LANGUAGES = ['en']
|
LANGUAGES = ['de', 'en']
|
||||||
FULL_AP_CONTEXT = bool(int(os.environ.get('FULL_AP_CONTEXT', 0)))
|
FULL_AP_CONTEXT = bool(int(os.environ.get('FULL_AP_CONTEXT', 0)))
|
||||||
CACHE_TYPE = os.environ.get('CACHE_TYPE') or 'FileSystemCache'
|
CACHE_TYPE = os.environ.get('CACHE_TYPE') or 'FileSystemCache'
|
||||||
CACHE_REDIS_URL = os.environ.get('CACHE_REDIS_URL') or 'redis://localhost:6379/1'
|
CACHE_REDIS_URL = os.environ.get('CACHE_REDIS_URL') or 'redis://localhost:6379/1'
|
||||||
|
|
|
@ -4,6 +4,8 @@ date > updated.txt
|
||||||
sudo systemctl stop celery.service
|
sudo systemctl stop celery.service
|
||||||
git pull
|
git pull
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
export FLASK_APP=pyfedi.py
|
||||||
flask db upgrade
|
flask db upgrade
|
||||||
|
pybabel compile -d app/translations
|
||||||
sudo systemctl start celery.service
|
sudo systemctl start celery.service
|
||||||
sudo systemctl restart pyfedi.service
|
sudo systemctl restart pyfedi.service
|
||||||
|
|
|
@ -16,3 +16,18 @@ python profile_app.py
|
||||||
instead of
|
instead of
|
||||||
flask run
|
flask run
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
translations:
|
||||||
|
|
||||||
|
See https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xiii-i18n-and-l10n-2018
|
||||||
|
|
||||||
|
to add a new language which can be worked on:
|
||||||
|
|
||||||
|
pybabel init -i messages.pot -d app/translations -l <language code>
|
||||||
|
|
||||||
|
after changes to the files in app/translations/* are mode, they need to be compiled
|
||||||
|
pybabel compile -d app/translations
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
38
migrations/versions/81175e11c083_report_in_community.py
Normal file
38
migrations/versions/81175e11c083_report_in_community.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
"""report in community
|
||||||
|
|
||||||
|
Revision ID: 81175e11c083
|
||||||
|
Revises: e72aa356e4d0
|
||||||
|
Create Date: 2024-03-18 20:37:43.216482
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '81175e11c083'
|
||||||
|
down_revision = 'e72aa356e4d0'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('report', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('in_community_id', sa.Integer(), nullable=True))
|
||||||
|
batch_op.drop_constraint('report_suspect_community_id_fkey', type_='foreignkey')
|
||||||
|
batch_op.create_foreign_key(None, 'community', ['suspect_community_id'], ['id'])
|
||||||
|
batch_op.create_foreign_key(None, 'community', ['in_community_id'], ['id'])
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('report', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint(None, type_='foreignkey')
|
||||||
|
batch_op.drop_constraint(None, type_='foreignkey')
|
||||||
|
batch_op.create_foreign_key('report_suspect_community_id_fkey', 'user', ['suspect_community_id'], ['id'])
|
||||||
|
batch_op.drop_column('in_community_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
Loading…
Add table
Reference in a new issue