Merge remote-tracking branch 'origin/dev' into new_editor
This commit is contained in:
commit
6159776cc6
|
@ -19,6 +19,8 @@ Pipfile
|
|||
Pipfile.lock
|
||||
# Tests files
|
||||
test.*
|
||||
# Autosaves
|
||||
*.dia~
|
||||
|
||||
|
||||
## Deployment files
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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__ = {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -2,8 +2,6 @@
|
|||
|
||||
.editor {
|
||||
& > button {
|
||||
background-color: #000;
|
||||
|
||||
& > svg {
|
||||
width: 25px;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"],
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 |
|
@ -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 ###
|
Loading…
Reference in New Issue