mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
upload images for community icon and banner
This commit is contained in:
parent
d5d7122a3d
commit
9efda995e3
14 changed files with 283 additions and 33 deletions
|
@ -18,7 +18,7 @@ from sqlalchemy_searchable import make_searchable
|
|||
from config import Config
|
||||
|
||||
|
||||
db = SQLAlchemy(session_options={"autoflush": False})
|
||||
db = SQLAlchemy(session_options={"autoflush": False}) # engine_options={'pool_size': 5, 'max_overflow': 10}
|
||||
migrate = Migrate()
|
||||
login = LoginManager()
|
||||
login.login_view = 'auth.login'
|
||||
|
|
|
@ -14,7 +14,7 @@ from app.activitypub.util import public_key, users_total, active_half_year, acti
|
|||
post_to_activity, find_actor_or_create, default_context, instance_blocked, find_reply_parent, find_liked_object, \
|
||||
lemmy_site_data, instance_weight
|
||||
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
|
||||
domain_from_url, markdown_to_html, community_membership, ap_datetime
|
||||
import werkzeug.exceptions
|
||||
|
||||
INBOX = []
|
||||
|
@ -159,12 +159,17 @@ def user_profile(actor):
|
|||
"endpoints": {
|
||||
"sharedInbox": f"https://{server}/inbox"
|
||||
},
|
||||
"published": user.created.isoformat() + '+00:00',
|
||||
"published": ap_datetime(user.created),
|
||||
}
|
||||
if user.avatar_id is not None:
|
||||
actor_data["icon"] = {
|
||||
"type": "Image",
|
||||
"url": f"https://{server}/avatars/{user.avatar.file_path}"
|
||||
"url": f"https://{current_app.config['SERVER_NAME']}{user.avatar_image()}"
|
||||
}
|
||||
if user.cover_id is not None:
|
||||
actor_data["image"] = {
|
||||
"type": "Image",
|
||||
"url": f"https://{current_app.config['SERVER_NAME']}{user.cover_image()}"
|
||||
}
|
||||
if user.about:
|
||||
actor_data['source'] = {
|
||||
|
@ -219,13 +224,18 @@ def community_profile(actor):
|
|||
"endpoints": {
|
||||
"sharedInbox": f"https://{server}/inbox"
|
||||
},
|
||||
"published": community.created_at.isoformat() + '+00:00',
|
||||
"updated": community.last_active.isoformat() + '+00:00',
|
||||
"published": ap_datetime(community.created_at),
|
||||
"updated": ap_datetime(community.last_active),
|
||||
}
|
||||
if community.icon_id is not None:
|
||||
actor_data["icon"] = {
|
||||
"type": "Image",
|
||||
"url": f"https://{server}/avatars/{community.icon.file_path}"
|
||||
"url": f"https://{current_app.config['SERVER_NAME']}{community.icon_image()}"
|
||||
}
|
||||
if community.image_id is not None:
|
||||
actor_data["image"] = {
|
||||
"type": "Image",
|
||||
"url": f"https://{current_app.config['SERVER_NAME']}{community.header_image()}"
|
||||
}
|
||||
resp = jsonify(actor_data)
|
||||
resp.content_type = 'application/activity+json'
|
||||
|
|
|
@ -8,8 +8,10 @@ from app.utils import domain_from_url
|
|||
|
||||
class AddLocalCommunity(FlaskForm):
|
||||
community_name = StringField(_l('Name'), validators=[DataRequired()])
|
||||
url = StringField(_l('Url'), render_kw={'placeholder': '/c/'})
|
||||
url = StringField(_l('Url'))
|
||||
description = TextAreaField(_l('Description'))
|
||||
icon_file = FileField(_('Icon image'))
|
||||
banner_file = FileField(_('Banner image'))
|
||||
rules = TextAreaField(_l('Rules'))
|
||||
nsfw = BooleanField('18+ NSFW')
|
||||
submit = SubmitField(_l('Create'))
|
||||
|
|
|
@ -9,14 +9,14 @@ from app.activitypub.signature import RsaKeys, HttpSignature
|
|||
from app.activitypub.util import default_context
|
||||
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePostForm
|
||||
from app.community.util import search_for_community, community_url_exists, actor_to_community, \
|
||||
ensure_directory_exists, opengraph_parse, url_to_thumbnail_file, save_post
|
||||
ensure_directory_exists, opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file
|
||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE, \
|
||||
SUBSCRIPTION_PENDING
|
||||
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
||||
File, PostVote
|
||||
from app.community import bp
|
||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership
|
||||
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership, ap_datetime
|
||||
import os
|
||||
from PIL import Image, ImageOps
|
||||
from datetime import datetime
|
||||
|
@ -39,6 +39,16 @@ def add_local():
|
|||
public_key=public_key,
|
||||
ap_profile_id=current_app.config['SERVER_NAME'] + '/c/' + form.url.data,
|
||||
subscriptions_count=1)
|
||||
icon_file = request.files['icon_file']
|
||||
if icon_file and icon_file.filename != '':
|
||||
file = save_icon_file(icon_file)
|
||||
if file:
|
||||
community.icon = file
|
||||
banner_file = request.files['banner_file']
|
||||
if banner_file and banner_file.filename != '':
|
||||
file = save_banner_file(banner_file)
|
||||
if file:
|
||||
community.image = file
|
||||
db.session.add(community)
|
||||
db.session.commit()
|
||||
membership = CommunityMember(user_id=current_user.id, community_id=community.id, is_moderator=True,
|
||||
|
@ -228,7 +238,7 @@ def add_post(actor):
|
|||
form.nsfw.render_kw = {'disabled': True}
|
||||
if get_setting('allow_nsfl', False) is False:
|
||||
form.nsfl.render_kw = {'disabled': True}
|
||||
images_disabled = 'disabled' if not get_setting('allow_local_image_posts', True) else ''
|
||||
images_disabled = 'disabled' if not get_setting('allow_local_image_posts', True) else '' # bug: this will disable posting of images to *remote* hosts too
|
||||
|
||||
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
||||
|
||||
|
@ -239,10 +249,58 @@ def add_post(actor):
|
|||
community.last_active = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
if community.ap_id: # this is a remote community - send the post to the instance that hosts it
|
||||
page = {
|
||||
'type': 'Page',
|
||||
'id': f"https://{current_app.config['SERVER_NAME']}/post/{post.id}",
|
||||
'attributedTo': current_user.ap_profile_id,
|
||||
'to': [
|
||||
community.ap_profile_id,
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'name': post.title,
|
||||
'cc': [],
|
||||
'content': post.body_html,
|
||||
'mediaType': 'text/html',
|
||||
'source': {
|
||||
'content': post.body,
|
||||
'mediaType': 'text/markdown'
|
||||
},
|
||||
'attachment': [],
|
||||
'commentsEnabled': post.comments_enabled,
|
||||
'sensitive': post.nsfw,
|
||||
'nsfl': post.nsfl,
|
||||
'published': ap_datetime(datetime.utcnow()),
|
||||
'audience': community.ap_profile_id
|
||||
}
|
||||
create = {
|
||||
"id": f"https://{current_app.config['SERVER_NAME']}/activities/create/{gibberish(15)}",
|
||||
"actor": current_user.ap_profile_id,
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
],
|
||||
"cc": [
|
||||
community.ap_profile_id
|
||||
],
|
||||
"type": "Create",
|
||||
"audience": community.ap_profile_id,
|
||||
"object": page
|
||||
}
|
||||
try:
|
||||
message = HttpSignature.signed_request(community.ap_inbox_url, create, current_user.private_key,
|
||||
current_user.ap_profile_id + '#main-key')
|
||||
if message.status_code == 200:
|
||||
flash('Your post has been sent to ' + community.title)
|
||||
else:
|
||||
flash('Response status code was not 200', 'warning')
|
||||
current_app.logger.error('Response code for post attempt was ' +
|
||||
str(message.status_code) + ' ' + message.text)
|
||||
except Exception as ex:
|
||||
flash('Failed to send request to subscribe: ' + str(ex), 'error')
|
||||
current_app.logger.error("Exception while trying to subscribe" + str(ex))
|
||||
else: # local community - send post out to followers
|
||||
...
|
||||
|
||||
# todo: federate post creation out to followers
|
||||
|
||||
flash('Post has been added')
|
||||
return redirect(f"/c/{community.link()}")
|
||||
else:
|
||||
form.communities.data = community.id
|
||||
|
|
|
@ -16,6 +16,8 @@ import os
|
|||
from opengraph_parse import parse_page
|
||||
|
||||
|
||||
allowed_extensions = ['.gif', '.jpg', '.jpeg', '.png', '.webp', '.heic']
|
||||
|
||||
def search_for_community(address: str):
|
||||
if address.startswith('!'):
|
||||
name, server = address[1:].split('@')
|
||||
|
@ -176,7 +178,6 @@ def save_post(form, post):
|
|||
db.session.add(file)
|
||||
|
||||
elif form.type.data == 'image':
|
||||
allowed_extensions = ['.gif', '.jpg', '.jpeg', '.png', '.webp', '.heic']
|
||||
post.title = form.image_title.data
|
||||
post.type = POST_TYPE_IMAGE
|
||||
uploaded_file = request.files['image_file']
|
||||
|
@ -185,15 +186,18 @@ def save_post(form, post):
|
|||
remove_old_file(post.image_id)
|
||||
post.image_id = None
|
||||
|
||||
# check if this is an allowed type of file
|
||||
file_ext = os.path.splitext(uploaded_file.filename)[1]
|
||||
if file_ext.lower() not in allowed_extensions or file_ext != validate_image(
|
||||
uploaded_file.stream):
|
||||
abort(400)
|
||||
new_filename = gibberish(15)
|
||||
|
||||
# set up the storage directory
|
||||
directory = 'app/static/media/posts/' + new_filename[0:2] + '/' + new_filename[2:4]
|
||||
ensure_directory_exists(directory)
|
||||
|
||||
# save the file
|
||||
final_place = os.path.join(directory, new_filename + file_ext)
|
||||
final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp')
|
||||
uploaded_file.save(final_place)
|
||||
|
@ -203,14 +207,15 @@ def save_post(form, post):
|
|||
|
||||
# resize if necessary
|
||||
img = Image.open(final_place)
|
||||
img = ImageOps.exif_transpose(img)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
img = ImageOps.exif_transpose(img)
|
||||
if img.width > 2000 or img.height > 2000:
|
||||
img.thumbnail((2000, 2000))
|
||||
img.save(final_place)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
# save a second, smaller, version as a thumbnail
|
||||
img.thumbnail((256, 256))
|
||||
img.save(final_place_thumbnail, format="WebP", quality=93)
|
||||
thumbnail_width = img.width
|
||||
|
@ -237,3 +242,83 @@ def save_post(form, post):
|
|||
def remove_old_file(file_id):
|
||||
remove_file = File.query.get(file_id)
|
||||
remove_file.delete_from_disk()
|
||||
|
||||
|
||||
def save_icon_file(icon_file) -> File:
|
||||
# check if this is an allowed type of file
|
||||
file_ext = os.path.splitext(icon_file.filename)[1]
|
||||
if file_ext.lower() not in allowed_extensions or file_ext != validate_image(
|
||||
icon_file.stream):
|
||||
abort(400)
|
||||
new_filename = gibberish(15)
|
||||
|
||||
# set up the storage directory
|
||||
directory = 'app/static/media/communities/' + new_filename[0:2] + '/' + new_filename[2:4]
|
||||
ensure_directory_exists(directory)
|
||||
|
||||
# save the file
|
||||
final_place = os.path.join(directory, new_filename + file_ext)
|
||||
final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp')
|
||||
icon_file.save(final_place)
|
||||
|
||||
if file_ext.lower() == '.heic':
|
||||
register_heif_opener()
|
||||
|
||||
# resize if necessary
|
||||
img = Image.open(final_place)
|
||||
img = ImageOps.exif_transpose(img)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
if img.width > 200 or img.height > 200:
|
||||
img.thumbnail((200, 200))
|
||||
img.save(final_place)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
# save a second, smaller, version as a thumbnail
|
||||
img.thumbnail((32, 32))
|
||||
img.save(final_place_thumbnail, format="WebP", quality=93)
|
||||
thumbnail_width = img.width
|
||||
thumbnail_height = img.height
|
||||
|
||||
file = File(file_path=final_place, file_name=new_filename + file_ext, alt_text='community icon',
|
||||
width=img_width, height=img_height, thumbnail_width=thumbnail_width,
|
||||
thumbnail_height=thumbnail_height, thumbnail_path=final_place_thumbnail)
|
||||
db.session.add(file)
|
||||
return file
|
||||
|
||||
|
||||
def save_banner_file(banner_file) -> File:
|
||||
# check if this is an allowed type of file
|
||||
file_ext = os.path.splitext(banner_file.filename)[1]
|
||||
if file_ext.lower() not in allowed_extensions or file_ext != validate_image(
|
||||
banner_file.stream):
|
||||
abort(400)
|
||||
new_filename = gibberish(15)
|
||||
|
||||
# set up the storage directory
|
||||
directory = 'app/static/media/communities/' + new_filename[0:2] + '/' + new_filename[2:4]
|
||||
ensure_directory_exists(directory)
|
||||
|
||||
# save the file
|
||||
final_place = os.path.join(directory, new_filename + file_ext)
|
||||
final_place_thumbnail = os.path.join(directory, new_filename + '_thumbnail.webp')
|
||||
banner_file.save(final_place)
|
||||
|
||||
if file_ext.lower() == '.heic':
|
||||
register_heif_opener()
|
||||
|
||||
# resize if necessary
|
||||
img = Image.open(final_place)
|
||||
img = ImageOps.exif_transpose(img)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
if img.width > 1000 or img.height > 300:
|
||||
img.thumbnail((1000, 300))
|
||||
img.save(final_place)
|
||||
img_width = img.width
|
||||
img_height = img.height
|
||||
|
||||
file = File(file_path=final_place, file_name=new_filename + file_ext, alt_text='community banner',
|
||||
width=img_width, height=img_height)
|
||||
db.session.add(file)
|
||||
return file
|
|
@ -103,21 +103,45 @@ class Community(db.Model):
|
|||
image = db.relationship('File', foreign_keys=[image_id], single_parent=True, cascade="all, delete-orphan")
|
||||
|
||||
@cache.memoize(timeout=500)
|
||||
def icon_image(self) -> str:
|
||||
def icon_image(self, size='default') -> str:
|
||||
if self.icon_id is not None:
|
||||
if self.icon.file_path is not None:
|
||||
return self.icon.file_path
|
||||
if self.icon.source_url is not None:
|
||||
return self.icon.source_url
|
||||
if size == 'default':
|
||||
if self.icon.file_path is not None:
|
||||
if self.icon.file_path.startswith('app/'):
|
||||
return self.icon.file_path.replace('app/', '/')
|
||||
else:
|
||||
return self.icon.file_path
|
||||
if self.icon.source_url is not None:
|
||||
if self.icon.source_url.startswith('app/'):
|
||||
return self.icon.source_url.replace('app/', '/')
|
||||
else:
|
||||
return self.icon.source_url
|
||||
elif size == 'tiny':
|
||||
if self.icon.thumbnail_path is not None:
|
||||
if self.icon.thumbnail_path.startswith('app/'):
|
||||
return self.icon.thumbnail_path.replace('app/', '/')
|
||||
else:
|
||||
return self.icon.thumbnail_path
|
||||
if self.icon.source_url is not None:
|
||||
if self.icon.source_url.startswith('app/'):
|
||||
return self.icon.source_url.replace('app/', '/')
|
||||
else:
|
||||
return self.icon.source_url
|
||||
return ''
|
||||
|
||||
@cache.memoize(timeout=500)
|
||||
def header_image(self) -> str:
|
||||
if self.image_id is not None:
|
||||
if self.image.file_path is not None:
|
||||
return self.image.file_path
|
||||
if self.image.file_path.startswith('app/'):
|
||||
return self.image.file_path.replace('app/', '/')
|
||||
else:
|
||||
return self.image.file_path
|
||||
if self.image.source_url is not None:
|
||||
return self.image.source_url
|
||||
if self.image.source_url.startswith('app/'):
|
||||
return self.image.source_url.replace('app/', '/')
|
||||
else:
|
||||
return self.image.source_url
|
||||
return ''
|
||||
|
||||
def display_name(self) -> str:
|
||||
|
@ -143,6 +167,9 @@ class Community(db.Model):
|
|||
def is_moderator(self):
|
||||
return any(moderator.user_id == current_user.id for moderator in self.moderators())
|
||||
|
||||
def profile_id(self):
|
||||
return self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/c/{self.name}"
|
||||
|
||||
|
||||
user_role = db.Table('user_role',
|
||||
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
|
||||
|
@ -232,18 +259,30 @@ class User(UserMixin, db.Model):
|
|||
def avatar_image(self) -> str:
|
||||
if self.avatar_id is not None:
|
||||
if self.avatar.file_path is not None:
|
||||
return self.avatar.file_path
|
||||
if self.avatar.file_path.startswith('app/'):
|
||||
return self.avatar.file_path.replace('app/', '/')
|
||||
else:
|
||||
return self.avatar.file_path
|
||||
if self.avatar.source_url is not None:
|
||||
return self.avatar.source_url
|
||||
if self.avatar.source_url.startswith('app/'):
|
||||
return self.avatar.source_url.replace('app/', '/')
|
||||
else:
|
||||
return self.avatar.source_url
|
||||
return ''
|
||||
|
||||
@cache.memoize(timeout=500)
|
||||
def cover_image(self) -> str:
|
||||
if self.cover_id is not None:
|
||||
if self.cover.file_path is not None:
|
||||
return self.cover.file_path
|
||||
if self.cover.file_path.startswith('app/'):
|
||||
return self.cover.file_path.replace('app/', '/')
|
||||
else:
|
||||
return self.cover.file_path
|
||||
if self.cover.source_url is not None:
|
||||
return self.cover.source_url
|
||||
if self.cover.source_url.startswith('app/'):
|
||||
return self.cover.source_url.replace('app/', '/')
|
||||
else:
|
||||
return self.cover.source_url
|
||||
return ''
|
||||
|
||||
def link(self) -> str:
|
||||
|
@ -302,7 +341,7 @@ class User(UserMixin, db.Model):
|
|||
join(CommunityMember).filter(CommunityMember.is_banned == False).all()
|
||||
|
||||
def profile_id(self):
|
||||
return self.ap_profile_id if self.ap_profile_id else f"{self.user_name}@{current_app.config['SERVER_NAME']}"
|
||||
return self.ap_profile_id if self.ap_profile_id else f"https://{current_app.config['SERVER_NAME']}/u/{self.user_name}"
|
||||
|
||||
def created_recently(self):
|
||||
return self.created and self.created > datetime.utcnow() - timedelta(days=7)
|
||||
|
@ -413,6 +452,9 @@ class Post(db.Model):
|
|||
if vpos != -1:
|
||||
return self.url[vpos + 2:vpos + 13]
|
||||
|
||||
def profile_id(self):
|
||||
return f"https://{current_app.config['SERVER_NAME']}/post/{self.id}"
|
||||
|
||||
|
||||
class PostReply(db.Model):
|
||||
query_class = FullTextSearchQuery
|
||||
|
@ -452,6 +494,9 @@ class PostReply(db.Model):
|
|||
def get_by_ap_id(cls, ap_id):
|
||||
return cls.query.filter_by(ap_id=ap_id).first()
|
||||
|
||||
def profile_id(self):
|
||||
return f"https://{current_app.config['SERVER_NAME']}/comment/{self.id}"
|
||||
|
||||
|
||||
class Domain(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
|
|
@ -569,6 +569,12 @@ fieldset legend {
|
|||
border-top: solid 1px #ddd;
|
||||
}
|
||||
|
||||
#add_local_community_form #url {
|
||||
width: 297px;
|
||||
display: inline-block;
|
||||
padding-left: 3px;
|
||||
}
|
||||
|
||||
.table tr th {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
|
|
@ -317,6 +317,14 @@ nav, etc which are used site-wide */
|
|||
}
|
||||
}
|
||||
|
||||
#add_local_community_form {
|
||||
#url {
|
||||
width: 297px;
|
||||
display: inline-block;
|
||||
padding-left: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.table {
|
||||
tr th {
|
||||
vertical-align: middle;
|
||||
|
|
|
@ -470,6 +470,12 @@ nav.navbar {
|
|||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.field_hint {
|
||||
margin-top: -15px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #777;
|
||||
|
|
|
@ -191,6 +191,12 @@ nav.navbar {
|
|||
}
|
||||
}
|
||||
|
||||
.field_hint {
|
||||
margin-top: -15px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: $dark-grey;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
|
@ -7,7 +7,24 @@
|
|||
<div class="card mt-5">
|
||||
<div class="card-body p-6">
|
||||
<div class="card-title">{{ _('Create community') }}</div>
|
||||
{{ render_form(form) }}
|
||||
<form method="post" enctype="multipart/form-data" id="add_local_community_form">
|
||||
{{ form.csrf_token() }}
|
||||
{{ render_field(form.community_name) }}
|
||||
<div class="form-group">{{ form.url.label(class_="form-control-label required") }}
|
||||
/c/{{ form.url(class_="form-control", maxlength=255) }}
|
||||
{% for error in form.url.errors %}
|
||||
<span style="color: red;">[{{ error }}]</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ render_field(form.description) }}
|
||||
{{ render_field(form.icon_file) }}
|
||||
<small class="field_hint">Provide a square image that looks good when small.</small>
|
||||
{{ render_field(form.banner_file) }}
|
||||
<small class="field_hint">Provide a wide image - letterbox orientation.</small>
|
||||
{{ render_field(form.rules) }}
|
||||
{{ render_field(form.nsfw) }}
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% from 'bootstrap/form.html' import render_form, render_field %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
<tbody>
|
||||
{% for community in communities %}
|
||||
<tr class="">
|
||||
<td width="46"><a href="/c/{{ community.link() }}"><img src="{{ community.icon_image() }}" class="community_icon rounded-circle" loading="lazy" /></a></td>
|
||||
<td width="46"><a href="/c/{{ community.link() }}"><img src="{{ community.icon_image('tiny') }}" class="community_icon rounded-circle" loading="lazy" /></a></td>
|
||||
<td scope="row" class="pl-0"><a href="/c/{{ community.link() }}">{{ community.display_name() }}</a></td>
|
||||
<td>{{ community.post_count }}</td>
|
||||
<td>{{ community.post_reply_count }}</td>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import random
|
||||
from datetime import datetime
|
||||
|
||||
import markdown2
|
||||
import math
|
||||
from urllib.parse import urlparse
|
||||
|
@ -198,7 +200,7 @@ def user_access(permission: str, user_id: int) -> bool:
|
|||
return has_access is not None
|
||||
|
||||
|
||||
@cache.memoize(timeout=10)
|
||||
@cache.memoize(timeout=500)
|
||||
def community_membership(user: User, community: Community) -> int:
|
||||
# @cache.memoize works with User.subscribed but cache.delete_memoized does not, making it bad to use on class methods.
|
||||
# however cache.memoize and cache.delete_memoized works fine with normal functions
|
||||
|
@ -261,3 +263,8 @@ def back(default_url):
|
|||
|
||||
# If referrer is not available or is the same as the current request URL, redirect to the default URL
|
||||
return redirect(default_url)
|
||||
|
||||
|
||||
# format a datetime in a way that is used in ActivityPub
|
||||
def ap_datetime(date_time: datetime) -> str:
|
||||
return date_time.isoformat() + '+00:00'
|
||||
|
|
Loading…
Reference in a new issue