Merge branch 'dev' into new_editor

This commit is contained in:
Lephe 2022-04-27 10:14:13 +01:00
commit fae28982f2
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
19 changed files with 461 additions and 33 deletions

View File

@ -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)

View File

@ -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",

View File

@ -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()),
}

View File

@ -139,3 +139,16 @@ 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;
}
.gallery, .gallery-js {
--border: rgba(255, 255, 255, 0.8);
--selected: rgba(255, 0, 0, 1.0);
}

View File

@ -121,6 +121,19 @@ footer {
--icons: #000;
}
.dl-button {
--link: #149641;
--link-text: #ffffff;
--link-active: #0f7331;
--meta: rgba(0, 0, 0, .15);
--meta-text: #000000;
}
.gallery, .gallery-js {
--border: rgba(0, 0, 0, 0.5);
--selected: rgba(0, 0, 0, 0.75);
}
/* Extra style on top of the Pygments style */
table.codehilitetable td.linenos {
color: #888;

View File

@ -86,6 +86,9 @@
height: 64px;
}
}
hr.signature {
opacity: 0.2;
}
.trophies {
display: flex;
flex-wrap: wrap;
@ -125,6 +128,83 @@
.trophy span {
font-size: 80%;
}
hr.signature {
opacity: 0.2;
.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%;
}
.gallery {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin: auto;
}
.gallery * {
margin: 3px;
border: 1px solid var(--border);
}
.gallery-js {
display: flex;
overflow-x: auto;
overflow-y: hidden;
margin: auto;
padding: 15px;
height: 180px;
}
.gallery-js img,
.gallery-js video {
height: 100%;
border: 1px solid var(--border);
cursor: pointer;
}
.gallery-js img:not(:first-child),
.gallery-js video:not(:first-child) {
margin-left: 15px;
}
.gallery-js img.selected,
.gallery-js video.selected {
box-shadow: 0 0 7.5px var(--selected);
}
@media screen and (max-width:1199px) {
.gallery-js {
height: 150px;
}
}
@media screen and (max-width:499px) {
.gallery-js {
height: 130px;
}
}
.gallery-spot {
justify-content: center;
margin: 10px auto;
}
.gallery-spot * {
cursor: pointer;
}

View File

@ -109,6 +109,11 @@
}
}
hr.signature {
opacity: 0.2;
}
/* Trophies */
.trophies {
display: flex;
@ -157,6 +162,80 @@
}
}
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 */
.gallery {
display: flex; flex-wrap: wrap;
justify-content: center; margin: auto;
* {
margin: 3px;
border: 1px solid var(--border);
}
}
/* Gallery with Javascript */
.gallery-js {
@padding: 15px;
display: flex; overflow-x: auto; overflow-y: hidden;
margin: auto; padding: @padding;
height: 150px + 2 * @padding;
@media screen and (max-width: @small) {
height: 120px + 2 * @padding;
}
@media screen and (max-width: @micro) {
height: 100px + 2 * @padding;
}
img, video {
height: 100%;
border: 1px solid var(--border);
cursor: pointer; //box-sizing: content-box;
&:not(:first-child) {
margin-left: @padding;
}
&.selected {
box-shadow: 0 0 @padding/2 var(--selected);
}
}
}
.gallery-spot {
justify-content: center;
margin: 10px auto;
* {
cursor: pointer;
}
}

View File

@ -0,0 +1,55 @@
document.querySelectorAll(".gallery").forEach(item => {
// Switch to gallery-js stylesheet
item.className = "gallery-js";
// Create the spotlight container
let spot = document.createElement('div');
spot.className = "gallery-spot";
spot.style.display = "none";
spot.appendChild(item.firstElementChild.cloneNode(true));
item.after(spot);
// Add some logic
// item.addEventListener("click", function(e) {
// console.log(e.target);
// console.log(e.currentTarget);
// // Select the clicked media
// Array.from(item.children).forEach(child => {
// child.classList.remove('selected');
// });
// e.target.classList.add('selected');
//
// // Display the current
// e.currentTarget.nextElementSibling.querySelector('div').innerHTML = e.target.outerHTML;
// });
});
document.querySelectorAll(".gallery-js > *").forEach(item => {
item.addEventListener("click", function(e) {
console.log(e.target);
// Manage selected media
if(e.target.classList.contains('selected')) {
e.target.classList.remove('selected');
} else {
e.target.classList.add('selected');
}
Array.from(e.target.parentElement.children).forEach(el => {
if(el != e.target) el.classList.remove('selected');
});
// Change content of spotlight
let spot = e.target.parentElement.nextElementSibling;
spot.replaceChild(e.target.cloneNode(true), spot.firstElementChild);
// Open spotlight media in a new tab
spot.firstElementChild.addEventListener("click", function(e) {
window.open(spot.firstElementChild.src, "_blank");
});
// Display the spotlight
if(e.target.classList.contains('selected')) {
spot.style.display = "flex";
} else {
spot.style.display = "none";
}
});
});

View File

@ -69,5 +69,5 @@ let keyboard_trigger = function(event) {
}
}
document.onclick = mouse_trigger;
document.onkeypress = keyboard_trigger;
document.addEventListener("click", mouse_trigger);
document.addEventListener("keydown", keyboard_trigger);

View File

@ -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>

View File

@ -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>

View File

