Compare commits

..

1 Commits

Author SHA1 Message Date
Darks fa6a89b562
WIP 2022-05-06 01:05:11 +02:00
151 changed files with 1128 additions and 2940 deletions

7
.gitignore vendored
View File

@ -22,9 +22,6 @@ test.*
# Autosaves
*.dia~
## Logging files
*.log
## Deployment files
@ -42,10 +39,6 @@ local_config.py
wiki/
## JavaScript submodules buld files
# Emoji picker
app/static/scripts/emoji-picker-element/
## Personal folder

View File

@ -23,10 +23,4 @@ python-psycopg2
python-pillow
python-pyyaml
python-slugify
flask-crontab
```
Optionnel:
```
python-flask-debugtoolbar (Disponible dans l'AUR)
```

View File

@ -4,22 +4,19 @@ from flask_migrate import Migrate
from flask_login import LoginManager
from flask_mail import Mail
from flask_wtf.csrf import CSRFProtect
from flask_crontab import Crontab
from config import FlaskApplicationSettings, V5Config
from config import Config
app = Flask(__name__)
app.config.from_object(FlaskApplicationSettings)
app.v5logger = V5Config.v5logger()
app.config.from_object(Config)
# Check security of secret
if FlaskApplicationSettings.SECRET_KEY == "a-random-secret-key":
if Config.SECRET_KEY == "a-random-secret-key":
raise Exception("Please use a strong secret key!")
db = SQLAlchemy(app)
migrate = Migrate(app, db)
mail = Mail(app)
csrf = CSRFProtect(app)
crontab = Crontab(app)
login = LoginManager(app)
login.login_view = 'login'
@ -30,7 +27,6 @@ login.login_message = "Veuillez vous authentifier avant de continuer."
from app.utils.converters import *
app.url_map.converters['forum'] = ForumConverter
app.url_map.converters['topicpage'] = TopicPageConverter
app.url_map.converters['programpage'] = ProgramPageConverter
# Register routes
from app import routes
@ -40,12 +36,3 @@ from app.utils import filters
# Register processors
from app import processors
# Register scheduled jobs
from app import jobs
# Enable flask-debug-toolbar if requested
if V5Config.ENABLE_FLASK_DEBUG_TOOLBAR:
from flask_debugtoolbar import DebugToolbarExtension
app.config['DEBUG_TB_PROFILER_ENABLED'] = True
toolbar = DebugToolbarExtension(app)

View File

@ -25,7 +25,6 @@
# delete.accounts
# delete.shared-files
# move.posts
# lock.threads
#
# Shoutbox:
# shoutbox.kick
@ -59,7 +58,7 @@
publish.schedule-posts publish.pin-posts publish.shared-files
edit.posts edit.tests edit.accounts edit.trophies
delete.posts delete.tests delete.accounts delete.shared-files
move.posts lock.threads
move.posts
shoutbox.kick shoutbox.ban
misc.unlimited-pms misc.dev-infos misc.admin-panel
misc.no-upload-limits misc.arbitrary-login
@ -70,7 +69,7 @@
privs: forum.access.admin
edit.posts edit.tests
delete.posts delete.tests
move.posts lock.threads
move.posts
shoutbox.kick shoutbox.ban
misc.unlimited-pms misc.no-upload-limits
-

View File

@ -1,108 +0,0 @@
# This is a list of all tags, sorted by category. The category names are used
# to name CSS rules and shouldn't be changed directly.
# The following category groups calculators by common compatibility properties.
# Each comment indicates why the group should exist on its own rather than
# being merged with another one.
calc:
# Middle-school level, only basic algorithms; a unique property in this list.
fx92:
pretty: fx-92 Scientifique Collège+
# Some of the most limited Graph models, no add-ins.
g25:
pretty: Graph 25/25+E/25+EII
# The whole series with more Basic constructs than g25, but SH3 for add-ins
# We don't separate based on whether an OS update is required (deemed safe)
gsh3:
pretty: Graph 35+/75/85/95 (SH3)
# Same as gsh3, but with SH4 for add-ins; support CasioPython
gsh4:
pretty: Graph 35+/35+E/75+/75+E (SH4)
# Like gsh3, but has Python; also; issues with the display and MonochromLib
g35+e2:
pretty: Graph 35+E II
# Color display, nothing like the previous models
cg20:
pretty: fx-CG 10/20/Prizm
# Like cg20, but has Python, and some incompatibilities on add-in
g90+e:
pretty: Graph 90+E
# Different series entirely; has an SDK for add-ins
cp300:
pretty: Classpad 300/330
# Like cp300, but does not have an SDK
cp330+:
pretty: Classpad 330+
# Color display, entirely new model; no SDK
cp400:
pretty: Classpad 400/400+E
lang:
basic:
pretty: Basic CASIO
cbasic:
pretty: C.Basic
python:
pretty: Python
c:
pretty: C/C++ (add-in)
lua:
pretty: LuaFX
other:
pretty: "Langage: autre"
games:
action:
pretty: Action
adventure:
pretty: Aventure
fighting:
pretty: Combat
narrative:
pretty: Narratif
other:
pretty: "Jeu: autre"
platform:
pretty: Plateforme
puzzle:
pretty: Puzzle
rpg:
pretty: RPG
rythm:
pretty: Rythme
shooting:
pretty: Tir/FPS
simulation:
pretty: Simulation
sport:
pretty: Sport
strategy:
pretty: Stratégie
survival:
pretty: Survie
tools:
conversion:
pretty: Outil de conversion
graphics:
pretty: Outil graphique
science:
pretty: Outil scientifique
programming:
pretty: Outil pour programmer
other:
pretty: "Outil: autre"
courses:
math:
pretty: Maths
physics:
pretty: Physique
engineering:
pretty: SI/Électronique
economics:
pretty: Économie
informatics:
pretty: Informatique
other:
pretty: "Cours: autre"

View File

@ -1,15 +0,0 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField
from wtforms.validators import InputRequired, Length
import app.utils.validators as vf
from app.utils.antibot_field import AntibotField
from app.utils.tag_field import TagListField
from app.forms.forum import CommentForm
class ProgramCreationForm(CommentForm):
name = StringField('Nom du programme',
validators=[InputRequired(), Length(min=3, max=64)])
tags = TagListField('Liste de tags')
submit = SubmitField('Soumettre le programme')

View File

@ -1 +0,0 @@
from app.jobs.update_progrank import update_progrank

View File

@ -1,11 +0,0 @@
from app import db, crontab
from app.models.program import Program
from datetime import datetime
@crontab.job(minute="0", hour="4")
def update_progrank():
for p in Program.query.all():
p.progrank = 0
p.progrank_date = datetime.now()
db.session.merge(p)
db.session.commit()

View File

@ -4,5 +4,3 @@ from app.models.forum import Forum
from app.models.topic import Topic
from app.models.notification import Notification
from app.models.program import Program
from app.models.tag import Tag
from app.models.event import Event

View File

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

View File

@ -1,6 +1,5 @@
from app import db
from app.models.post import Post
from app.models.attachment import Attachment
from sqlalchemy.orm import backref
@ -21,19 +20,12 @@ class Comment(Post):
backref=backref('comments', lazy='dynamic'),
foreign_keys=thread_id)
attachments = db.relationship('Attachment', back_populates='comment',
lazy='joined')
# attachments (relation from Attachment)
@property
def is_top_comment(self):
return self.id == self.thread.top_comment_id
@property
def is_metacontent(self):
"""Whether if this post is metacontent (topic, program) or actual content"""
return False
def __init__(self, author, text, thread):
"""
Create a new Comment in a thread.
@ -61,17 +53,5 @@ class Comment(Post):
db.session.commit()
db.session.delete(self)
def create_attachments(self, multiple_file_field_data):
"""Create attachements from a form's MultipleFileField.data."""
attachments = []
for file in multiple_file_field_data:
if file.filename != "":
a = Attachment(file, self)
attachments.append((a, file))
db.session.add(a)
db.session.commit()
for a, file in attachments:
a.set_file(file)
def __repr__(self):
return f'<Comment: #{self.id}>'

View File

@ -1,11 +0,0 @@
from app import db
class Event(db.Model):
__tablename__ = 'event'
id = db.Column(db.Integer, primary_key=True)
# Pretty event name, eg. "CPC #28"
name = db.Column(db.Unicode(128))
# Main topic, used to automatically insert links
main_topic = db.Column(db.Integer, db.ForeignKey('topic.id'))

View File

@ -21,20 +21,11 @@ class Post(db.Model):
index=True)
author = db.relationship('User', backref="posts", foreign_keys=author_id)
# Tags, for programs and tutorials
tags = db.relationship('Tag', back_populates='post', lazy='joined')
__mapper_args__ = {
'polymorphic_identity': __tablename__,
'polymorphic_on': type
}
@property
def is_metacontent(self):
"""Whether if this post is metacontent (topic, program) or actual content"""
return True
def __init__(self, author):
"""
Create a new Post.

View File

@ -16,18 +16,16 @@ class SpecialPrivilege(db.Model):
id = db.Column(db.Integer, primary_key=True)
# Member that is granted the privilege
member_id = db.Column(db.Integer, db.ForeignKey('member.id'), index=True)
member = db.relationship('Member', back_populates="special_privs",
foreign_keys=member_id)
mid = db.Column(db.Integer, db.ForeignKey('member.id'), index=True)
# Privilege name
priv = db.Column(db.String(64))
def __init__(self, member, priv):
self.member = member
self.mid = member.id
self.priv = priv
def __repr__(self):
return f'<Privilege: {self.priv} of member #{self.member_id}>'
return f'<Privilege: {self.priv} of member #{self.mid}>'
# Group: User group, corresponds to a community role and a set of privileges
@ -45,9 +43,7 @@ class Group(db.Model):
description = db.Column(db.UnicodeText)
# List of members (lambda delays evaluation)
members = db.relationship('Member', secondary=lambda: GroupMember,
back_populates='groups', lazy='joined')
# List of privileges
privileges = db.relationship('GroupPrivilege', back_populates='group')
back_populates='groups')
def __init__(self, name, css, descr):
self.name = name
@ -61,7 +57,7 @@ class Group(db.Model):
* Group privileges
"""
for gp in self.privileges:
for gp in GroupPrivilege.query.filter_by(gid=self.id).all():
db.session.delete(gp)
db.session.commit()
@ -69,7 +65,8 @@ class Group(db.Model):
db.session.commit()
def privs(self):
return sorted(gp.priv for gp in self.privileges)
gps = GroupPrivilege.query.filter_by(gid=self.id).all()
return sorted(gp.priv for gp in gps)
def __repr__(self):
return f'<Group: {self.name}>'
@ -80,17 +77,15 @@ GroupMember = db.Table('group_member', db.Model.metadata,
db.Column('gid', db.Integer, db.ForeignKey('group.id')),
db.Column('uid', db.Integer, db.ForeignKey('member.id')))
# GroupPrivilege: A list of privileges for groups, materialized as a table
# Many-to-many relationship for privileges granted to groups
class GroupPrivilege(db.Model):
__tablename__ = 'group_privilege'
id = db.Column(db.Integer, primary_key=True)
group_id = db.Column(db.Integer, db.ForeignKey('group.id'))
group = db.relationship('Group', back_populates='privileges',
foreign_keys=group_id)
gid = db.Column(db.Integer, db.ForeignKey('group.id'))
priv = db.Column(db.String(64))
def __init__(self, group, priv):
self.group = group
self.gid = group.id
self.priv = priv

View File

@ -9,48 +9,30 @@ class Program(Post):
id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True)
# Program name
name = db.Column(db.Unicode(128))
# Author, when different from the poster
real_author = db.Column(db.Unicode(128))
# Version
version = db.Column(db.Unicode(64))
# Approximate size as indicated by poster
size = db.Column(db.Unicode(64))
# License identifier
license = db.Column(db.String(32))
title = db.Column(db.Unicode(128))
# Label de qualité
label = db.Column(db.Boolean, nullable=False, server_default="FALSE")
# Event for which the program was posted
event = db.Column(db.Integer, db.ForeignKey('event.id'), nullable=True)
# TODO: Category (games/utilities/lessons)
# TODO: Tags
# TODO: Compatible calculator models
# TODO: Number of downloads
# Thread with the program description (top comment) and comments
thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False)
thread = db.relationship('Thread', foreign_keys=thread_id,
back_populates='owner_program')
# Progrank, and last date of progrank update
progrank = db.Column(db.Integer)
progrank_date = db.Column(db.DateTime)
# TODO: Number of views, statistics, attached files, etc
# Implicit attributes:
# * tags (inherited from Post)
# * attachements (available at thread.top_comment.attachments)
def __init__(self, author, name, thread):
def __init__(self, author, title, thread):
"""
Create a Program.
Arguments:
author -- post author (User, though only Members can post)
name -- program name (unicode string)
title -- program title (unicode string)
thread -- discussion thread attached to the topic
"""
Post.__init__(self, author)
self.name = name
self.title = title
self.thread = thread
@staticmethod
@ -62,4 +44,4 @@ class Program(Post):
db.session.delete(self)
def __repr__(self):
return f'<Program: #{self.id} "{self.name}">'
return f'<Program: #{self.id} "{self.title}">'

View File

@ -1,56 +0,0 @@
from app import db
class TagInformation(db.Model):
"""Detailed information about tags, by dot-string tag identifier."""
__tablename__ = 'tag_information'
# The ID is the dot-string of the tag (eg. "calc.g35+e2")
id = db.Column(db.String(64), primary_key=True)
# List of uses. Note how we load tag information along individual tags, but
# we don't load uses unless the field is accessed.
uses = db.relationship('Tag', back_populates='tag', lazy='dynamic')
# Pretty name
pretty = db.Column(db.String(64))
# ... any other static information about tags
def __init__(self, id):
self.id = id
def category(self):
return self.id.split(".", 1)[0]
@staticmethod
def all_tags():
all_tags = {}
for ti in TagInformation.query.all():
ctgy = ti.category()
if ctgy not in all_tags:
all_tags[ctgy] = []
all_tags[ctgy].append(ti)
return all_tags
class Tag(db.Model):
"""Association between a Post and a dot-string tag identifier."""
__tablename__ = 'tag'
id = db.Column(db.Integer, primary_key=True)
# Tagged post
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), index=True)
post = db.relationship('Post', back_populates='tags', foreign_keys=post_id)
# Tag name. Note how we always load the information along the tag, but not
# the other way around.
tag_id = db.Column(db.String(64), db.ForeignKey('tag_information.id'),
index=True)
tag = db.relationship('TagInformation', back_populates='uses',
foreign_keys=tag_id, lazy='joined')
def __init__(self, post, tag):
self.post = post
if isinstance(tag, str):
tag = TagInformation.query.filter_by(id=tag).one()
self.tag = tag

View File

@ -18,9 +18,6 @@ class Thread(db.Model):
owner_topic = db.relationship('Topic')
owner_program = db.relationship('Program')
# Whether the thread is locked
locked = db.Column(db.Boolean, default=False)
# Other fields populated automatically through relations:
# <comments> The list of comments (of type Comment)

View File

@ -1,7 +1,8 @@
from flask import url_for
from datetime import date
from flask_login import UserMixin
from sqlalchemy import func as SQLfunc
import werkzeug.security
from os.path import isfile
from PIL import Image
from app import app, db
from app.models.priv import SpecialPrivilege, Group, GroupMember, \
@ -18,9 +19,7 @@ import app.utils.ldap as ldap
from app.utils.unicode_names import normalize
from config import V5Config
from os.path import isfile
from datetime import date
from PIL import Image
import werkzeug.security
import math
import app
import os
@ -90,9 +89,19 @@ class Member(User):
xp = db.Column(db.Integer)
register_date = db.Column(db.Date, default=date.today)
avatar_id = db.Column(db.Integer, default=0)
@property
def avatar(self):
return f'{self.id}_{self.avatar_id}.png'
@property
def level(self):
level = math.asinh(self.xp / 1000) * (100 / math.asinh(10))
return int(level), int(level * 100) % 100
# Groups and related privileges
groups = db.relationship('Group', secondary=GroupMember,
back_populates='members', lazy='joined')
back_populates='members')
# Personal information, all optional
bio = db.Column(db.UnicodeText)
@ -106,21 +115,11 @@ class Member(User):
# Settings
newsletter = db.Column(db.Boolean, default=False)
theme = db.Column(db.Unicode(32))
avatar_id = db.Column(db.Integer, default=0)
# Relations
trophies = db.relationship('Trophy', secondary=TrophyMember,
back_populates='owners')
# Specially-offered privileges (use self.special_privileges())
special_privs = db.relationship('SpecialPrivilege',
back_populates='member', lazy='joined')
# Other fields populated automatically through relations:
# <notifications> List of unseen notifications (of type Notification)
# <polls> Polls created by the member (of class Poll)
# Access to polymorphic posts
# TODO: Check that the query uses the double index on Post.{author_id,type}
def comments(self):
@ -130,22 +129,9 @@ class Member(User):
def programs(self):
return db.session.query(Program).filter(Post.author_id==self.id).all()
@property
def avatar_filename(self):
return f'{self.id}_{self.avatar_id}.png'
@property
def avatar_url(self):
if self.avatar_id == 0:
return url_for('static', filename='images/default_avatar.png')
else:
return url_for('avatar',filename=self.avatar_filename)
@property
def level(self):
level = math.asinh(self.xp / 1000) * (100 / math.asinh(10))
return int(level), int(level * 100) % 100
# Other fields populated automatically through relations:
# <notifications> List of unseen notifications (of type Notification)
# <polls> Polls created by the member (of class Poll)
def __init__(self, name, email, password):
"""Register a new user."""
@ -199,7 +185,7 @@ class Member(User):
Deletes the user, but not the posts; use either transfer_posts() or
delete_posts() before calling this.
"""
for sp in self.special_privs:
for sp in SpecialPrivilege.query.filter_by(mid=self.id).all():
db.session.delete(sp)
self.trophies = []
@ -212,16 +198,17 @@ class Member(User):
def priv(self, priv):
"""Check whether the member has the specified privilege."""
if priv in self.special_privileges():
if SpecialPrivilege.query.filter_by(mid=self.id, priv=priv).first():
return True
for g in self.groups:
if priv in g.privs():
return True
return False
return db.session.query(Group, GroupPrivilege).filter(
Group.id.in_([g.id for g in self.groups]),
GroupPrivilege.gid==Group.id,
GroupPrivilege.priv==priv).first() is not None
def special_privileges(self):
"""List member's special privileges as list of strings."""
return sorted([p.priv for p in self.special_privs])
"""List member's special privileges."""
sp = SpecialPrivilege.query.filter_by(mid=self.id).all()
return sorted(row.priv for row in sp)
def can_access_forum(self, forum):
"""Whether this member can read the forum's contents."""
@ -263,13 +250,6 @@ class Member(User):
post = comment.thread.owner_post
return self.can_edit_post(post) and (comment.author == post.author)
def can_lock_thread(self, post):
"""Whether this member can lock the thread associated with the post"""
print(post.id, post.is_metacontent)
if not post.is_metacontent:
return False
return self.priv("lock.threads")
def can_access_file(self, file):
"""Whether this member can access the file."""
return self.can_access_post(file.comment)
@ -332,22 +312,20 @@ class Member(User):
def set_avatar(self, avatar):
# Save old avatar filepath
old_avatar = os.path.join(V5Config.DATA_FOLDER, "avatars",
self.avatar_filename)
old_avatar = os.path.join(V5Config.DATA_FOLDER, "avatars", self.avatar)
# Resize & convert image
size = 128, 128
im = Image.open(avatar)
im.thumbnail((128, 128), Image.ANTIALIAS)
im.thumbnail(size, Image.ANTIALIAS)
# Change avatar id
# TODO: verify concurrency behavior
current_id = db.session.query(SQLfunc.max(Member.avatar_id)).first()[0]
self.avatar_id = current_id + 1
db.session.merge(self)
db.session.commit()
# Save the new avatar
im.save(os.path.join(V5Config.DATA_FOLDER, "avatars",
self.avatar_filename), 'PNG')
im.save(os.path.join(V5Config.DATA_FOLDER, "avatars", self.avatar),
'PNG')
# If nothing has failed, remove old one (allow failure to regularize
# exceptional situations like missing avatar or folder migration)
try:
@ -397,7 +375,8 @@ class Member(User):
Notify a user with a message.
An hyperlink can be added to redirect to the notification source
"""
n = Notification(self, message, href=href)
return
n = Notification(self.id, message, href=href)
db.session.add(n)
db.session.commit()
@ -574,7 +553,8 @@ class Member(User):
# TODO: Trophy "actif"
if context in ["on-profile-update", None]:
if self.avatar_id != 0:
if isfile(os.path.join(
V5Config.DATA_FOLDER, "avatars", self.avatar)):
self.add_trophy("Artiste")
else:
self.del_trophy("Artiste")

View File

@ -16,17 +16,13 @@ def menu_processor():
main_forum = Forum.query.filter_by(parent=None).first()
# Constructing last active topics
rows = db.session.execute( """SELECT topic.id FROM topic
raw = db.session.execute( """SELECT topic.id FROM topic
INNER JOIN comment ON topic.thread_id = comment.thread_id
INNER JOIN post ON post.id = comment.id
GROUP BY topic.id
ORDER BY MAX(post.date_created) DESC
LIMIT 20;""")
ids = [row[0] for row in rows]
# Somewhat inelegant, but much better than loading individually
recent_topics = db.session.query(Topic).filter(Topic.id.in_(ids)).all()
recent_topics = sorted(recent_topics, key=lambda t: ids.index(t.id))
last_active_topics = [Topic.query.get(id) for id in raw]
# Filter the topics the user can view and limit to 10
if current_user.is_authenticated:
@ -34,10 +30,10 @@ def menu_processor():
else:
f = lambda t: t.forum.is_default_accessible()
recent_topics = list(filter(f, recent_topics))[:10]
last_active_topics = list(filter(f, last_active_topics))[:10]
# Constructing last news
rows = db.session.execute( """SELECT topic.id FROM topic
raw = db.session.execute( """SELECT topic.id FROM topic
INNER JOIN forum ON topic.forum_id = forum.id
INNER JOIN comment ON topic.thread_id = comment.thread_id
INNER JOIN post ON post.id = comment.id
@ -46,10 +42,8 @@ def menu_processor():
ORDER BY MIN(post.date_created) DESC
LIMIT 10;
""")
ids = [row[0] for row in rows]
recent_news = db.session.query(Topic).filter(Topic.id.in_(ids)).all()
recent_news = sorted(recent_news, key=lambda t: ids.index(t.id))
last_news = [Topic.query.get(id) for id in raw]
return dict(login_form=login_form, search_form=search_form,
main_forum=main_forum, last_active_topics=recent_topics,
last_news=recent_news)
main_forum=main_forum, last_active_topics=last_active_topics,
last_news=last_news)

View File

@ -3,16 +3,15 @@ from flask import url_for
from config import V5Config
from slugify import slugify
from app.utils.login_as import is_vandal
from app.models.tag import TagInformation
@app.context_processor
def utilities_processor():
""" Add some utilities to render context """
return dict(
len=len,
# enumerate=enumerate,
_url_for=lambda route, args, **other: url_for(route, **args, **other),
V5Config=V5Config,
slugify=slugify,
is_vandal=is_vandal,
db_all_tags=TagInformation.all_tags,
is_vandal=is_vandal
)

View File

@ -1,13 +1,13 @@
# Register routes here
from app.routes import index, search, users, tools, development, chat
from app.routes import index, search, users, tools, development
from app.routes.account import login, account, notification, polls
from app.routes.admin import index, groups, account, forums, \
from app.routes.admin import index, groups, account, trophies, forums, \
attachments, config, members, polls, login_as
from app.routes.forum import index, topic
from app.routes.polls import vote, delete
from app.routes.posts import edit
from app.routes.programs import index, submit, program
from app.routes.programs import index
from app.routes.api import markdown
try:

View File

@ -8,7 +8,6 @@ from app.models.trophy import Title
from app.utils.render import render
from app.utils.send_mail import send_validation_mail, send_reset_password_mail
from app.utils.priv_required import guest_only
from app.utils.glados import say, BOLD
import app.utils.ldap as ldap
import app.utils.validators as vd
from itsdangerous import URLSafeTimedSerializer
@ -31,7 +30,6 @@ def edit_account():
if form.submit.data:
if form.is_submitted() and form.validate(extra_validators=extra_vd):
old_username = current_user.norm
current_user.update(
avatar=form.avatar.data or None,
email=form.email.data or None,
@ -43,13 +41,10 @@ def edit_account():
newsletter=form.newsletter.data,
theme=form.theme.data
)
ldap.edit(old_username, current_user)
current_user.update(password=form.password.data or None)
db.session.merge(current_user)
db.session.commit()
current_user.update_trophies("on-profile-update")
flash('Modifications effectuées', 'ok')
app.v5logger.info(f"<{current_user.name}> has edited their account")
return redirect(request.url)
else:
flash('Erreur lors de la modification', 'error')
@ -67,7 +62,6 @@ def ask_reset_password():
m = Member.query.filter_by(email=form.email.data).first()
if m is not None:
send_reset_password_mail(m.name, m.email)
app.v5logger.info(f"<{m.name}> has asked a password reset token")
flash('Un email a été envoyé à l\'adresse renseignée', 'ok')
return redirect(url_for('login'))
elif request.method == "POST":
@ -93,7 +87,6 @@ def reset_password(token):
db.session.merge(m)
db.session.commit()
flash('Modifications effectuées', 'ok')
app.v5logger.info(f"<{m.name}> has reset their password")
return redirect(url_for('login'))
else:
flash('Erreur lors de la modification', 'error')
@ -109,7 +102,6 @@ def delete_account():
if del_form.submit.data:
if del_form.validate_on_submit():
name = current_user.name
if del_form.transfer.data:
guest = Guest(current_user.generate_guest_name())
db.session.add(guest)
@ -120,14 +112,10 @@ def delete_account():
current_user.delete_posts()
db.session.commit()
if (V5Config.USE_LDAP):
ldap.delete_member(current_user)
current_user.delete()
logout_user()
db.session.commit()
flash('Compte supprimé', 'ok')
app.v5logger.info(f"<{name}> has deleted their account ({'with' if del_form.transfer.data else 'without'} guest transfer)")
return redirect(url_for('index'))
else:
flash('Erreur lors de la suppression du compte', 'error')
@ -153,7 +141,6 @@ def register():
# Email validation message
send_validation_mail(member.name, member.email)
app.v5logger.info(f"<{member.name}> registered")
return redirect(url_for('validation') + "?email=" + form.email.data)
return render('account/register.html', title='Register',
@ -191,8 +178,4 @@ def activate_account(token):
db.session.commit()
flash("L'email a bien été confirmé", "ok")
app.v5logger.info(f"<{m.name}> has activated their account")
say(f"Un nouveau membre sest inscrit ! Il sagit de {BOLD}{m.name}{BOLD}.")
say(url_for('user', username=m.name, _external=True))
return redirect(url_for('login'))

View File

@ -49,7 +49,6 @@ def login():
login_user(member, remember=form.remember_me.data,
duration=datetime.timedelta(days=7))
member.update_trophies("on-login")
app.v5logger.info(f"<{member.name}> has logged in")
# Redirect safely (https://huit.re/open-redirect)
def is_safe_url(target):
@ -72,10 +71,8 @@ def login():
@login_required
@check_csrf
def logout():
name = current_user.name
logout_user()
flash('Déconnexion réussie', 'info')
app.v5logger.info(f"<{name}> has logged out")
if request.referrer:
return redirect(request.referrer)
return redirect(url_for('index'))

View File

@ -28,6 +28,5 @@ def account_polls():
db.session.commit()
flash(f"Le sondage {p.id} a été créé", "info")
app.v5logger.info(f"<{current_user.name}> has created the form #{p.id}")
return render("account/polls.html", polls=polls, form=form)

View File

@ -9,7 +9,6 @@ from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm, \
AdminAccountEditTrophyForm, AdminAccountEditGroupForm
from app.utils.render import render
from app.utils.notify import notify
from app.utils import ldap as ldap
from app import app, db
from config import V5Config
@ -51,12 +50,12 @@ def adm_edit_account(user_id):
# You cannot user vd.name_available because name will always be
# invalid! Maybe you can add another validator with arguments
raise Exception(f'{newname} is not available')
old_username = user.norm
user.update(
avatar=form.avatar.data or None,
name=form.username.data or None,
email=form.email.data or None,
email_confirmed=form.email_confirmed.data,
password=form.password.data or None,
birthday=form.birthday.data,
signature=form.signature.data,
title=form.title.data,
@ -64,14 +63,11 @@ def adm_edit_account(user_id):
newsletter=form.newsletter.data,
xp=form.xp.data or None,
)
ldap.edit(old_username, user)
user.update(password=form.password.data or None)
db.session.merge(user)
db.session.commit()
# TODO: send an email to member saying his account has been modified
user.notify(f"Vos informations personnelles ont été modifiées par {current_user.name}.")
flash('Modifications effectuées', 'ok')
app.v5logger.info(f"[admin] <{current_user.name}> has edited <{user.name}>'s data")
return redirect(request.url)
else:
flash('Erreur lors de la modification', 'error')
@ -89,7 +85,6 @@ def adm_edit_account(user_id):
db.session.merge(user)
db.session.commit()
flash('Modifications effectuées', 'ok')
app.v5logger.info(f"[admin] <{current_user.name}> has edited <{user.name}>'s trophies")
return redirect(request.url)
else:
flash("Erreur lors de la modification des trophées", 'error')
@ -107,7 +102,6 @@ def adm_edit_account(user_id):
db.session.merge(user)
db.session.commit()
flash('Modifications effectuées', 'ok')
app.v5logger.info(f"[admin] <{current_user.name}> has edited <{user.name}>'s groups")
return redirect(request.url)
else:
flash("Erreur lors de la modification des groupes", 'error')
@ -154,13 +148,9 @@ def adm_delete_account(user_id):
user.delete_posts()
db.session.commit()
if (V5Config.USE_LDAP):
ldap.delete_member(user)
user.delete()
db.session.commit()
flash('Compte supprimé', 'ok')
app.v5logger.info(f"[admin] <{current_user.name}> has deleted <{user.name}> account")
return redirect(url_for('adm'))
else:
flash('Erreur lors de la suppression du compte', 'error')

View File

@ -15,7 +15,7 @@ def adm_groups():
# Users with either groups or special privileges
users_groups = Member.query.join(GroupMember)
users_special = Member.query \
.join(SpecialPrivilege, Member.id == SpecialPrivilege.member_id)
.join(SpecialPrivilege, Member.id == SpecialPrivilege.mid)
users = users_groups.union(users_special)
users = sorted(users, key = lambda x: x.name)

View File

@ -42,7 +42,6 @@ def adm_login_as():
# Create a safe token to flee when needed
s = Serializer(app.config["SECRET_KEY"])
vandal_token = s.dumps(current_user.id)
vandal_name = current_user.name
# Login and display some messages
login_user(user)
@ -52,11 +51,9 @@ def adm_login_as():
else:
flash(f"Connecté en tant que {user.name}")
app.v5logger.info(f"[admin] <{vandal_name}> has logged in as <{user.name}>")
# Return the response
resp = make_response(redirect(url_for('index')))
resp.set_cookie('vandale', vandal_token, path='/')
resp.set_cookie('vandale', vandal_token)
return resp
# Else return form
@ -79,22 +76,13 @@ def adm_logout_as():
abort(403)
user = Member.query.get(id)
# Send a notification to vandalized user
current_user.notify(f"{user.name} a accédé à ce compte à des fins de modération",
url_for('user', username=user.name))
# Switch back to admin
victim_name = current_user.name
logout_user()
login_user(user)
app.v5logger.info(f"[admin] <{user.name}> has logged out from <{victim_name}>'s account")
if request.referrer:
resp = make_response(redirect(request.referrer))
else:
resp = make_response(redirect(url_for('index')))
resp.set_cookie('vandale', '', expires=0, path='/')
resp.set_cookie('vandale', '', expires=0)
return resp

View File

@ -4,7 +4,7 @@ from app.utils.render import render
from app.models.poll import Poll
@app.route('/admin/sondages', methods=['GET'])
@priv_required('misc.admin-panel')
@priv_required('access-admin-panel')
def adm_polls():
polls = Poll.query.order_by(Poll.end.desc()).all()

View File

@ -0,0 +1,77 @@
from flask import request, flash, redirect, url_for
from app.utils.priv_required import priv_required
from app.models.trophy import Trophy, Title
from app.forms.trophy import TrophyForm, DeleteTrophyForm
from app.utils.render import render
from app import app, db
@app.route('/admin/trophees', methods=['GET', 'POST'])
@priv_required('misc.admin-panel', 'edit.trophies')
def adm_trophies():
form = TrophyForm()
if request.method == "POST":
if form.validate_on_submit():
is_title = form.title.data
if is_title:
trophy = Title(form.name.data, form.desc.data,
form.hidden.data, form.css.data)
else:
trophy = Trophy(form.name.data, form.desc.data,
form.hidden.data)
db.session.add(trophy)
db.session.commit()
flash(f'Nouveau {["trophée", "titre"][is_title]} ajouté', 'ok')
else:
flash('Erreur lors de la création du trophée', 'error')
trophies = Trophy.query.all()
return render('admin/trophies.html', trophies=trophies,
form=form)
@app.route('/admin/trophees/<trophy_id>/editer', methods=['GET', 'POST'])
@priv_required('misc.admin-panel', 'edit.trophies')
def adm_edit_trophy(trophy_id):
trophy = Trophy.query.filter_by(id=trophy_id).first_or_404()
form = TrophyForm()
if request.method == "POST":
if form.validate_on_submit():
is_title = form.title.data != ""
if is_title:
trophy.name = form.name.data
trophy.description = form.desc.data
trophy.title = form.title.data
trophy.hidden = form.hidden.data
trophy.css = form.css.data
else:
trophy.name = form.name.data
trophy.description = form.desc.data
trophy.hidden = form.hidden.data
db.session.merge(trophy)
db.session.commit()
flash(f'{["Trophée", "Titre"][is_title]} modifié', 'ok')
return redirect(url_for('adm_trophies'))
else:
flash('Erreur lors de la création du trophée', 'error')
return render('admin/edit_trophy.html', trophy=trophy, form=form)
@app.route('/admin/trophees/<trophy_id>/supprimer', methods=['GET', 'POST'])
@priv_required('misc.admin-panel', 'edit.trophies')
def adm_delete_trophy(trophy_id):
trophy = Trophy.query.filter_by(id=trophy_id).first_or_404()
# TODO: Add an overview of what will be deleted.
del_form = DeleteTrophyForm()
if request.method == "POST":
if del_form.validate_on_submit():
trophy.delete()
db.session.commit()
flash('Trophée supprimé', 'ok')
return redirect(url_for('adm_trophies'))
else:
flash('Erreur lors de la suppression du trophée', 'error')
del_form.delete.data = False # Force to tick to delete the trophy
return render('admin/delete_trophy.html', trophy=trophy, del_form=del_form)

View File

@ -2,11 +2,9 @@ from app import app
from app.utils.filters.markdown import md
from flask import request, abort
from werkzeug.exceptions import BadRequestKeyError
from app import csrf
class API():
@app.route("/api/markdown", methods=["POST"])
@csrf.exempt
def api_markdown():
try:
markdown = request.get_json()['text']

View File

@ -1,16 +0,0 @@
from app import app
from app.utils.render import render
from flask import send_file, url_for
@app.route('/chat')
def chat():
return render('chat.html',
styles=[
'+css/v5shoutbox.css'],
scripts=[
'-scripts/trigger_menu.js',
'-scripts/editor.js'])
@app.route('/v5shoutbox.js')
def v5shoutbox_js():
return send_file('static/scripts/v5shoutbox.js')

View File

@ -11,7 +11,9 @@ import os
def avatar(filename):
filename = secure_filename(filename) # No h4ckers allowed
filepath = os.path.join(V5Config.DATA_FOLDER, "avatars", filename)
return send_file(filepath)
if os.path.isfile(filepath):
return send_file(filepath)
return redirect(url_for('static', filename='images/default_avatar.png'))
@app.route('/fichiers/<path>/<name>')
def attachment(path, name):

View File

@ -4,7 +4,6 @@ from flask import request, redirect, url_for, abort, flash
from app import app, db
from config import V5Config
from app.utils.render import render
from app.utils.glados import say, BOLD
from app.forms.forum import TopicCreationForm, AnonymousTopicCreationForm
from app.models.forum import Forum
from app.models.topic import Topic
@ -74,11 +73,6 @@ def forum_page(f, page=1):
current_user.update_trophies('new-post')
flash('Le sujet a bien été créé', 'ok')
app.v5logger.info(f"<{t.author.name}> has created the topic #{t.id}")
if f.is_default_accessible():
say(f"Nouveau topic de {author.name} : {BOLD}{t.title}{BOLD}")
say(url_for('forum_topic', f=f, page=(t, 1), _external=True))
return redirect(url_for('forum_topic', f=f, page=(t,1)))
# Paginate topic pages

View File

@ -5,7 +5,6 @@ from sqlalchemy import desc
from app import app, db
from config import V5Config
from app.utils.render import render
from app.utils.glados import say, BOLD
from app.forms.forum import CommentForm, AnonymousCommentForm
from app.models.thread import Thread
from app.models.comment import Comment
@ -32,7 +31,7 @@ def forum_topic(f, page):
else:
form = AnonymousCommentForm()
if form.validate_on_submit() and not t.thread.locked and (
if form.validate_on_submit() and (
V5Config.ENABLE_GUEST_POST or \
(current_user.is_authenticated and current_user.can_post_in_forum(f))):
@ -47,7 +46,17 @@ def forum_topic(f, page):
c = Comment(author, form.message.data, t.thread)
db.session.add(c)
db.session.commit()
c.create_attachments(form.attachments.data)
# Manage files
attachments = []
for file in form.attachments.data:
if file.filename != "":
a = Attachment(file, c)
attachments.append((a, file))
db.session.add(a)
db.session.commit()
for a, file in attachments:
a.set_file(file)
# Update member's xp and trophies
if current_user.is_authenticated:
@ -55,14 +64,9 @@ def forum_topic(f, page):
current_user.update_trophies('new-post')
flash('Message envoyé', 'ok')
app.v5logger.info(f"<{c.author.name}> has posted a the comment #{c.id}")
if f.is_default_accessible():
say(f"Nouveau commentaire de {author.name} sur le topic : {BOLD}{t.title}{BOLD}")
say(url_for('forum_topic', f=f, page=(t, "fin"), _anchor=str(c.id), _external=True))
# Redirect to empty the form
return redirect(url_for('forum_topic', f=f, page=(t, "fin"),
_anchor=str(c.id)))
_anchor=c.id))
# Update views
t.views += 1

View File

@ -5,7 +5,7 @@ from app.utils.render import render
@app.route('/')
def index():
return render('index.html', styles=["+css/homepage.css"])
return render('index.html')
@app.errorhandler(404)

View File

@ -34,8 +34,7 @@ def poll_vote(poll_id):
db.session.add(answer)
db.session.commit()
flash('Le vote a été pris en compte', 'ok')
app.v5logger.info(f"<{current_user.name}> has voted on the poll #{poll.id}")
flash('Le vote a été pris en compte', 'info')
if request.referrer:
return redirect(request.referrer)

View File

@ -9,7 +9,6 @@ from app.models.topic import Topic
from app.models.user import Member
from app.utils.render import render
from app.utils.check_csrf import check_csrf
from app.utils.priv_required import priv_required
from app.forms.forum import CommentEditForm, AnonymousCommentEditForm, TopicEditForm
from app.forms.post import MovePost, SearchThread
from wtforms import BooleanField
@ -25,9 +24,9 @@ def edit_post(postid):
referrer = urlparse(request.args.get('r', default = '/', type = str)).path
print(referrer)
p = Post.query.get_or_404(postid)
p = Post.query.filter_by(id=postid).first_or_404()
# Check permissions
# Check permissions. TODO: Allow guests to edit their posts?
if not current_user.can_edit_post(p):
abort(403)
@ -69,7 +68,6 @@ def edit_post(postid):
attachments.append((a, file))
db.session.add(a)
comment.touch()
db.session.add(comment)
if isinstance(p, Topic):
@ -84,10 +82,6 @@ def edit_post(postid):
for a, file in attachments:
a.set_file(file)
flash('Modifications enregistrées', 'ok')
admin_msg = "[admin] " if current_user != p.author else ""
app.v5logger.info(f"{admin_msg}<{current_user.name}> has edited the post #{p.id}")
# Determine topic URL now, in case forum was changed
if isinstance(p, Topic):
return redirect(url_for('forum_topic', f=p.forum, page=(p,1)))
@ -109,16 +103,12 @@ def edit_post(postid):
@check_csrf
def delete_post(postid):
next_page = request.referrer
p = Post.query.get_or_404(postid)
p = Post.query.filter_by(id=postid).first_or_404()
xp = -1
if not current_user.can_delete_post(p):
abort(403)
# Is a penalty deletion
is_penalty = request.args.get('penalty') == 'True' \
and current_user.priv('delete.posts')
# Users who need to have their trophies updated
authors = set()
@ -134,21 +124,16 @@ def delete_post(postid):
authors.add(comment.author)
if isinstance(p.author, Member):
factor = 3 if is_penalty else 1
factor = 3 if request.args.get('penalty') == 'True' else 1
p.author.add_xp(xp * factor)
db.session.merge(p.author)
authors.add(p.author)
admin_msg = "[admin] " if current_user != p.author else ""
p.delete()
db.session.commit()
for author in authors:
author.update_trophies("new-post")
flash("Le contenu a été supprimé", 'ok')
penalty_msg = " (with penalty)" if is_penalty else ""
app.v5logger.info(f"{admin_msg}<{current_user.name}> has deleted the post #{p.id}{penalty_msg}")
return redirect(next_page)
@ -156,15 +141,12 @@ def delete_post(postid):
@login_required
@check_csrf
def set_post_topcomment(postid):
comment = Post.query.get_or_404(postid)
comment = Post.query.filter_by(id=postid).first_or_404()
if current_user.can_set_topcomment(comment):
comment.thread.top_comment = comment
db.session.add(comment.thread)
db.session.commit()
flash("Le post a été défini comme nouvel en-tête", 'ok')
admin_msg = "[admin] " if current_user != comment.author else ""
app.v5logger.info(f"{admin_msg}<{current_user.name}> has set a new top comment on thread #{comment.thread.id}")
return redirect(request.referrer)
@ -172,7 +154,7 @@ def set_post_topcomment(postid):
@app.route('/post/deplacer/<int:postid>', methods=['GET', 'POST'])
@login_required
def move_post(postid):
comment = Post.query.get_or_404(postid)
comment = Post.query.filter_by(id=postid).first_or_404()
if not current_user.can_edit_post(comment):
abort(403)
@ -183,9 +165,7 @@ def move_post(postid):
move_form = MovePost(prefix="move_")
search_form = SearchThread(prefix="thread_")
# There is a bug with validate_on_submit
keyword = search_form.name.data if search_form.search.data else ""
keyword = search_form.name.data if search_form.validate_on_submit() else ""
# Get 10 last corresponding threads
# TODO: add support for every MainPost
@ -207,34 +187,7 @@ def move_post(postid):
comment.thread = thread
db.session.add(comment)
db.session.commit()
flash("Le topic a été déplacé", 'ok')
admin_msg = "[admin] " if current_user != comment.author else ""
app.v5logger.info(f"{admin_msg}<{current_user.name}> has moved the comment #{comment.id} to thread #{thread.id}")
return redirect(url_for('forum_topic', f=t.forum, page=(t,1)))
return render('post/move_post.html', comment=comment,
search_form=search_form, move_form=move_form)
@app.route('/post/verrouiller/<int:postid>', methods=['GET'])
@priv_required("lock.threads")
@check_csrf
def lock_thread(postid):
post = Post.query.get_or_404(postid)
if not post.is_metacontent:
flash("Vous ne pouvez pas verrouiller ce contenu (n'est pas de type metacontenu)", 'error')
abort(403)
post.thread.locked = not post.thread.locked
db.session.add(post.thread)
db.session.commit()
if post.thread.locked:
flash(f"Le thread a été verrouillé", 'ok')
app.v5logger.info(f"[admin] <{current_user.name}> has locked the thread #{post.thread.id}")
else:
flash(f"Le thread a été déverrouillé", 'ok')
app.v5logger.info(f"[admin] <{current_user.name}> has unlocked the thread #{post.thread.id}")
return redirect(request.referrer)

View File

@ -4,5 +4,5 @@ from app.utils.render import render
@app.route('/programmes')
def program_index():
programs = Program.query.order_by(Program.date_created.desc()).all()
return render('/programs/index.html', programs=programs)
programs = Program.query.all()
return render('/programs/index.html')

View File

@ -1,58 +0,0 @@
from app import app, db
from app.models.user import Guest
from app.models.program import Program
from app.models.comment import Comment
from app.models.thread import Thread
from app.utils.render import render
from app.utils.glados import say, BOLD
from app.forms.forum import CommentForm, AnonymousCommentForm
from config import V5Config
from flask_login import current_user
from flask import redirect, url_for, flash
@app.route('/programmes/<programpage:page>', methods=['GET','POST'])
def program_view(page):
p, page = page
if current_user.is_authenticated:
form = CommentForm()
else:
form = AnonymousCommentForm()
if form.validate_on_submit() and not p.thread.locked and (
V5Config.ENABLE_GUEST_POST or current_user.is_authenticated):
# Manage author
if current_user.is_authenticated:
author = current_user
else:
author = Guest(form.pseudo.data)
db.session.add(author)
# Create comment
c = Comment(author, form.message.data, p.thread)
db.session.add(c)
db.session.commit()
c.create_attachments(form.attachments.data)
# Update member's xp and trophies
if current_user.is_authenticated:
current_user.add_xp(1)
current_user.update_trophies('new-post')
flash('Message envoyé', 'ok')
app.v5logger.info(f"<{c.author.name}> has posted a the comment #{c.id}")
say(f"Nouveau commentaire de {author.name} sur le programme : {BOLD}{p.name}{BOLD}")
say(url_for('program_view', page=(p, "fin"), _anchor=str(c.id), _external=True))
# Redirect to empty the form
return redirect(url_for('program_view', page=(p, "fin"), _anchor=str(c.id)))
if page == -1:
page = (p.thread.comments.count() - 1) // Thread.COMMENTS_PER_PAGE + 1
comments = p.thread.comments.order_by(Comment.date_created.asc()) \
.paginate(page, Thread.COMMENTS_PER_PAGE, True)
return render('/programs/program.html', p=p, form=form, comments=comments)

View File

@ -1,64 +0,0 @@
from app import app, db
from app.models.program import Program
from app.models.thread import Thread
from app.models.comment import Comment
from app.models.tag import Tag
from app.models.attachment import Attachment
from app.utils.render import render
from app.utils.glados import say, BOLD
from app.forms.programs import ProgramCreationForm
from flask_login import login_required, current_user
from flask import redirect, url_for, flash
@app.route('/programmes/soumettre', methods=['GET', 'POST'])
@login_required
def program_submit():
form = ProgramCreationForm()
if form.validate_on_submit():
# First create a new thread
# TODO: Reuse a thread when performing topic promotion
th = Thread()
db.session.add(th)
db.session.commit()
# Create its top comment
c = Comment(current_user, form.message.data, th)
db.session.add(c)
db.session.commit()
th.set_top_comment(c)
db.session.merge(th)
# Then build the actual program
p = Program(current_user, form.name.data, th)
db.session.add(p)
db.session.commit()
# Add tags
for tag in form.tags.selected_tags():
db.session.add(Tag(p, tag))
db.session.commit()
# Manage files
attachments = []
for file in form.attachments.data:
if file.filename != "":
a = Attachment(file, c)
attachments.append((a, file))
db.session.add(a)
db.session.commit()
for a, file in attachments:
a.set_file(file)
current_user.add_xp(20)
current_user.update_trophies('new-program')
flash('Le programme a bien été soumis', 'ok')
app.v5logger.info(f"<{p.author.name}> has submitted the program #{c.id}")
say(f"Nouveau programme de {current_user.name} : {BOLD}{p.name}{BOLD}")
say(url_for('program_view', page=(p, 1), _external=True))
return redirect(url_for('program_view', page=(p, 1)))
return render('/programs/submit.html', form=form)

View File

@ -1,3 +0,0 @@
#flDebug * {
overflow: auto !important;
}

View File

@ -1,88 +0,0 @@
.editor .btn-group {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
align-items: center;
}
.editor .btn-group #filler {
flex-grow: 1;
}
.editor .btn-group button {
padding: 6px;
background-color: var(--background);
}
.editor .btn-group button:hover {
background: var(--background-hover);
}
.editor .btn-group button > svg {
width: 20px;
height: 20px;
}
.editor .btn-group button > svg > path,
.editor .btn-group button > svg > rect {
fill: var(--icons);
}
.editor .btn-group button,
.editor .btn-group .separator {
margin: 0 8px 8px 0;
height: 32px;
position: relative;
}
.editor .btn-group > a {
margin: 0 0 8px 0;
}
.editor .btn-group .separator {
display: inline-block;
width: 0;
border: 1px solid var(--text);
color: transparent;
text-indent: -10px;
}
.editor textarea {
min-height: 15rem;
}
.editor #editor_content_preview {
padding: 10px;
margin-top: 5px;
border: var(--border);
background-color: rgba(0,0,0,0.2);
}
.editor .modal {
position: absolute;
left: 0px;
width: auto;
min-width: min-content;
text-align: left;
right: inherit;
background: var(--background-hover);
border: var(--border);
color: var(--text);
padding: .2rem;
top: 2.3rem;
z-index: 100;
list-style-position: initial;
list-style-type: none;
}
.editor .modal > div {
margin: 0.8rem;
margin-top: 0.4rem;
margin-bottom: 1rem;
min-width: 30vw;
}
.editor .modal > div label {
margin-top: 0.4rem;
}
.editor .modal a.editor-emoji-close-btn {
display: inline-block;
margin: 0.3rem;
margin-top: 0.5rem;
}
@media screen and (max-width:849px) {
.editor .modal {
width: 80vw;
position: fixed;
left: 50vw;
transform: translateX(-50%);
top: 50vh;
}
}

View File

@ -8,7 +8,7 @@
.form form label + .desc {
margin: 0 0 4px 0;
font-size: 80%;
opacity: .65;
opacity: .75;
}
.form form .avatar {
width: 128px;
@ -23,7 +23,6 @@
.form input[type='date'],
.form input[type='password'],
.form input[type='search'],
.form input[type='url'],
.form textarea,
.form select {
display: block;
@ -39,7 +38,6 @@
.form input[type='date']:focus,
.form input[type='password']:focus,
.form input[type='search']:focus,
.form input[type='url']:focus,
.form textarea:focus,
.form select:focus {
border-color: var(--border-focused);
@ -50,7 +48,6 @@
.form input[type='date']:focus-within,
.form input[type='password']:focus-within,
.form input[type='search']:focus-within,
.form input[type='url']:focus-within,
.form textarea:focus-within,
.form select:focus-within {
outline: none;
@ -87,27 +84,18 @@
.form progress.entropy.high::-webkit-progress-bar {
background: var(--ok);
}
.form hr {
height: 3px;
border: var(--hr-border);
border-width: 1px 0;
margin: 24px 0;
}
.form .msgerror {
color: var(--error);
font-weight: 400;
margin-top: 5px;
}
.form input[type='email'].abfield {
display: none;
}
form .dynamic-tag-selector {
display: none;
}
form .dynamic-tag-selector input[type="text"] {
display: none;
}
form .dynamic-tag-selector .tag {
cursor: pointer;
}
form .dynamic-tag-selector .tags-selected {
margin: 0 0 4px 0;
}
form .dynamic-tag-selector .tags-selected .tag {
.form input.abfield[type="email"] {
display: none;
}
.form.filter {
@ -137,4 +125,4 @@ form .dynamic-tag-selector .tags-selected .tag {
background: rgba(0,0,0,.05);
padding: 1px 2px;
border-radius: 2px;
}
}

View File

@ -48,12 +48,6 @@ a:focus {
img.pixelated {
image-rendering: pixelated;
}
hr {
height: 3px;
border: var(--hr-border);
border-width: 1px 0;
margin: 24px 0;
}
section p {
line-height: 20px;
word-wrap: anywhere;
@ -78,13 +72,6 @@ section h2 {
color: var(--text-light);
padding-bottom: 2px;
}
section blockquote {
margin: 0 0 10px 0;
border-left: 3px solid var(--border);
background: var(--background);
padding-left: 15px;
}
button,
.button,
input[type="button"],
input[type="submit"] {
@ -95,11 +82,9 @@ input[type="submit"] {
font-weight: 400;
border: 0;
}
button:hover,
.button:hover,
input[type="button"]:hover,
input[type="submit"]:hover,
button:focus,
.button:focus,
input[type="button"]:focus,
input[type="submit"]:focus {
@ -163,7 +148,6 @@ input[type="submit"]:focus {
background: var(--links);
color: var(--warn-text);
border-radius: 1px;
overflow: hidden;
}
.skip-to-content-link:focus {
transform: translateY(0%);

View File

@ -1,100 +1,135 @@
.home-pinned-content {
width: 90%;
display: grid;
grid-template-areas: 'banner news''welcome news''shout news''projects projects';
grid-template-rows: auto auto minmax(200px,1fr)auto;
grid-template-columns: 4fr 3fr;
.home-title {
margin: 20px 0;
padding: 10px 5%;
background: #bf1c11;
box-shadow: 0 2px 2px rgba(0,0,0,.3);
border-top: 10px solid #ab170c;
}
.home-pinned-content > * {
margin: 10px 20px;
.home-title h1 {
margin-top: 0;
color: #ffffff;
border-color: #ffffff;
}
.home-pinned-content > * h1 {
font-size: 18px;
}
@media screen and (max-width:1449px) {
.home-pinned-content {
width: 97%;
}
}
@media screen and (max-width:1199px) {
.home-pinned-content {
width: 100%;
grid-template-areas: 'welcome''banner''news''shout''projects';
grid-template-rows: auto;
grid-template-columns: 1fr;
}
}
.home-banner {
grid-area: banner;
text-align: center;
}
.home-banner img {
max-width: 100%;
}
.home-welcome {
grid-area: welcome;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.home-welcome h1 {
width: 100%;
.home-title p {
margin-bottom: 0;
text-align: justify;
color: #ffffff;
}
.home-welcome ul {
padding-left: 20px;
.home-title a {
color: inherit;
text-decoration: underline;
}
.home-welcome div {
flex-grow: 1;
}
.home-welcome h2 {
margin: 5px 0;
}
.home-news {
grid-area: news;
}
.home-news ul {
padding: 0;
}
.home-news li {
.home-pinned-content > div {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
padding: 10px 0;
border-bottom: var(--hr-border);
justify-content: space-between;
}
.home-news li > a {
align-self: baseline;
.home-pinned-content h2 {
display: block;
margin: 5px 0;
font-size: 18px;
font-family: NotoSans;
font-weight: 200;
line-height: 20px;
}
.home-news li img {
max-width: 100px;
max-height: 100px;
margin-right: 8px;
.home-pinned-content a {
display: block;
}
.home-news li h3 {
.home-pinned-content a:hover img,
.home-pinned-content a:focus img {
filter: blur(3px);
}
.home-pinned-content a:hover div,
.home-pinned-content a:focus div {
padding: 200px 5% 10px 5%;
background-image: linear-gradient(180deg,transparent 0,rgba(0,0,0,.7)40px,rgba(0,0,0,.8));
}
.home-pinned-content img {
width: 100%;
filter: blur(0px);
}
.home-pinned-content article {
flex-grow: 1;
margin: 0 1px;
padding: 0;
position: relative;
max-width: 250px;
overflow: hidden;
}
.home-pinned-content article div {
position: absolute;
bottom: 0;
z-index: 3;
width: 90%;
margin: 0;
padding: 30px 5% 10px 5%;
color: #ffffff;
text-shadow: 1px 1px 0 rgba(0,0,0,.6);
background-image: linear-gradient(180deg,transparent 0,rgba(0,0,0,.7)40px,rgba(0,0,0,.8));
}
.home-articles {
display: flex;
justify-content: space-between;
}
.home-articles > div {
flex-grow: 1;
max-width: 48%;
}
.home-articles h1 {
display: flex;
justify-content: space-between;
align-items: center;
}
.home-articles h1 a {
padding: 0;
font-family: NotoSans;
font-size: 16px;
font-weight: bold;
font-family: Cantarell;
font-weight: 400;
color: #234d5f;
}
.home-news li .date {
margin: 4px 0 10px 0;
.home-articles h1 a:hover,
.home-articles h1 a:focus {
padding-right: 10px;
}
.home-news li div {
font-size: 13px;
line-height: 150%;
.home-articles p {
margin: 5px 0;
text-align: justify;
color: #808080;
}
@media screen and (max-width:499px) {
.home-news li {
flex-direction: column;
align-items: start;
}
.home-articles article {
padding: 10px;
margin: 10px 0;
display: flex;
align-items: center;
background: #ffffff;
border: 1px solid rgba(0,0,0,.2);
}
.home-shoutbox {
grid-area: shout;
.home-articles article > img {
float: left;
margin-right: 10px;
flex-shrink: 0;
}
.home-projects {
grid-area: projects;
.home-articles article > img.screeshot {
width: 128px;
height: 64px;
}
.home-articles article > div {
flex-shrink: 1;
}
.home-articles article h3 {
margin: 0;
color: #424242;
font-weight: normal;
}
.home-articles article a:hover,
.home-articles article a:focus {
text-decoration: underline;
}
.home-articles .metadata {
margin: 0;
color: #22292c;
}
.home-articles .metadata a {
color: #22292c;
font-weight: 400;
font-style: italic;
}

View File

@ -1,22 +0,0 @@
#program-banner {
background: navy;
height: 144px;
margin: 0 0 32px 0;
}
section .program-infos {
display: flex;
width: 100%;
justify-content: space-between;
}
section .program-infos span.progrank {
border-width: 0 0 1px 0;
border-color: var(--color);
border-style: dotted;
}
section .program-infos > div {
flex-shrink: 0;
margin: 0 8px;
}
section .program-infos div.program-tags {
flex-shrink: 1;
}

View File

@ -0,0 +1,71 @@
/* SimpleMDE overwrite that allows us to customize from themes */
div.editor-toolbar {
border-color: var(--border);
}
div.editor-toolbar > a {
color: var(--text) !important;
}
div.editor-toolbar > a.active,
div.editor-toolbar > a:hover {
background: var(--background-light);
border-color: var(--background-light);
}
div.editor-toolbar > i.separator {
border-right-color: transparent;
border-left-color: var(--separator);
}
div.editor-toolbar.disabled-for-preview a:not(.no-disable) {
background: none;
color: var(--text-disabled) !important;
}
div.editor-toolbar.disabled-for-preview > i.separator {
border-left-color: var(--text-disabled);
}
div.CodeMirror,
div.editor-preview {
background: var(--background);
color: var(--text);
border-color: var(--border);
}
div.editor-preview {
background: var(--background-preview);
}
div.editor-preview table th,
div.editor-preview-side table th,
div.editor-preview table td,
div.editor-preview-side table td {
border: inherit;
padding: inherit;
}
div.editor-preview table.codehilitetable pre,
div.editor-preview-side table.codehilitetable pre {
background: transparent;
}
div.CodeMirror .CodeMirror-selected,
div.CodeMirror .CodeMirror-selectedtext {
background: var(--background-light);
}
div.CodeMirror .CodeMirror-focused .CodeMirror-selected,
div.CodeMirror .CodeMirror-focused .CodeMirror-selectedtext,
div.CodeMirror .CodeMirror-line::selection,
div.CodeMirror .CodeMirror-line > span::selection,
div.CodeMirror .CodeMirror-line > span > span::selection {
background: var(--background-light);
}
div.CodeMirror .CodeMirror-line::-moz-selection,
div.CodeMirror .CodeMirror-line > span::-moz-selection,
div.CodeMirror .CodeMirror-line > span > span::-moz-selection {
background: var(--background-light);
}
div.CodeMirror-cursor {
border-color: var(--text);
}

7
app/static/css/simplemde.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -111,6 +111,8 @@ table.thread:not(.topcomment) div.info {
table.thread div.info {
text-align: right;
position: relative;
margin-left: 24px;
margin-bottom: 8px;
}
table.thread div.info > * {
display: inline-block;

View File

@ -43,12 +43,10 @@
}
.editor button {
--background: #1d2326;
--text: #ffffff;
--background-hover: #262c2f;
}
.editor svg {
--icons: #eeeeee;
--background: #ffffff;
--text: #000000;
--border: 1px solid rgba(0, 0, 0, 0);
--border-focused: 1px solid rgba(0, 0, 0, .5);
}
#light-menu {
@ -130,11 +128,6 @@ table.codehilitetable {
--background: #263238;
}
blockquote {
--border: rgba(255, 255, 255, .3);
--background: transparent;
}
div.editor-toolbar, div.CodeMirror {
--border: #404040;
--background-light: #404040;
@ -155,23 +148,3 @@ div.editor-toolbar, div.CodeMirror {
--border: rgba(255, 255, 255, 0.8);
--selected: rgba(255, 0, 0, 1.0);
}
.tag {
--background: #22292c;
--color: white;
}
.tag.tag-calc {
--background: #917e1a;
}
.tag.tag-lang {
--background: #4a8033;
}
.tag.tag-games {
--background: #488695;
}
.tag.tag-tools {
--background: #70538a;
}
.tag.tag-courses {
--background: #884646;
}

View File

@ -38,9 +38,10 @@
}
.editor button {
--background: #eee;
--background: #ffffff;
--text: #030303;
--background-hover: #ddd;
--border: 1px solid rgba(0, 0, 0, 0);
--border-focused: 1px solid rgba(0, 0, 0, .5);
}
#light-menu {
@ -145,11 +146,6 @@ table.thread.topcomment {
border: 1px solid #c0c0c0;
}
blockquote {
--border: rgba(236, 36, 36, .7);
--background: transparent;
}
div.editor-toolbar {
--border: #aaa2a2;
--background-light: #c0c0c0;
@ -176,22 +172,3 @@ div.pagination {
font-size: 14px;
margin: 13px;
}
.tag {
--background: #e0e0e0;
}
.tag.tag-calc {
--background: #f0ca81;
}
.tag.tag-lang {
--background: #aad796;
}
.tag.tag-games {
--background: #a7ccd5;
}
.tag.tag-tools {
--background: #c6aae1;
}
.tag.tag-courses {
--background: #f0a0a0;
}

View File

@ -1,26 +1,26 @@
/* Some colors, variables etc. to be used as theme */
:root {
--background: #fff;
--text: #000;
--text-light: #111;
--background: #ffffff;
--text: #000000;
--text-light: #101010;
--links: #c61a1a;
--ok: #149641;
--ok-text: #fff;
--ok-text: #ffffff;
--ok-active: #0f7331;
--warn: #f59f25;
--warn-text: #fff;
--warn-text: #ffffff;
--warn-active: #ea9720;
--error: #d23a2f;
--error-text: #fff;
--error-text: #ffffff;
--error-active: #b32a20;
--info: #2e7aec;
--info-text: #fff;
--info-text: #ffffff;
--info-active: #215ab0;
--hr-border: 1px solid #d8d8d8;
@ -33,27 +33,23 @@ table tr:nth-child(odd) {
--background: rgba(0, 0, 0, .1);
}
table th {
--background: #eee;
--border: #ddd;
}
blockquote {
--border: rgba(0, 0, 0, .3);
--background: transparent;
--background: #e0e0e0;
--border: #d0d0d0;
}
.form {
--background: #fff;
--text: #000;
--background: #ffffff;
--text: #000000;
--border: 1px solid #c8c8c8;
--border-focused: #7cade0;
--shadow-focused: rgba(87, 143, 228, 0.5);
}
.editor button {
--background: #eee;
--text: #000;
--background-hover: #ddd;
--background: #ffffff;
--text: #000000;
--border: 1px solid rgba(0, 0, 0, 0);
--border-focused: 1px solid rgba(0, 0, 0, .5);
}
#light-menu {
@ -86,13 +82,13 @@ header {
footer {
--background: #ffffff;
--text: #aaa;
--border: #ddd;
--text: #a0a0a0;
--border: #d0d0d0;
}
.flash {
--background: #fff;
--text: #222;
--background: #ffffff;
--text: #212121;
--shadow: 0 1px 12px rgba(0, 0, 0, 0.3);
/* Uncomment to inherit :root values
@ -102,27 +98,31 @@ footer {
--info: #2e7aec; */
--btn-bg: rgba(0, 0, 0, 0);
--btn-text: #000;
--btn-text: #000000;
--btn-bg-active: rgba(0, 0, 0, .15);
}
.profile-xp {
--background: #eee;
--border: 1px solid #ccc;
--background-xp: #f55;
--background-xp-100: #d33;
--border-xp: 1px solid #d33;
--background: #e0e0e0;
--border: 1px solid #c0c0c0;
--background-xp: #f85555;
--background-xp-100: #d03333;
--border-xp: 1px solid #d03333;
}
.context-menu {
--background: #fff;
--shadow: 0 0 12px -9px #000;
--border: #ddd;
--background-light: #fff;
--background: #ffffff;
--shadow: 0 0 12px -9px #000000;
--border: #d0d0d0;
--background-light: #f0f0f0;
}
.editor svg {
--icons: #000;
div.editor-toolbar, div.CodeMirror {
--border: #c0c0c0;
--background-light: #d9d9d9;
--background-preview: #f4f4f6;
--separator: #a0a0a0;
--text-disabled: #c0c0c0;
}
.dl-button {
@ -140,24 +140,5 @@ footer {
/* Extra style on top of the Pygments style */
table.codehilitetable td.linenos {
color: #888;
}
.tag {
--background: #e0e0e0;
}
.tag.tag-calc {
--background: #f0ca81;
}
.tag.tag-lang {
--background: #aad796;
}
.tag.tag-games {
--background: #a7ccd5;
}
.tag.tag-tools {
--background: #c6aae1;
}
.tag.tag-courses {
--background: #f0a0a0;
color: #808080;
}

View File

@ -1 +0,0 @@
../../../submodules/v5shoutbox/style.css

View File

@ -191,6 +191,10 @@ hr.signature {
.gallery-js video.selected {
box-shadow: 0 0 7.5px var(--selected);
}
.gallery-js video::-webkit-media-controls-panel {
display: flex !important;
opacity: 1 !important;
}
@media screen and (max-width:1199px) {
.gallery-js {
height: 150px;
@ -207,19 +211,4 @@ hr.signature {
}
.gallery-spot * {
cursor: pointer;
}
.tag {
display: inline-block;
background: var(--background);
color: var(--color);
padding: 4px 12px;
margin: 4px 0;
border-radius: 8px;
border-radius: calc(4.5em);
user-select: none;
cursor: default;
}
.locked {
text-align: center;
font-style: italic;
}

View File

@ -1,4 +0,0 @@
/* Some styles to enhance debugger */
#flDebug * {
overflow: auto !important;
}

View File

@ -1,105 +0,0 @@
@import "vars";
.editor {
.btn-group {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
align-items: center;
#filler {
flex-grow: 1;
}
button {
/* This centers the 20x20 SVG in the button */
padding: 6px;
background-color: var(--background);
&:hover {
background: var(--background-hover);
}
& > svg {
width: 20px;
height: 20px;
& > path, & > rect {
fill: var(--icons);
}
}
}
button, .separator {
margin: 0 8px 8px 0;
height: 32px;
position: relative;
}
& > a {
margin: 0 0 8px 0;
}
.separator {
display: inline-block;
width: 0;
border: 1px solid var(--text);
color: transparent;
text-indent: -10px;
}
}
textarea {
min-height: 15rem;
}
#editor_content_preview {
padding: 10px;
margin-top: 5px;
border: var(--border);
background-color: rgba(0, 0, 0, 0.2);
}
.modal {
position: absolute;
left: 0px;
width: auto;
min-width: min-content;
text-align: left;
right: inherit;
@media screen and (max-width: @tiny) {
width: 80vw;
position: fixed;
left: 50vw;
transform: translateX(-50%);
top: 50vh;
}
background: var(--background-hover);
border: var(--border);
color: var(--text);
padding: .2rem;
top: 2.3rem;
z-index: 100;
list-style-position: initial;
list-style-type: none;
& > div {
margin: 0.8rem;
margin-top: 0.4rem;
margin-bottom: 1rem;
min-width: 30vw;
label {
margin-top: 0.4rem;
}
}
a.editor-emoji-close-btn {
display: inline-block;
margin: 0.3rem;
margin-top: 0.5rem;
}
}
}

View File

@ -13,7 +13,7 @@
& + .desc {
margin: 0 0 4px 0;
font-size: 80%;
opacity: .65;
opacity: .75;
}
}
@ -32,7 +32,6 @@
input[type='date'],
input[type='password'],
input[type='search'],
input[type='url'],
textarea,
select {
display: block;
@ -96,6 +95,13 @@
}
}
hr {
height: 3px;
border: var(--hr-border);
border-width: 1px 0;
margin: 24px 0;
}
.msgerror {
color: var(--error);
font-weight: 400;
@ -103,35 +109,12 @@
}
/* anti-bots field */
input[type='email'].abfield {
.abfield {
display: none;
}
}
/* Interactive tag selector */
form .dynamic-tag-selector {
display: none;
input[type="text"] {
display: none;
}
.tag {
cursor: pointer;
}
.tags-selected {
margin: 0 0 4px 0;
.tag {
display: none;
}
}
}
/* Interactive filter forms */
.form.filter {

View File

@ -44,13 +44,6 @@ img.pixelated {
image-rendering: pixelated;
}
hr {
height: 3px;
border: var(--hr-border);
border-width: 1px 0;
margin: 24px 0;
}
section {
p {
line-height: 20px;
@ -77,17 +70,10 @@ section {
color: var(--text-light);
padding-bottom: 2px;
}
blockquote {
margin: 0 0 10px 0;
border-left: 3px solid var(--border);
background: var(--background);
padding-left: 15px;
}
}
/* Buttons */
button, .button, input[type="button"], input[type="submit"] {
.button, input[type="button"], input[type="submit"] {
padding: 6px 10px; border-radius: 2px;
cursor: pointer;
font-family: 'DejaVu Sans', sans-serif; font-weight: 400;
@ -161,7 +147,6 @@ button, .button, input[type="button"], input[type="submit"] {
background: var(--links);
color: var(--warn-text);
border-radius: 1px;
overflow: hidden;
&:focus {
transform: translateY(0%);

View File

@ -1,131 +1,140 @@
@import "vars";
/*
home-title
*/
.home-pinned-content {
width: 90%;
display: grid;
grid-template-areas:
'banner news'
'welcome news'
'shout news'
'projects projects';
grid-template-rows: auto auto minmax(200px, 1fr) auto;
grid-template-columns: 4fr 3fr;
@media screen and (max-width: @normal) {
width: 97%;
}
@media screen and (max-width: @small) {
width: 100%;
grid-template-areas:
'welcome'
'banner'
'news'
'shout'
'projects';
grid-template-rows: auto;
grid-template-columns: 1fr;
}
& > * {
//border: 1px solid red;
margin: 10px 20px;
h1 {
font-size: 18px;
}
}
}
.home-banner {
grid-area: banner;
text-align: center;
img {
max-width: 100%;
}
}
.home-welcome {
grid-area: welcome;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
.home-title {
margin: 20px 0; padding: 10px 5%;
background: #bf1c11; box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
border-top: 10px solid #ab170c;
h1 {
width: 100%;
margin-bottom: 0;
margin-top: 0;
color: #ffffff; border-color: #ffffff;
}
ul {
padding-left: 20px;
p {
margin-bottom: 0; text-align: justify;
color: #ffffff;
}
div {
flex-grow: 1;
a {
color: inherit; text-decoration: underline;
}
}
/*
pinned-content
*/
.home-pinned-content {
& > div {
display: flex; justify-content: space-between;
}
h2 {
margin: 5px 0;
display: block; margin: 5px 0;
font-size: 18px; font-family: NotoSans; font-weight: 200;
line-height: 20px;
}
a {
display: block;
&:hover, &:focus {
img {
filter: blur(3px);
}
div {
padding: 200px 5% 10px 5%;
background-image: linear-gradient(180deg,transparent 0,rgba(0,0,0,.7) 40px,rgba(0,0,0,.8));
}
}
}
img {
width: 100%; filter: blur(0px);
}
article {
flex-grow: 1; margin: 0 1px; padding: 0;
position: relative;
max-width: 250px; overflow: hidden;
div {
position: absolute; bottom: 0; z-index: 3;
width: 90%; margin: 0;
padding: 30px 5% 10px 5%;
color: #ffffff; text-shadow: 1px 1px 0 rgba(0,0,0,.6);
background-image: linear-gradient(180deg,transparent 0,rgba(0,0,0,.7) 40px,rgba(0,0,0,.8));
}
}
}
.home-news {
grid-area: news;
ul {
padding: 0;
/*
home-articles
*/
.home-articles {
display: flex; justify-content: space-between;
& > div {
flex-grow: 1; max-width: 48%;
}
li {
display: flex;
flex-direction: row;
align-items: center;
flex-wrap: nowrap;
padding: 10px 0;
h1 {
display: flex; justify-content: space-between; align-items: center;
border-bottom: var(--hr-border);
a {
padding: 0;
font-family: NotoSans; font-size: 16px;
font-weight: 400; color: /*#015078*/ /*#bf1c11*/ #234d5f;
@media screen and (max-width: @micro) {
flex-direction: column;
align-items: start;
&:hover, &:focus {
padding-right: 10px;
}
}
}
p {
margin: 5px 0;
text-align: justify;
color: #808080;
}
article {
padding: 10px; margin: 10px 0; display: flex; align-items: center;
background: #ffffff; border: 1px solid rgba(0, 0, 0, .2);
& > img {
float: left; margin-right: 10px; flex-shrink: 0;
&.screeshot {
width: 128px; height: 64px;
}
}
& > a {
align-self: baseline;
}
img {
max-width: 100px;
max-height: 100px;
margin-right: 8px;
& > div {
flex-shrink: 1;
}
h3 {
margin: 0;
font-size: 16px;
font-weight: bold;
font-family: Cantarell;
color: #424242; font-weight: normal;
}
.date {
margin: 4px 0 10px 0;
a:hover, a:focus {
text-decoration: underline;
}
div {
font-size: 13px;
line-height: 150%;
}
.metadata {
margin: 0;
color: #22292c;
a {
color: #22292c; font-weight: 400; font-style: italic;
}
}
}
.home-shoutbox {
grid-area: shout;
}
.home-projects {
grid-area: projects;
}

View File

@ -1,26 +0,0 @@
#program-banner {
background: navy; /* debugging */
height: 144px;
margin: 0 0 32px 0;
}
section .program-infos {
display: flex;
width: 100%;
justify-content: space-between;
span.progrank {
border-width: 0 0 1px 0;
border-color: var(--color); /* use text color */
border-style: dotted;
}
& > div {
flex-shrink: 0;
margin: 0 8px;
}
div.program-tags {
flex-shrink: 1;
}
}

View File

@ -229,6 +229,10 @@ hr.signature {
box-shadow: 0 0 @padding/2 var(--selected);
}
}
video::-webkit-media-controls-panel {
display: flex !important;
opacity: 1 !important;
}
}
.gallery-spot {
@ -239,23 +243,3 @@ hr.signature {
cursor: pointer;
}
}
/* Tags */
.tag {
display: inline-block;
background: var(--background);
color: var(--color);
padding: 4px 12px;
margin: 4px 0;
border-radius: 8px;
border-radius: calc(0.5em + 4px);
user-select: none;
cursor: default;
}
/* Thread locked state */
.locked {
text-align: center;
font-style: italic;
}

View File

@ -1,426 +1,113 @@
/* Add callbacks on text formatting buttons */
/* Locate the editor associated to an edition event.
event: Global event emitted by one of the editor buttons
Returns [the div.editor, the button, the textarea] */
function editor_event_source(event)
{
let button = undefined;
let editor = undefined;
function edit(e, type) {
function inline(type, str, repeat, insert) {
// Characters used to format inline blocs
// repeat: if true, add one more char to the longest suite found
// insert: insert <insert> between char and str (before and after)
var chars = {
'bold': '*',
'italic': '/',
'underline': '_',
'strikethrough': '~',
'inline-code': '`',
'h1': '===',
'h2': '---',
'h3': '...',
}
/* Grab the button and the parent editor block. The onclick event itself
usually reports the SVG in the button as the source */
let node = event.target || event.srcElement;
while (node != document.body) {
if (node.tagName == "BUTTON" && !button) {
button = node;
}
if (node.classList.contains("editor") && !editor) {
editor = node;
// Hack to use keybinds
if (!button) {
button = node.firstElementChild.firstElementChild
}
break;
}
node = node.parentNode;
}
if (!button || !editor) return;
if (repeat) {
// Detect longest suite of similar chars
var n = 1; var tmp = 1;
for(var i = 0; i < str.length; i++) {
if(str[i] == chars[type]) tmp++;
else tmp = 1;
n = (tmp > n) ? tmp : n;
}
return chars[type].repeat(n) + insert + str + insert + chars[type].repeat(n);
}
const ta = editor.querySelector(".editor textarea");
return [editor, button, ta];
return chars[type] + insert + str + insert + chars[type];
}
function list(type, str) {
switch(type) {
case 'list-bulleted':
return '* ' + str.replaceAll('\n', '\n* ');
break;
case 'list-numbered':
return '1. ' + str;
break;
}
}
var ta = e.parentNode.parentNode.querySelector('textarea');
var start = ta.selectionStart;
var end = ta.selectionEnd;
switch(type) {
case 'bold':
case 'italic':
case 'underline':
case 'strikethrough':
case 'inline-code':
ta.value = ta.value.substring(0, start)
+ inline(type, ta.value.substring(start, end), true, '')
+ ta.value.substring(end);
break;
case 'h1':
case 'h2':
case 'h3':
ta.value = ta.value.substring(0, start)
+ inline(type, ta.value.substring(start, end), false, ' ')
+ ta.value.substring(end);
break;
case 'list-bulleted':
case 'list-numbered':
ta.value = ta.value.substring(0, start)
+ list(type, ta.value.substring(start, end))
+ ta.value.substring(end);
break;
}
}
/* Replace the range [start:end) with the new contents, and returns the new
interval [start:end) (ie. the range where the contents are now located). */
function editor_replace_range(textarea, start, end, contents)
{
ta.value = ta.value.substring(0, start)
+ contents
+ ta.value.substring(end);
function pre(type, str, multiline) {
return [start, start + contents.length];
}
/* Event handler that inserts specified tokens around the selection.
after token is the same as before if not specified */
function editor_insert_around(event, before="", after=null)
{
const [editor, button, ta] = editor_event_source(event);
ta.focus();
let indexStart = ta.selectionStart;
let indexEnd = ta.selectionEnd;
if (after === null) {
after = before;
}
let [start, end] = editor_replace_range(ta, indexStart, indexEnd,
before + ta.value.substring(indexStart, indexEnd) + after);
/* Restore selection */
if (indexStart != indexEnd) {
ta.selectionStart = start;
ta.selectionEnd = end;
}
else {
ta.selectionStart = ta.selectionEnd = start + before.length;
}
preview();
}
/* Event handler that modifies each line within the selection through a
generic function. */
function editor_act_on_lines(event, fn)
{
const [editor, button, ta] = editor_event_source(event);
ta.focus();
let indexStart = ta.selectionStart;
let indexEnd = ta.selectionEnd;
let firstLineIndex = ta.value.substring(0, indexStart).lastIndexOf('\n');
if (firstLineIndex < 0)
firstLineIndex = 0;
else
firstLineIndex += 1;
let lastLineIndex = ta.value.substring(indexEnd).indexOf('\n');
if (lastLineIndex < 0)
lastLineIndex = ta.value.length;
else
lastLineIndex += indexEnd;
let lines = ta.value.substring(firstLineIndex, lastLineIndex).split('\n');
for(let i = 0; i < lines.length; i++)
lines[i] = fn(lines[i], i);
let [start, end] = editor_replace_range(ta, firstLineIndex, lastLineIndex,
lines.join('\n'));
ta.selectionStart = start;
ta.selectionEnd = end;
preview();
}
function editor_clear_modals(event, close = true)
{
// Stop the propagation of the event
event.stopPropagation()
// Reset all modal inputs
document.getElementById('media-alt-input').value = '';
document.getElementById('media-link-input').value = '';
document.getElementById('link-desc-input').value = '';
document.getElementById('link-link-input').value = '';
const media_type = document.getElementsByName("media-type");
for(i = 0; i < media_type.length; i++) {
media_type[i].checked = false;
}
// Close all modal if requested
if (!close) { return }
const modals = document.getElementsByClassName('modal');
for (const i of modals) {i.style.display = 'none'};
}
/* End-user functions */
function editor_inline(event, type, enable_preview = true)
{
tokens = {
bold: "**",
italic: "*",
underline: "__",
strike: "~~",
inlinecode: "`",
};
if (type in tokens) {
editor_insert_around(event, tokens[type]);
}
if (enable_preview) {
preview();
}
function bold(e) {
var ta = e.parentNode.parentNode.querySelector('textarea');
var indexStart = ta.selectionStart;
var indexEnd = ta.selectionEnd;
var txt = ta.value.substring(indexStart, indexEnd);
ta.value += '\n' + inline('bold', txt);
}
function editor_display_link_modal(event) {
const [editor, button, ta] = editor_event_source(event);
let indexStart = ta.selectionStart;
let indexEnd = ta.selectionEnd;
let selection = ta.value.substring(indexStart, indexEnd);
// Assuming it's a link
if (selection.match(/^https?:\/\/\S+/)) {
event.currentTarget.querySelector("#link-link-input").value = selection;
}
// Or text
else if (selection != "") {
event.currentTarget.querySelector("#link-desc-input").value = selection;
}
editor_display_child_modal(event);
}
function editor_insert_link(event, link_id, text_id, media = false)
{
const [editor, button, ta] = editor_event_source(event);
ta.focus();
let indexStart = ta.selectionStart;
let indexEnd = ta.selectionEnd;
const link = document.getElementById(link_id).value;
const text = document.getElementById(text_id).value;
let media_type = "";
const media_selector = document.getElementsByName("media-type");
for(i = 0; i < media_selector.length; i++) {
if (media_selector[i].checked) {
media_type = `{type=${media_selector[i].value}}`;
}
}
editor_clear_modals(event);
let [start, end] = editor_replace_range(ta, indexStart, indexEnd,
`${media ? "!" : ""}[${text.length === 0 ? ta.value.substring(indexStart, indexEnd) : text}](${link})${media ? media_type : ""}`);
/* Restore selection */
if (indexStart != indexEnd) {
ta.selectionStart = start;
ta.selectionEnd = end;
}
else {
ta.selectionStart = ta.selectionEnd = start + 1;
}
preview();
}
function editor_title(event, level, diff)
{
editor_act_on_lines(event, function(line, _) {
/* Strip all the initial # (and count them) */
let count = 0;
while(count < line.length && line[count] == '#') count++;
let contents_index = count;
if (count < line.length && line[count] == ' ') contents_index++;
let contents = line.slice(contents_index);
if (level > 0 || count == 1 && diff == -1) {
/* Remove the title if the corresponding level is re-requested */
if (count == level || count == 1 && diff == -1)
return contents;
/* Otherwise, add it */
else
return '#'.repeat(level) + ' ' + contents;
}
else if (count > 0) {
/* Apply the difference */
let new_level = Math.max(1, Math.min(6, count + diff));
return '#'.repeat(new_level) + ' ' + contents;
}
return line;
});
}
function editor_quote(event)
{
editor_act_on_lines(event, function(line, _) {
/* Strip all the initial > (and count them) */
let count = 0;
while(count < line.length && line[count] == '>') count++;
let contents_index = count;
if (count < line.length && line[count] == ' ') contents_index++;
let contents = line.slice(contents_index);
/* Apply the difference */
return '>'.repeat(count + 1) + ' ' + contents;
});
}
function editor_bullet_list(event)
{
editor_act_on_lines(event, function(line, _) {
let ident_match = line.match(/^[\t]+/m) ?? [''];
let ident = ident_match[0];
let count = ident.length;
const contents = line.slice(count);
if ((count < line.length || count == 0) && line[count] != '-') return '- ' + contents;
return ident + "\t" + contents;
});
}
function editor_numbered_list(event)
{
editor_act_on_lines(event, function(line, number) {
let ident_match = line.match(/^[\t]+/m) ?? [''];
let ident = ident_match[0];
let count = ident.length;
const contents = line.slice(count);
if ((count < line.length || count == 0) && isNaN(line[count])) return `${number + 1}. ` + contents;
return ident + "\t" + contents;
});
}
function editor_table(event) {
let table = `| Column 1 | Column 2 | Column 3 |
| -------- | -------- | -------- |
| Text | Text | Text |`;
editor_insert_around(event, "", table);
}
function editor_separator(event) {
editor_insert_around(event, "", "\n---\n");
}
function editor_display_child_modal(event) {
editor_clear_modals(event);
event.currentTarget.children[1].style = {'display': 'block'};
}
const DISABLE_PREVIEW_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye-slash" viewBox="0 0 16 16"><path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/><path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/><path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884-12-12 .708-.708 12 12-.708.708z"/></svg>';
const ENABLE_PREVIEW_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16"><path d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8zM1.173 8a13.133 13.133 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.133 13.133 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5c-2.12 0-3.879-1.168-5.168-2.457A13.134 13.134 0 0 1 1.172 8z"/><path d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5zM4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0z"/></svg>';
function toggle_auto_preview() {
let auto_preview;
if (document.cookie.split(";").some((item) => item.trim().startsWith("auto-preview="))) {
auto_preview = document.cookie.split(";").some((item) => item.includes("auto-preview=true"));
} else {
auto_preview = true;
}
document.cookie = `auto-preview=${!auto_preview}; max-age=31536000; SameSite=Strict; Secure`
if (!auto_preview) {
document.getElementById("toggle_preview").title = "Désactiver la prévisualisation";
document.getElementById("toggle_preview").innerHTML = DISABLE_PREVIEW_ICON;
document.getElementById("manual_preview").style = "display: none";
} else {
document.getElementById("toggle_preview").title = "Activer la prévisualisation";
document.getElementById("toggle_preview").innerHTML = ENABLE_PREVIEW_ICON;
document.getElementById("manual_preview").style = "display: block";
}
}
/* This request the server to get a complete render of the current text in the textarea */
function preview(manual=false) {
// If auto-preview is disabled and the preview is not manually requested by the user
if (document.cookie.split(";").some((item) => item.includes("auto-preview=false")) && !manual) {
return;
}
const previewArea = document.querySelector("#editor_content_preview");
const textarea = document.querySelector(".editor textarea");
const payload = {text: ta.value};
const headers = new Headers();
headers.append("Content-Type", "application/json");
const params = {
method: "POST",
body: JSON.stringify(payload),
headers
};
fetch("/api/markdown", params).then(
(response) => {
response.text().then(
(text) => {
previewArea.innerHTML = text;
}
);
});
}
if (document.cookie.split(";").some((item) => item.trim().startsWith("auto-preview="))) {
if (document.cookie.split(";").some((item) => item.includes("auto-preview=false"))) {
document.getElementById("toggle_preview").title = "Activer la prévisualisation";
document.getElementById("toggle_preview").innerHTML = ENABLE_PREVIEW_ICON;
document.getElementById("manual_preview").style = "display: block";
}
}
let previewTimeout = null;
let ta = document.querySelector(".editor textarea");
// Tab insert some spaces
// Ctrl+Enter send the form
ta = document.querySelector(".editor textarea");
ta.addEventListener('keydown', function(e) {
// Tab insert some spaces
let keyCode = e.keyCode || e.which;
if (keyCode == 9) {
// TODO Add one tab to selected text without replacing it
e.preventDefault();
var keyCode = e.keyCode || e.which;
if (keyCode == 9) {
e.preventDefault();
let start = e.target.selectionStart;
let end = e.target.selectionEnd;
// set textarea value to: text before caret + tab + text after caret
e.target.value = e.target.value.substring(0, start) + "\t" + e.target.value.substring(end);
e.target.selectionEnd = start + 1;
}
/*
* Keybindings for buttons. The default action of the keybinding is prevented.
* Ctrl+B adds bold
* Ctrl+I adds italic
* Ctrl+U adds underline
* Ctrl+S adds strikethrough
* Ctrl+H adds Header +1
* Ctrl+Enter send the form
*/
if (e.ctrlKey) {
switch (keyCode) {
case 13:
let t = e.target;
while(! (t instanceof HTMLFormElement)) {
t = t.parentNode;
}
try {
t.submit();
} catch(exception) {
t.submit.click();
}
e.preventDefault();
break;
case 66: // B
editor_inline(e, "bold", false);
e.preventDefault();
break;
case 72: // H
editor_title(e, 0, +1);
e.preventDefault();
break;
case 73: // I
editor_inline(e, "italic", false);
e.preventDefault();
break;
case 83: // S
editor_inline(e, "strike", false);
e.preventDefault();
break;
case 85: // U
editor_inline(e, "underline", false);
e.preventDefault();
break;
}
}
// Set a timeout for refreshing the preview
if (previewTimeout != null) {
clearTimeout(previewTimeout);
}
previewTimeout = setTimeout(preview, 3000);
});
document.querySelector('emoji-picker').addEventListener('emoji-click', event => {
editor_clear_modals(event);
editor_insert_around(event, "", event.detail.unicode)
preview();
var start = e.target.selectionStart;
var end = e.target.selectionEnd;
// set textarea value to: text before caret + tab + text after caret
e.target.value = e.target.value.substring(0, start) + "\t" + e.target.value.substring(end);
e.target.selectionEnd = start + 1;
}
if (e.ctrlKey && keyCode == 13) {
var e = e.target;
while(! (e instanceof HTMLFormElement)) {
e = e.parentNode;
}
try {
e.submit();
} catch(exception) {
e.submit.click();
}
}
});

View File

@ -1,10 +1,10 @@
function entropy(password) {
let chars = [
var chars = [
"abcdefghijklmnopqrstuvwxyz",
"ABCDFEGHIJKLMNOPQRSTUVWXYZ",
"0123456789",
" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", // OWASP special chars
"áàâéèêíìîóòôúùûçÁÀÂÉÈÊÍÌÎÓÒÔÚÙÛǵ²³" // French layout special chars
"áàâéèêíìîóòôúùûç"
];
used = new Set();
@ -19,9 +19,9 @@ function entropy(password) {
}
function update_entropy(ev) {
let i = document.querySelector(".entropy").previousElementSibling;
let p = document.querySelector(".entropy");
let e = entropy(i.value);
var i = document.querySelector(".entropy").previousElementSibling;
var p = document.querySelector(".entropy");
var e = entropy(i.value);
p.classList.remove('low');
p.classList.remove('medium');

View File

@ -8,7 +8,7 @@ const patterns = [
function* lex(str) {
while(str = str.trim()) {
let t = T.ERR, best = undefined;
var t = T.ERR, best = undefined;
for(const i in patterns) {
const m = str.match(patterns[i]);
@ -86,7 +86,7 @@ class Parser {
return e;
}
let e = {
var e = {
type: "Atom",
field: this.expect(T.NAME),
op: this.expect(T.COMP),
@ -124,8 +124,8 @@ function filter_update(input) {
const th = t.querySelectorAll("tr:first-child > th");
/* Generate the names of fields from the header */
let fields = {};
for(let i = 0; i < th.length; i++) {
var fields = {};
for(var i = 0; i < th.length; i++) {
const name = th[i].dataset.filter;
if(name) fields[name] = i;
}

View File

@ -24,9 +24,42 @@ document.querySelectorAll(".gallery").forEach(item => {
// });
});
document.querySelectorAll(".gallery-js > video").forEach(item => {
// Remove the video controls
item.removeAttribute('controls');
// Generate a thumbnail
item.addEventListener('loadedmetadata', e => {
const item = e.target;
item.currentTime = item.duration * 0.5; // 10% of the video
item.addEventListener('loadeddata', e => {
const v = e.target;
const canvas = document.createElement("canvas");
canvas.width = v.videoWidth;
canvas.height = v.videoHeight;
// draw the video frame to canvas
const ctx = canvas.getContext("2d");
// ctx.drawImage(v, 0, 0, canvas.width, canvas.height);
// draw the play button
// icon = new Path2D("M10,16.5V7.5L16,12M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z");
icon = new Path2D("M10 10 h 80 v 80 h -80 Z");
console.log(icon);
ctx.fill(icon);
// return the canvas image as a blob
ctx.canvas.toBlob(
blob => {
console.log(blob);
v.setAttribute('poster', URL.createObjectURL(blob));
},
"image/png",
0.75 /* quality */
);
});
});
});
document.querySelectorAll(".gallery-js > *").forEach(item => {
// Add some dynamic behavior
item.addEventListener("click", function(e) {
console.log(e.target);
// Manage selected media
if(e.target.classList.contains('selected')) {
e.target.classList.remove('selected');
@ -40,6 +73,8 @@ document.querySelectorAll(".gallery-js > *").forEach(item => {
// Change content of spotlight
let spot = e.target.parentElement.nextElementSibling;
spot.replaceChild(e.target.cloneNode(true), spot.firstElementChild);
spot.firstElementChild.setAttribute('controls', "");
// Open spotlight media in a new tab
spot.firstElementChild.addEventListener("click", function(e) {
window.open(spot.firstElementChild.src, "_blank");

View File

@ -1,13 +1,13 @@
function setCookie(name, value) {
let end = new Date();
var end = new Date();
end.setTime( end.getTime() + 3600 * 1000 );
let str=name+"="+escape(value)+"; expires="+end.toGMTString()+"; path=/; Secure; SameSite=lax";
var str=name+"="+escape(value)+"; expires="+end.toGMTString()+"; path=/; Secure; SameSite=lax";
document.cookie = str;
}
function getCookie(name) {
let debut = document.cookie.indexOf(name);
var debut = document.cookie.indexOf(name);
if( debut == -1 ) return null;
let end = document.cookie.indexOf( ";", debut+name.length+1 );
var end = document.cookie.indexOf( ";", debut+name.length+1 );
if( end == -1 ) end = document.cookie.length;
return unescape( document.cookie.substring( debut+name.length+1, end ) );
}

15
app/static/scripts/simplemde.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,11 @@
/* Smartphone patch for menu */
/* It don't work if links haven't any href attribute */
let w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0)
var w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0)
if(w < 700) {
let buttons = document.getElementById('light-menu').getElementsByTagName('li');
for(let i = 0; i < buttons.length; i++) {
var buttons = document.getElementById('light-menu').getElementsByTagName('li');
for(var i = 0; i < buttons.length; i++) {
buttons[i].getElementsByTagName('a')[0].setAttribute('href', '#');
}
}
}

View File

@ -1,61 +0,0 @@
function tag_selector_find(node) {
while(node != document.body) {
if(node.classList.contains("dynamic-tag-selector"))
return node;
node = node.parentNode;
}
return undefined;
}
function tag_selector_get(ts) {
return ts.querySelector("input").value
.split(",")
.map(str => str.trim())
.filter(str => str !== "");
}
function tag_selector_set(ts, values) {
ts.querySelector("input").value = values.join(", ");
tag_selector_update(ts);
}
function tag_selector_update(ts) {
if(ts === undefined) return;
const input_names = tag_selector_get(ts);
/* Update visibility of selected tags */
ts.querySelectorAll(".tags-selected .tag[data-name]").forEach(tag => {
const visible = input_names.includes(tag.dataset.name);
tag.style.display = visible ? "inline-block" : "none";
});
/* Update visibility of pool tags */
ts.querySelectorAll(".tags-pool .tag[data-name]").forEach(tag => {
const visible = !input_names.includes(tag.dataset.name);
tag.style.display = visible ? "inline-block" : "none";
});
}
function tag_selector_add(ts, id) {
if(ts === undefined) return;
let tags = tag_selector_get(ts);
if(!tags.includes(id))
tags.push(id);
tag_selector_set(ts, tags);
}
function tag_selector_remove(ts, id) {
if(ts === undefined) return;
let tags = tag_selector_get(ts);
tags = tags.filter(str => str !== id);
tag_selector_set(ts, tags);
}
document.querySelectorAll(".dynamic-tag-selector").forEach(ts => {
ts.style.display = "block";
tag_selector_update(ts);
});

View File

@ -1,24 +1,24 @@
/* Trigger actions for the menu */
/* Initialization */
let b = document.querySelectorAll('#light-menu a');
for(let i = 1; i < b.length; i++) {
var b = document.querySelectorAll('#light-menu a');
for(var i = 1; i < b.length; i++) {
b[i].setAttribute('onfocus', "this.setAttribute('f', 'true');");
b[i].setAttribute('onblur', "this.setAttribute('f', 'false');");
b[i].removeAttribute('href');
}
let trigger_menu = function(active) {
let display = function(element) {
var trigger_menu = function(active) {
var display = function(element) {
element.classList.add('opened');
}
let hide = function(element) {
var hide = function(element) {
element.classList.remove('opened');
}
let menu = document.querySelector('#menu');
let buttons = document.querySelectorAll('#light-menu li');
let menus = document.querySelectorAll('#menu > div');
var menu = document.querySelector('#menu');
var buttons = document.querySelectorAll('#light-menu li');
var menus = document.querySelectorAll('#menu > div');
if(active == -1 || buttons[active].classList.contains('opened')) {
hide(menu);
@ -39,14 +39,14 @@ let trigger_menu = function(active) {
}
}
let mouse_trigger = function(event) {
let menu = document.querySelector('#menu');
let buttons = document.querySelectorAll('#light-menu li');
var mouse_trigger = function(event) {
var menu = document.querySelector('#menu');
var buttons = document.querySelectorAll('#light-menu li');
if(!menu.contains(event.target)) {
let active = -1;
var active = -1;
for(let i = 0; i < buttons.length; i++) {
for(i = 0; i < buttons.length; i++) {
if(buttons[i].contains(event.target))
active = i;
buttons[i].querySelector('a').blur();
@ -56,12 +56,12 @@ let mouse_trigger = function(event) {
}
}
let keyboard_trigger = function(event) {
let menu = document.getElementById('menu');
let buttons = document.querySelectorAll('#light-menu li');
var keyboard_trigger = function(event) {
var menu = document.getElementById('menu');
var buttons = document.querySelectorAll('#light-menu li');
if(event.keyCode == 13) {
for(let i = 0; i < buttons.length; i++) {
for(var i = 0; i < buttons.length; i++) {
if(buttons[i].querySelector('a').getAttribute('f') == 'true') {
trigger_menu(i);
}

View File

@ -1 +0,0 @@
../../../submodules/v5shoutbox/v5shoutbox.js

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Gestion du compte" %}
{% block title %}
<h1>Gestion du compte</h1>
{% endblock %}
@ -15,7 +13,7 @@
<div>
{{ form.avatar.label }}
<div>
<img class="avatar" src="{{ current_user.avatar_url }}" />
<img class="avatar" src="{{ url_for('avatar', filename=current_user.avatar) }}" meta="{{ current_user.avatar }}" />
{{ form.avatar }}
</div>
{% for error in form.avatar.errors %}

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Réinitialiser le mot de passe" %}
{% block content %}
<section class="form" style="width:40%;">
<h1>Réinitialiser le mot de passe</h1>

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Suppression du compte" %}
{% block content %}
<section class="form">
<h1>Suppression du compte</h2>

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Connexion" %}
{% block content %}
<section class="form" style="width:40%;">
<h1>Connexion</h1>

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Notifications" %}
{% block title %}
<h1>Notifications</h1>
{% endblock %}

View File

@ -1,8 +1,6 @@
{% extends "base/base.html" %}
{% import "widgets/poll.html" as poll_widget with context %}
{% set tabtitle = "Gestion des sondages" %}
{% block title %}
<h1>Gestion des sondages</h1>
{% endblock %}

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Inscription" %}
{% block content %}
<section class="form" style="width:40%;">
<h1>Inscription</h1>
@ -39,7 +37,7 @@
{% endfor %}
</div>
<div>
<label for="guidelines">J'accepte les <a href="#">CGU</a></label>
{{ form.guidelines.label }}
{{ form.guidelines() }}
{% for error in form.guidelines.errors %}
<span class="msgerror">{{ error }}</span>
@ -48,7 +46,7 @@
<div>
{{ form.newsletter.label }}
{{ form.newsletter() }}
<div class="desc">{{ form.newsletter.description }}</div>
<div style="font-size:80%;color:rgba(0,0,0,.5)">{{ form.newsletter.description }}</div>
{% for error in form.newsletter.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Réinitialiser le mot de passe" %}
{% block content %}
<section class="form" style="width:40%;">
<h1>Réinitialiser le mot de passe</h1>

View File

@ -1,8 +1,6 @@
{% extends "base/base.html" %}
{% import "widgets/user.html" as widget_member %}
{% set tabtitle = "Profil de " + member.name %}
{% block title %}
<h1>Profil de {{ member.name }}</h1>
{% endblock %}

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Inscription réussie" %}
{% block content %}
<section>
<div>

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Administration - Pièces-jointes" %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Pièces jointes</h1>
{% endblock %}

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Administration - Configuration du site" %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Configuration du site</h1>
{% endblock %}

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Administration - Suppression du compte de " + user.name %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Suppression du compte de '{{ user.name }}'</h1>
{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends "base/base.html" %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <a href={{ url_for('adm_trophies') }}>Titres et trophées</a> » <h1>Suppression du trophée '{{ trophy.name }}'</h1>
{% endblock %}
{% block content %}
<section class="form">
<h2>Confirmer la suppression du trophée</h2>
<p>Le trophée '{{ trophy.name }}' que vous allez supprimer est lié à :</p>
<ul>
<li>{{ trophy.owners | length }} membre{{ trophy.owners|length|pluralize }}</li>
</ul>
<form action="{{ url_for('adm_delete_trophy', trophy_id=trophy.id) }}" method=post>
{{ del_form.hidden_tag() }}
<div>
{{ del_form.delete.label }}
{{ del_form.delete(checked=False) }}
<div style="font-size: 80%; color: gray">{{ del_form.delete.description }}</div>
{% for error in del_form.delete.errors %}
<span class=msgerror>{{ error }}</span>
{% endfor %}
</div>
<div>{{ del_form.submit(class_="bg-error") }}</div>
</form>
</section>
{% endblock %}

View File

@ -1,9 +1,7 @@
{% extends "base/base.html" %}
{% set tabtitle = "Administration - Édition du compte de " + user.name %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Édition du compte de {{ user.name }}</h1>
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Édition du compte de {{ user.name }}</h1>
{% endblock %}
{% block content %}
@ -16,7 +14,7 @@
<div>
{{ form.avatar.label }}
<div>
<img class="avatar" src="{{ user.avatar_url }}" />
<img class="avatar" src="{{ url_for('avatar', filename=user.avatar) }}" meta="{{ user.avatar }}" />
{{ form.avatar }}
</div>
{% for error in form.avatar.errors %}

View File

@ -0,0 +1,58 @@
{% extends "base/base.html" %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <a href={{ url_for('adm_trophies') }}>Titres et trophées</a> » <h1>Édition du trophée '{{ trophy.name }}'</h1>
{% endblock %}
{% block content %}
<section class="form">
<form action="{{ url_for('adm_edit_trophy', trophy_id=trophy.id) }}" method="post" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<h2>Éditer le trophée</h2>
<div>
<img src="{{ url_for('static', filename='images/trophies/'+slugify(trophy.name))+'.png' }}" style="vertical-align: middle; margin-right: 8px">
<b>{{ trophy.name }}</b>
</div>
<div>
{{ form.name.label }}
{{ form.name(value=trophy.name) }}
{% for error in form.name.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.desc.label }}
{{ form.desc(value=trophy.description) }}
{% for error in form.desc.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.hidden.label }}
{{ form.hidden(checked=trophy.hidden) }}
<div class=desc>{{ form.hidden.description }}</div>
{% for error in form.hidden.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.title.label }}
{{ form.title() }}
<div class=desc>{{ form.title.description }}</div>
{% for error in form.title.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.css.label }}
<div class=desc>{{ form.css.description }}</div>
{{ form.css(value=trophy.css) }}
{% for error in form.css.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>{{ form.submit(class_="bg-ok") }}</div>
</section>
{% endblock %}

View File

@ -16,8 +16,6 @@
{% endfor %}
{% endmacro %}
{% set tabtitle = "Administration - Forums" %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Forums</h1>
{% endblock %}

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Administration - Groupes et privilèges" %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Groupes et privilèges</h1>
{% endblock %}

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Panneau dadministration" %}
{% block title %}
<h1>Panneau d'administration</h1>
{% endblock %}
@ -12,6 +10,7 @@
<ul>
<li><a href="{{ url_for('adm_groups') }}">Groupes et privilèges</a></li>
<li><a href="{{ url_for('adm_members') }}">Liste des membres</a></li>
<li><a href="{{ url_for('adm_trophies') }}">Titres et trophées</a></li>
<li><a href="{{ url_for('adm_forums') }}">Arbre des forums</a></li>
<li><a href="{{ url_for('adm_polls') }}">Sondages</a></li>
<li><a href="{{ url_for('adm_attachments') }}">Pièces-jointes</a></li>

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Administration - Vandaliser un compte" %}
{% block title %}
<h1>Vandaliser un compte</h1>
{% endblock %}

View File

@ -1,7 +1,5 @@
{% extends "base/base.html" %}
{% set tabtitle = "Administration - Liste des membres" %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Liste des membres</h1>
{% endblock %}

View File

@ -1,10 +1,8 @@
{% extends "base/base.html" %}
{% import "widgets/poll.html" as poll_widget with context %}
{% set tabtitle = "Administration - Gestion des sondages" %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Gestion des sondages</h1>
<h1>Gestion des sondages</h1>
{% endblock %}
{% block content %}

View File

@ -0,0 +1,80 @@
{% extends "base/base.html" %}
{% block title %}
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Titres et trophées</h1>
{% endblock %}
{% block content %}
<section>
<p>Cette page présente une vue d'ensemble des titres et trophées. Les
conditions d'obtention exactes des trophées sont définies dans le code et
non dans la base de données.</p>
<h2>Titres et trophées</h2>
<table style="width:90%; margin: auto;">
<tr><td></td><th>Nom</th><th>Titre</th>
<th>Style</th><th>Modifier</th><th>Supprimer</th></tr>
{% for trophy in trophies %}
<tr><td style="background: white; padding: 0; width: 64px">
<img src="{{ url_for('static', filename='images/trophies/'+slugify(trophy.name))+'.png' }}" alt="{{ trophy.name }}"></td>
<td style="{{ trophy.css }}">{{ trophy.name }}</td>
{% if trophy | is_title %}
<td style="text-align: center; color:green">Oui</td>
{% else %}
<td style="text-align: center; color:red">Non</td>
{% endif %}
<td><code>{{ trophy.css }}</code></td>
<td style="text-align: center"><a href="{{ url_for('adm_edit_trophy', trophy_id=trophy.id) }}">Modifier</a></td>
<td style="text-align: center"><a href="{{ url_for('adm_delete_trophy', trophy_id=trophy.id) }}">Supprimer</a></td>
</tr>
{% endfor %}
</table>
</section>
<section class="form">
<form action="{{ url_for('adm_trophies') }}" method="post" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<h2>Nouveau trophée</h2>
<div>
{{ form.name.label }}
{{ form.name }}
{% for error in form.name.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.desc.label }}
{{ form.desc }}
{% for error in form.desc.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.hidden.label }}
{{ form.hidden }}
<div class=desc>{{ form.hidden.description }}</div>
{% for error in form.hidden.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.title.label }}
{{ form.title }}
<div class=desc>{{ form.title.description }}</div>
{% for error in form.title.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.css.label }}
<div class=desc>{{ form.css.description }}</div>
{{ form.css }}
{% for error in form.css.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>{{ form.submit(class_="bg-ok") }}</div>
</section>
{% endblock %}

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="fr-FR">
{% include "base/head.html" with context %}
{% include "base/head.html" %}
<body>
<div><a class="skip-to-content-link" href="#main-content">Aller directement au contenu</a></div>
{% include "base/navbar.html" %}
@ -14,8 +14,8 @@
{% include "base/flash.html" %}
{% block content %}
<div id="main-content"></div>
{% block content %}
{% endblock %}
{% include "base/footer.html" %}

View File

@ -4,11 +4,5 @@
{% if current_user.is_authenticated and current_user.priv('misc.dev-infos') %}
<p>Page générée en {{ "%.3f" % g.request_time() }} secondes.</p>
{% endif %}
<<<<<<< HEAD
<<<<<<< HEAD
<p>Ceci est un environnement de test. Tout contenu peut être supprimé sans avertissement préalable.</p>
=======
>>>>>>> e15005a... Ajout des stats sur la durée de chargement
=======
>>>>>>> e15005a427f95829bbbad8f0d625ab9cb0c30e69
</footer>

View File

@ -1,5 +1,5 @@
<head>
<title>{{ V5Config.TABTITLE_PREFIX + (tabtitle or "Planète Casio") }}</title>
<title>Planète Casio</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">

View File

@ -9,7 +9,7 @@
</form>
<div id="spotlight">
<a href="/forum/projets/2/fin/avancees-de-la-v5" class="button bg-error">Infos sur l'avancée de la v5</a>
<a href="#" class="button bg-error">Jeu du mois : février 2019</a>
</div>
{% if current_user.is_authenticated and current_user.priv('misc.dev-infos') %}

View File

@ -61,7 +61,7 @@
<li>
{% if current_user.is_authenticated %}
<a href="{{ url_for('user', username=current_user.name) }}" role="button" label="Compte" tabindex="0">
<img src="{{ current_user.avatar_url }}" alt="Avatar de {{ current_user.name }}">
<img src="{{ url_for('avatar', filename=current_user.avatar) }}" alt="Avatar de {{ current_user.name }}">
<div>Compte</div>
</a>
{% else %}

View File

@ -2,7 +2,7 @@
<div>
<h2>
<a href="{{ url_for('user', username=current_user.name) }}">
<img src="{{ current_user.avatar_url }}" alt="Avatar de {{ current_user.name }}"></a>
<img src="{{ url_for('avatar', filename=current_user.avatar) }}" alt="Avatar de {{ current_user.name }}"></a>
<a href="{{ url_for('user', username=current_user.name) }}">
{{ current_user.name }}</a>
</h2>

Some files were not shown because too many files have changed in this diff Show More