Compare commits

..

67 Commits
master ... dev

Author SHA1 Message Date
Eragon 5813de1208
Fix redirection to topic page after comment merge 2024-03-10 19:35:12 +01:00
Eragon 08054175bf
Allow to merge a comment with a comment which was posted later 2024-03-10 19:29:46 +01:00
Eragon 2685ffcbc7
Better display of the original author and author of the new main post 2024-03-09 18:21:08 +01:00
Eragon f9c4d05121
Allow any post to be set as main post #130 2024-03-09 16:15:52 +01:00
Eragon f7e91c4793
Show the thread date and the top comment date if they differ #138 2024-03-09 14:18:12 +01:00
Eragon 1fa5dd8085
Nevermind we don't need the button as a source of the events anyway
Closes #134
2024-03-09 13:59:55 +01:00
Eragon 5283d45c36
Two editors on one page works, but hotkeys no longer work 2024-03-09 13:59:53 +01:00
Eragon e41bfaddff
Load shoutbox UI scripts only when the shoutbox is displayed 2024-03-09 13:58:22 +01:00
Eragon 0b524f19b7
fix: Set tab title for the comparator 2024-03-09 13:58:21 +01:00
Eragon 6af47872cb
fix: Set tab title for the comparator 2024-03-09 13:58:21 +01:00
Eragon cf21b4ee56
feat: Add CalcDB to the tools
see #143
2024-03-09 13:58:21 +01:00
Eragon 39ec4c3c4a
chore: Use PCv5-extra releases instead of submodules 2024-03-09 13:58:21 +01:00
Lephe cddd4baa8a
shoutbox: update chat and show it on homepage 2024-03-09 13:58:21 +01:00
Eragon 47c3b82e1a
accounts: PIL.Image.ANTIALIAS got removed, using LANCZOS for resampling now 2024-03-09 13:58:21 +01:00
Eragon 4a23aee7d9
search: Get publish date selector working 2024-03-09 13:58:21 +01:00
Eragon 0c3fa843d8
search: Add 'Tous' to user choices 2024-03-09 13:58:21 +01:00
Eragon 9c49671b73
search: Use user ordering choice 2024-03-09 13:58:20 +01:00
Eragon 4ba7c10786
search: Add links for programs and programs comments 2024-03-09 13:58:20 +01:00
Eragon fee66d22bd
search: Fix theme on search 2024-03-09 13:58:20 +01:00
Eragon 2d1695e1ee
programs: Fix pagination 2024-03-09 13:58:20 +01:00
Eragon 25f63b4648
search: fix check for topic/programs 2024-03-09 13:58:20 +01:00
Eragon f194baae00
Revert "style: Push the footer to the bottom regardless of the size of maincontent"
This reverts commit c374fb44f6.
2024-03-09 13:58:20 +01:00
Eragon 7c098b2209
post redirect: Remove unused imports 2024-03-09 13:58:20 +01:00
Eragon f02d70a7e5
search: Nicer CSS for search elements 2024-03-09 13:58:20 +01:00
Eragon 0e31abbdfd
search: Display form errors 2024-03-09 13:58:19 +01:00
Eragon 21b73eef19
search: nicer layout on search page 2024-03-09 13:58:19 +01:00
Eragon da037f677e
search: Fix links for comments 2024-03-09 13:58:19 +01:00
Eragon a039d1b500
search: Add links for comments 2024-03-09 13:58:19 +01:00
Eragon 8960ca22cd
search: Add search over programs 2024-03-09 13:58:19 +01:00
Eragon 8937bc902e
search: Fix url GET params in pagination links 2024-03-09 13:58:19 +01:00
Eragon e52b3ebe41
search: Add pagination 2024-03-09 13:58:19 +01:00
Eragon 7e28531106
search: Restore multi-lang search 2024-03-09 13:58:18 +01:00
Eragon 621cd40659
search: Move from hand-crafted SQL to ORM 2024-03-09 13:58:18 +01:00
Eragon 2d7271723f
css: Compile css 2024-03-09 13:58:18 +01:00
Eragon 17f67682b6
db: merge migration scripts 2024-03-09 13:58:18 +01:00
Eragon 3a875253b4
search: Search in topic titles and comments 2024-03-09 13:58:18 +01:00
Eragon b94f4c5944
search: Basic search without style or options 2024-03-09 13:58:18 +01:00
Eragon d39067e586
style: Push the footer to the bottom regardless of the size of maincontent 2024-03-09 13:58:18 +01:00
Eragon bb5534c0ee
search: search page template 2024-03-09 13:58:18 +01:00
Eragon 4f6586e3f6
search: Add most of the choices for advanced search 2024-03-09 13:58:17 +01:00
Lephe 40a5d54c49
ldap: fix use of LDAP not guarded by V5Config.USE_LDAP 2024-03-09 13:58:17 +01:00
Lephe 13c1b30ad6
shoutbox: integrate custom v5shoutbox style 2024-03-09 13:58:17 +01:00
Lephe 4e80932588
shoutbox: update shoutbox to 8bda9f96a
Keep the submodule approach until we can deploy it properly on
PCv5-extra.
2024-03-09 13:58:17 +01:00
Darks af61b21fc8
news: add summary and thumbnails to topics
Provides data for homepage, as well as others topics
2024-03-09 13:58:17 +01:00
Darks c68b9b2048
account: add antibot fileld to registration form 2024-03-09 13:58:17 +01:00
Darks 2b4f3f34b0
submodules: moved to PCv5-extra 2024-03-09 13:58:17 +01:00
Darks 4797433dcd
scripts: add modules to render helper 2024-03-09 13:58:17 +01:00
Eragon 69dcff26ea
config: Change default sender for mails 2024-03-09 13:58:16 +01:00
Darks d3c790db2d
fixed 'avancées de la v5' button 2024-03-09 13:58:16 +01:00
Darks 5b1ebfbf7d
homepage: good enough for preview release 2024-03-09 13:58:16 +01:00
Darks 14f7dbc34f
homepage: still WIP, but better 2024-03-09 13:58:16 +01:00
Eragon cc725eb92f
homepage: Fix grid style 2024-03-09 13:58:16 +01:00
Darks 586c045604
landing page: WIP 2024-03-09 13:58:16 +01:00
IniKiwi 3d22fc5e03
post: fix duplicate code 2024-03-09 13:58:16 +01:00
IniKiwi 5369da453b
post: unique delete button for guest posts 2024-03-09 13:58:15 +01:00
Lephe 6a34a42081
shoutbox: add standalone shoutbox at /chat 2024-03-09 13:58:15 +01:00
Lephe c679f65406
meta: add shoutbox submodule 2024-03-09 13:58:15 +01:00
Eragon a2ce27eee3
makefile: copy all scripts for emoji picker 2024-03-09 13:58:15 +01:00
Eragon 643ff995d4
makefile: Mkdir folders for emoji JS 2024-03-09 13:58:15 +01:00
Eragon 912380ddf1
ldap: Update user informations in LDAP when edited from PCv5 2024-03-09 13:58:15 +01:00
Darks 45e34718f5
registration: fix link to CGU 2024-03-09 13:58:15 +01:00
Eragon a582053ba6
member: Delete members from LDAP on account deletion 2024-03-09 13:58:15 +01:00
Darks 50a2ec69c2
glados: updated announces 2024-03-09 13:58:13 +01:00
Darks 6b74b6fea6
glados: add some 'say' messages 2024-03-09 13:57:00 +01:00
Darks d05942b660
notifications: fixed notifications 2024-03-09 13:54:11 +01:00
Darks 8d90f640b6
logging: add some logging for v5 events 2024-03-09 13:54:11 +01:00
Darks 4df78eb0c3
account: set markdown editor for signature and bio 2023-06-13 20:02:15 +02:00
94 changed files with 1822 additions and 304 deletions

View File