@ -5,14 +5,14 @@
</svg>
Actualités
</h2>
<a href='/forum/news'>Toutes les nouveautés</a>
<a href='/forum/actus'>Toutes les nouveautés</a>
<hr>
<a href='/forum/news/calc'>Nouveautés Casio</a>
<a href='/forum/news/projects'>Projets communutaires</a>
<a href='/forum/news/events'>Événements de Planète Casio</a>
<a href='/forum/news/other'>Autres nouveautés</a>
<a href='/forum/actus/calc'>Nouveautés Casio</a>
<a href='/forum/actus/projets'>Projets communutaires</a>
<a href='/forum/actus/evenements'>Événements de Planète Casio</a>
<a href='/forum/actus/autres'>Autres nouveautés</a>
<hr>

View File

@ -0,0 +1,81 @@
{% extends "base/base.html" %}
{% import "widgets/editor.html" as widget_editor %}
{% import "widgets/thread.html" as widget_thread with context %}
{% import "widgets/user.html" as widget_user %}
{% import "widgets/pagination.html" as widget_pagination with context %}
{% import "widgets/attachments.html" as widget_attachments %}
{% block title %}
<h1>Programme {{ program.name }}</h1>
{% endblock %}
{% block content %}
<section>
<div style="display:flex;flex-wrap:wrap;align-items:center;">
<div>
{{ widget_user.profile(program.author) }}
</div>
<div style="padding:30px;">
<div style="font-size:115%;font-style:italic;margin-bottom:15px;">
{{ program.title }}
</div>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae
feugiat ante. Pellentesque luctus lorem tincidunt vestibulum condimentum.
Nullam sed tempus sem. Phasellus quis diam vitae sapien luctus consequat
ac eget lacus. Sed rutrum condimentum sagittis. Nullam erat nibh, euismod
ac metus at, consequat tincidunt ipsum. Fusce sagittis iaculis orci sedporta.
Etiam bibendum purus et ipsum pellentesque, quis sodales libero congue. Nunc
lectus quam, cursus non dictum nec, volutpat eget felis. Praesent sollicitudin
massa erat, nec venenatis lorem lacinia et. Etiam ullamcorper neque quis
nisi sodales vulputate. Integer scelerisque luctus arcu, ut elementum justo
auctor a. Praesent sit amet libero risus.</p>
<div class="gallery">
<img src="https://www.planet-casio.com/storage/staff/IDK_3_manoir.png" alt="">
<img src="https://www.planet-casio.com/storage/staff/IDK_7_stats.png" alt="">
<video>
<source src="https://linx.breizh.pm/selif/xgh9k9sq.mp4">
</video>
<img src="https://www.planet-casio.com/storage/staff/IDK_3_manoir.png" alt="">
<img src="https://www.planet-casio.com/storage/staff/IDK_7_stats.png" alt="">
<video>
<source src="https://www.planet-casio.com/storage/staff/IDK_5_intrigue.mp4">
</video>
<img src="https://www.planet-casio.com/storage/staff/IDK_3_manoir.png" alt="">
<img src="https://www.planet-casio.com/storage/staff/IDK_7_stats.png" alt="">
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae
feugiat ante. Pellentesque luctus lorem tincidunt vestibulum condimentum.
Nullam sed tempus sem. Phasellus quis diam vitae sapien luctus consequat
ac eget lacus. Sed rutrum condimentum sagittis. Nullam erat nibh, euismod
ac metus at, consequat tincidunt ipsum. Fusce sagittis iaculis orci sedporta.
Etiam bibendum purus et ipsum pellentesque, quis sodales libero congue. Nunc
lectus quam, cursus non dictum nec, volutpat eget felis. Praesent sollicitudin
massa erat, nec venenatis lorem lacinia et. Etiam ullamcorper neque quis
nisi sodales vulputate. Integer scelerisque luctus arcu, ut elementum justo
auctor a. Praesent sit amet libero risus.</p>
<div class="gallery">
<img src="https://www.planet-casio.com/storage/staff/IDK_3_manoir.png" alt="">
<img src="https://www.planet-casio.com/storage/staff/IDK_7_stats.png" alt="">
<video>
<source src="https://linx.breizh.pm/selif/xgh9k9sq.mp4">
</video>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae
feugiat ante. Pellentesque luctus lorem tincidunt vestibulum condimentum.
Nullam sed tempus sem. Phasellus quis diam vitae sapien luctus consequat
ac eget lacus. Sed rutrum condimentum sagittis. Nullam erat nibh, euismod
ac metus at, consequat tincidunt ipsum. Fusce sagittis iaculis orci sedporta.
Etiam bibendum purus et ipsum pellentesque, quis sodales libero congue. Nunc
lectus quam, cursus non dictum nec, volutpat eget felis. Praesent sollicitudin
massa erat, nec venenatis lorem lacinia et. Etiam ullamcorper neque quis
nisi sodales vulputate. Integer scelerisque luctus arcu, ut elementum justo
auctor a. Praesent sit amet libero risus.</p>
</div>
{% endblock %}

View File

@ -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 }}

View File

@ -12,6 +12,7 @@ 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')
@ -35,6 +36,7 @@ def md(text):
TocExtension(baselevel=2),
PCLinkExtension(),
MediaExtension(),
GalleryExtension(),
]
html = markdown(text, options=options, extensions=extensions)

View File

@ -0,0 +1,40 @@
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:
el = etree.Element("div")
el.set('class', 'gallery')
parent.remove(ul)
for li in ul:
# Filter out items that are not single medias
if len(li) == 1 and li[0].tag in ["img", "video"]:
el.append(li[0])
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)

View File

@ -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)

View File

@ -23,6 +23,7 @@ def render(*args, styles=[], scripts=[], **kwargs):
'scripts/pc-utils.js',
'scripts/smartphone_patch.js',
'scripts/editor.js',
'scripts/gallery.js',
'scripts/filter.js'
]

View File

@ -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 ###