Merge remote-tracking branch 'origin/dev' into new_editor

This commit is contained in:
Eragon 2022-04-21 21:47:06 +02:00
commit 6159776cc6
Signed by: Eragon
GPG Key ID: 087126EBFC725006
18 changed files with 326 additions and 12 deletions

2
.gitignore vendored
View File

@ -19,6 +19,8 @@ Pipfile
Pipfile.lock
# Tests files
test.*
# Autosaves
*.dia~
## Deployment files

View File

@ -13,7 +13,8 @@ class Attachment(db.Model):
name = db.Column(db.Unicode(64))
# The comment linked with
comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'), nullable=False)
comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'),
nullable=False, index=True)
comment = db.relationship('Comment', backref=backref('attachments'))
# The size of the file

View File

@ -11,7 +11,8 @@ class Notification(db.Model):
href = db.Column(db.UnicodeText)
date = db.Column(db.DateTime, default=datetime.now())
owner_id = db.Column(db.Integer, db.ForeignKey('member.id'),nullable=False)
owner_id = db.Column(db.Integer, db.ForeignKey('member.id'),
nullable=False, index=True)
owner = db.relationship('Member', backref='notifications',
foreign_keys=owner_id)

View File

@ -103,7 +103,7 @@ class PollAnswer(db.Model):
id = db.Column(db.Integer, primary_key=True)
# Poll
poll_id = db.Column(db.Integer, db.ForeignKey('poll.id'))
poll_id = db.Column(db.Integer, db.ForeignKey('poll.id'), index=True)
poll = db.relationship('Poll', backref=backref('answers'),
foreign_keys=poll_id)

View File

@ -14,10 +14,11 @@ class Post(db.Model):
# Creation and edition date
date_created = db.Column(db.DateTime)
date_modified = db.Column(db.DateTime)
date_modified = db.Column(db.DateTime, index=True)
# Post author
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False,
index=True)
author = db.relationship('User', backref="posts", foreign_keys=author_id)
__mapper_args__ = {

View File

@ -23,7 +23,8 @@ class Topic(Post):
title = db.Column(db.Unicode(128))
# Parent forum
forum_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=False)
forum_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=False,
index=True)
forum = db.relationship('Forum',
backref=backref('topics', lazy='dynamic'), foreign_keys=forum_id)

View File

@ -59,4 +59,4 @@ class Title(Trophy):
# Many-to-many relation for users earning trophies
TrophyMember = db.Table('trophy_member', db.Model.metadata,
db.Column('tid', db.Integer, db.ForeignKey('trophy.id')),
db.Column('uid', db.Integer, db.ForeignKey('member.id')))
db.Column('uid', db.Integer, db.ForeignKey('member.id'), index=True))

View File