@ -2,6 +2,13 @@ CSSC := lesscpy
src := $(wildcard app/static/less/*.less)
obj := $(src:app/static/less/%.less=app/static/css/%.css)
# PCv5-extra
TAR := tar
extra-uri := https://gitea.planet-casio.com/devs/PCv5-extra/releases/download/v20240308/PCv5-extra.tar.zstd
extra-path := PCv5-extra.tar.zstd
# wget or curl or something else ?
DOWNLOADER := wget -O $(extra-path) $(extra-uri)
run: css
@flask run
@ -10,4 +17,12 @@ css: $(obj)
app/static/css/%.css: app/static/less/%.less
$(CSSC) $< $@
clean-extra:
rm -rf extra/*
get-extra: clean-extra
$(DOWNLOADER)
$(TAR) -xf $(extra-path)
rm $(extra-path)
.PHONY: css run

View File

@ -4,6 +4,7 @@ from wtforms.fields.datetime import DateField
from wtforms.fields.simple import EmailField
from wtforms.validators import InputRequired, Optional, Email, EqualTo
from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty
from app.utils.antibot_field import AntibotField
import app.utils.validators as vd
@ -39,6 +40,8 @@ class RegistrationForm(FlaskForm):
newsletter = BooleanField(
'Inscription à la newsletter',
description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.')
ab = AntibotField()
submit = SubmitField("S'inscrire")

View File

@ -1,6 +1,6 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField, SelectField
from wtforms.validators import InputRequired, Length
from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField, SelectField, DecimalField
from wtforms.validators import InputRequired, Length, Optional
import app.utils.validators as vd
from app.utils.antibot_field import AntibotField
@ -54,6 +54,14 @@ class TopicCreationForm(CommentForm):
'Nom du sujet',
validators=[InputRequired(), Length(min=3, max=128)])
summary = TextAreaField(
'Résumé',
validators=[Optional(), Length(min=3, max=128)])
thumbnail = DecimalField(
'N° de lillustration',
validators=[Optional(), vd.attachment_exists])
submit = SubmitField('Créer le sujet')
@ -61,11 +69,7 @@ class AnonymousTopicCreationForm(TopicCreationForm, AnonymousCommentForm):
ab = AntibotField()
class TopicEditForm(CommentEditForm):
title = StringField(
'Nom du sujet',
validators=[InputRequired(), Length(min=3, max=128)])
class TopicEditForm(TopicCreationForm):
# List of forums is generated at runtime
forum = SelectField(
'Forum',

View File

@ -10,3 +10,8 @@ class MovePost(FlaskForm):
class SearchThread(FlaskForm):
name = StringField("Nom d'un topic, programme, …")
search = SubmitField('Rechercher')
class MergePost(FlaskForm):
# List of posts is generated at runtime
post = SelectField('Fusionner avec', coerce=int, validators=[])
submit = SubmitField('Fusionner')

View File

@ -1,13 +1,63 @@
from flask_login import current_user
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.fields.datetime import DateField
from wtforms.validators import InputRequired, Optional
from wtforms.fields import StringField, SubmitField, SelectField, SelectMultipleField, DateField
from app.models.forum import Forum
# TODO: compléter le formulaire de recherche avancée
class SearchForm(FlaskForm):
class Meta:
csrf = False
q = StringField('Rechercher', validators=[InputRequired()])
class AdvancedSearchForm(SearchForm):
date = DateField('Date', validators=[Optional()])
def generate_choices():
choices = {'Forum': [
'Tous',
'Actualités',
'Aide et questions',
'Forum des projets',
'Vie communautaire',
], 'Programmes': [
'Tous',
'Jeux',
'Utilitaires',
'Logiciels'
], 'Utilisateurs': [
'Tous'
], 'Tutoriels': [
'Tous',
'Basic',
'C/C++',
'Arduino',
'Python'
], 'Sprites': [
'Tous',
'Personnages',
'Environnements',
'Objets',
'Interfaces'
]}
# Forum reserved for admins and moderators
f = Forum.query.filter_by(url='/admin').first()
if (current_user.is_authenticated and current_user.can_access_forum(f)):
choices['Forum'].append('Administration')
# Forum reserved to members of CreativeCalc
f = Forum.query.filter_by(url='/creativecalc').first()
if (current_user.is_authenticated and current_user.can_access_forum(f)):
choices['Forum'].append('CreativeCalc')
return choices
sortBy = SelectField('Trier',
choices={'Pertinence': ['Pertinence'],
'Date': ['Date croissante',
'Date décroissante'],
'Ordre Alphabétique': [
'Alphabétique croissant',
'Alphabétique décroissant',]},
validators=[Optional()])
date = DateField('Date de publication', validators=[Optional()])
scope = SelectMultipleField('', choices=generate_choices, validators=[Optional()])
submit = SubmitField('Affiner la recherche')

View File

@ -1,6 +1,7 @@
from app import db
from app.models.post import Post
from sqlalchemy.orm import backref
from sqlalchemy.dialects.postgresql import UUID
class Topic(Post):
__tablename__ = 'topic'
@ -29,10 +30,17 @@ class Topic(Post):
backref=backref('topics', lazy='dynamic'), foreign_keys=forum_id)
# Associated thread
thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False)
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'), nullable=False)
thread = db.relationship('Thread', foreign_keys=thread_id,
back_populates='owner_topic')
# Summary
summary = db.Column(db.UnicodeText)
# ID of thumbnail
thumbnail_id = db.Column(UUID(as_uuid=True), db.ForeignKey('attachment.id'), nullable=True)
thumbnail = db.relationship('Attachment', foreign_keys=thumbnail_id, lazy='joined')
# Number of views in the forum
views = db.Column(db.Integer)

View File

@ -261,7 +261,7 @@ class Member(User):
if comment.type != "comment":
return False
post = comment.thread.owner_post
return self.can_edit_post(post) and (comment.author == post.author)
return self.can_edit_post(post)
def can_lock_thread(self, post):
"""Whether this member can lock the thread associated with the post"""
@ -270,6 +270,11 @@ class Member(User):
return False
return self.priv("lock.threads")
def can_merge_post(self, post):
"""Whether this member can merge the post"""
# NOTE: Might need more check than this
return self.can_edit_post(post)
def can_access_file(self, file):
"""Whether this member can access the file."""
return self.can_access_post(file.comment)
@ -336,7 +341,7 @@ class Member(User):
self.avatar_filename)
# Resize & convert image
im = Image.open(avatar)
im.thumbnail((128, 128), Image.ANTIALIAS)
im.thumbnail((128, 128), Image.Resampling.LANCZOS)
# Change avatar id
# TODO: verify concurrency behavior

View File

@ -6,7 +6,7 @@ from app.routes.admin import index, groups, account, 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.posts import edit, redirect
from app.routes.programs import index, submit, program
from app.routes.api import markdown

View File

@ -43,7 +43,8 @@ def edit_account():
newsletter=form.newsletter.data,
theme=form.theme.data
)
ldap.edit(old_username, current_user)
if V5Config.USE_LDAP:
ldap.edit(old_username, current_user)
current_user.update(password=form.password.data or None)
db.session.merge(current_user)
db.session.commit()
@ -55,7 +56,11 @@ def edit_account():
flash('Erreur lors de la modification', 'error')
else:
form.theme.data = current_user.theme or 'default_theme'
form.signature.data = current_user.signature
form.biography.data = current_user.bio
form.signature.data = current_user.signature
form.biography.data = current_user.bio
return render('account/account.html', scripts=["+scripts/entropy.js"],
form=form)

View File

@ -64,7 +64,8 @@ def adm_edit_account(user_id):
newsletter=form.newsletter.data,
xp=form.xp.data or None,
)
ldap.edit(old_username, user)
if V5Config.USE_LDAP:
ldap.edit(old_username, user)
user.update(password=form.password.data or None)
db.session.merge(user)
db.session.commit()
@ -75,6 +76,9 @@ def adm_edit_account(user_id):
return redirect(request.url)
else:
flash('Erreur lors de la modification', 'error')
else:
form.signature.data = user.signature
form.biography.data = user.bio
# Trophies
if trophy_form.submit.data:

View File

@ -5,12 +5,10 @@ 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')
@app.route('/v5shoutbox_worker.js')
def v5shoutbox_worker_js():
return send_file('static/scripts/v5shoutbox_worker.js')

View File

@ -54,6 +54,7 @@ def forum_page(f, page=1):
db.session.merge(th)
t = Topic(f, author, form.title.data, th)
t.summary = form.summary.data
db.session.add(t)
db.session.commit()
@ -68,6 +69,10 @@ def forum_page(f, page=1):
for a, file in attachments:
a.set_file(file)
# If there's a thumbnail, set it
if form.thumbnail.data:
t.thumbnail = c.attachments[int(form.thumbnail.data)-1]
# Update member's xp and trophies
if current_user.is_authenticated:
current_user.add_xp(2) # 2 points for a topic
@ -78,7 +83,7 @@ def forum_page(f, page=1):
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,12 @@ from app.utils.render import render
@app.route('/')
def index():
return render('index.html', styles=["+css/homepage.css"])
return render('index.html',
styles=["+css/homepage.css"],
scripts=[
"+scripts/v5shoutbox_ui.js",
"+scripts/v5shoutbox.js"
])
@app.errorhandler(404)

View File

@ -1,4 +1,4 @@
from app import app, db
from app import app, db, V5Config
from app.models.attachment import Attachment
from app.models.comment import Comment
from app.models.forum import Forum
@ -11,17 +11,18 @@ 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 app.forms.post import MovePost, SearchThread, MergePost
from wtforms import BooleanField
from urllib.parse import urlparse
from flask import redirect, url_for, abort, request, flash
from flask_login import login_required, current_user
from sqlalchemy import text
from sqlalchemy import text, and_
from datetime import timedelta
@app.route('/post/editer/<int:postid>', methods=['GET','POST'])
@login_required
def edit_post(postid):
# TODO: Maybe not safe
# FIXME: Maybe not safe
referrer = urlparse(request.args.get('r', default = '/', type = str)).path
print(referrer)
@ -74,6 +75,12 @@ def edit_post(postid):
if isinstance(p, Topic):
p.title = form.title.data
p.summary = form.summary.data
# If there's a thumbnail, set it
if form.thumbnail.data:
p.thumbnail = comment.attachments[int(form.thumbnail.data)-1]
else:
p.thumbnail = None
f = Forum.query.filter_by(url=form.forum.data).first_or_404()
if current_user.can_post_in_forum(f):
p.forum = f
@ -102,6 +109,7 @@ def edit_post(postid):
form.message.data = p.thread.top_comment.text
form.title.data = p.title
form.forum.data = p.forum.url
form.summary.data = p.summary
return render('forum/edit_topic.html', t=p, form=form)
@app.route('/post/supprimer/<int:postid>', methods=['GET','POST'])
@ -237,4 +245,48 @@ def lock_thread(postid):
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)
return redirect(request.referrer)
@app.route('/post/fusionner/<int:postid>', methods=['GET', 'POST'])
@login_required
def merge_post(postid):
comment = Comment.query.get_or_404(postid)
# Get the posts from the same user in the same topic that are separated
# by less than V5Config.MERGE_AGE_THRESHOLD
compatible_comments = Comment.query.filter(and_(
Comment.thread_id == comment.thread_id,
and_(
Post.author_id == comment.author_id,
and_(
Post.date_created > comment.date_created,
Post.date_created <= comment.date_created + timedelta(0, V5Config.MERGE_AGE_THRESHOLD)
)
)
))
merge_form = MergePost()
merge_form.post.choices = [(t.id, f"{t.text[:30]}[…]") for t in list(compatible_comments)]
if merge_form.validate_on_submit():
merge_comment = Comment.query.filter_by(id=merge_form.post.data).first_or_404()
comment.text += f"\n\n---\n\n{merge_comment.text}"
# Change the modification date only if the other post is more recent
if merge_comment.date_created > comment.date_modified:
comment.date_modified = merge_comment.date_created
if merge_comment.is_top_comment:
comment.thread.set_top_comment(comment)
db.session.add(comment)
merge_comment.delete()
db.session.commit()
if isinstance(comment.thread.owner_post, Topic):
return redirect(url_for('forum_topic', f=comment.thread.owner_post.forum, page=[comment.thread.owner_post, -1]))
elif isinstance(comment.thread.owner_post, Program):
return redirect(url_for('program_view', page=[comment.thread.owner_post, -1]))
return render('post/merge_post.html', comment=comment, merge_form=merge_form)

View File

@ -0,0 +1,29 @@
from app import app
from app.models.comment import Comment
from app.models.thread import Thread
from app.models.program import Program
from flask import redirect, url_for
@app.route('/post/<int:postid>', methods=['GET', 'POST'])
def redirect_post(postid):
c = Comment.query.get_or_404(postid)
owner = c.thread.owner_post
# Get the comments for the thread
comments = Comment.query.where(
Comment.thread_id == c.thread.id,
Comment.date_created <= c.date_created
).order_by(
Comment.date_created.asc()
).paginate(per_page=Thread.COMMENTS_PER_PAGE, error_out=False)
if owner.type == 'topic':
# Is a topic
url = url_for('forum_topic', f=owner.forum, page=(owner, comments.pages), _anchor=str(c.id))
else:
# Is a program
url = url_for('program_view', page=(owner, comments.pages), _anchor=str(c.id))
return redirect(url, 301)

View File

@ -1,9 +1,116 @@
from app import app
from app.forms.search import AdvancedSearchForm
from app import app, db
from app.forms.search import AdvancedSearchForm, SearchForm
from app.models.post import Post
from app.models.comment import Comment
from app.models.topic import Topic
from app.models.forum import Forum
from app.models.program import Program
from app.utils.render import render
from sqlalchemy import text, func, Date
from flask import request
from flask_sqlalchemy import Pagination
@app.route('/rechercher')
def search():
form = AdvancedSearchForm()
return render('search.html', form=form)
SEARCH_RESULTS_PER_PAGE = 20
def paginate(data, page, per_page):
# Based on page and per_page info, calculate start and end index of items to keep
start_index = (page - 1) * per_page
end_index = start_index + per_page
# Get the paginated list of items
items = data[start_index:end_index]
# Create Pagination object
return Pagination(None, page, per_page, len(data), items)
def websearch_to_tsquery_multilang(search):
return func.websearch_to_tsquery('french', search).op('||')(func.websearch_to_tsquery('english', search))
def to_tsvector_multilang(text):
return func.to_tsvector('french', text).op('||')(func.to_tsvector('english', text))
@app.route('/rechercher/')
@app.route('/rechercher/<int:page>/')
def search(page=1):
form = AdvancedSearchForm(request.args)
results = list()
if form.validate():
tsquery = websearch_to_tsquery_multilang(form.q.data)
# Topics are sorted first in results
topic_query = db.session.query(Topic).where(
to_tsvector_multilang(Topic.title).bool_op('@@')(tsquery)
).group_by(
Topic.id,
Post.id
)
# Programms follow directly in the results
program_query = db.session.query(Program).where(
to_tsvector_multilang(Program.name).bool_op('@@')(tsquery)
).group_by(
Program.id,
Post.id
)
# Comments are less important than topics and programs
comment_query = db.session.query(Comment).where(
to_tsvector_multilang(Comment.text).bool_op('@@')(tsquery)
).group_by(
Comment.id,
Post.id
)
if (form.date.data):
topic_query = topic_query.where(
Topic.date_created.cast(Date) == form.date.data
)
program_query = program_query.where(
Program.date_created.cast(Date) == form.date.data
)
comment_query = comment_query.where(
Comment.date_created.cast(Date) == form.date.data
)
if (form.sortBy.data == "Date croissante"):
topic_query = topic_query.order_by(
Topic.date_created.asc()
)
program_query = program_query.order_by(
Program.date_created.asc()
)
comment_query = comment_query.order_by(
Post.date_created.asc()
)
elif (form.sortBy.data == "Date décroissante"):
topic_query = topic_query.order_by(
Topic.date_created.desc()
)
program_query = program_query.order_by(
Program.date_created.desc()
)
comment_query = comment_query.order_by(
Post.date_created.desc()
)
elif (form.sortBy.data == "Alphabétique croissant"):
topic_query = topic_query.order_by(
Topic.title.asc()
)
program_query = program_query.order_by(
Program.name.asc()
)
comment_query = comment_query.order_by(
Comment.text.asc()
)
elif (form.sortBy.data == "Alphabétique décroissant"):
topic_query = topic_query.order_by(
Topic.title.desc()
)
program_query = program_query.order_by(
Program.name.desc()
)
comment_query = comment_query.order_by(
Comment.text.desc()
)
results = list(topic_query) + list(program_query) + list(comment_query)
results = paginate(results, page, SEARCH_RESULTS_PER_PAGE)
return render('search.html', form=form, results=results)

View File

@ -6,3 +6,7 @@ from app.utils.render import render
@app.route('/outils')
def tools():
return render('tools.html')
@app.route('/outils/comparateur')
def calc_comparator():
return render('calcdb.html', scripts=['+scripts/calcdb_info.js'], styles=['+css/calcdb.css'])

17
app/static/css/calcdb.css Normal file
View File

@ -0,0 +1,17 @@
/* Controlling checkboxes */
#calcdb-checkboxes {
display: grid;
grid: repeat(5,auto) / repeat(5, 1fr);
grid-auto-flow: column;
}
#calcdb-checkboxes div {
display: block;
}
#calcdb-checkboxes div > input {
margin: 0 0 4px 0;
vertical-align: top;
}
#calcdb-checkboxes div > span {
vertical-align: middle;
}

