Compare commits

...

69 Commits

Author SHA1 Message Date
Eragon c051f754a1
Fix redirection to topic page after comment merge 2024-03-10 19:42:07 +01:00
Eragon 853193cbd8
Allow to merge a comment with a comment which was posted later 2024-03-10 19:42:07 +01:00
Eragon 2801f8c1cd
Better display of the original author and author of the new main post 2024-03-10 19:42:07 +01:00
Eragon 92a5e4f05a
Allow any post to be set as main post #130 2024-03-10 19:42:07 +01:00
Darks 333892aa54
glados: updated announces 2024-03-09 14:23:47 +01:00
Darks 87090111ae
glados: add some 'say' messages 2024-03-09 14:22:08 +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
113 changed files with 2219 additions and 502 deletions

3
.gitignore vendored
View File

@ -22,6 +22,9 @@ test.*
# Autosaves
*.dia~
## Logging files
*.log
## Deployment files

6
.gitmodules vendored
View File

@ -1,6 +0,0 @@
[submodule "submodules/emoji-picker-element"]
path = submodules/emoji-picker-element
url = https://gitea.planet-casio.com/devs/emoji-picker-element.git
[submodule "submodules/emoji-picker-element-data"]
path = submodules/emoji-picker-element-data
url = https://gitea.planet-casio.com/devs/emoji-picker-element-data.git

View File