@ -45,6 +45,9 @@ a:focus {
text-decoration: underline;
outline: none;
}
img.pixelated {
image-rendering: pixelated;
}
section p {
line-height: 20px;
word-wrap: anywhere;
@ -120,6 +123,24 @@ input[type="submit"]:focus {
.bg-warn:active {
background: var(--warn-active);
}
.align-left {
text-align: left;
}
.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
.align-right {
display: block;
margin-left: auto;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
.skip-to-content-link {
height: 30px;
left: 50%;

View File

@ -2,8 +2,6 @@
.editor {
& > button {
background-color: #000;
& > svg {
width: 25px;

View File

@ -40,6 +40,10 @@ a {
}
}
img.pixelated {
image-rendering: pixelated;
}
section {
p {
line-height: 20px;
@ -113,6 +117,25 @@ button, .button, input[type="button"], input[type="submit"] {
}
}
.align-left {
text-align: left;
}
.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
.align-right {
display: block;
margin-left: auto;
}
.float-left {
float: left;
}
.float-right {
float: right;
}
.skip-to-content-link {
height: 30px;

View File

@ -14,7 +14,7 @@
<table class="thread topcomment">
<tr>
<td class="author">{{ widget_user.profile(comment.author) }}</td>
<td><div>{{ comment.text | md }}</div></td>
<td class="message"><div>{{ comment.text | md }}</div></td>
</tr>
</table>

View File

@ -10,15 +10,19 @@ markdown_tags = [
"sub", "sup",
"table", "thead", "tbody", "tr", "th", "td",
"form", "fieldset", "input", "textarea",
"label", "progress"
"label", "progress",
"video", "source", "iframe",
]
markdown_attrs = {
"*": ["id", "class"],
"img": ["src", "alt", "title"],
"img": ["src", "alt", "title", "width", "height"],
"a": ["href", "alt", "title", "rel"],
"form": ["action", "method", "enctype"],
"input": ["id", "name", "type", "value"],
"label": ["for"],
"progress": ["value", "min", "max"],
"video": ["controls", "width", "height"],
"source": ["src"],
"iframe": ["src", "width", "height", "frameborder", "allowfullscreen"],
}

View File

@ -11,6 +11,7 @@ from app.utils.markdown_extensions.pclinks import PCLinkExtension
from app.utils.markdown_extensions.hardbreaks import HardBreakExtension
from app.utils.markdown_extensions.escape_html import EscapeHtmlExtension
from app.utils.markdown_extensions.linkify import LinkifyExtension
from app.utils.markdown_extensions.media import MediaExtension
@app.template_filter('md')
@ -33,6 +34,7 @@ def md(text):
LinkifyExtension(),
TocExtension(baselevel=2),
PCLinkExtension(),
MediaExtension(),
]
html = markdown(text, options=options, extensions=extensions)

View File

@ -0,0 +1,220 @@
from markdown.extensions import Extension
from markdown.inlinepatterns import LinkInlineProcessor
import xml.etree.ElementTree as etree
import urllib
import re
class MediaExtension(Extension):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def extendMarkdown(self, md):
self.md = md
# Override image detection
MEDIA_RE = r'\!\['
media_processor = MediaInlineProcessor(MEDIA_RE)
media_processor.md = md
md.inlinePatterns.register(media_processor, 'media_link', 155)
class AttrDict:
def __init__(self, attrs):
self.attrs = attrs
def has(self, name):
return name in self.attrs
def getString(self, name):
for attr in self.attrs:
if attr.startswith(name + "="):
return attr[len(name)+1:]
def getInt(self, name):
try:
s = self.getString(name)
return int(s) if s is not None else None
except ValueError:
return None
def getSize(self, name):
s = self.getString(name)
if s is None:
return None, None
dims = s.split("x", 1)
try:
if len(dims) == 1:
return int(dims[0]), None
else:
w = int(dims[0]) if dims[0] else None
h = int(dims[1]) if dims[1] else None
return w, h
except ValueError:
return None, None
def getAlignmentClass(self):
if self.has("left"):
return "align-left"
if self.has("center"):
return "align-center"
elif self.has("right"):
return "align-right"
elif self.has("float-left"):
return "float-left"
elif self.has("float-right"):
return "float-right"
return ""
class AttributeLinkInlineProcessor(LinkInlineProcessor):
"""
A LinkInlineProcessor which additionally supports attributes after links,
with the bracket syntax `{ <item>, <item>, ... }` where each item is a
key/value pair with either single or double quotes: `key='value'` or
`key="value"`.
"""
def getAttributes(self, data, index):
current_quote = ""
has_closing_brace = False
attrs = []
current_attr_text = ""
if index >= len(data) or data[index] != '{':
return AttrDict([]), index, True
index += 1
for pos in range(index, len(data)):
c = data[pos]
index += 1
# Close quote
if current_quote != "" and c == current_quote:
current_quote = ""
continue
# Open new quote
if current_quote == "" and c in ["'", '"']:
current_quote = c
continue
# Close brace
if current_quote == "" and c == "}":
has_closing_brace = True
break
if current_quote == "" and c == " ":
if current_attr_text:
attrs.append(current_attr_text)
current_attr_text = ""
else:
current_attr_text += c
if current_attr_text:
attrs.append(current_attr_text)
return AttrDict(attrs), index, has_closing_brace
class MediaInlineProcessor(AttributeLinkInlineProcessor):
""" Return a media element from the given match. """
def isVideo(self, url):
if url.endswith(".mp4") or url.endswith(".webm"):
return True
url = urllib.parse.urlparse(url)
# TODO: Better detect YouTube URLs
return url.hostname in ["youtu.be", "www.youtube.com"]
def isAudio(self, url):
return url.endswith(".mp3") or url.endswith(".ogg")
def handleMatch(self, m, data):
text, index, handled = self.getText(data, m.end(0))
if not handled:
return None, None, None
src, title, index, handled = self.getLink(data, index)
if not handled:
return None, None, None
attrs, index, handled = self.getAttributes(data, index)
if not handled:
return None, None, None
kind = "image"
if attrs.has("image"):
kind = "image"
elif attrs.has("audio") or self.isAudio(src):
kind = "audio"
elif attrs.has("video") or self.isVideo(src):
kind = "video"
if kind == "image":
w, h = attrs.getSize("size")
class_ = ""
# TODO: Media converter: Find a way to clear atfer a float
if attrs.has("pixelated"):
class_ += " pixelated"
class_ += " " + attrs.getAlignmentClass()
el = etree.Element("img")
el.set("src", src)
if title is not None:
el.set("title", title)
if class_ != "":
el.set("class", class_)
if w is not None:
el.set("width", str(w))
if h is not None:
el.set("height", str(h))
el.set('alt', self.unescape(text))
return el, m.start(0), index
elif kind == "audio":
# TODO: Media converter: support audio files
pass
elif kind == "video":
w, h = attrs.getSize("size")
class_ = attrs.getAlignmentClass()
url = urllib.parse.urlparse(src)
args = urllib.parse.parse_qs(url.query)
youtube_source = None
if url.hostname == "youtu.be" and \
re.fullmatch(r'\/[a-zA-Z0-9_-]+', url.path):
youtube_source = url.path[1:]
elif url.hostname == "www.youtube.com" and "v" in args and \
re.fullmatch(r'[a-zA-Z0-9_-]+', args["v"][0]):
youtube_source = args["v"][0]
if youtube_source:
if w is None and h is None:
w, h = (470, 300) if attrs.has("tiny") else (560, 340)
el = etree.Element("iframe")
el.set("src",f"https://www.youtube.com/embed/{youtube_source}")
el.set("frameborder", "0")
el.set("allowfullscreen", "")
# <iframe
# src="https://www.youtube.com/embed/{url.path}"
# frameborder="0" allowfullscreen></iframe>
pass
else:
el = etree.Element("video")
el.set("controls", "")
source = etree.Element("source")
source.set("src", src)
el.append(source)
# <video controls
# <source src="{url}">
# </video>
el.set("class", class_)
if w is not None:
el.set("width", str(min(w, 560)))
if h is not None:
el.set("height", str(min(h, 340)))
return el, m.start(0), index
return None, None, None

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -0,0 +1,40 @@
"""Add a number of missing indexes
Revision ID: bcfdb271b88d
Revises: adcd1577f301
Create Date: 2022-04-21 17:45:09.787769
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'bcfdb271b88d'
down_revision = 'adcd1577f301'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_index(op.f('ix_attachment_comment_id'), 'attachment', ['comment_id'], unique=False)
op.create_index(op.f('ix_notification_owner_id'), 'notification', ['owner_id'], unique=False)
op.create_index(op.f('ix_pollanswer_poll_id'), 'pollanswer', ['poll_id'], unique=False)
op.create_index(op.f('ix_post_author_id'), 'post', ['author_id'], unique=False)
op.create_index(op.f('ix_post_date_modified'), 'post', ['date_modified'], unique=False)
op.create_index(op.f('ix_topic_forum_id'), 'topic', ['forum_id'], unique=False)
op.create_index(op.f('ix_trophy_member_uid'), 'trophy_member', ['uid'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_trophy_member_uid'), table_name='trophy_member')
op.drop_index(op.f('ix_topic_forum_id'), table_name='topic')
op.drop_index(op.f('ix_post_date_modified'), table_name='post')
op.drop_index(op.f('ix_post_author_id'), table_name='post')
op.drop_index(op.f('ix_pollanswer_poll_id'), table_name='pollanswer')
op.drop_index(op.f('ix_notification_owner_id'), table_name='notification')
op.drop_index(op.f('ix_attachment_comment_id'), table_name='attachment')
# ### end Alembic commands ###