View File

@ -4,7 +4,7 @@
flex-wrap: wrap;
align-items: center;
}
.editor .btn-group #filler {
.editor .btn-group .filler {
flex-grow: 1;
}
.editor .btn-group button {
@ -85,4 +85,4 @@
transform: translateX(-50%);
top: 50vh;
}
}
}

View File

@ -158,7 +158,7 @@ input[type="submit"]:focus {
left: 50%;
padding: 8px;
position: absolute;
transform: translateY(-100%);
transform: translateY(-200%);
transition: transform 0.3s;
background: var(--links);
color: var(--warn-text);

60
app/static/css/search.css Normal file
View File

@ -0,0 +1,60 @@
.search-page > form {
display: grid;
grid-template-areas: 'search search submit''date sort scope''results results scope';
grid-template-rows: 40% 40% 20%;
grid-template-rows: 5em 5em 100%;
}
.search-page > form input,
.search-page > form select {
width: 100%;
height: 2rem;
}
.search-page > form label {
margin-right: 1em;
}
.search-page > form div.query {
grid-area: search;
display: flex;
align-items: center;
}
.search-page > form div.query label {
margin-right: 1em;
}
.search-page > form div.submit {
grid-area: submit;
display: flex;
align-items: center;
margin-left: 1em;
}
.search-page > form div.submit input#submit {
width: fit-content;
}
.search-page > form div.date {
grid-area: date;
display: flex;
align-items: center;
}
.search-page > form div.date input#date {
width: 80%;
}
.search-page > form div.sort {
grid-area: sort;
display: flex;
align-items: center;
margin-left: 2em;
}
.search-page > form div.scope {
grid-area: scope;
width: 80%;
margin-left: 1em;
}
.search-page > form div.scope select {
width: 100%;
height: 31rem;
overflow: auto;
}
.search-page > form div.search-results {
grid-area: results;
width: 100%;
min-height: 50vh;
}

View File

@ -1,34 +1,7 @@
#shoutbox {
margin: 20px 5% 10px 5%;
background: #ffffff;
#v5shoutbox {
--shoutbox-color: var(--text);
--shoutbox-border-color: var(--border, #d8d8d8);
--shoutbox-header-bg: var(--background-alt);
--shoutbox-link-color: var(--links);
height: 240px;
}
#shoutbox > div {
margin: 0;
padding: 0;
height: 125px;
width: 100%;
overflow-y: scroll;
border-bottom: 1px solid var(--border);
border-radius: 5px 5px 0 0;
}
#shoutbox > div > div {
padding: 2px 10px;
border-bottom: 1px solid rgba(0,0,0,.3);
font-size: 11px;
}
#shoutbox > div > div:hover {
background: var(--background);
}
#shoutbox > div > div:last-child {
border-bottom: none;
}
#shoutbox > input {
width: 100%;
padding: 5px 0;
border-radius: 0 0 5px 5px;
border: 1px solid var(--border);
}
#shoutbox > input:focus {
border-color: var(--border-focus);
box-shadow: inset 0 1px 1px rgba(0,0,0,0.075), 0 0 8px rgba(161,34,34,0.6);
}

View File