@ -2,20 +2,27 @@ 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
css: $(obj)
emoji: emoji-data
cd submodules/emoji-picker-element && npm install && npm run build
cp submodules/emoji-picker-element/i18n/*.js app/static/scripts/emoji-picker-element/
emoji-data:
cd submodules/emoji-picker-element-data/ && npm install && npm run build
cp submodules/emoji-picker-element-data/fr/emojibase/data.json app/static/scripts/emoji-picker-element/
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

@ -9,6 +9,7 @@ from config import FlaskApplicationSettings, V5Config
app = Flask(__name__)
app.config.from_object(FlaskApplicationSettings)
app.v5logger = V5Config.v5logger()
# Check security of secret
if FlaskApplicationSettings.SECRET_KEY == "a-random-secret-key":

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
@ -397,8 +402,7 @@ class Member(User):
Notify a user with a message.
An hyperlink can be added to redirect to the notification source
"""
return
n = Notification(self.id, message, href=href)
n = Notification(self, message, href=href)
db.session.add(n)
db.session.commit()

View File

@ -1,12 +1,12 @@
# Register routes here
from app.routes import index, search, users, tools, development
from app.routes import index, search, users, tools, development, chat
from app.routes.account import login, account, notification, polls
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

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

View File

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

View File

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

View File

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

View File

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

14
app/routes/chat.py Normal file
View File

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

View File

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

View File

@ -5,6 +5,7 @@ from sqlalchemy import desc
from app import app, db
from config import V5Config
from app.utils.render import render
from app.utils.glados import say, BOLD
from app.forms.forum import CommentForm, AnonymousCommentForm
from app.models.thread import Thread
from app.models.comment import Comment
@ -54,6 +55,11 @@ def forum_topic(f, page):
current_user.update_trophies('new-post')
flash('Message envoyé', 'ok')
app.v5logger.info(f"<{c.author.name}> has posted a the comment #{c.id}")
if f.is_default_accessible():
say(f"Nouveau commentaire de {author.name} sur le topic : {BOLD}{t.title}{BOLD}")
say(url_for('forum_topic', f=f, page=(t, "fin"), _anchor=str(c.id), _external=True))
# Redirect to empty the form
return redirect(url_for('forum_topic', f=f, page=(t, "fin"),
_anchor=str(c.id)))

View File

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

View File

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

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
@ -84,6 +91,10 @@ def edit_post(postid):
for a, file in attachments:
a.set_file(file)
flash('Modifications enregistrées', 'ok')
admin_msg = "[admin] " if current_user != p.author else ""
app.v5logger.info(f"{admin_msg}<{current_user.name}> has edited the post #{p.id}")
# Determine topic URL now, in case forum was changed
if isinstance(p, Topic):
return redirect(url_for('forum_topic', f=p.forum, page=(p,1)))
@ -98,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'])
@ -111,6 +123,10 @@ def delete_post(postid):
if not current_user.can_delete_post(p):
abort(403)
# Is a penalty deletion
is_penalty = request.args.get('penalty') == 'True' \
and current_user.priv('delete.posts')
# Users who need to have their trophies updated
authors = set()
@ -126,16 +142,21 @@ def delete_post(postid):
authors.add(comment.author)
if isinstance(p.author, Member):
factor = 3 if request.args.get('penalty') == 'True' else 1
factor = 3 if is_penalty else 1
p.author.add_xp(xp * factor)
db.session.merge(p.author)
authors.add(p.author)
admin_msg = "[admin] " if current_user != p.author else ""
p.delete()
db.session.commit()
for author in authors:
author.update_trophies("new-post")
flash("Le contenu a été supprimé", 'ok')
penalty_msg = " (with penalty)" if is_penalty else ""
app.v5logger.info(f"{admin_msg}<{current_user.name}> has deleted the post #{p.id}{penalty_msg}")
return redirect(next_page)
@ -149,6 +170,9 @@ def set_post_topcomment(postid):
comment.thread.top_comment = comment
db.session.add(comment.thread)
db.session.commit()
flash("Le post a été défini comme nouvel en-tête", 'ok')
admin_msg = "[admin] " if current_user != comment.author else ""
app.v5logger.info(f"{admin_msg}<{current_user.name}> has set a new top comment on thread #{comment.thread.id}")
return redirect(request.referrer)
@ -191,8 +215,11 @@ def move_post(postid):
comment.thread = thread
db.session.add(comment)
db.session.commit()
flash("Le topic a été déplacé", 'ok')
admin_msg = "[admin] " if current_user != comment.author else ""
app.v5logger.info(f"{admin_msg}<{current_user.name}> has moved the comment #{comment.id} to thread #{thread.id}")
return redirect(url_for('forum_topic', f=t.forum, page=(t,1)))
return render('post/move_post.html', comment=comment,
search_form=search_form, move_form=move_form)
@ -212,8 +239,54 @@ def lock_thread(postid):
db.session.commit()
if post.thread.locked:
flash(f"Le thread a été verrouillé")
flash(f"Le thread a été verrouillé", 'ok')
app.v5logger.info(f"[admin] <{current_user.name}> has locked the thread #{post.thread.id}")
else:
flash(f"Le thread a été déverrouillé")
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

@ -4,6 +4,7 @@ from app.models.program import Program
from app.models.comment import Comment
from app.models.thread import Thread
from app.utils.render import render
from app.utils.glados import say, BOLD
from app.forms.forum import CommentForm, AnonymousCommentForm
from config import V5Config
@ -41,6 +42,9 @@ def program_view(page):
current_user.update_trophies('new-post')
flash('Message envoyé', 'ok')
say(f"Nouveau commentaire de {author.name} sur le programme : {BOLD}{p.name}{BOLD}")
say(url_for('program_view', page=(p, "fin"), _anchor=str(c.id), _external=True))
# Redirect to empty the form
return redirect(url_for('program_view', page=(p, "fin"), _anchor=str(c.id)))

View File

@ -5,6 +5,7 @@ from app.models.comment import Comment
from app.models.tag import Tag
from app.models.attachment import Attachment
from app.utils.render import render
from app.utils.glados import say, BOLD
from app.forms.programs import ProgramCreationForm
from flask_login import login_required, current_user
@ -54,6 +55,10 @@ def program_submit():
current_user.update_trophies('new-program')
flash('Le programme a bien été soumis', 'ok')
return redirect(url_for('program_index'))
app.v5logger.info(f"<{p.author.name}> has submitted the program #{c.id}")
say(f"Nouveau programme de {current_user.name} : {BOLD}{p.name}{BOLD}")
say(url_for('program_view', page=(p, 1), _external=True))
return redirect(url_for('program_view', page=(p, 1)))
return render('/programs/submit.html', form=form)

View File

@ -1,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,11 +158,12 @@ 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);
border-radius: 1px;
overflow: hidden;
}
.skip-to-content-link:focus {
transform: translateY(0%);

View File

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

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

@ -0,0 +1 @@
../../../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

@ -161,6 +161,7 @@ button, .button, input[type="button"], input[type="submit"] {
background: var(--links);
color: var(--warn-text);
border-radius: 1px;
overflow: hidden;
&:focus {
transform: translateY(0%);

View File

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

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

@ -0,0 +1 @@
../../../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

@ -39,7 +39,7 @@
{% endfor %}
</div>
<div>
{{ form.guidelines.label }}
<label for="guidelines">J'accepte les <a href="#">CGU</a></label>
{{ form.guidelines() }}
{% for error in form.guidelines.errors %}
<span class="msgerror">{{ error }}</span>
@ -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

@ -14,8 +14,8 @@
{% include "base/flash.html" %}
<div id="main-content"></div>
{% block content %}
<div id="main-content"></div>
{% endblock %}
{% include "base/footer.html" %}

View File

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

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

@ -1,4 +1,6 @@
{% for s in scripts %}
<script type="text/javascript" src={{url_for('static', filename=s)}}></script>
{% endfor %}
<script type="module" src={{url_for('static', filename='scripts/emoji-picker-element/index.js')}}></script>
{% for m in modules %}
<script type="module" src={{url_for('static', filename=m)}}></script>
{% 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 %}

11
app/templates/chat.html Normal file
View File

@ -0,0 +1,11 @@
{% set tabtitle = "Shoutbox" %}
<!DOCTYPE html>
<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>
</html>

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

@ -8,11 +8,67 @@
{% block content %}
<section class="home-pinned-content">
<div>
<div class="home-banner">
<img src="https://www.planet-casio.com/storage/staff/CPC30-banner.png" />
</div>
<div class="home-welcome">
<h1>Bienvenue sur Planète Casio !</h1>
<p>Planète Casio est la communauté française de référence pour toutes les calculatrices Casio.
Apprenez à utiliser votre machine, téléchargez et partagez des programmes, ou initiez-vous à l'informatique sur le forum.
Ou bien venez développer des jeux avec nous pour passer le temps !</p>
<div>
<h2>Les calculatrices</h2>
<ul>
<li>Tout sur sa Casio</li>
<li>Graph 25+E II</li>
<li>Graph 35+E II</li>
<li>Graph 90+E</li>
<li>Classpad 400+E</li>
<li>Comparer les calculatrices CASIO</li>
</ul>
</div>
<div>
<h2>La communauté</h2>
<ul>
<li>Sinscrire ou se connecter</li>
<li>Index du forum</li>
<li>Nos jeux outils et cours</li>
</ul>
</div>
<div>
<h2>Programmer</h2>
<ul>
<li>Apprendre à programmer</li>
<li>Articles et astuces</li>
</ul>
</div>
</div>
<div class="home-news">
<h1>Actualitées</h1>
<ul>
<li>Inscription : dans le menu "Compte" à gauche (les comptes seront ultimement reconnectés à la version originale du site)</li>
<li>Le forum est fonctionnel, les programmes arrivent sous peu.</li>
<li>Pour toute demande particulière, vous pouvez envoyer un email à <code>contact (at) planet-casio (dot) com</code>.</li>
{% for n in last_news %}
<li>
<a href="{{ url_for('forum_topic', f=n.forum, page=(n,'fin'))}}"><img src="{{ n.thumbnail.url }}"/></a>
<div>
<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>
{% 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 #e0e0e0">
{% include "widgets/v5shoutbox.html" %}
</div>
<div class="home-projects">
<h1>Projets du moment</h1>
<ul>
<li>La v5, bien sûr</li>
<li>Chaos drop</li>
<li>Un easter egg</li>
</ul>
</div>
</section>

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

@ -2,7 +2,7 @@
{% import "widgets/editor.html" as widget_editor %}
{% import "widgets/tag_selector.html" as widget_tag_selector with context %}
{% set tabtitle = f"Programmes - Soumettre un programme" %}
{% set tabtitle = "Programmes - Soumettre un programme" %}
{% block title %}
<a href="{{ url_for('program_index') }}">Programmes</a> » <h1>Soumettre un programme</h1>

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,7 +33,11 @@
<a href="{{ url_for('move_post', postid=post.id) }}">Déplacer</a>
{% endif %}
{% if can_punish %}
{% 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>
{% elif can_delete %}
@ -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 %}

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