gallery: rebase from dev
This commit is contained in:
commit
7b66e1ec20
|
@ -10,7 +10,7 @@ class Post(db.Model):
|
|||
# Unique Post ID for the whole site
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
# Post type (polymorphic discriminator)
|
||||
type = db.Column(db.String(20))
|
||||
type = db.Column(db.String(20), index=True)
|
||||
|
||||
# Creation and edition date
|
||||
date_created = db.Column(db.DateTime)
|
||||
|
|
|
@ -3,11 +3,17 @@ from flask_login import UserMixin
|
|||
from sqlalchemy import func as SQLfunc
|
||||
from os.path import isfile
|
||||
from PIL import Image
|
||||
|
||||
from app import app, db
|
||||
from app.models.priv import SpecialPrivilege, Group, GroupMember, \
|
||||
GroupPrivilege
|
||||
from app.models.trophy import Trophy, TrophyMember, Title
|
||||
from app.models.notification import Notification
|
||||
from app.models.post import Post
|
||||
from app.models.comment import Comment
|
||||
from app.models.topic import Topic
|
||||
from app.models.program import Program
|
||||
|
||||
import app.utils.unicode_names as unicode_names
|
||||
import app.utils.ldap as ldap
|
||||
from app.utils.unicode_names import normalize
|
||||
|
@ -113,9 +119,15 @@ class Member(User):
|
|||
# Relations
|
||||
trophies = db.relationship('Trophy', secondary=TrophyMember,
|
||||
back_populates='owners')
|
||||
topics = db.relationship('Topic')
|
||||
programs = db.relationship('Program')
|
||||
comments = db.relationship('Comment')
|
||||
|
||||
# Access to polymorphic posts
|
||||
# TODO: Check that the query uses the double index on Post.{author_id,type}
|
||||
def comments(self):
|
||||
return db.session.query(Comment).filter(Post.author_id==self.id).all()
|
||||
def topics(self):
|
||||
return db.session.query(Topic).filter(Post.author_id==self.id).all()
|
||||
def programs(self):
|
||||
return db.session.query(Program).filter(Post.author_id==self.id).all()
|
||||
|
||||
# Other fields populated automatically through relations:
|
||||
# <notifications> List of unseen notifications (of type Notification)
|
||||
|
@ -129,7 +141,7 @@ class Member(User):
|
|||
self.email_confirmed = not V5Config.ENABLE_EMAIL_CONFIRMATION
|
||||
if not V5Config.USE_LDAP:
|
||||
self.set_password(password)
|
||||
# Workflow with LDAP enabled is User → Postgresql → LDAP → set password
|
||||
# Workflow with LDAP enabled is User → PostgreSQL → LDAP → set password
|
||||
self.xp = 0
|
||||
|
||||
self.theme = 'default_theme'
|
||||
|
@ -149,23 +161,23 @@ class Member(User):
|
|||
Transfers all the posts to another user. This is generally used to
|
||||
transfer ownership to a newly-created Guest before deleting an account.
|
||||
"""
|
||||
for t in self.topics:
|
||||
for t in self.topics():
|
||||
t.author = other
|
||||
db.session.add(t)
|
||||
for p in self.programs:
|
||||
for p in self.programs():
|
||||
p.author = other
|
||||
db.session.add(p)
|
||||
for c in self.comments:
|
||||
for c in self.comments():
|
||||
c.author = other
|
||||
db.session.add(c)
|
||||
|
||||
def delete_posts(self):
|
||||
"""Deletes the user's posts."""
|
||||
for t in self.topics:
|
||||
for t in self.topics():
|
||||
t.delete()
|
||||
for p in self.programs:
|
||||
for p in self.programs():
|
||||
p.delete()
|
||||
for c in self.comments:
|
||||
for c in self.comments():
|
||||
c.delete()
|
||||
|
||||
def delete(self):
|
||||
|
@ -450,7 +462,7 @@ class Member(User):
|
|||
progress(levels, post_count)
|
||||
|
||||
if context in ["new-program", None]:
|
||||
program_count = len(self.programs)
|
||||
program_count = len(self.programs())
|
||||
|
||||
levels = {
|
||||
5: "Programmeur du dimanche",
|
||||
|
|
|
@ -128,9 +128,9 @@ def adm_delete_account(user_id):
|
|||
|
||||
# TODO: Number of comments by *other* members which will be deleted
|
||||
stats = {
|
||||
'comments': len(user.comments),
|
||||
'topics': len(user.topics),
|
||||
'programs': len(user.programs),
|
||||
'comments': len(user.comments()),
|
||||
'topics': len(user.topics()),
|
||||
'programs': len(user.programs()),
|
||||
'groups': len(user.groups),
|
||||
'privs': len(user.special_privileges()),
|
||||
}
|
||||
|
|
|
@ -45,6 +45,9 @@ a:focus {
|
|||
text-decoration: underline;
|
||||
outline: none;
|
||||
}
|
||||
img.pixelated {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
section p {
|
||||
line-height: 20px;
|
||||
word-wrap: anywhere;
|
||||
|
@ -117,6 +120,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%;
|
||||
|
|
|
@ -135,3 +135,11 @@ div.editor-toolbar, div.CodeMirror {
|
|||
--separator: #404040;
|
||||
--text-disabled: #262c2f;
|
||||
}
|
||||
|
||||
.dl-button {
|
||||
--link: #149641;
|
||||
--link-text: #ffffff;
|
||||
--link-active: #0f7331;
|
||||
--meta: rgba(255, 255, 255, .15);
|
||||
--meta-text: #ffffff;
|
||||
}
|
||||
|
|
|
@ -125,6 +125,14 @@ div.editor-toolbar, div.CodeMirror {
|
|||
--text-disabled: #c0c0c0;
|
||||
}
|
||||
|
||||
.dl-button {
|
||||
--link: #149641;
|
||||
--link-text: #ffffff;
|
||||
--link-active: #0f7331;
|
||||
--meta: rgba(0, 0, 0, .15);
|
||||
--meta-text: #000000;
|
||||
}
|
||||
|
||||
/* Extra style on top of the Pygments style */
|
||||
table.codehilitetable td.linenos {
|
||||
color: #808080;
|
||||
|
|
|
@ -86,6 +86,9 @@
|
|||
height: 64px;
|
||||
}
|
||||
}
|
||||
hr.signature {
|
||||
opacity: 0.2;
|
||||
}
|
||||
.trophies {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -125,6 +128,7 @@
|
|||
.trophy span {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
hr.signature {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
@ -173,4 +177,34 @@ hr.signature {
|
|||
}
|
||||
.gallery-spot {
|
||||
margin: 10px auto;
|
||||
}
|
||||
.dl-button {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
margin: 3px 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.dl-button a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 15px;
|
||||
font-size: 110%;
|
||||
background: var(--link);
|
||||
color: var(--link-text);
|
||||
}
|
||||
.dl-button a:hover,
|
||||
.dl-button a:focus,
|
||||
.dl-button a:active {
|
||||
background: var(--link-active);
|
||||
text-decoration: none;
|
||||
}
|
||||
.dl-button span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 5px 8px;
|
||||
background: var(--meta);
|
||||
color: var(--meta-text);
|
||||
font-size: 90%;
|
||||
}
|
||||
|
|
|
@ -40,6 +40,10 @@ a {
|
|||
}
|
||||
}
|
||||
|
||||
img.pixelated {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
section {
|
||||
p {
|
||||
line-height: 20px;
|
||||
|
@ -113,6 +117,25 @@ section {
|
|||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
|
|
@ -109,6 +109,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
hr.signature {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
|
||||
/* Trophies */
|
||||
.trophies {
|
||||
display: flex;
|
||||
|
@ -157,8 +162,33 @@
|
|||
}
|
||||
}
|
||||
|
||||
hr.signature {
|
||||
opacity: 0.2;
|
||||
|
||||
/* Download button */
|
||||
.dl-button {
|
||||
display: inline-flex; flex-direction: row; align-items: stretch;
|
||||
|
||||
border-radius: 5px; overflow: hidden;
|
||||
margin: 3px 5px; vertical-align: middle;
|
||||
|
||||
a {
|
||||
display:flex; align-items:center;
|
||||
padding: 5px 15px;
|
||||
|
||||
font-size: 110%;
|
||||
background: var(--link); color: var(--link-text);
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
background: var(--link-active);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
display: flex; align-items:center;
|
||||
padding: 5px 8px;
|
||||
background: var(--meta); color: var(--meta-text);
|
||||
font-size: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Gallery without Javascript */
|
||||
|
|
|
@ -69,5 +69,5 @@ var keyboard_trigger = function(event) {
|
|||
}
|
||||
}
|
||||
|
||||
document.onclick = mouse_trigger;
|
||||
document.onkeypress = keyboard_trigger;
|
||||
document.addEventListener("click", mouse_trigger);
|
||||
document.addEventListener("keydown", keyboard_trigger);
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
<th>Forum</th>
|
||||
<th>Création</th>
|
||||
</tr>
|
||||
{% for t in member.topics %}
|
||||
{% for t in member.topics() %}
|
||||
<tr>
|
||||
<td><a href="{{ url_for('forum_topic', f=t.forum, page=(t, 1)) }}">{{ t.title }}</a></td>
|
||||
<td><a href="{{ url_for('forum_page', f=t.forum) }}">{{ t.forum.name }}</a></td>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<ul>
|
||||
<li>{{ stats.comments }} commentaire{{ stats.comments | pluralize }}</li>
|
||||
<li>{{ stats.topics }} topic{{ stats.topics | pluralize }}</li>
|
||||
<li>{{ stats.programs }} topic{{ stats.programs | pluralize }}</li>
|
||||
<li>{{ stats.programs }} programme{{ stats.programs | pluralize }}</li>
|
||||
</ul>
|
||||
<p>Les propriétés suivantes seront supprimées :</p>
|
||||
<ul>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{% macro download_button(file) %}
|
||||
<span class="dl-button">
|
||||
<a href="{{ file.url }}">{{ file.name }}</a>
|
||||
<span>{{ file.size | humanize(unit='o') }}</span>
|
||||
</span>
|
||||
{% endmacro %}
|
||||
|
||||
{{ download_button(file) if 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"],
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ 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
|
||||
from app.utils.markdown_extensions.gallery import GalleryExtension
|
||||
|
||||
|
||||
@app.template_filter('md')
|
||||
|
@ -33,6 +35,8 @@ def md(text):
|
|||
LinkifyExtension(),
|
||||
TocExtension(baselevel=2),
|
||||
PCLinkExtension(),
|
||||
MediaExtension(),
|
||||
GalleryExtension(),
|
||||
]
|
||||
|
||||
html = markdown(text, options=options, extensions=extensions)
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
from markdown.extensions import Extension
|
||||
from markdown.treeprocessors import Treeprocessor
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
class GalleryTreeprocessor(Treeprocessor):
|
||||
def run(self, doc):
|
||||
for parent in doc.findall(".//ul/.."):
|
||||
for idx, ul in enumerate(parent):
|
||||
if ul.tag != "ul" or len(ul) == 0:
|
||||
continue
|
||||
has_gallery = False
|
||||
|
||||
# Option 1: In raw text in the <li>
|
||||
if ul[-1].text and ul[-1].text.endswith("{gallery}"):
|
||||
has_gallery = True
|
||||
ul[-1].text = ul[-1].text[:-9]
|
||||
# Option 2: After the last child (in its tail) \
|
||||
if len(ul[-1]) and ul[-1][-1].tail and \
|
||||
ul[-1][-1].tail.endswith("{gallery}"):
|
||||
has_gallery = True
|
||||
ul[-1][-1].tail = ul[-1][-1].tail[:-9]
|
||||
|
||||
if has_gallery:
|
||||
# TODO: Manipulate the output tree
|
||||
el = etree.Element("div")
|
||||
p = etree.Element("p")
|
||||
p.text = "<There is a gallery here:>"
|
||||
parent.remove(ul)
|
||||
el.append(p)
|
||||
el.append(ul)
|
||||
parent.insert(idx, el)
|
||||
|
||||
class GalleryExtension(Extension):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
md.treeprocessors.register(GalleryTreeprocessor(md), 'gallery', 8)
|
||||
md.registerExtension(self)
|
|
@ -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
|
|
@ -16,9 +16,11 @@ from markdown.inlinepatterns import InlineProcessor
|
|||
import xml.etree.ElementTree as etree
|
||||
from flask import url_for, render_template
|
||||
from app.utils.unicode_names import normalize
|
||||
from app.utils.filters.humanize import humanize
|
||||
from app.models.poll import Poll
|
||||
from app.models.topic import Topic
|
||||
from app.models.user import Member
|
||||
from app.models.attachment import Attachment
|
||||
|
||||
|
||||
class PCLinkExtension(Extension):
|
||||
|
@ -34,7 +36,7 @@ class PCLinkExtension(Extension):
|
|||
self.md = md
|
||||
|
||||
# append to end of inline patterns
|
||||
PCLINK_RE = r'\[\[([a-z]+): ?(\w+)\]\]'
|
||||
PCLINK_RE = r'<([a-z]+): ?(\w+)>'
|
||||
pclinkPattern = PCLinksInlineProcessor(PCLINK_RE, self.getConfigs())
|
||||
pclinkPattern.md = md
|
||||
md.inlinePatterns.register(pclinkPattern, 'pclink', 135)
|
||||
|
@ -48,6 +50,7 @@ class PCLinksInlineProcessor(InlineProcessor):
|
|||
'membre': handleUser, 'user': handleUser, 'u': handleUser,
|
||||
'sondage': handlePoll, 'poll': handlePoll,
|
||||
'topic': handleTopic, 't': handleTopic,
|
||||
'fichier': handleFile, 'file': handleFile, 'f': handleFile,
|
||||
}
|
||||
|
||||
def handleMatch(self, m, data):
|
||||
|
@ -70,12 +73,10 @@ class PCLinksInlineProcessor(InlineProcessor):
|
|||
# - either an xml.etree.ElementTree
|
||||
|
||||
def handlePoll(content_id, context):
|
||||
if not context.startswith("[[") or not context.endswith("]]"):
|
||||
return "[Sondage invalide]"
|
||||
try:
|
||||
id = int(content_id)
|
||||
except ValueError:
|
||||
return "[ID du sondage invalide]"
|
||||
return "[ID de sondage invalide]"
|
||||
|
||||
poll = Poll.query.get(content_id)
|
||||
|
||||
|
@ -90,7 +91,7 @@ def handleTopic(content_id, context):
|
|||
try:
|
||||
id = int(content_id)
|
||||
except ValueError:
|
||||
return "[ID du topic invalide]"
|
||||
return "[ID de topic invalide]"
|
||||
|
||||
topic = Topic.query.get(content_id)
|
||||
|
||||
|
@ -121,3 +122,18 @@ def handleUser(content_id, context):
|
|||
a.set('class', 'profile-link')
|
||||
|
||||
return a
|
||||
|
||||
def handleFile(content_id, context):
|
||||
try:
|
||||
content_id = int(content_id)
|
||||
except ValueError:
|
||||
return "[ID de fichier invalide]"
|
||||
|
||||
file = Attachment.query.get(content_id)
|
||||
|
||||
if file is None:
|
||||
return "[Fichier non trouvé]"
|
||||
|
||||
html = render_template('widgets/download_button.html', file=file)
|
||||
html = html.replace('\n', '') # Needed to avoid lots of <br> due to etree
|
||||
return etree.fromstring(html)
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
"""Add an index on Post.type
|
||||
|
||||
Revision ID: d2227d2479e2
|
||||
Revises: bcfdb271b88d
|
||||
Create Date: 2022-04-25 16:44:51.241965
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd2227d2479e2'
|
||||
down_revision = 'bcfdb271b88d'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index(op.f('ix_post_type'), 'post', ['type'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_post_type'), table_name='post')
|
||||
# ### end Alembic commands ###
|
Loading…
Reference in New Issue