@ -11,6 +11,7 @@
:root {
--background: #171a1c; /*22292c, 1c2124, 1E1E1E, 242424,*/
--background-alt: #171a1c;
--text: #eaeaea;
--text-light: #e2e2e2;
@ -77,7 +78,7 @@
header {
--background: #0d1215;
--text: #000000;
--text: #eaeaea;
--border: 1px solid #404040;
}

View File

@ -5,6 +5,7 @@
:root {
--background: #fff;
--background-alt: #eee;
--text: #030303;
--text-light: #b62727;

View File

@ -2,6 +2,7 @@
:root {
--background: #fff;
--background-alt: rgba(0, 0, 0, .1);
--text: #000;
--text-light: #111;
@ -30,7 +31,7 @@ table {
--border: #d8d8d8;
}
table tr:nth-child(odd) {
--background: rgba(0, 0, 0, .1);
--background: var(--background-alt);
}
table th {
--background: #eee;

View File

@ -1 +1 @@
../../../submodules/v5shoutbox/style.css
../../../extra/v5shoutbox/style.css

View File

@ -3,6 +3,13 @@
align-items: center;
width: 265px;
}
.profile.minimal {
width: 176px;
}
.profile.minimal .profile-avatar {
width: 64px;
height: 64px;
}
.profile-avatar {
width: 128px;
height: 128px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 397 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

View File

@ -0,0 +1,74 @@
.search-page > form {
display: grid;
grid-template-areas:
'search search submit'
'date sort scope'
'results results scope';
grid-template-rows: 40% 40% 20%;
grid-template-rows: 5em 5em 100%;
input, select {
width: 100%;
height: 2rem;
}
label {
margin-right: 1em;
}
& div.query {
grid-area: search;
display: flex;
align-items: center;
& label {
margin-right: 1em;
}
}
& div.submit {
grid-area: submit;
display: flex;
align-items: center;
margin-left: 1em;
& input#submit {
width: fit-content;
}
}
& div.date {
grid-area: date;
display: flex;
align-items: center;
& input#date {
width: 80%;
}
}
& div.sort {
grid-area: sort;
display: flex;
align-items: center;
margin-left: 2em;
}
& div.scope {
grid-area: scope;
width: 80%;
margin-left: 1em;
& select {
width: 100%;
height: 31rem;
overflow: auto;
}
}
& div.search-results {
grid-area: results;
width: 100%;
min-height: 50vh;
}
}

View File

@ -8,6 +8,15 @@
width: 265px;
}
.profile.minimal {
width: 176px;
.profile-avatar {
width: 64px;
height: 64px;
}
}
.profile-avatar {
width: 128px;
height: 128px;

View File

@ -0,0 +1,709 @@
/* Currently-active calculators */
const default_set = [ "g25+e2", "g35+e2", "g90+e", "cp400+e" ];
/* Hide or show a calculator, by controlling checkbox */
function toggle_calc(checkbox) {
const selector = "#calcdb .calc-" + checkbox.dataset.calc;
document.querySelectorAll(selector).forEach(td => {
if(checkbox.checked)
td.style.display = "table-cell";
else
td.style.display = "none";
});
update_permalink();
}
/* Select a fixed set set */
function select_set(set) {
document.querySelectorAll("#calcdb-checkboxes input").forEach(box => {
box.checked = set.includes(box.dataset.name) || set.includes(box.dataset.calc);
box.dispatchEvent(new Event("change"));
});
update_permalink();
}
/* Select all calculators */
function select_all() {
document.querySelectorAll("#calcdb-checkboxes input").forEach(box => {
box.checked = true;
box.dispatchEvent(new Event("change"));
});
update_permalink();
}
/* Udpate permalink and page URL*/
function update_permalink() {
var search = [];
document.querySelectorAll("#calcdb-checkboxes input").forEach(box => {
if(box.checked) search.push(box.dataset.name);
});
var loc = window.location.href.split("?")[0] + "?" + search.join(",");
history.pushState({}, "Comparateur Planète Casio", loc);
document.getElementById("calcdb-permalink").href = loc;
}
/* The browser might load the page with some boxes pre-checked because of
forms being cached, so upate to make sure the display is consistent */
document.addEventListener('DOMContentLoaded', function() {
const query = window.location.search;
document.querySelectorAll("#calcdb-checkboxes input").forEach(toggle_calc);
/* Also read the query string for a potential fixed set */
if(!query) return;
var set = [];
query.substr(1).split(",").forEach(name => {
if(name == "current") set = set.concat(default_set);
else set.push(name);
});
select_set(set);
}, false);
var x = 0;
var y = 0;
selectbl = new Array;
selectbl[0] = "sel";
selectbl[1] = "sel1";
selectbl[2] = "sel2";
selectbl[3] = "sel3";
selectbl[4] = "sel4";
selectbl[5] = "sel5";
selectbl[6] = "sel6";
selectbl[7] = "selform1";
selectbl[8] = "selform2";
selectbl[9] = "selform3";
document.onmousemove = position;
function hideselects(){
for(i=0;i<selectbl.length;i++){
if(document.getElementById(selectbl[i])){
document.getElementById(selectbl[i]).style.visibility = 'hidden';
}
}
}
function showselects(){
for(i=0;i<selectbl.length;i++){
if(document.getElementById(selectbl[i])){
document.getElementById(selectbl[i]).style.visibility = 'visible';
}
}
}
function position(e)
{
x = e.pageX
y = e.pageY
}
function showinfos(txt)
{
hideselects();
position;
info = document.getElementById('infobulle').style;
txt = '<table class=\"infobulle\"><tr><td class=\"infobulletitle\">Aide :</td></tr><tr><td class=\"infobulle\">'+txt+'</td></tr></table>';
document.getElementById('infobulle').innerHTML = txt;
info.top = (y+10)+"px";
info.left = (x+10)+"px";
if(screen.availWidth <= 1024 && x > 850)
info.left = x-80;
info.visibility = 'visible';
}
function hideinfos()
{
document.getElementById('infobulle').style.visibility = 'hidden';
showselects();
}
function openimg(name,url)
{
linkimg = window.open(url,name,'resizable=yes,toolbar=no,scrollbar=no,width=50,height=50,top=0,left=0');
}
function toggleSpoiler(spoiler, action) {
var elements = spoiler.getElementsByTagName('div');
switch(action) {
case 'open':
elements[0].className = 'title off';
elements[1].className = 'title on';
elements[2].className = 'on';
break;
case 'close':
elements[0].className = 'title on';
elements[1].className = 'title off';
elements[2].className = 'off';
break;
}
}
$(function() {
//Ajout d'un lien sur les images qui sont reduites par min-width
var virtualImg = new Image();
$(".news img, .topic img").one('load', function()
{
virtualImg.src = $(this).attr('src')
if($(this).attr('resized')!="true" && $(this).parent().get(0).tagName !="A" && virtualImg.width > $(this).css('max-width').slice(0,-2))
$(this).wrap('<a href="'+ $(this).attr('src') +'"></a>');
}).each(function() {
if(this.complete) $(this).load();
});
//formulaire de notation des programmes
//vars
var noteSelectionnee = -1
var LiensNote = $('.PrgmNote').find('div.boutonsNote').children('a')
//events
LiensNote.click(function() {
noteSelectionneUpdate($(this).parent().parent(), $(this).attr('note'), true)
return false;
})
LiensNote.mouseover(function() {
noteSelectionneUpdate($(this).parent().parent(), $(this).attr('note'))
})
LiensNote.mouseout(function() {
noteSelectionneUpdate($(this).parent().parent())
})
$('.PrgmNote').children("a.retourNote").click(function() {
if($('.PrgmNote').children("div").css('display') == 'block')
$('.PrgmNote').children("div").css('display','none');
else
$('.PrgmNote').children("div").css('display','block');
return false;
})
//fonctions
noteSelectionneUpdate = function(PrgmNoteDiv, note, clique)
{
if(note >= 0)
{
PrgmNoteDiv.find(".note").html(note + '/10')
if(clique){
PrgmNoteDiv.find(".note").css('font-weight','bold')
PrgmNoteDiv.find(".note").css('font-style','normal')
noteSelectionnee = note
PrgmNoteDiv.find("select").children('option[selected=selected]').removeAttr('selected')
PrgmNoteDiv.find("select").children('option[value=' + note + ']').attr('selected','selected')
}
else
{
PrgmNoteDiv.find(".note").css('font-style','italic')
PrgmNoteDiv.find(".note").css('font-weight','normal')
}
}
else
{
if(noteSelectionnee < 0)
{
PrgmNoteDiv.find(".note").html('')
}
else
{
PrgmNoteDiv.find(".note").css('font-weight','bold')
PrgmNoteDiv.find(".note").css('font-style','normal')
PrgmNoteDiv.find(".note").html(noteSelectionnee + '/10')
}
}
}
//init
$('.PrgmNote').find("select").css('display','none')
$('.PrgmNote').find(".boutonsNote").css('display','block')
$('.PrgmNote').children("div").css('display','none')
noteSelectionneUpdate($('.PrgmNote'), $('.PrgmNote').find("option[selected]").attr('value'), true)
//addthis retardateur
var isTimeFinish = false
var isOut = true;
function timeFinish(that)
{
isTimeFinish = true
that.mouseover()
}
$('.addthis_button').children('img').mouseenter(function() {
if(isTimeFinish)
{
clearTimeout(timeoutID);
return true;
}
else
{
timeoutID = setTimeout(function(that){timeFinish(that);}, 200, $(this));
return false;
}
})
$('.addthis_button').children('img').mouseleave(function() {
isTimeFinish = false;
clearTimeout(timeoutID);
return true;
})
//antibot JS pour invité
$(document).ready(function(){
$('.messageForm').css('display','block');
$('.messageForm').children('form').append('<input type="hidden" name="jsActive" value="Oui"/>');
})
//Editeur - namespace:ed
//vars
var ed_boutons = $('.editeur').find('.boutons').find('img,td,span');
var ed_textarea = $('.editeur').find('textarea');
var ed_pins = $('.editeur').find('.pinhidden,.pinshow');
var ed_cross = $('.editeur').find('.cross');
var ed_inputs = $('.editeur').find('.AskBoxInputText');
var ed_smileys = $('.editeur').find('.ABsmiley').find('td');
//speciale IE
$(".editeur").data('isfocused',false);
$(".editeur").data('selecStart',0);
$(".editeur").data('selecEnd',0);
$(".editeur").data('isPinned',false);
$(".editeur").data('tag','');
//events
$('.editeur').find('.AskBoxBottom').children('img').click(function(){
ed_addtag($(this).closest(".editeur"));
});
$('.editeur').find('.ABprofil').click(function(){
var formname = $(this).closest('.editeur').data('formname');
fenetre=window.open('/Fr/compte/liste_des_membres.php?id=ed_'+formname+'_lienMembre','fenetre','resizable=yes,toolbar=no,scrollbars=yes,width=280,height=450,top=0,left=0')
});
ed_cross.click(function()//fermeture de toutes les askbox si on clique ailleur
{
close_AskBox($(this).closest(".editeur"),false);
});
$('.editeur').find('.edSelect').change(function(){
var tag = $(this).children('option:checked').val();
if(tag.length > 0)
add_text($(this).closest('.editeur').find('textarea'),'['+tag+']', '[/'+tag+']', '')
$(this).children('option').removeAttr('selected');
$(this).children('option[value=]').attr('selected','selected');
});
ed_pins.click(function(){
var editeur = $(this).closest(".editeur");
if(editeur.data('isPinned')==false)
{
editeur.data('isPinned',true);
$(this).css('display','none');
$(this).parent().children('.pinshow').css('display','block');
}
else
{
editeur.data('isPinned',false);
$(this).css('display','none');
$(this).parent().children('.pinhidden').css('display','block');
}
});
ed_boutons.click(function(){
if($(this).data('type')=='text')
{
add_text($(this).closest(".editeur").find('textarea'), $(this).data('avant'),$(this).data('apres'),$(this).data('valeur'));
}
else if($(this).data('type')=='askbox')
{
show_AskBox($(this).closest(".editeur"), $(this).data('name'))
}
});
ed_smileys.click(function(){
if($(this).data('type')=='text')
{
add_text($(this).closest(".editeur").find('textarea'), $(this).data('avant'),$(this).data('apres'),$(this).data('valeur'));
}
else if($(this).data('type')=='askbox')
{
show_AskBox($(this).closest(".editeur"), $(this).data('name'))
}
close_AskBox($(this).closest(".editeur"));
});
ed_inputs.keypress(function(e) {
if(e.which == 13) {
ed_addtag($(this).closest(".editeur"));
return false;
}
});
ed_textarea.focus(function(){
$(this).closest(".editeur").data('isfocused',true);
});
ed_textarea.blur(function(){
$(this).closest(".editeur").data('isfocused',false);
});
//Askbox fonctions
//affiche une askbox
function show_AskBox(editeur, AskBoxName)
{
close_AskBox(editeur, false,false);
var AskBox = editeur.children('.AskBox');
var actualAskBox = AskBox.children('.AskBoxText.AB'+AskBoxName);
var textarea = editeur.find('textarea')[0];
var title = "";
editeur.data('tag',AskBoxName);
//Get selected text
var selectedText = '';
if(typeof document.selection != 'undefined')//IE support
{
var range = document.selection.createRange();
selectedText=range.text;
}
else if(typeof textarea.selectionStart != 'undefined')//Firefox, GC support
{
selectedText=textarea.value.substring(textarea.selectionStart, textarea.selectionEnd);
}
//Open the askbox
switch(AskBoxName)
{
case 'url' :
var title = "Ajouter un lien";
var focusOn = null;
if(selectedText.substring(0,7)=="http://" || selectedText.substring(0,8)=="https://" || selectedText.substring(0,6)=="ftp://")
{
actualAskBox.find('.AskBoxInputText[name=ed-urlLien]').val(selectedText);
focusOn = actualAskBox.find('.AskBoxInputText[name=ed-urlNom]');
}
else
{
actualAskBox.find('.AskBoxInputText[name=ed-urlNom]').val(selectedText);
focusOn = actualAskBox.find('.AskBoxInputText[name=ed-urlLien]').val("http://");
}
break;
case 'img' :
title = "Ajouter une image";
actualAskBox.find('.AskBoxInputRadio[name=ed-imgType][value=img]').prop('checked', true);
actualAskBox.find('.AskBoxInputRadio[name=ed-imgAlign][value=center]').prop('checked', true);
if(selectedText.substring(0,7)=="http://" || selectedText.substring(0,8)=="https://" || selectedText.substring(0,6)=="ftp://")
{
focusOn = actualAskBox.find('.AskBoxInputText[name=ed-imgAdresse]').val(selectedText);
}
else
{
focusOn = actualAskBox.find('.AskBoxInputText[name=ed-imgAdresse]').val("http://");
}
break;
case 'video' :
title = "Ajouter une vidéo";
actualAskBox.find('.AskBoxInputRadio[name=ed-videoType][value=video]').prop('checked', true);
if(selectedText.substring(0,7)=="http://" || selectedText.substring(0,8)=="https://")
focusOn = actualAskBox.find('.AskBoxInputText[name=ed-videoAdresse]').val(selectedText);
else
focusOn = actualAskBox.find('.AskBoxInputText[name=ed-videoAdresse]').val("http://");
break;
case 'profil' :
title = "Ajouter un lien vers un profil";
focusOn = actualAskBox.find('.AskBoxInputText[name=ed-profilPseudo]').val(selectedText);
break;
case 'quote' :
title = "Citer";
focusOn = actualAskBox.find('.AskBoxInputText[name=ed-quoteAuteur]');
break;
case 'spoiler' :
title = "Ajouter un spoiler";
focusOn = actualAskBox.find('.AskBoxInputText[name=ed-spoilerOpen]');
break;
case 'progress' :
title = "Ajouter une barre de progression";
focusOn = actualAskBox.find('.AskBoxInputText[name=ed-progressTitle]').val(selectedText);
break;
case 'smiley' :
title = "Plus de smileys !";
AskBox.children('.AskBoxBottom').css('display','none');
break;
}
AskBox.children('.AskBoxTop').children('span').html(title);
// AskBox.css('display','block');
editeur.children('.AskBox').show('fast');
actualAskBox.css('display','block');
if(focusOn!=null)focusOn.focus();
}
// masque tous les askbox(en verifiant s'ils sont pinned si verif=true)
function close_AskBox(editeur, verif, anim)
{ if(anim != false)anim = true;
if(editeur.data('isPinned')==false || verif==false)
{
editeur.children('.AskBox').children('.AskBoxBottom').css('display','block');
if(anim)
editeur.children('.AskBox').hide('fast');
else
editeur.children('.AskBox').css('display','non');
editeur.children('.AskBox').children('.AskBoxText').css('display','none');
editeur.data('tag','');
editeur.data('isPinned',false);
editeur.find('.pinshow').css('display','none');
editeur.find('.pinhidden').css('display','block');
}
}
//Validation des askbox
function ed_addtag(editeur)
{
var tag = editeur.data('tag');
var valeur='';
var valeurSup='';
// var selectionVide=true;
var cursorAtEnd=false;
switch(tag)
{
case 'url':
valeur=editeur.find('.AskBoxInputText[name=ed-urlNom]').val();
valeurSup=editeur.find('.AskBoxInputText[name=ed-urlLien]').val();
if(valeur==""){valeur=valeurSup;cursorAtEnd=false;}else cursorAtEnd=true;
valeurSup="="+valeurSup;
break;
case 'img' :
//adimg
if(editeur.find('.AskBoxInputRadio[name=ed-imgType]:checked').val() == "adimg")
tag = "adimg";
//get value
valeur=editeur.find('.AskBoxInputText[name=ed-imgAdresse]').val();
//prepare valeurSup
valeurSup = "=";
var premierArg= true;
var largeur = parseInt(editeur.find('.AskBoxInputText[name=ed-imgWidth]').val());
if(largeur > 0)//if int
{
valeurSup += largeur;
premierArg = false;
}
var hauteur = parseInt(editeur.find('.AskBoxInputText[name=ed-imgHeight]').val());
if(hauteur > 0)//if int
{
valeurSup += "x" + hauteur;
premierArg = false;
}
var align = editeur.find('.AskBoxInputRadio[name=ed-imgAlign]:checked').val()
if(align.length >0)
{
if(premierArg==false){valeurSup += "|"}
valeurSup += align;
premierArg = false;
}
if(premierArg){valeurSup = ""}
cursorAtEnd=true;
break;
case 'video' :
valeur=editeur.find('.AskBoxInputText[name=ed-videoAdresse]').val();
if(editeur.find('.AskBoxInputRadio[name=ed-videoType]:checked').val() == "video tiny")
tag = "video mini";
cursorAtEnd=true;
break;
case 'profil' :
valeur = editeur.find('.AskBoxInputText[name=ed-profilPseudo]').val();
if(valeur.length > 0) cursorAtEnd=true;
break;
case 'quote' :
valeurSup = editeur.find('.AskBoxInputText[name=ed-quoteAuteur]').val();
if(valeurSup!="")valeurSup="="+valeurSup;
break;
case 'spoiler':
valeurSup = "=" + editeur.find('.AskBoxInputText[name=ed-spoilerOpen]').val() + "|" + editeur.find('.AskBoxInputText[name=ed-spoilerClose]').val();
if(valeurSup == '=Cliquer pour dérouler|Cliquer pour enrouler')
valeurSup = "";
break;
case 'progress':
valeur= editeur.find('.AskBoxInputText[name=ed-progressTitle]').val();
if(valeur.length <=0)
{
alert('Le titre de la barre de progression est obligatoire.');
editeur.find('.AskBoxInputText[name=ed-progressTitle]').focus();
return;
}
valeurSup= parseInt(editeur.find('.AskBoxInputText[name=ed-progressPourcent]').val());
if(!valeurSup>=1 && !valeurSup<=100)
{
alert('Le pourcentage de la barre de progression est obligatoire et doit être compris entre 1 et 100');
editeur.find('.AskBoxInputText[name=ed-progressPourcent]').focus();
return;
}
valeurSup="="+valeurSup;
cursorAtEnd=true;
break;
}
//fermeture des askbox
close_AskBox(editeur);
//Envoi sur le textarea
if(cursorAtEnd){add_text(editeur.find('textarea'), '[' + tag + valeurSup + ']'+ valeur +'[/' + tag + ']')}
else{add_text(editeur.find('textarea'), '[' + tag + valeurSup + ']','[/' + tag + ']',valeur)}
}
//IE compatibility
document.onmouseup = updateIECursorPosition;
document.onkeydown = updateIECursorPosition;
function updateIECursorPosition()//IE - detection de la position du curseur pour IE qu'il l'oublie quand on clique autrepart que sur le textarea..
{
$('.editeur').each(function(){
var thisTxtarea = $(this).find('textarea');
if($(this).data('isfocused') && typeof(thisTxtarea[0].createTextRange) == 'function')
{
var range = thisTxtarea[0].createTextRange();
range.moveToBookmark(document.selection.createRange().getBookmark());
range.moveEnd('character', thisTxtarea[0].value.length);
$(".editeur").data('selecStart',(thisTxtarea[0].value.length - range.text.length))
var range = thisTxtarea[0].createTextRange();
range.moveToBookmark(document.selection.createRange().getBookmark());
range.moveStart('character', - thisTxtarea[0].value.length);
$(".editeur").data('selecEnd',range.text.length);
}
});
}
//general textarea functions
function add_text(textarea, before, after, valeur)
{
selecStart = textarea.data('selecStart');
selecEnd = textarea.data('selecEnd');
if(before==null){before='';}
if(after==null){after='';}
if(valeur==null){valeur='';}
if(typeof document.selection != 'undefined')//IE support
{
var insText;
textarea.focus();
if(selecStart!=null && selecEnd!=null)
{ var range = document.selection.createRange();
if (textarea[0].setSelectionRange)
textarea[0].setSelectionRange(selecStart, selecEnd);
else if (document.selection) {
var range = textarea[0].createTextRange();
range.moveStart('character', selecStart);
range.moveEnd('character', - textarea[0].value.length + selecEnd);
range.select();
}
}
textarea.focus();
range = document.selection.createRange();
if(valeur!=''){insText=valeur;}else{insText = range.text;}
if(after=='' && valeur=='')
{
range.text = before;
range.select();
}
else
{
range.text = before + insText + after ;
range.moveStart('character', -after.length -insText.length);
range.moveEnd('character', -after.length);
range.select();
}
}
else if(typeof textarea[0].selectionStart != 'undefined')//Firefox, GC support
{
var start = textarea[0].selectionStart;
var end = textarea[0].selectionEnd;
var insText;
if(valeur!='')
insText=valeur;
else
insText = textarea.val().substring(start, end);
if(after=='' && valeur=='')
{
textarea.val(textarea.val().substr(0, start) + before + textarea.val().substr(end));
textarea[0].selectionStart = start + before.length;
textarea[0].selectionEnd = start + before.length;
}
else
{
textarea.val(textarea.val().substr(0, start) + before + insText + after + textarea.val().substr(end));
textarea[0].selectionStart = start + before.length;
textarea[0].selectionEnd = start + before.length + insText.length;
}
textarea.focus();
}
else
{
textarea.val(before + valeur + after);
textarea.focus();
}
}
// Preview button on textarea
$('.pctextarea_preview').on('shown.bs.modal', function () {
const message = $(this).parent().find('.editeurTextareaDiv > textarea').val();
const body = $(this).find('.modal-body');
$.post('/Fr/forums/preview.php', {message}, function(data, status) {
body.html(data.preview);
})
.fail(function() {
body.html('<p class="align-center">Erreur pendant la récupération de la prévisualisation.</p>');
});
})
//fonction citer dans le forum
$('.lien_citation').click(function(){
var auteur = $(this).data('membre');
var id_message = $(this).data('id');
var textarea = $('textarea[name=message]');
$.ajax({type: "POST",
url: '/Fr/forums/quote.php',
data: {id : id_message}
}).done(function( msg )
{
add_text(textarea,'[quote=' + auteur + ']' + msg + '[/quote]','','');
});
});
//bouton d'affichage des stats
$('#stats').click(function(){
window.open($(this).attr('href'),'connectes','resizable=yes,toolbar=no,scrollbars=yes,width=550,height=300,top=0,left=0')
return false;
});
//Lien de control de la fenetre principale depuis un popoup
$('.popuplink').click(function(){
window.opener.location.href=$(this).data('link');return false;
return false;
});
/**
* Confirmation modal
*
* Add the 'need-confirm' class to a link to show a prompt asking for confirmation.
* 'data-confirm-text' attribute can be used to customize the confirm message
*/
$('a.need-confirm').click(function(){
const modal = $('#confirm-modal');
// Get text from 'data-confirm-text' arg
let text = 'Êtes-vous sûr de vouloir continuer ?';
console.log($(this).data())
if ($(this).data('confirm-text')) {
text = $(this).data('confirm-text');
}
modal.find('.confirm-modal-text').first().html(text)
// Set "Yes" link to original href of $(this)
modal.find('.confirm-modal-yes').first().attr('href', $(this).attr('href'))
// Configure and open modal
$('#confirm-modal').modal('show');
return false;
});
});

View File

@ -2,42 +2,34 @@
/* 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] */
Returns [the div.editor, the textarea] */
function editor_event_source(event)
{
let button = undefined;
let editor = undefined;
/* Grab the button and the parent editor block. The onclick event itself
/* Grab the the parent editor block. The onclick event itself
usually reports the SVG in the button as the source */
let node = event.target || event.srcElement;
let node = event.current || 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 (!editor) return;
const ta = editor.querySelector(".editor textarea");
return [editor, button, ta];
return [editor, ta];
}
/* 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);
textarea.value = textarea.value.substring(0, start)
+ contents
+ textarea.value.substring(end);
return [start, start + contents.length];
}
@ -46,7 +38,7 @@ function editor_replace_range(textarea, start, end, contents)
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);
const [editor, ta] = editor_event_source(event);
ta.focus();
let indexStart = ta.selectionStart;
let indexEnd = ta.selectionEnd;
@ -67,14 +59,14 @@ function editor_insert_around(event, before="", after=null)
ta.selectionStart = ta.selectionEnd = start + before.length;
}
preview();
preview(editor);
}
/* 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);
const [editor, ta] = editor_event_source(event);
ta.focus();
let indexStart = ta.selectionStart;
let indexEnd = ta.selectionEnd;
@ -102,27 +94,28 @@ function editor_act_on_lines(event, fn)
ta.selectionStart = start;
ta.selectionEnd = end;
preview();
preview(editor);
}
function editor_clear_modals(event, close = true)
{
// Stop the propagation of the event
event.stopPropagation()
const [editor, ta] = editor_event_source(event);
// 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");
editor.getElementsByClassName('media-alt-input')[0].value = '';
editor.getElementsByClassName('media-link-input')[0].value = '';
editor.getElementsByClassName('link-desc-input')[0].value = '';
editor.getElementsByClassName('link-link-input')[0].value = '';
const media_type = editor.getElementsByClassName("media-type")[0];
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');
const modals = editor.getElementsByClassName('modal');
for (const i of modals) {i.style.display = 'none'};
}
@ -143,12 +136,13 @@ function editor_inline(event, type, enable_preview = true)
}
if (enable_preview) {
preview();
const [editor, ta] = editor_event_source(event);
preview(editor);
}
}
function editor_display_link_modal(event) {
const [editor, button, ta] = editor_event_source(event);
const [editor, ta] = editor_event_source(event);
let indexStart = ta.selectionStart;
let indexEnd = ta.selectionEnd;
let selection = ta.value.substring(indexStart, indexEnd);
@ -167,16 +161,16 @@ function editor_display_link_modal(event) {
function editor_insert_link(event, link_id, text_id, media = false)
{
const [editor, button, ta] = editor_event_source(event);
const [editor, 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;
const link = editor.getElementsByClassName(link_id)[0].value;
const text = editor.getElementsByClassName(text_id)[0].value;
let media_type = "";
const media_selector = document.getElementsByName("media-type");
const media_selector = editor.getElementsByClassName("media-type")[0];
for(i = 0; i < media_selector.length; i++) {
if (media_selector[i].checked) {
media_type = `{type=${media_selector[i].value}}`;
@ -197,7 +191,7 @@ function editor_insert_link(event, link_id, text_id, media = false)
ta.selectionStart = ta.selectionEnd = start + 1;
}
preview();
preview(editor);
}
function editor_title(event, level, diff)
@ -293,6 +287,8 @@ const DISABLE_PREVIEW_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="16"
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() {
const [editor, ta] = editor_event_source(event);
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"));
@ -301,25 +297,24 @@ function toggle_auto_preview() {
}
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";
editor.getElementsByClassName("toggle_preview")[0].title = "Désactiver la prévisualisation";
editor.getElementsByClassName("toggle_preview")[0].innerHTML = DISABLE_PREVIEW_ICON;
editor.getElementsByClassName("manual_preview")[0].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";
editor.getElementsByClassName("toggle_preview")[0].title = "Activer la prévisualisation";
editor.getElementsByClassName("toggle_preview")[0].innerHTML = ENABLE_PREVIEW_ICON;
editor.getElementsByClassName("manual_preview")[0].style = "display: block";
}
}
/* This request the server to get a complete render of the current text in the textarea */
function preview(manual=false) {
function preview(editor, 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 previewArea = editor.querySelector(".editor_content_preview");
const ta = editor.querySelector("textarea");
const payload = {text: ta.value};
const headers = new Headers();
@ -341,86 +336,95 @@ function preview(manual=false) {
});
}
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";
}
/* Wrapper for user-requested preview refresh */
function manual_preview(editor_id) {
const editor = document.getElementById("editor_" + editor_id);
preview(editor, manual = true);
}
let previewTimeout = null;
let 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();
/* Add the event listener for the textarea hotkeys and auto-preview */
function editor_setup(editor_id) {
const editor = document.getElementById("editor_" + editor_id);
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;
if (document.cookie.split(";").some((item) => item.trim().startsWith("auto-preview="))) {
if (document.cookie.split(";").some((item) => item.includes("auto-preview=false"))) {
editor.getElementsByClassName("toggle_preview")[0].title = "Activer la prévisualisation";
editor.getElementsByClassName("toggle_preview")[0].innerHTML = ENABLE_PREVIEW_ICON;
editor.getElementsByClassName("manual_preview")[0].style = "display: block";
}
}
// Set a timeout for refreshing the preview
if (previewTimeout != null) {
let previewTimeout = null;
let ta = editor.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();
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
clearTimeout(previewTimeout);
}
previewTimeout = setTimeout(preview, 3000);
});
previewTimeout = setTimeout(() => { preview(editor) }, 3000);
});
document.querySelector('emoji-picker').addEventListener('emoji-click', event => {
editor_clear_modals(event);
editor_insert_around(event, "", event.detail.unicode)
preview();
});
editor.querySelector('emoji-picker').addEventListener('emoji-click', event => {
editor_clear_modals(event);
editor_insert_around(event, "", event.detail.unicode)
preview(editor);
});
}

View File

@ -0,0 +1 @@
../../../extra/emoji-picker-element/

View File

@ -1 +1 @@
../../../submodules/v5shoutbox/v5shoutbox.js
../../../extra/v5shoutbox/v5shoutbox.js

View File

@ -0,0 +1 @@
../../../extra/v5shoutbox/v5shoutbox_irc.js

View File

@ -0,0 +1 @@
../../../extra/v5shoutbox/v5shoutbox_ui.js

View File

@ -0,0 +1 @@
../../../extra/v5shoutbox/v5shoutbox_worker.js

View File

@ -1,4 +1,5 @@
{% extends "base/base.html" %}
{% import "widgets/editor.html" as widget_editor %}
{% set tabtitle = "Gestion du compte" %}
@ -68,16 +69,11 @@
{% endfor %}
</div>
<div>
{{ form.signature.label }}
<textarea id="{{ form.signature.name }}" name="{{ form.signature.name }}">{{ current_user.signature }}</textarea>
{% for error in form.signature.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
{{ widget_editor.text_editor(form.signature) }}
</div>
<div>
{{ form.biography.label }}
<textarea id="{{ form.biography.name }}" name="{{ form.biography.name }}">{{ current_user.bio }}</textarea>
{% for error in form.biography.errors %}
{{ widget_editor.text_editor(form.biography) }}
{% for error in form.signature.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>

View File

@ -53,6 +53,7 @@
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
{{ form.ab }}
<div>{{ form.submit(class_="bg-ok") }}</div>
</form>
</div>

View File

@ -1,4 +1,5 @@
{% extends "base/base.html" %}
{% import "widgets/editor.html" as widget_editor %}
{% set tabtitle = "Administration - Édition du compte de " + user.name %}
@ -81,18 +82,10 @@
{% endfor %}
</div>
<div>
{{ form.signature.label }}
<textarea id="{{ form.signature.name }}" name="{{ form.signature.name }}">{{ user.signature }}</textarea>
{% for error in form.signature.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
{{ widget_editor.text_editor(form.signature) }}
</div>
<div>
{{ form.biography.label }}
<textarea id="{{ form.biography.name }}" name="{{ form.biography.name }}">{{ user.bio }}</textarea>
{% for error in form.biography.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
{{ widget_editor.text_editor(form.biography) }}
</div>
<h2>Préférences</h2>

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

@ -20,4 +20,9 @@
<path fill="#ffffff" d="M19,8L15,12H18A6,6 0 0,1 12,18C11,18 10.03,17.75 9.2,17.3L7.74,18.76C8.97,19.54 10.43,20 12,20A8,8 0 0,0 20,12H23M6,12A6,6 0 0,1 12,6C13,6 13.97,6.25 14.8,6.7L16.26,5.24C15.03,4.46 13.57,4 12,4A8,8 0 0,0 4,12H1L5,16L9,12"></path>
</svg>SH4 Compatibility Tool
</a>
<a href="{{ url_for('calc_comparator') }}">
<svg viewBox="0 0 24 24" fill="none">
<path d="M5 9H19M15 18V15M9 18H9.01M12 18H12.01M12 15H12.01M9 15H9.01M15 12H15.01M12 12H12.01M9 12H9.01M8.2 21H15.8C16.9201 21 17.4802 21 17.908 20.782C18.2843 20.5903 18.5903 20.2843 18.782 19.908C19 19.4802 19 18.9201 19 17.8V6.2C19 5.0799 19 4.51984 18.782 4.09202C18.5903 3.71569 18.2843 3.40973 17.908 3.21799C17.4802 3 16.9201 3 15.8 3H8.2C7.0799 3 6.51984 3 6.09202 3.21799C5.71569 3.40973 5.40973 3.71569 5.21799 4.09202C5 4.51984 5 5.07989 5 6.2V17.8C5 18.9201 5 19.4802 5.21799 19.908C5.40973 20.2843 5.71569 20.5903 6.09202 20.782C6.51984 21 7.07989 21 8.2 21Z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>Comparateur de calculatrices
</a>
</div>

View File

@ -3,4 +3,4 @@
{% endfor %}
{% for m in modules %}
<script type="module" src={{url_for('static', filename=m)}}></script>
{% endfor %}
{% endfor %}

View File

@ -0,0 +1 @@
../../extra/calcdb/calcdb-fr.html

141
app/templates/calcdb.html Normal file
View File

@ -0,0 +1,141 @@
{% extends "base/base.html" %}
{% set tabtitle = "Comparateur des calculatrices CASIO" %}
{% block title %}
<h1>Comparateur des calculatrices CASIO</h1>
{% endblock %}
{% block content %}
<section>
<p>
Cette page recense la majorité des calculatrices CASIO distribuées en
France depuis 2005. Le tableau comparatif contient tous les détails
logiciels et matériels des modèles, ainsi que de nombreux liens vers
les ressources associées.
</p>
<h4>Sélection des modèles</h4>
<p>
<button onclick="select_set(default_set)">Afficher les modèles actuels</button>
<button onclick="select_all()">Tout afficher</button>
</p>
<div>
<a id="calcdb-permalink" href="https://www.planet-casio.com/Fr/infos/comparateur?g25+e2,g35+e2,g90+e,cp400+e">Lien permanent vers cette configuration</a>
</div>
<div id="calcdb-checkboxes">
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="g25ppro" data-name="g25+pro">
<img src="/static/icons/calc/g25+pro.png">
<span>Graph 25+ Pro</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="g25pe" data-name="g25+e">
<img src="/static/icons/calc/g25+e.png">
<span>Graph 25+E</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" checked="" data-calc="g25pe2" data-name="g25+e2">
<img src="/static/icons/calc/g25+e2.png">
<span>Graph 25+E II</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="g35p" data-name="g35+">
<img src="/static/icons/calc/g35+.png">
<span>Graph 35+</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="g35pusb3" data-name="g35+usb3">
<img src="/static/icons/calc/g35+usb3.png">
<span>Graph 35+ USB (SH3)</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="g35pusb4" data-name="g35+usb4">
<img src="/static/icons/calc/g35+usb4.png">
<span>Graph 35+ USB (SH4)</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="g35pe" data-name="g35+e">
<img src="/static/icons/calc/g35+e.png">
<span>Graph 35+E</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" checked="" data-calc="g35pe2" data-name="g35+e2">
<img src="/static/icons/calc/g35+e2.png">
<span>Graph 35+E II</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="g75" data-name="g75">
<img src="/static/icons/calc/g75.png">
<span>Graph 75</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="g75p" data-name="g75+">
<img src="/static/icons/calc/g75+.png">
<span>Graph 75+</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="g75pe" data-name="g75+e">
<img src="/static/icons/calc/g75+e.png">
<span>Graph 75+E</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="g85" data-name="g85">
<img src="/static/icons/calc/g85.png">
<span>Graph 85</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="g85sd" data-name="g85sd">
<img src="/static/icons/calc/g85sd.png">
<span>Graph 85 SD</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="g95" data-name="g95">
<img src="/static/icons/calc/g95.png">
<span>Graph 95 (SD)</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="cg20" data-name="cg20">
<img src="/static/icons/calc/cg20.png">
<span>Prizm</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" checked="" data-calc="g90pe" data-name="g90+e">
<img src="/static/icons/calc/g90+e.png">
<span>Graph 90+E</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="cp300" data-name="cp300">
<img src="/static/icons/calc/cp300.png">
<span>Classpad 300</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="cp330" data-name="cp330">
<img src="/static/icons/calc/cp330.png">
<span>Classpad 330</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="cp330p" data-name="cp330+">
<img src="/static/icons/calc/cp330+.png">
<span>Classpad 330+</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" unchecked="" data-calc="cp400" data-name="cp400">
<img src="/static/icons/calc/cp400.png">
<span>Classpad 400</span>
</div>
<div>
<input type="checkbox" onchange="toggle_calc(this)" checked="" data-calc="cp400pe" data-name="cp400+e">
<img src="/static/icons/calc/cp400+e.png">
<span>Classpad 400+E</span>
</div>
</div>
<h4>Tableau comparatif</h4>
<div id="calcdb">
{% include "calcdb-fr.html" %}
</div>
</section>
{% endblock %}

View File

@ -4,6 +4,7 @@
<html lang="fr-FR">
{% include "base/head.html" with context %}
<body>
<div id="v5shoutbox-fullscreen"></div>
{% include "widgets/v5shoutbox.html" %}
{% include "base/scripts.html" %}
</body>

View File

@ -48,6 +48,22 @@
{% endfor %}
</div>
<div>
{{ form.summary.label }}
{{ form.summary }}
{% for error in form.summary.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.thumbnail.label }}
{{ form.thumbnail }}
{% for error in form.thumbnail.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
{% if form.pseudo %}
<div>
{{ form.pseudo.label }}

View File

@ -75,6 +75,22 @@
{% endfor %}
</div>
<div>
{{ form.summary.label }}
{{ form.summary }}
{% for error in form.summary.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.thumbnail.label }}
{{ form.thumbnail }}
{% for error in form.thumbnail.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
{{ widget_editor.text_editor(form.message) }}
<div>

View File

@ -15,10 +15,20 @@
<section>
<h1>{{ t.title }}</h1>
{% if t.summary %}
<div>{{ t.summary | md }}</div>
{% endif %}
{% if t.thread.top_comment %}
{% call widget_thread.thread_leader(t.thread.top_comment) %}
{% call widget_thread.thread_leader(t.thread.top_comment, t) %}
<div class="info">
<div>Posté le {{ t.date_created | dyndate }}</div>
<div>
Créé le {{ t.date_created | dyndate }}
<!-- We check if the formatted date is not the same, because the date might differ slightly between the two even if it's the same -->
{% if t.thread.top_comment.date_created | dyndate != t.date_created | dyndate %}
(Posté le {{ t.thread.top_comment.date_created | dyndate }})
{% endif %}
</div>
{{ widget_thread.post_actions(t) }}
</div>
{{ t.thread.top_comment.text | md }}

View File

@ -46,69 +46,22 @@
<div class="home-news">
<h1>Actualitées</h1>
<ul>
{% for n in last_news %}
<li>
<a href="#"><img src="https://www.planet-casio.com/images/staff/tdm9_collision2.jpg"/></a>
<a href="{{ url_for('forum_topic', f=n.forum, page=(n,'fin'))}}"><img src="{{ n.thumbnail.url }}"/></a>
<div>
<h3><a href="#">Les collisions — partie 2</a></h3>
<p class="date"><i>Publié par <a href="#">Shadow15510</a> le <time>09/06/2023 09:10</time></i></p>
La deuxième partie du tutoriel du mercredi sur les collisions est maintenant disponible en vidéo.
<h3><a href="{{ url_for('forum_topic', f=n.forum, page=(n,'fin'))}}">{{ n.title }}</a></h3>
<p class="date"><i>Publié par <a href="{{ url_for('user_by_id', user_id=n.author.id) }}">{{ n.author.name }}</a>
le <time>{{ n.date_created | dyndate }}</time></i></p>
{{ (n.summary or "") | md }}
</div>
</li>
<li>
<a href="#"><img src="https://www.planet-casio.com/images/staff/CPC30-image-thumb-results.png"/></a>
<div>
<h3><a href="#">Résultats du CPC #30 — Les profondeurs !</a>
</h3>
<p class="date"><i>Publié par <a href="#">Lephenixnoir</a> le <time>08/06/2023 22:10</time></i></p>
Les programmes sont profonds et avec un peu de chance les tests aussi ! </div>
</li>
<li>
<a href="#"><img src="https://www.planet-casio.com/images/staff/tituya-thumb.png"/></a>
<div>
<h3><a href="#">Un second renardministrateur !</a>
</h3>
<p class="date"><i>Publié par <a href="#">Lephenixnoir</a> le <time>07/06/2023 22:55</time></i></p>
Vive la renardocratie ! </div>
</li>
<li>
<a href="#"><img src="https://www.planet-casio.com/images/staff/massy2.jpg"/></a>
<div>
<h3><a href="#">Visite chez Casio France à Massy, musée inclus</a>
</h3>
<p class="date"><i>Publié par <a href="#">Critor</a> le <time>05/06/2023 11:51</time></i></p>
Compte-rendu de notre visite au sein même des locaux de Casio France à Massy en mai 2023, passage par leur musée privé inclus. </div>
</li>
<li>
<a href="#"><img src="https://www.planet-casio.com/images/staff/CPC30-image-title.jpg"/></a>
<div>
<h3><a href="#">Le CPC#30 - Les Profondeurs ... C'est désormais terminé ... Bravo à tous les participants.</a>
</h3>
<p class="date"><i>Publié par <a href="#">Slyvtt</a> le <time>03/06/2023 21:36</time></i></p>
Aujourd'hui sonne la fin du CPC#30 avec à la clef 6 participations pour vous donner du fun. Graphs Monochromes et Couleurs sont à la fête avec des programmes en Basic et des Addins. </div>
</li>
<li>
<a href="#"><img src="https://www.planet-casio.com/images/staff/tdm9_miniature.jpg"/></a>
<div>
<h3><a href="#">Les Tutos du Mercredi débarquent sur Youtube !</a>
</h3>
<p class="date"><i>Publié par <a href="#">Shadow15510</a> le <time>29/05/2023 14:15</time></i></p>
</div>
</li>
<li>
<a href="#"><img src="https://www.planet-casio.com/images/staff/CPC30-image-thumb.jpg"/></a>
<div>
<h3><a href="#">Le CPC #30 - Les profondeurs !</a>
</h3>
<p class="date"><i>Publié par <a href="#">Lephenixnoir</a> le <time>27/05/2023 18:00</time></i></p>
Le concours CPC revient en force et c'est le moment de se plonger (métaphoriquement <i>et</i> littéralement !) dans le game design fin et la programmation sportive. Une semaine pour les programmer tous, et dans les profondeurs les li— oups ! </div>
</li>
<li><p><i><a href="https://www.planet-casio.com/Fr/forums/partie6-.html">Voir toutes les news</a></i></p></li>
{% endfor %}
<li><p><i><a href="/forum/actus/">Voir toutes les news</a></i></p></li>
</ul>
</div>
<div class="home-shoutbox" style="border: 1px solid #737373; padding: 20px">
<div id="v5shoutbox">
Ici yaura la shoutbox (plus tard)
</div>
<div class="home-shoutbox" style="border: 1px solid #e0e0e0">
{% include "widgets/v5shoutbox.html" %}
</div>
<div class="home-projects">
<h1>Projets du moment</h1>

View File

@ -0,0 +1,37 @@
{% extends "base/base.html" %}
{% import "widgets/editor.html" as widget_editor %}
{% import "widgets/user.html" as widget_user %}
{% set tabtitle = "Déplacer un commentaire" %}
{% block title %}
<a href='/forum'>Forum de Planète Casio</a> » Fusion de commentaire</h1>
{% endblock %}
{% block content %}
<section>
<h1>Fusionner deux commentaires</h1>
<table class="thread comment">
<tr>
<td class="author">{{ widget_user.profile(comment.author) }}</td>
<td><div>{{ comment.text | md }}</div></td>
</tr>
</table>
<form action="" method="post">
<h3></h3>
{{ merge_form.hidden_tag() }}
<div>
{{ merge_form.post.label }}
{{ merge_form.post }}
{% for error in merge_form.post.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>{{ merge_form.submit(class_='bg-ok') }}</div>
</form>
</section>
{% endblock %}

View File

@ -17,7 +17,7 @@
<section>
{% if p.thread.top_comment %}
{% call widget_thread.thread_leader(p.thread.top_comment) %}
{% call widget_thread.thread_leader(p.thread.top_comment, p) %}
<div class="info">
{{ widget_thread.post_actions(p) }}
</div>
@ -55,11 +55,11 @@
{% endcall %}
{% endif %}
{{ widget_pagination.paginate(comments, 'program_view', p) }}
{{ widget_pagination.paginate(comments, 'program_view', p, {}) }}
{{ widget_thread.thread(comments.items, p.thread.top_comment) }}
{{ widget_pagination.paginate(comments, 'program_view', p) }}
{{ widget_pagination.paginate(comments, 'program_view', p, {}) }}
{% if p.thread.locked %}

View File

@ -1,21 +1,77 @@
{% extends "base/base.html" %}
{% import "widgets/pagination.html" as widget_pagination with context %}
{% set tabtitle = "Recherche avancée" %}
{% block content %}
<section class="form">
<section class="search-page">
<h1>Recherche avancée</h1>
<form action="" method="get">
<div>
<form class="form" action="{{ url_for('search') }}" method="get">
{{ form.csrf_token }}
<div class="query">
{{ form.q.label }}
{{ form.q(value=request.args.get('q')) }}
{% for error in form.q.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
<div class="submit">
{{ form.submit(class_="bg-ok") }}
{% for error in form.submit.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div class="date">
{{ form.date.label }}
{{ form.date }}
{{ form.date(value=request.args.get('date')) }}
{% for error in form.date.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div class="sort">
{{ form.sortBy.label }}
{{ form.sortBy(value=request.args.get('sortBy')) }}
{% for error in form.sortBy.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div class="scope">
{{ form.scope.label }}
{{ form.scope(value=request.args.get('scope')) }}
{% for error in form.scope.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div class="search-results">
{{ widget_pagination.paginate(results, 'search', None, {
'q': request.args.get('q'),
'date': request.args.get('date'),
'sortBy': request.args.get('sortBy')}) }}
{% for i in results.items %}
<div>
{{ i.id }} {{ i.title }}<br>
{% if i.forum %}
<a href="{{ url_for('forum_topic', f=i.forum, page=(i , 'fin')) }}">{{ i.title }}</a>
{% elif i.thread %}
{% if i.thread.owner_program %}
{% if i.thread.owner_program[0].id == i.id %}
<a href="{{ url_for('program_view', page=(i.thread.owner_program[0], '')) }}">{{ i.thread.owner_program[0].name }}</a>
{% else %}
<a href="{{ url_for('redirect_post', postid=i.id) }}">{{ i.thread.owner_program[0].name }}</a>
{% endif %}
{% elif i.thread.owner_topic %}
<a href="{{ url_for('redirect_post', postid=i.id) }}">{{ i.thread.owner_topic[0].title }}</a>
{% endif %}
{% endif %}
{{ i.headline }}<br>
</div>
{% endfor %}
{{ widget_pagination.paginate(results, 'search', None, {
'q': request.args.get('q'),
'date': request.args.get('date'),
'sortBy': request.args.get('sortBy')}) }}
</div>
<div>{{ form.submit(class_="bg-ok") }}</div>
</form>
</section>
{% endblock %}

View File

@ -14,6 +14,7 @@
<li><a href="https://gitea.planet-casio.com">Gitea</a> (forge Git)</li>
<li><a href="https://wiki.planet-casio.com">Wiki</a> (wiki répétoriant tout un tas de trucs)</li>
<li><a href="https://bible.planet-casio.com">Bible</a> (la bible du programmeur Casio bas niveau)</li>
<li><a href="{{ url_for('calc_comparator') }}">Comparateur de calculatrices</a> (Un comparateur des calculatrices CASIO)</li>
</ul>
</p>
</section>

View File

@ -1,5 +1,8 @@
{% macro text_editor(field, label=True, autofocus=false) %}
<div class="editor">
<div id="editor_{{ field.name}}" class="editor">
<!-- Field label if needed -->
{{ field.label if label }}
<!-- Buttons for the text editor -->
<div class="btn-group">
<!-- Underline, Bold, Italic, Strikethrough -->
@ -110,9 +113,9 @@
<div class="modal" style="display: none">
<div>
<label for="link-desc-input">Description</label>
<input type="text" id="link-desc-input" name="link-desc-input">
<input type="text" class="link-desc-input" name="link-desc-input">
<label for="link-link-input">Lien</label>
<input type="url" id="link-link-input" name="link-link-input">
<input type="url" class="link-link-input" name="link-link-input">
</div>
<div>
<a type="button" class="button bg-ok" onclick="editor_insert_link(event, 'link-link-input', 'link-desc-input')">Valider</a>
@ -128,17 +131,17 @@
<div class="modal" style="display: none">
<div>
<label for="media-alt-input">Texte alternatif (sera affiché en cas d'erreur de chargement)</label>
<input type="text" id="media-alt-input" name="media-alt-input">
<input type="text" class="media-alt-input" name="media-alt-input">
<label for="media-link-input">Lien vers le média</label>
<input type="url" id="media-link-input" name="media-link-input">
<input type="url" class="media-link-input" name="media-link-input">
<fieldset title="Optionel mais peut être important si le type de média n'est pas correctement détecté">
<legend>
Type de média
</legend>
<label for="media-type-video" title="Vidéo">Vidéo</label>
<input type="radio" id="media-type-video" name="media-type" value="video" title="Vidéo" />
<input type="radio" class="media-type media-type-video" name="media-type" value="video" title="Vidéo" />
<label for="media-type-image" title="Image">Image</label>
<input type="radio" id="media-type-image" name="media-type" value="image" title="Image" />
<input type="radio" class="media-type media-type-image" name="media-type" value="image" title="Image" />
</fieldset>
</div>
<div>
@ -155,14 +158,14 @@
</svg>
</button>
-->
<div id="filler"></div>
<button id="manual_preview" type="button" onclick="preview(manual=true)" style="display: none" title="Rafraichir la prévisualisation">
<div class="filler"></div>
<button class="manual_preview" type="button" onclick="manual_preview('{{ field.name }}')" style="display: none" title="Rafraichir la prévisualisation">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/>
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/>
</svg>
</button>
<button id="toggle_preview" type="button" onclick="toggle_auto_preview()" title="Désactiver la prévisualisation">
<button class="toggle_preview" type="button" onclick="toggle_auto_preview()" title="Désactiver la prévisualisation">
<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"/>
@ -172,16 +175,21 @@
<a href="#">Aide</a>
</div>
<!-- Field & desc -->
{{ field.label if label }}
<!-- Field -->
{{ field() }}
<!-- Comment preview -->
<div id="editor_content_preview"></div>
<div class="editor_content_preview"></div>
<!-- Display errors -->
{% for error in field.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<script>
window.addEventListener("load", function(){
editor_setup("{{ field.name }}");
});
</script>
{% endmacro %}

View File

@ -13,6 +13,7 @@
{% set can_topcomm = auth and current_user.can_set_topcomment(post) %}
{% set can_move = auth and current_user.can_edit_post(post) and post.type == "comment" %}
{% set can_lock = auth and current_user.can_lock_thread(post) %}
{% set can_merge = auth and current_user.can_merge_post(post) %}
{% if post.type == "topic" %}
{% set suffix = " le sujet" %}
@ -32,6 +33,10 @@
<a href="{{ url_for('move_post', postid=post.id) }}">Déplacer</a>
{% endif %}
{% if can_merge %}
<a href="{{ url_for('merge_post', postid=post.id) }}">Fusioner</a>
{% endif %}
{% if can_punish and post.author.type == "member"%}
<a href="{{ url_for('delete_post', postid=post.id, penalty=False, csrf_token=csrf_token()) }}" onclick="return confirm('Le post sera supprimé.')">Supprimer{{ suffix }} (normal)</a>
<a href="{{ url_for('delete_post', postid=post.id, penalty=True, csrf_token=csrf_token()) }}" onclick="return confirm('Le post sera supprimé avec pénalité d\'XP.')">Supprimer{{ suffix }} (pénalité)</a>
@ -98,12 +103,20 @@
leader: Posts's top comment (actual rendering is delegated to caller) #}
{% macro thread_leader(leader) %}
{% macro thread_leader(leader, thread) %}
<table class="thread topcomment">
{# Empty line to get normal background (instead of alternate one) #}
<tr></tr>
<tr id="{{ leader.id }}">
<td class="author">{{ widget_user.profile(leader.author) }}</td>
<td class="author">
{% if thread.author_id != leader.author_id %}
<em>Auteur de la page:</em>
{{ widget_user.profile(thread.author, minimal = True) }}
<hr/>
<em>Auteur du post principal:</em>
{% endif %}
{{ widget_user.profile(leader.author) }}
</td>
<td class="message">{{ caller() }}</td>
</tr>
</table>

View File

@ -1,9 +1,10 @@
{% macro profile(user) %}
{% macro profile(user, minimal = False) %}
{% if user.type == "member" %}
<div class="profile">
<div class="profile{% if minimal %} minimal {% endif %}">
<img class="profile-avatar" src="{{ user.avatar_url }}" alt="Avatar de {{ user.name }}">
<div>
<div class="profile-name"><a href="{{ url_for('user', username=user.name) }}">{{ user.name }}</a></div>
{% if minimal == False %}
{% if user.title %}
<div class="profile-title" style="{{ user.title.css }}">{{ user.title.name }}</div>
{% else %}
@ -16,6 +17,7 @@
{% else %}
<div class="profile-xp profile-xp-100"><div style='width: {{ user.level[0] - 100 }}%;'></div></div>
{% endif %}
{% endif %}
</div>
</div>
{% else %}

View File

@ -1 +1 @@
../../../submodules/v5shoutbox/widget.html
../../../extra/v5shoutbox/widget.html

View File

@ -3,7 +3,7 @@ from wtforms.validators import Optional, ValidationError
def antibot_validator(form, field):
if field.data:
raise ValidationError('Bas les pattes!')
raise ValidationError('Bas les pattes!')
return True
class AntibotField(EmailField):

View File

@ -19,6 +19,10 @@ def render(*args, styles=[], scripts=[], modules=[], **kwargs):
'css/debugger.css',
'css/programs.css',
'css/editor.css',
'css/search.css',
# The order of these two is important; shoutbox.css overrides
'css/v5shoutbox.css',
'css/shoutbox.css',
]
scripts_ = [
'scripts/trigger_menu.js',
@ -28,6 +32,7 @@ def render(*args, styles=[], scripts=[], modules=[], **kwargs):
'scripts/filter.js',
'scripts/tag_selector.js',
'scripts/editor.js',
'scripts/v5shoutbox_irc.js',
]
modules_ = [
'scripts/emoji-picker-element/index.js',

View File

@ -56,3 +56,13 @@ def own_title(form, title):
return True
else:
return False
def attachment_exists(form, number):
try:
n = int(number.data)
if n <= 0 or n > len(form.attachments.data):
raise Exception()
except Exception as e:
raise ValidationError('Lillustration doit être le numéro dune pièce-jointe')
return True

View File

@ -86,6 +86,9 @@ class DefaultConfig(object):
ENABLE_FLASK_DEBUG_TOOLBAR = False
# Tab title prefix. Useful to dissociate local/dev/prod tabs
TABTITLE_PREFIX = ""
# Posts which are separated by more than this value (in seconds)
# can't be merged together (I thought that 10 minutes was a good default)
MERGE_AGE_THRESHOLD = 600
@staticmethod
def v5logger():

3
extra/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
calcdb/
emoji-picker-element/
v5shoutbox/

View File

@ -0,0 +1,34 @@
"""Search functions
Revision ID: a803745f7840
Revises: 5ffc4e562ed8
Create Date: 2023-06-27 23:10:06.088917
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a803745f7840'
down_revision = '5ffc4e562ed8'
branch_labels = None
depends_on = None
def upgrade():
op.execute("""CREATE FUNCTION websearch_to_tsquery_multilang(text) RETURNS tsquery AS $$
SELECT websearch_to_tsquery('french', $1) ||
websearch_to_tsquery('english', $1) ||
websearch_to_tsquery('simple', $1)
$$ LANGUAGE sql IMMUTABLE;""")
op.execute("""CREATE FUNCTION to_tsvector_multilang(text) RETURNS tsvector AS $$
SELECT to_tsvector('french', $1) ||
to_tsvector('english', $1) ||
to_tsvector('simple', $1)
$$ LANGUAGE sql IMMUTABLE;""")
def downgrade():
op.execute("DROP FUNCTION websearch_to_tsquery_multilang(text);")
op.execute("DROP FUNCTION to_tsvector_multilang(text);")

View File

@ -0,0 +1,36 @@
"""Topics: add summary and thumbnail
Revision ID: a8f539a93bd5
Revises: 5ffc4e562ed8
Create Date: 2023-07-25 21:44:55.782425
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'a8f539a93bd5'
down_revision = '5ffc4e562ed8'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('topic', schema=None) as batch_op:
batch_op.add_column(sa.Column('summary', sa.UnicodeText(), nullable=True))
batch_op.add_column(sa.Column('thumbnail_id', postgresql.UUID(as_uuid=True), nullable=True))
batch_op.create_foreign_key(None, 'attachment', ['thumbnail_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('topic', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.drop_column('thumbnail_id')
batch_op.drop_column('summary')
# ### end Alembic commands ###

View File

@ -0,0 +1,24 @@
"""empty message
Revision ID: dc99ddd17cf3
Revises: a803745f7840, a8f539a93bd5
Create Date: 2023-08-08 19:46:55.441606
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'dc99ddd17cf3'
down_revision = ('a803745f7840', 'a8f539a93bd5')
branch_labels = None
depends_on = None
def upgrade():
pass
def downgrade():
pass