Compare commits
126 Commits
Author | SHA1 | Date |
---|---|---|
Darks | a2cf0b2c78 | |
Darks | 54d27478e1 | |
Darks | 04fb3225c1 | |
Darks | 0974e73411 | |
Darks | 30be296fd6 | |
Darks | 74862634df | |
Darks | e8ffbd598e | |
Darks | 98878bda7d | |
Darks | 1e618fafd1 | |
Darks | e0e02d5423 | |
Darks | 83d5a0b385 | |
Darks | 4b8ce0334a | |
Darks | 19d09a71df | |
Darks | 11daef02a1 | |
Darks | 255ce8ad60 | |
Darks | 415cfd8d8f | |
Darks | bf8f766131 | |
Darks | d63173b48d | |
Darks | bd559b9fad | |
Darks | 5e00e41bf1 | |
Darks | 4516f775cc | |
Darks | 177fb7d84f | |
Darks | 9f5b607c45 | |
Darks | 275eacbcbb | |
Darks | 51d0ce1129 | |
Darks | 07f980c207 | |
Darks | b7930c96a4 | |
Darks | 6afb6085d1 | |
Darks | 9341c5883c | |
Darks | d447372bf3 | |
Darks | 04e317285f | |
Darks | 4427688193 | |
Darks | b4341ed0f8 | |
Darks | bdf23d8a67 | |
Darks | a2e408daf9 | |
Darks | 94badf4bad | |
Darks | cdbecac166 | |
Darks | d480a95e43 | |
Eragon | 90ac259177 | |
Eragon | 9da11b62ca | |
Darks | 16bcfe9e30 | |
Darks | ab03daf527 | |
Darks | da96de2f14 | |
Darks | 4e4508c4fd | |
Darks | 0a85f1fbee | |
Darks | ecefb03bb1 | |
Darks | 47fdd68e30 | |
Darks | cf61b43e17 | |
Eragon | 907708154d | |
Darks | 1be5ec93da | |
Darks | f6be314ed7 | |
Eragon | 27ca00ffce | |
Darks | e99e45b4ca | |
Darks | a4d514f2d3 | |
Eragon | 055db1d164 | |
Darks | 82b402229f | |
Darks | 9de5b27d6e | |
Eragon | dac218b3b9 | |
Eragon | 134eaa4d58 | |
Darks | 1434b3152b | |
Darks | b5e875e136 | |
Darks | 662882cc15 | |
Darks | 089e851b4c | |
Darks | 087dd56cb2 | |
Darks | c7318c6dd6 | |
Darks | a38c5378e8 | |
Darks | 7f63577c4f | |
Eragon | 3dc935c4f6 | |
Eragon | 7884ca8bec | |
Darks | f7e9715572 | |
Darks | 9ef8ae26d0 | |
Eragon | f3e47bd082 | |
Darks | f111983bde | |
Darks | d4e1b05c29 | |
Darks | f6706f2b66 | |
Darks | 279c194a59 | |
Darks | a8756c2990 | |
Darks | 3691520399 | |
Eragon | f62842216d | |
Darks | ad41b5be38 | |
Lephe | 2ed10a5a9d | |
Darks | 0cb3966de6 | |
Darks | a194136d47 | |
Darks | 67899f3e32 | |
Louis Chauvet | a8090908e9 | |
Louis Chauvet | c7743bfa78 | |
Louis Chauvet | 49a93db5d9 | |
Darks | aebe09de68 | |
Darks | c5d9b39f06 | |
Eragon | e3e38fde6f | |
Eragon | c80398cba3 | |
Eragon | ac654d0232 | |
Eragon | d2365a8444 | |
Eragon | 75756c3b36 | |
Lephe | e0dc6944f7 | |
Lephe | 3ad3eca470 | |
Lephe | 79e5af7924 | |
Lephe | 8a0ba309e0 | |
Lephe | d1a8333cae | |
Lephe | 10e3c88bd4 | |
Lephe | 9f30bd36a0 | |
Lephe | 35f1335f64 | |
Lephe | 153f303857 | |
Lephe | aa75ff09a1 | |
Darks | de83f09024 | |
Darks | a29657da24 | |
Darks | 035e4f9062 | |
Eragon | b628510455 | |
Eragon | 2b8a78fe20 | |
Darks | 0a33161af0 | |
Darks | 7cad3d4345 | |
Darks | 5bf90f9d05 | |
Darks | ab6275c08f | |
Eragon | dbef50cb86 | |
Darks | 2e80a56596 | |
Darks | 6d43d742c8 | |
Darks | 7971e47522 | |
Darks | c2fbef7ace | |
Darks | 15a4d38ea0 | |
Darks | 0c7c408e40 | |
Darks | 4868774b96 | |
Darks | f508536805 | |
Lephe | eeaab86d0a | |
Lephe | 11b19af199 | |
Darks | 201e961ba2 | |
Darks | 81c910832b |
|
@ -6,13 +6,12 @@ app/static/avatars/
|
|||
## Devlopement files
|
||||
|
||||
# virtualenv
|
||||
requirements.txt
|
||||
venv/
|
||||
.venv/
|
||||
# pipenv
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
# Sublime Text files
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
## Deployment files
|
||||
|
||||
|
@ -22,7 +21,13 @@ uwsgi.ini
|
|||
run.sh
|
||||
# Update script to pull repository from SSH
|
||||
update.sh
|
||||
# Config to set up some server specific config
|
||||
local_config.py
|
||||
|
||||
## Wiki
|
||||
|
||||
wiki/
|
||||
|
||||
## Personal folder
|
||||
|
||||
exclude/
|
||||
|
|
25
Pipfile
|
@ -1,25 +0,0 @@
|
|||
[[source]]
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
flask = ">=1.0"
|
||||
flask-wtf = ">=0.14"
|
||||
flask-login = ">=0.4"
|
||||
flask-migrate = ">=2.3"
|
||||
flask-sqlalchemy = ">=2.3"
|
||||
flask-script = ">=2.0"
|
||||
uwsgi = ">=2.0"
|
||||
psycopg2-binary = ">=2.7"
|
||||
pyyaml = ">=3.13"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
[scripts]
|
||||
init = "scripts/init.sh"
|
||||
migrate = "scripts/migrate.sh"
|
||||
run_dev = "scripts/run_dev.sh"
|
|
@ -1,6 +1,6 @@
|
|||
# Bibliothèques nécessaires
|
||||
|
||||
Attention, l'environnement est sous `python3`. Vérifiez que ce soit bien le cas sur votre config, quitte
|
||||
Attention, l'environnement est sous `python3`. Vérifiez que ce soit bien le cas sur votre config, quitte
|
||||
à faire un virtual environment.
|
||||
|
||||
La liste de paquets fourni est pour Archlinux, les paquets peuvent avoir des noms légèrement différents dans votre distribution.
|
||||
|
@ -12,7 +12,10 @@ python-flask-migrate
|
|||
python-flask-script
|
||||
python-flask-sqlalchemy
|
||||
python-flask-wtf
|
||||
python-ldap
|
||||
python-uwsgi
|
||||
python-psycopg2
|
||||
python-pillow
|
||||
python-pyyaml
|
||||
python-slugify
|
||||
```
|
||||
|
|
2
V5.py
|
@ -1,6 +1,6 @@
|
|||
from app import app, db
|
||||
from app.models.users import User, Guest, Member, Group, GroupPrivilege
|
||||
# from app.models.models import Post
|
||||
from app.models.topic import Topic
|
||||
|
||||
|
||||
@app.shell_context_processor
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
from flask import Flask
|
||||
from flask import Flask, g
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_migrate import Migrate
|
||||
from flask_login import LoginManager
|
||||
from config import Config
|
||||
import time
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(Config)
|
||||
|
||||
# Check security of secret
|
||||
if Config.SECRET_KEY == "a-random-secret-key":
|
||||
raise Exception("Please use a strong secret key!")
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
migrate = Migrate(app, db)
|
||||
|
||||
|
@ -13,9 +19,32 @@ login = LoginManager(app)
|
|||
login.login_view = 'login'
|
||||
login.login_message = "Veuillez vous authentifier avant de continuer."
|
||||
|
||||
from app.utils.converters import *
|
||||
app.url_map.converters['topicslug'] = TopicSlugConverter
|
||||
app.url_map.converters['forum'] = ForumConverter
|
||||
|
||||
|
||||
@app.before_request
|
||||
def request_time():
|
||||
g.request_start_time = time.time()
|
||||
g.request_time = lambda: "%.5fs" % (time.time() - g.request_start_time)
|
||||
|
||||
|
||||
from app.processors.menu import menu_processor
|
||||
from app.processors.utilities import utilities_processor
|
||||
|
||||
from app import models # IDK why this is here, but it works
|
||||
from app.routes import index, search, users # To load routes at initialization
|
||||
from app.routes.account import login, account
|
||||
from app.routes.admin import index, groups, account, trophies
|
||||
from app.models.comment import Comment
|
||||
from app.models.thread import Thread
|
||||
from app.models.forum import Forum
|
||||
from app.models.topic import Topic
|
||||
from app.models.notification import Notification
|
||||
|
||||
from app.routes import index, search, users, tools # To load routes at initialization
|
||||
from app.routes.account import login, account, notification
|
||||
from app.routes.admin import index, groups, account, trophies, forums
|
||||
from app.routes.forum import index, topic
|
||||
|
||||
from app.utils import pluralize # To use pluralize into the templates
|
||||
from app.utils import date
|
||||
from app.utils import is_title
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
# This file is a list of forums to create when setting up Planète Casio.
|
||||
#
|
||||
# * Keys are used as URLs paths and for unique identification.
|
||||
# * Prefixes represent the privilege category for a forum. Owning privileges
|
||||
# with this prefix allows the user to post in this forum and all its
|
||||
# sub-forum regardless of their settings ("forum-root-*" are hyper powerful).
|
||||
# * For open forums, use the prefix "open".
|
||||
|
||||
/:
|
||||
name: Forum de Planète Casio
|
||||
prefix: root
|
||||
|
||||
# News
|
||||
|
||||
/actus:
|
||||
name: Actualités
|
||||
prefix: news
|
||||
|
||||
/actus/projets:
|
||||
name: Actualités des projets
|
||||
prefix: projectnews
|
||||
descr: Nouveautés des projets de la communauté.
|
||||
|
||||
/actus/calc:
|
||||
name: Actualités des constructeurs de calculatrices
|
||||
prefix: calcnews
|
||||
descr: Nouveautés CASIO, nouveaux modèles de calculatrices, mises à jour du
|
||||
système ou nouveautés d'autres constructeurs.
|
||||
|
||||
/actus/evenements:
|
||||
name: Événements organisés par Planète Casio
|
||||
prefix: eventnews
|
||||
descr: Tous les événements organisés par Planète Casio ou la communauté.
|
||||
|
||||
/actus/autres:
|
||||
name: Autres nouveautés
|
||||
prefix: othernews
|
||||
descr: Actualités non catégorisées.
|
||||
|
||||
# Help
|
||||
|
||||
/aide:
|
||||
name: Aide et questions
|
||||
prefix: help
|
||||
|
||||
/aide/transferts:
|
||||
name: Questions sur les tranferts
|
||||
prefix: transferhelp
|
||||
descr: Questions sur le transfert de fichiers et l'installation de programmes
|
||||
sur la calculatrice.
|
||||
|
||||
/aide/calc:
|
||||
name: Question sur l'utilisation des calculatrices
|
||||
prefix: calchelp
|
||||
descr: Questions sur l'utilisation des applications de la calculatrice,
|
||||
paramètres, formats de fichiers...
|
||||
|
||||
/aide/prog:
|
||||
name: Questions de programmation
|
||||
prefix: proghelp
|
||||
descr: Questions sur le développement et le debuggage de programmes.
|
||||
|
||||
/aide/autres:
|
||||
name: Autres questions
|
||||
prefix: otherhelp
|
||||
descr: Questions non catégorisées.
|
||||
|
||||
# Projects
|
||||
|
||||
/projets:
|
||||
name: Forum des projets
|
||||
prefix: projects
|
||||
|
||||
/projets/jeux:
|
||||
name: Projets de jeux
|
||||
prefix: gameprojects
|
||||
descr: Projets de jeux pour calculatrices, tous langages confondus et tous
|
||||
modèles de calculatrices confondus.
|
||||
|
||||
/projets/applis:
|
||||
name: Projets d'applications, utilitaires, outils pour calculatrice
|
||||
prefix: appprojects
|
||||
descr: Projets d'applications (hors jeux) pour calculatrice, tous langages et
|
||||
modèles confondus.
|
||||
|
||||
/projets/outils:
|
||||
name: Projets pour d'autres plateformes
|
||||
prefix: toolprojetcs
|
||||
descr: Tous les projets tournant sur ordinateur, téléphone, ou toute autre
|
||||
plateforme que la calculatrice.
|
||||
|
||||
# Community
|
||||
|
||||
/communaute:
|
||||
name: Vie communautaire
|
||||
prefix: community
|
||||
descr: Projets pour Planète Casio, remarques sur le fonctionnement du site et
|
||||
de sa communauté.
|
||||
|
||||
# Discussion
|
||||
|
||||
/discussion:
|
||||
name: Discussion
|
||||
prefix: discussion
|
||||
descr: Sujets hors-sujet et discussion libre.
|
|
@ -1,8 +1,8 @@
|
|||
-
|
||||
name: Administrateur
|
||||
css: "color: #ee0000"
|
||||
css: "color: #ee0000;"
|
||||
descr: "Vous voyez Chuck Norris ? Pareil."
|
||||
privs: access-admin-board access-assoc-board write-news
|
||||
privs: access-admin-board access-assoc-board write-news write-anywhere
|
||||
upload-shared-files delete-shared-files
|
||||
edit-posts delete-posts scheduled-posting
|
||||
delete-content move-public-content move-private-content showcase-content
|
||||
|
@ -11,9 +11,10 @@
|
|||
shoutbox-kick shoutbox-ban
|
||||
unlimited-pms footer-statistics community-login
|
||||
access-admin-panel edit-account delete-account edit-trophies
|
||||
delete_notification
|
||||
-
|
||||
name: Modérateur
|
||||
css: "color: green"
|
||||
css: "color: green;"
|
||||
descr: "Maîtres du kick, ils sont là pour faire respecter un semblant d'ordre."
|
||||
privs: access-admin-board
|
||||
edit-posts delete-posts
|
||||
|
@ -23,7 +24,7 @@
|
|||
unlimited-pms
|
||||
-
|
||||
name: Développeur
|
||||
css: "color: #4169e1"
|
||||
css: "color: #4169e1;"
|
||||
descr: "Les développeurs maintiennent et améliorent le code du site."
|
||||
privs: access-admin-board
|
||||
upload-shared-files delete-shared-files
|
||||
|
@ -33,7 +34,7 @@
|
|||
access-admin-panel
|
||||
-
|
||||
name: Rédacteur
|
||||
css: "color: blue"
|
||||
css: "color: blue;"
|
||||
descr: "Rédigent les meilleurs articles de la page d'accueil, rien que pour
|
||||
vous <3"
|
||||
privs: access-admin-board write-news
|
||||
|
@ -42,7 +43,7 @@
|
|||
showcase-content edit-static-content
|
||||
-
|
||||
name: Responsable communauté
|
||||
css: "color: DarkOrange"
|
||||
css: "color: DarkOrange;"
|
||||
descr: "Anime les pages Twitter et Facebook de Planète Casio et surveille
|
||||
l'évolution du monde autour de nous !"
|
||||
privs: access-admin-board write-news
|
||||
|
@ -51,22 +52,26 @@
|
|||
showcase-content
|
||||
-
|
||||
name: Partenaire
|
||||
css: "color: purple"
|
||||
css: "color: purple;"
|
||||
descr: "Membres de l'équipe d'administration des sites partenaires."
|
||||
privs: write-news
|
||||
upload-shared-files delete-shared-files
|
||||
scheduled-posting
|
||||
-
|
||||
name: Compte communautaire
|
||||
css: "background:#d8d8d8; border-radius:4px; color:#303030; padding:1px 2px"
|
||||
css: "background:#d8d8d8; border-radius:4px; color:#303030; padding:1px 2px;"
|
||||
descr: "Compte à usage général de l'équipe de Planète Casio."
|
||||
-
|
||||
name: Robot
|
||||
css: "color: #cf25d0"
|
||||
css: "color: #cf25d0;"
|
||||
descr: "♫ Je suis Nono, le petit robot, l'ami d'Ulysse ♫"
|
||||
privs: shoutbox-post shoutbox-kick shoutbox-ban
|
||||
-
|
||||
name: Membre de CreativeCalc
|
||||
css: "color: #222222"
|
||||
css: "color: #222222;"
|
||||
descr: "CreativeCalc est l'association qui gère Planète Casio."
|
||||
privs: access-assoc-board
|
||||
-
|
||||
name: No login
|
||||
css: "color: #888888;"
|
||||
descr: "Compte dont l'accès au site est désactivé."
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SubmitField, DecimalField, SelectField
|
||||
from wtforms.fields.html5 import DateField
|
||||
from wtforms.fields.html5 import DateField, EmailField
|
||||
from wtforms.validators import DataRequired, InputRequired, Optional, Email, EqualTo
|
||||
from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty
|
||||
from app.models.trophies import Trophy
|
||||
|
@ -8,53 +8,226 @@ import app.utils.validators as vd
|
|||
|
||||
|
||||
class RegistrationForm(FlaskForm):
|
||||
username = StringField('Pseudonyme', description='Ce nom est définitif !', validators=[DataRequired(), vd.name_valid, vd.name_available])
|
||||
email = StringField('Adresse Email', validators=[DataRequired(), Email(), vd.email])
|
||||
password = PasswordField('Mot de passe', validators=[DataRequired(), vd.password])
|
||||
password2 = PasswordField('Répéter le mot de passe', validators=[DataRequired(), EqualTo('password')])
|
||||
guidelines = BooleanField("""J'accepte les <a href="#">CGU</a>""", validators=[DataRequired()])
|
||||
newsletter = BooleanField('Inscription à la newsletter', description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.')
|
||||
submit = SubmitField("S'inscrire")
|
||||
username = StringField(
|
||||
'Pseudonyme',
|
||||
description='Ce nom est définitif !',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
vd.name_valid,
|
||||
vd.name_available,
|
||||
],
|
||||
)
|
||||
email = EmailField(
|
||||
'Adresse Email',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
Email(message="Adresse email invalide."),
|
||||
vd.email,
|
||||
],
|
||||
)
|
||||
password = PasswordField(
|
||||
'Mot de passe',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
vd.password,
|
||||
],
|
||||
)
|
||||
password2 = PasswordField(
|
||||
'Répéter le mot de passe',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
EqualTo('password', message="Les mots de passe doivent être identiques."),
|
||||
],
|
||||
)
|
||||
guidelines = BooleanField(
|
||||
"""J'accepte les <a href="#">CGU</a>""",
|
||||
validators=[
|
||||
DataRequired(),
|
||||
],
|
||||
)
|
||||
newsletter = BooleanField(
|
||||
'Inscription à la newsletter',
|
||||
description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.',
|
||||
)
|
||||
submit = SubmitField(
|
||||
"S'inscrire",
|
||||
)
|
||||
|
||||
|
||||
class UpdateAccountForm(FlaskForm):
|
||||
avatar = FileField('Avatar', validators=[Optional(), vd.avatar])
|
||||
email = StringField('Adresse email', validators=[Optional(), Email(), vd.email, vd.old_password])
|
||||
password = PasswordField('Mot de passe', validators=[Optional(), vd.password, vd.old_password])
|
||||
password2 = PasswordField('Répéter le mot de passe', validators=[Optional(), EqualTo('password')])
|
||||
old_password = PasswordField('Mot de passe actuel', validators=[Optional()])
|
||||
birthday = DateField('Anniversaire', validators=[Optional()])
|
||||
signature = TextAreaField('Signature', validators=[Optional()])
|
||||
biography = TextAreaField('Présentation', validators=[Optional()])
|
||||
newsletter = BooleanField('Inscription à la newsletter', description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.')
|
||||
avatar = FileField(
|
||||
'Avatar',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.avatar,
|
||||
],
|
||||
)
|
||||
email = EmailField(
|
||||
'Adresse email',
|
||||
validators=[
|
||||
Optional(),
|
||||
Email(message="Addresse email invalide."),
|
||||
vd.email,
|
||||
vd.old_password,
|
||||
],
|
||||
)
|
||||
password = PasswordField(
|
||||
'Mot de passe',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.password,
|
||||
vd.old_password,
|
||||
],
|
||||
)
|
||||
password2 = PasswordField(
|
||||
'Répéter le mot de passe',
|
||||
validators=[
|
||||
Optional(),
|
||||
EqualTo('password', message="Les mots de passe doivent être identiques."),
|
||||
],
|
||||
)
|
||||
old_password = PasswordField(
|
||||
'Mot de passe actuel',
|
||||
validators=[
|
||||
Optional(),
|
||||
],
|
||||
)
|
||||
birthday = DateField(
|
||||
'Anniversaire',
|
||||
validators=[
|
||||
Optional(),
|
||||
],
|
||||
)
|
||||
signature = TextAreaField(
|
||||
'Signature',
|
||||
validators=[
|
||||
Optional(),
|
||||
]
|
||||
)
|
||||
biography = TextAreaField(
|
||||
'Présentation',
|
||||
validators=[
|
||||
Optional(),
|
||||
]
|
||||
)
|
||||
newsletter = BooleanField(
|
||||
'Inscription à la newsletter',
|
||||
description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.',
|
||||
)
|
||||
submit = SubmitField('Mettre à jour')
|
||||
|
||||
|
||||
class DeleteAccountForm(FlaskForm):
|
||||
delete = BooleanField('Confirmer la suppression', validators=[DataRequired()], description='Attention, cette opération est irréversible !')
|
||||
old_password = PasswordField('Mot de passe', validators=[DataRequired(), vd.old_password])
|
||||
submit = SubmitField('Supprimer le compte')
|
||||
delete = BooleanField(
|
||||
'Confirmer la suppression',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
],
|
||||
description='Attention, cette opération est irréversible !'
|
||||
)
|
||||
old_password = PasswordField(
|
||||
'Mot de passe',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
vd.old_password,
|
||||
],
|
||||
)
|
||||
submit = SubmitField(
|
||||
'Supprimer le compte',
|
||||
)
|
||||
|
||||
|
||||
class AdminUpdateAccountForm(FlaskForm):
|
||||
username = StringField('Pseudonyme', validators=[Optional(), vd.name_valid])
|
||||
avatar = FileField('Avatar', validators=[Optional(), vd.avatar])
|
||||
email = StringField('Adresse email', validators=[Optional(), Email(), vd.email])
|
||||
email_validate = BooleanField("Envoyer un email de validation à la nouvelle adresse", description="Si décoché, l'utilisateur devra demander explicitement un email de validation, ou faire valider son adresse email par un administrateur.")
|
||||
password = PasswordField('Mot de passe', description="L'ancien mot de passe ne pourra pas être récupéré !", validators=[Optional(), vd.password])
|
||||
xp = DecimalField('XP', validators=[Optional()])
|
||||
birthday = DateField('Anniversaire', validators=[Optional()])
|
||||
signature = TextAreaField('Signature', validators=[Optional()])
|
||||
biography = TextAreaField('Présentation', validators=[Optional()])
|
||||
newsletter = BooleanField('Inscription à la newsletter', description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.')
|
||||
submit = SubmitField('Mettre à jour')
|
||||
username = StringField(
|
||||
'Pseudonyme',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.name_valid,
|
||||
],
|
||||
)
|
||||
avatar = FileField(
|
||||
'Avatar',
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.avatar,
|
||||
],
|
||||
)
|
||||
email = EmailField(
|
||||
'Adresse email',
|
||||
validators=[
|
||||
Optional(),
|
||||
Email(message="Addresse email invalide."),
|
||||
vd.email,
|
||||
],
|
||||
)
|
||||
email_validate = BooleanField(
|
||||
"Envoyer un email de validation à la nouvelle adresse",
|
||||
description="Si décoché, l'utilisateur devra demander explicitement un email "\
|
||||
"de validation, ou faire valider son adresse email par un administrateur.",
|
||||
)
|
||||
password = PasswordField(
|
||||
'Mot de passe',
|
||||
description="L'ancien mot de passe ne pourra pas être récupéré !",
|
||||
validators=[
|
||||
Optional(),
|
||||
vd.password,
|
||||
],
|
||||
)
|
||||
xp = DecimalField(
|
||||
'XP',
|
||||
validators=[
|
||||
Optional(),
|
||||
]
|
||||
)
|
||||
birthday = DateField(
|
||||
'Anniversaire',
|
||||
validators=[
|
||||
Optional(),
|
||||
],
|
||||
)
|
||||
signature = TextAreaField(
|
||||
'Signature',
|
||||
validators=[
|
||||
Optional(),
|
||||
],
|
||||
)
|
||||
biography = TextAreaField(
|
||||
'Présentation',
|
||||
validators=[
|
||||
Optional(),
|
||||
],
|
||||
)
|
||||
newsletter = BooleanField(
|
||||
'Inscription à la newsletter',
|
||||
description='Un mail par trimestre environ, pour être prévenu des concours, évènements et nouveautés.',
|
||||
)
|
||||
submit = SubmitField(
|
||||
'Mettre à jour',
|
||||
)
|
||||
|
||||
|
||||
class AdminAccountEditTrophyForm(FlaskForm):
|
||||
# Boolean inputs are generated on-the-fly from trophy list
|
||||
submit = SubmitField('Modifier')
|
||||
submit = SubmitField(
|
||||
'Modifier',
|
||||
)
|
||||
|
||||
|
||||
class AdminAccountEditGroupForm(FlaskForm):
|
||||
# Boolean inputs are generated on-the-fly from group list
|
||||
submit = SubmitField(
|
||||
'Modifier',
|
||||
)
|
||||
|
||||
|
||||
class AdminDeleteAccountForm(FlaskForm):
|
||||
delete = BooleanField('Confirmer la suppression', validators=[DataRequired()], description='Attention, cette opération est irréversible !')
|
||||
submit = SubmitField('Supprimer le compte')
|
||||
delete = BooleanField(
|
||||
'Confirmer la suppression',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
],
|
||||
description='Attention, cette opération est irréversible !',
|
||||
)
|
||||
submit = SubmitField(
|
||||
'Supprimer le compte',
|
||||
)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, FormField, SubmitField, TextAreaField
|
||||
from wtforms.validators import DataRequired, Length
|
||||
|
||||
class TopicCreationForm(FlaskForm):
|
||||
title = StringField('Nom du sujet',
|
||||
validators=[DataRequired(), Length(min=3, max=32)])
|
||||
message = TextAreaField('Message principal', validators=[DataRequired()])
|
||||
submit = SubmitField('Créer le sujet')
|
||||
|
||||
class CommentForm(FlaskForm):
|
||||
message = TextAreaField('Commentaire', validators=[DataRequired()])
|
||||
submit = SubmitField('Commenter')
|
|
@ -4,7 +4,21 @@ from wtforms.validators import DataRequired
|
|||
|
||||
|
||||
class LoginForm(FlaskForm):
|
||||
username = StringField('Identifiant', validators=[DataRequired()])
|
||||
password = PasswordField('Mot de passe', validators=[DataRequired()])
|
||||
remember_me = BooleanField('Se souvenir de moi')
|
||||
submit = SubmitField('Connexion')
|
||||
username = StringField(
|
||||
'Identifiant',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
],
|
||||
)
|
||||
password = PasswordField(
|
||||
'Mot de passe',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
],
|
||||
)
|
||||
remember_me = BooleanField(
|
||||
'Se souvenir de moi',
|
||||
)
|
||||
submit = SubmitField(
|
||||
'Connexion',
|
||||
)
|
||||
|
|
|
@ -6,10 +6,27 @@ from wtforms.validators import DataRequired, Optional
|
|||
|
||||
# TODO: compléter le formulaire de recherche avancée
|
||||
class AdvancedSearchForm(FlaskForm):
|
||||
q = StringField('Rechercher :', validators=[DataRequired()])
|
||||
date = DateField('Date', validators=[Optional()])
|
||||
submit = SubmitField('Affiner la recherche')
|
||||
q = StringField(
|
||||
'Rechercher :',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
],
|
||||
)
|
||||
date = DateField(
|
||||
'Date',
|
||||
validators=[
|
||||
Optional(),
|
||||
],
|
||||
)
|
||||
submit = SubmitField(
|
||||
'Affiner la recherche',
|
||||
)
|
||||
|
||||
|
||||
class SearchForm(FlaskForm):
|
||||
q = StringField('Rechercher', validators=[DataRequired()])
|
||||
q = StringField(
|
||||
'Rechercher',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
],
|
||||
)
|
||||
|
|
|
@ -5,12 +5,38 @@ from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty
|
|||
|
||||
|
||||
class TrophyForm(FlaskForm):
|
||||
name = StringField('Nom', validators=[DataRequired()])
|
||||
icon = FileField('Icône')
|
||||
title = BooleanField('Titre', description='Un titre peut être affiché en dessous du pseudo.', validators=[Optional()])
|
||||
css = StringField('CSS', description='CSS appliqué au titre, le cas échéant.')
|
||||
submit = SubmitField('Envoyer')
|
||||
name = StringField(
|
||||
'Nom',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
],
|
||||
)
|
||||
icon = FileField(
|
||||
'Icône',
|
||||
)
|
||||
title = BooleanField(
|
||||
'Titre',
|
||||
description='Un titre peut être affiché en dessous du pseudo.',
|
||||
validators=[
|
||||
Optional(),
|
||||
],
|
||||
)
|
||||
css = StringField(
|
||||
'CSS',
|
||||
description='CSS appliqué au titre, le cas échéant.',
|
||||
)
|
||||
submit = SubmitField(
|
||||
'Envoyer',
|
||||
)
|
||||
|
||||
class DeleteTrophyForm(FlaskForm):
|
||||
delete = BooleanField('Confirmer la suppression', validators=[DataRequired()], description='Attention, cette opération est irréversible !')
|
||||
submit = SubmitField('Supprimer le trophée')
|
||||
delete = BooleanField(
|
||||
'Confirmer la suppression',
|
||||
validators=[
|
||||
DataRequired(),
|
||||
],
|
||||
description='Attention, cette opération est irréversible !',
|
||||
)
|
||||
submit = SubmitField(
|
||||
'Supprimer le trophée',
|
||||
)
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
from app import db
|
||||
from app.models.post import Post
|
||||
from sqlalchemy.orm import backref
|
||||
|
||||
class Comment(Post):
|
||||
__tablename__ = 'comment'
|
||||
__mapper_args__ = {'polymorphic_identity': __tablename__}
|
||||
|
||||
# ID of the underlying Post object
|
||||
id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True)
|
||||
|
||||
# Comment contents
|
||||
text = db.Column(db.UnicodeText)
|
||||
|
||||
# Parent thread
|
||||
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'),
|
||||
nullable=False)
|
||||
thread = db.relationship('Thread',
|
||||
backref=backref('comments', lazy='dynamic'),
|
||||
foreign_keys=thread_id)
|
||||
|
||||
def __init__(self, author, text, thread):
|
||||
"""
|
||||
Create a new Comment in a thread.
|
||||
|
||||
Arguments:
|
||||
author -- comment poster (User)
|
||||
text -- contents (unicode string)
|
||||
thread -- parent discussion thread (Thread)
|
||||
"""
|
||||
|
||||
Post.__init__(self, author)
|
||||
self.thread = thread
|
||||
self.text = text
|
||||
|
||||
def edit(self, new_text):
|
||||
"""Edit a Comment's contents."""
|
||||
|
||||
self.text = new_text
|
||||
self.touch()
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Comment: #{self.id}>'
|
|
@ -0,0 +1,41 @@
|
|||
from app import db
|
||||
|
||||
|
||||
class Forum(db.Model):
|
||||
__tablename__ = 'forum'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Forum name, as displayed on the site (eg. "Problèmes de transfert")
|
||||
name = db.Column(db.Unicode(64))
|
||||
# Privilege prefix (sort of slug) for single-forum privileges (lowercase)
|
||||
prefix = db.Column(db.Unicode(64))
|
||||
# Forum description, as displayed on the site
|
||||
descr = db.Column(db.UnicodeText)
|
||||
# Forum URL, for dynamic routes
|
||||
url = db.Column(db.String(64))
|
||||
|
||||
# Relationships
|
||||
parent_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=True)
|
||||
parent = db.relationship('Forum', backref='sub_forums', remote_side=id,
|
||||
lazy=True, foreign_keys=parent_id)
|
||||
|
||||
# Other fields populated automatically through relations:
|
||||
# <topics> List of topics in this exact forum (of type Topic)
|
||||
|
||||
def __init__(self, url, name, prefix, descr="", parent=None):
|
||||
self.url = url
|
||||
self.name = name
|
||||
self.descr = descr
|
||||
self.prefix = prefix
|
||||
|
||||
if isinstance(parent, str):
|
||||
self.parent = Forum.query.filter_by(url=str).first()
|
||||
else:
|
||||
self.parent = parent
|
||||
|
||||
def post_count(self):
|
||||
"""Number of posts in every topic of the forum, without subforums."""
|
||||
return sum(len(t.thread.comments) for t in self.topics)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Forum: {self.name}>'
|
|
@ -0,0 +1,24 @@
|
|||
from app import db
|
||||
from datetime import datetime
|
||||
|
||||
class Notification(db.Model):
|
||||
""" A long-term `flash` notification. It is deleted when watched """
|
||||
__tablename__ = 'notification'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
text = db.Column(db.UnicodeText)
|
||||
href = db.Column(db.UnicodeText)
|
||||
date = db.Column(db.DateTime, default=datetime.now())
|
||||
|
||||
owner_id = db.Column(db.Integer, db.ForeignKey('member.id'),nullable=False)
|
||||
owner = db.relationship('Member', backref='notifications',
|
||||
foreign_keys=owner_id)
|
||||
|
||||
def __init__(self, owner, text, href=None):
|
||||
self.text = text
|
||||
self.href = href
|
||||
self.owner = owner
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Notification to {self.owner.name}: {self.text} ({self.href})>'
|
|
@ -1,42 +1,59 @@
|
|||
from datetime import datetime
|
||||
from app import db
|
||||
from app.models.users import *
|
||||
from app.models.users import User
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Post(db.Model):
|
||||
"""Contents created and published by Users."""
|
||||
|
||||
__tablename__ = 'post'
|
||||
|
||||
# Unique Post ID for the whole site
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
# Post type (polymorphic discriminator)
|
||||
type = db.Column(db.String(20))
|
||||
|
||||
# Creation and edition date
|
||||
date_created = db.Column(db.DateTime)
|
||||
date_modified = db.Column(db.DateTime)
|
||||
|
||||
# Post author
|
||||
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
author = db.relationship('User', backref="posts", foreign_keys=author_id)
|
||||
|
||||
# TODO: Post attachments?
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': __tablename__,
|
||||
'polymorphic_on': type
|
||||
}
|
||||
# Standalone properties
|
||||
text = db.Column(db.Text(convert_unicode=True))
|
||||
date_created = db.Column(db.DateTime, default=datetime.now)
|
||||
date_modified = db.Column(db.DateTime, default=datetime.now)
|
||||
# Relationships
|
||||
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
def __init__(self, author, text):
|
||||
""" Create a Post """
|
||||
self.text = text
|
||||
if type(author) == Member:
|
||||
author = author.id
|
||||
self.author_id = author
|
||||
def __init__(self, author):
|
||||
"""
|
||||
Create a new Post.
|
||||
|
||||
Arguments:
|
||||
author -- post author (User)
|
||||
"""
|
||||
|
||||
self.author = author
|
||||
self.date_created = datetime.now()
|
||||
self.date_modified = self.date_created
|
||||
|
||||
def touch(self):
|
||||
"""Touch a Post when it is edited."""
|
||||
|
||||
def update(self, text):
|
||||
""" Update a post. Check whether the request sender has the right to do
|
||||
this! """
|
||||
self.text = text
|
||||
self.date_modified = datetime.now()
|
||||
|
||||
def change_ownership(self, new_author):
|
||||
""" Change ownership of a post. Check whether the request sender has the
|
||||
right to do this! """
|
||||
if type(new_author) == Member:
|
||||
new_author = new_author.id
|
||||
self.author_id = new_author
|
||||
"""
|
||||
Change ownership of a Post. This is a privileged operation!
|
||||
|
||||
Arguments:
|
||||
new_author -- new post author (User)
|
||||
"""
|
||||
|
||||
self.author = new_author
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Post: #{self.id}>'
|
||||
|
|
|
@ -26,7 +26,7 @@ class SpecialPrivilege(db.Model):
|
|||
self.priv = priv
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Privilege "{self.priv}" of member #{self.mid}>'
|
||||
return f'<Privilege: {self.priv} of member #{self.mid}>'
|
||||
|
||||
|
||||
# Group: User group, corresponds to a community role and a set of privileges
|
||||
|
@ -70,7 +70,7 @@ class Group(db.Model):
|
|||
return sorted(gp.priv for gp in gps)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Group "{self.name}">'
|
||||
return f'<Group: {self.name}>'
|
||||
|
||||
|
||||
# Many-to-many relation for users belonging to groups
|
||||
|
@ -79,7 +79,7 @@ GroupMember = db.Table('group_member', db.Model.metadata,
|
|||
db.Column('uid', db.Integer, db.ForeignKey('member.id')))
|
||||
|
||||
|
||||
# Meny-to-many relationship for privileges granted to groups
|
||||
# Many-to-many relationship for privileges granted to groups
|
||||
class GroupPrivilege(db.Model):
|
||||
__tablename__ = 'group_privilege'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
from app import db
|
||||
|
||||
class Thread(db.Model):
|
||||
"""Some thread, such as a topic, program, tutorial."""
|
||||
|
||||
__tablename__ = 'thread'
|
||||
|
||||
# Unique ID
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Top comment
|
||||
top_comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'))
|
||||
top_comment = db.relationship('Comment', foreign_keys=top_comment_id)
|
||||
|
||||
# Other fields populated automatically through relations:
|
||||
# <comments> The list of comments (of type Comment)
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Create a empty Thread. Normally threads are not meant to be empty, so
|
||||
you should create a Comment with this thread as parent, then assign it
|
||||
as top comment with a call to set_top_comment().
|
||||
"""
|
||||
self.top_comment_id = None
|
||||
|
||||
def set_top_comment(self, top_comment):
|
||||
"""
|
||||
Changes the top comment of the thread. The old top comment will usually
|
||||
become visible in the flow of posts instead of being pinned at the top.
|
||||
|
||||
Arguments:
|
||||
top_comment -- new top comment, must belong to this thread
|
||||
"""
|
||||
|
||||
if top_comment not in self.comments:
|
||||
raise Exception("Cannot set foreign comment as top thread comment")
|
||||
|
||||
self.top_comment = top_comment
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Thread: #{self.id}>'
|
|
@ -0,0 +1,44 @@
|
|||
from app import db
|
||||
from app.models.post import Post
|
||||
from config import V5Config
|
||||
|
||||
class Topic(Post):
|
||||
__tablename__ = 'topic'
|
||||
__mapper_args__ = {'polymorphic_identity': __tablename__}
|
||||
|
||||
# ID of the underlying [Post] object
|
||||
id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True)
|
||||
|
||||
# Topic title
|
||||
title = db.Column(db.Unicode(V5Config.THREAD_NAME_MAXLEN))
|
||||
|
||||
# Parent forum
|
||||
forum_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=False)
|
||||
forum = db.relationship('Forum', backref='topics',foreign_keys=forum_id)
|
||||
|
||||
# Associated thread
|
||||
thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False)
|
||||
thread = db.relationship('Thread', foreign_keys=thread_id)
|
||||
|
||||
# Number of views in the forum
|
||||
views = db.Column(db.Integer)
|
||||
|
||||
def __init__(self, forum, author, title, thread):
|
||||
"""
|
||||
Create a Topic.
|
||||
|
||||
Arguments:
|
||||
forum -- parent forum or sub-forum (Forum)
|
||||
author -- post author (User)
|
||||
title -- topic title (unicode string)
|
||||
thread -- discussion thread attached to the topic (Thread)
|
||||
"""
|
||||
|
||||
Post.__init__(self, author)
|
||||
self.title = title
|
||||
self.views = 0
|
||||
self.thread = thread
|
||||
self.forum = forum
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Topic: #{self.id}>'
|
|
@ -1,21 +1,29 @@
|
|||
from datetime import date
|
||||
from app import db
|
||||
from flask import flash
|
||||
from flask_login import UserMixin
|
||||
from app.models.post import Post
|
||||
from sqlalchemy import func as SQLfunc
|
||||
from os.path import isfile
|
||||
from PIL import Image
|
||||
from app import app, db
|
||||
from app.models.privs import SpecialPrivilege, Group, GroupMember, \
|
||||
GroupPrivilege
|
||||
from app.models.trophies import Trophy, TrophyMember
|
||||
from app.models.notification import Notification
|
||||
import app.utils.unicode_names as unicode_names
|
||||
from app.utils.notify import notify
|
||||
import app.utils.ldap as ldap
|
||||
from config import V5Config
|
||||
|
||||
import werkzeug.security
|
||||
import re
|
||||
import math
|
||||
import app
|
||||
import os
|
||||
|
||||
|
||||
# User: Website user that performs actions on the post
|
||||
class User(UserMixin, db.Model):
|
||||
""" Website user that performs actions on the post """
|
||||
|
||||
__tablename__ = 'user'
|
||||
|
||||
# User ID, should be used to refer to any user. Thea actual user can either
|
||||
|
@ -24,8 +32,7 @@ class User(UserMixin, db.Model):
|
|||
# User type (polymorphic discriminator)
|
||||
type = db.Column(db.String(30))
|
||||
|
||||
# TODO: add good relation
|
||||
posts = db.relationship('Post', backref="author", lazy=False)
|
||||
# Also a [posts] relationship populated from the Post class.
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': __tablename__,
|
||||
|
@ -35,8 +42,10 @@ class User(UserMixin, db.Model):
|
|||
def __repr__(self):
|
||||
return f'<User: #{self.id}>'
|
||||
|
||||
# Guest: Unregistered user with minimal privileges
|
||||
class Guest(User, db.Model):
|
||||
|
||||
class Guest(User):
|
||||
""" Unregistered user with minimal privileges """
|
||||
|
||||
__tablename__ = 'guest'
|
||||
__mapper_args__ = {'polymorphic_identity': __tablename__}
|
||||
|
||||
|
@ -52,8 +61,9 @@ class Guest(User, db.Model):
|
|||
return f'<Guest: {self.username} ({self.ip})>'
|
||||
|
||||
|
||||
# Member: Registered user with full access to the website's services
|
||||
class Member(User, db.Model):
|
||||
class Member(User):
|
||||
""" Registered user with full access to the website's services """
|
||||
|
||||
__tablename__ = 'member'
|
||||
__mapper_args__ = {'polymorphic_identity': __tablename__}
|
||||
|
||||
|
@ -69,10 +79,10 @@ class Member(User, db.Model):
|
|||
xp = db.Column(db.Integer)
|
||||
register_date = db.Column(db.Date, default=date.today)
|
||||
|
||||
# Avatars # TODO: rendre ça un peu plus propre
|
||||
avatar_id = db.Column(db.Integer, default=0)
|
||||
@property
|
||||
def avatar(self):
|
||||
return 'avatars/' + str(self.id) + '.png'
|
||||
return f'{self.id}_{self.avatar_id}.png'
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
|
@ -95,12 +105,21 @@ class Member(User, db.Model):
|
|||
trophies = db.relationship('Trophy', secondary=TrophyMember,
|
||||
back_populates='owners')
|
||||
|
||||
# Displayed title
|
||||
# title_id = db.Column(db.Integer, db.ForeignKey('title.id'))
|
||||
# title = db.relationship('Title', foreign_keys=title_id)
|
||||
|
||||
# Other fields populated automatically through relations:
|
||||
# <notifications> List of unseen notifications (of type Notification)
|
||||
|
||||
def __init__(self, name, email, password):
|
||||
"""Register a new user."""
|
||||
self.name = name
|
||||
self.norm = unicode_names.normalize(name)
|
||||
self.email = email
|
||||
self.set_password(password)
|
||||
if not V5Config.USE_LDAP:
|
||||
self.set_password(password)
|
||||
# Workflow with LDAP enabled is User → Postgresql → LDAP → set password
|
||||
self.xp = 0
|
||||
|
||||
self.bio = ""
|
||||
|
@ -144,6 +163,8 @@ class Member(User, db.Model):
|
|||
"signature" str Post signature
|
||||
"birthday" date Birthday date
|
||||
"newsletter" bool Newsletter setting
|
||||
"xp" int Experience points
|
||||
"avatar" File Avatar image
|
||||
For future compatibility, other attributes are silently ignored. None
|
||||
values can be specified and are ignored.
|
||||
|
||||
|
@ -155,8 +176,11 @@ class Member(User, db.Model):
|
|||
data = {key: data[key] for key in data if data[key] is not None}
|
||||
|
||||
# TODO: verify good type of those args, think about the password mgt
|
||||
# Beware of LDAP injections
|
||||
if "email" in data:
|
||||
self.email = data["email"]
|
||||
if V5Config.USE_LDAP:
|
||||
ldap.set_email(self.norm, self.email)
|
||||
if "password" in data:
|
||||
self.set_password(data["password"])
|
||||
if "bio" in data:
|
||||
|
@ -167,13 +191,33 @@ class Member(User, db.Model):
|
|||
self.birthday = data["birthday"]
|
||||
if "newsletter" in data:
|
||||
self.newsletter = data["newsletter"]
|
||||
if "avatar" in data:
|
||||
self.set_avatar(data["avatar"])
|
||||
|
||||
# For admins only
|
||||
if "xp" in data:
|
||||
self.xp = data["xp"]
|
||||
|
||||
def set_avatar(self, avatar):
|
||||
# Save old avatar filepath
|
||||
old_avatar = V5Config.AVATARS_FOLDER + self.avatar
|
||||
# Resize & convert image
|
||||
size = 128, 128
|
||||
im = Image.open(avatar)
|
||||
im.thumbnail(size, Image.ANTIALIAS)
|
||||
# Change avatar id
|
||||
# TODO: verify concurrency behavior
|
||||
current_id = db.session.query(SQLfunc.max(Member.avatar_id)).first()[0]
|
||||
self.avatar_id = current_id + 1
|
||||
db.session.merge(self)
|
||||
db.session.commit()
|
||||
# Save the new avatar
|
||||
im.save(V5Config.AVATARS_FOLDER + self.avatar, 'PNG')
|
||||
# If nothing has failed, remove old one
|
||||
os.remove(old_avatar)
|
||||
|
||||
def get_public_data(self):
|
||||
"""Returns the public information of the member."""
|
||||
""" Returns the public information of the member."""
|
||||
return {
|
||||
"name": self.name,
|
||||
"xp": self.xp,
|
||||
|
@ -188,25 +232,66 @@ class Member(User, db.Model):
|
|||
Reward xp to a member. If [amount] is negative, the xp total of the
|
||||
member will decrease, down to zero.
|
||||
"""
|
||||
self.xp_points = min(max(self.xp_points + amount, 0), 1000000000)
|
||||
self.xp = min(max(self.xp + amount, 0), 1000000000)
|
||||
|
||||
def set_password(self, password):
|
||||
"""
|
||||
Set the user's password. Check whether the request sender has the right
|
||||
to do this!
|
||||
"""
|
||||
self.password_hash = werkzeug.security.generate_password_hash(password,
|
||||
method='pbkdf2:sha512', salt_length=10)
|
||||
if V5Config.USE_LDAP:
|
||||
ldap.set_password(self, password)
|
||||
else:
|
||||
self.password_hash = werkzeug.security.generate_password_hash(
|
||||
password, method='pbkdf2:sha512', salt_length=10)
|
||||
|
||||
def check_password(self, password):
|
||||
"""Compares password against member hash."""
|
||||
return werkzeug.security.check_password_hash(self.password_hash,
|
||||
password)
|
||||
""" Compares password against member hash or LDAP record """
|
||||
if V5Config.USE_LDAP:
|
||||
return ldap.check_password(self, password)
|
||||
else:
|
||||
return werkzeug.security.check_password_hash(self.password_hash,
|
||||
password)
|
||||
|
||||
def notify(self, message, href=None):
|
||||
"""
|
||||
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)
|
||||
db.session.add(n)
|
||||
db.session.commit()
|
||||
|
||||
def add_group(self, g):
|
||||
"""
|
||||
Add a group to the user.
|
||||
Check wheter or not the request sender has the right to do this!
|
||||
"""
|
||||
if type(g) == int:
|
||||
g = Group.query.get(g)
|
||||
if type(g) == str:
|
||||
g = Group.query.filter_by(name=g).first()
|
||||
if g not in self.groups:
|
||||
self.groups.append(g)
|
||||
self.notify(f"Vous avez été ajouté au groupe '{g.name}'")
|
||||
|
||||
def del_group(self, g):
|
||||
"""
|
||||
Remove a group to the user.
|
||||
Check wheter or not the request sender has the right to do this!
|
||||
"""
|
||||
if type(g) == int:
|
||||
g = Group.query.get(g)
|
||||
if type(g) == str:
|
||||
g = Group.query.filter_by(name=g).first()
|
||||
if g in self.groups:
|
||||
self.groups.remove(g)
|
||||
|
||||
def add_trophy(self, t):
|
||||
"""
|
||||
Add a trophy to the current user. Check whether the request sender has
|
||||
the right to do this!
|
||||
Add a trophy to the current user.
|
||||
Check whether the request sender has the right to do this!
|
||||
"""
|
||||
if type(t) == int:
|
||||
t = Trophy.query.get(t)
|
||||
|
@ -214,18 +299,17 @@ class Member(User, db.Model):
|
|||
t = Trophy.query.filter_by(name=t).first()
|
||||
if t not in self.trophies:
|
||||
self.trophies.append(t)
|
||||
# TODO: implement the notification system
|
||||
# self.notify(f"Vous venez de débloquer le trophée '{t.name}'")
|
||||
self.notify(f"Vous avez débloqué le trophée '{t.name}'")
|
||||
|
||||
def del_trophy(self, t):
|
||||
"""
|
||||
Add a trophy to the current user. Check whether the request sender has
|
||||
the right to do this!
|
||||
Delete a trophy to the current user.
|
||||
Check whether the request sender has the right to do this!
|
||||
"""
|
||||
if type(t) == int:
|
||||
t = Trophy.query.get(t)
|
||||
if type(t) == str:
|
||||
t = Trophy.query.filter_by(name=name).first()
|
||||
t = Trophy.query.filter_by(name=t).first()
|
||||
if t in self.trophies:
|
||||
self.trophies.remove(t)
|
||||
|
||||
|
@ -255,8 +339,10 @@ class Member(User, db.Model):
|
|||
|
||||
if context in ["new-post", "new-program", "new-tutorial", "new-test",
|
||||
None]:
|
||||
# TODO: Amount of posts by the user
|
||||
post_count = 0
|
||||
# Cannot use ORM tools because it adds circular import issues
|
||||
post_count = db.session.execute(f"""SELECT COUNT(*) FROM post
|
||||
INNER JOIN member ON member.id = post.author_id
|
||||
WHERE member.id = {self.id}""").first()[0]
|
||||
|
||||
levels = {
|
||||
20: "Premiers mots",
|
||||
|
@ -355,8 +441,10 @@ class Member(User, db.Model):
|
|||
# TODO: Trophy "actif"
|
||||
|
||||
if context in ["on-profile-update", None]:
|
||||
# TODO: add a better condition (this is for test)
|
||||
self.add_trophy("Artiste")
|
||||
if isfile(V5Config.AVATARS_FOLDER + self.avatar):
|
||||
self.add_trophy("Artiste")
|
||||
else:
|
||||
self.del_trophy("Artiste")
|
||||
|
||||
db.session.merge(self)
|
||||
db.session.commit()
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
from app import app, db
|
||||
from app.forms.login import LoginForm
|
||||
from app.forms.search import SearchForm
|
||||
from app.models.forum import Forum
|
||||
from app.models.topic import Topic
|
||||
from app.models.thread import Thread
|
||||
from app.models.comment import Comment
|
||||
from app.models.users import Member
|
||||
|
||||
@app.context_processor
|
||||
def menu_processor():
|
||||
""" All items used to render main menu. Includes search form """
|
||||
|
||||
login_form = LoginForm(prefix="menu_")
|
||||
search_form = SearchForm()
|
||||
main_forum = Forum.query.filter_by(parent=None).first()
|
||||
|
||||
# Constructing last active topics
|
||||
raw = db.session.execute( """SELECT topic.id FROM topic
|
||||
INNER JOIN comment ON topic.thread_id = comment.thread_id
|
||||
INNER JOIN post ON post.id = comment.id
|
||||
GROUP BY topic.id
|
||||
ORDER BY MAX(post.date_created) DESC
|
||||
LIMIT 10;""")
|
||||
last_active_topics = [Topic.query.get(id) for id in raw]
|
||||
|
||||
return dict(login_form=login_form, search_form=search_form,
|
||||
main_forum=main_forum, last_active_topics=last_active_topics)
|
|
@ -0,0 +1,11 @@
|
|||
from app import app
|
||||
from flask import url_for
|
||||
|
||||
@app.context_processor
|
||||
def utilities_processor():
|
||||
""" Add some utilities to render context """
|
||||
return dict(
|
||||
len=len,
|
||||
# enumerate=enumerate,
|
||||
_url_for = lambda route, args, **other: url_for(route, **args, **other),
|
||||
)
|
|
@ -4,18 +4,18 @@ from app import app, db
|
|||
from app.forms.account import UpdateAccountForm, RegistrationForm, DeleteAccountForm
|
||||
from app.models.users import Member
|
||||
from app.utils.render import render
|
||||
import app.utils.ldap as ldap
|
||||
from config import V5Config
|
||||
|
||||
|
||||
@app.route('/account', methods=['GET', 'POST'])
|
||||
@app.route('/compte', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_account():
|
||||
form = UpdateAccountForm()
|
||||
if form.submit.data:
|
||||
if form.validate_on_submit():
|
||||
if form.avatar.data:
|
||||
f = form.avatar.data
|
||||
f.save("./app/static/" + current_user.avatar)
|
||||
current_user.update(
|
||||
avatar=form.avatar.data or None,
|
||||
email=form.email.data or None,
|
||||
password=form.password.data or None,
|
||||
birthday=form.birthday.data,
|
||||
|
@ -27,13 +27,14 @@ def edit_account():
|
|||
db.session.commit()
|
||||
current_user.update_trophies("on-profile-update")
|
||||
flash('Modifications effectuées', 'ok')
|
||||
return redirect(request.url)
|
||||
else:
|
||||
flash('Erreur lors de la modification', 'error')
|
||||
|
||||
return render('account.html', form=form)
|
||||
return render('account/account.html', form=form)
|
||||
|
||||
|
||||
@app.route('/account/delete', methods=['GET', 'POST'])
|
||||
@app.route('/compte/supprimer', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def delete_account():
|
||||
del_form = DeleteAccountForm()
|
||||
|
@ -47,25 +48,31 @@ def delete_account():
|
|||
else:
|
||||
flash('Erreur lors de la suppression du compte', 'error')
|
||||
del_form.delete.data = False # Force to tick to delete the account
|
||||
return render('delete_account.html', del_form=del_form)
|
||||
return render('account/delete_account.html', del_form=del_form)
|
||||
|
||||
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
@app.route('/inscription', methods=['GET', 'POST'])
|
||||
def register():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
form = RegistrationForm()
|
||||
if form.validate_on_submit():
|
||||
member = Member(form.username.data, form.email.data, form.password.data)
|
||||
member.newsletter = form.newsletter.data
|
||||
db.session.add(member)
|
||||
db.session.commit()
|
||||
# Workflow with LDAP is User → Postgresql → LDAP → Change password
|
||||
if V5Config.USE_LDAP:
|
||||
ldap.add_member(member)
|
||||
ldap.set_password(member, form.password.data)
|
||||
flash('Inscription réussie', 'ok')
|
||||
return redirect(url_for('validation'))
|
||||
return render('register.html', title='Register', form=form)
|
||||
return redirect(url_for('validation') + "?email=" + form.email.data)
|
||||
return render('account/register.html', title='Register', form=form)
|
||||
|
||||
|
||||
@app.route('/register/validation/')
|
||||
@app.route('/register/validation/', methods=['GET', 'POST'])
|
||||
def validation():
|
||||
mail = request.args['email']
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
return render('validation.html')
|
||||
return render('account/validation.html', mail=mail)
|
||||
|
|
|
@ -1,33 +1,64 @@
|
|||
from flask import redirect, url_for, request, flash
|
||||
from flask_login import login_user, logout_user, login_required, current_user
|
||||
from urllib.parse import urlparse, urljoin
|
||||
from app import app
|
||||
from app.forms.login import LoginForm
|
||||
from app.models.users import Member
|
||||
from app.models.privs import Group
|
||||
from app.utils.render import render
|
||||
from config import V5Config
|
||||
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
@app.route('/connexion', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
|
||||
form = LoginForm()
|
||||
lateral = LoginForm(prefix="menu_")
|
||||
|
||||
if lateral.validate_on_submit():
|
||||
form = lateral
|
||||
if form.validate_on_submit():
|
||||
member = Member.query.filter_by(name=form.username.data).first()
|
||||
|
||||
# Check if member can login
|
||||
if member is not None and "No login" in [g.name for g in member.groups]:
|
||||
flash('Cet utilisateur ne peut pas se connecter', 'error')
|
||||
if request.referrer:
|
||||
return redirect(request.referrer)
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Check if password is ok
|
||||
if member is None or not member.check_password(form.password.data):
|
||||
flash('Pseudo ou mot de passe invalide', 'error')
|
||||
return redirect(request.referrer)
|
||||
login_user(member, remember=form.remember_me.data)
|
||||
if request.referrer:
|
||||
return redirect(request.referrer)
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Login & update time-based trophies
|
||||
login_user(member, remember=form.remember_me.data,
|
||||
duration=V5Config.REMEMBER_COOKIE_DURATION)
|
||||
member.update_trophies("on-login")
|
||||
if request.args.get('next'):
|
||||
return redirect(request.args.get('next'))
|
||||
|
||||
# Redirect safely (https://huit.re/open-redirect)
|
||||
def is_safe_url(target):
|
||||
ref_url = urlparse(request.host_url)
|
||||
test_url = urlparse(urljoin(request.host_url, target))
|
||||
return test_url.scheme in ('http', 'https') and \
|
||||
ref_url.netloc == test_url.netloc
|
||||
|
||||
next = request.args.get('next')
|
||||
if next and is_safe_url(next):
|
||||
return redirect(next)
|
||||
if request.referrer:
|
||||
return redirect(request.referrer)
|
||||
return redirect(url_for('index'))
|
||||
return render('login.html', form=form)
|
||||
|
||||
return render('account/login.html', form=form)
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
@app.route('/deconnexion')
|
||||
@login_required
|
||||
def logout():
|
||||
try:
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
from flask import redirect, url_for, request, flash, abort
|
||||
from flask_login import login_required, current_user
|
||||
from app import app, db
|
||||
from app.models.notification import Notification
|
||||
from app.utils.render import render
|
||||
|
||||
|
||||
@app.route('/notifications', methods=['GET'])
|
||||
@login_required
|
||||
def list_notifications():
|
||||
notifications = current_user.notifications
|
||||
return render('account/notifications.html', notifications=notifications)
|
||||
|
||||
|
||||
@app.route('/notifications/supprimer/<id>', methods=['GET'])
|
||||
@login_required
|
||||
# TODO: [SECURITY ISSUE] prevent CSRF
|
||||
def delete_notification(id=None):
|
||||
# Try to convert id to int
|
||||
try:
|
||||
id = int(id)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if type(id) == int:
|
||||
notification = Notification.query.get(id)
|
||||
print(">", notification)
|
||||
if notification:
|
||||
# Only current user or admin can delete notifications
|
||||
if notification.owner_id == current_user.id:
|
||||
db.session.delete(notification)
|
||||
db.session.commit()
|
||||
return redirect(url_for('list_notifications'))
|
||||
elif 'delete_notification' in current_user.privs:
|
||||
db.session.delete(notification)
|
||||
db.session.commit()
|
||||
if request.referrer:
|
||||
return redirect(request.referrer)
|
||||
return redirect(url_for('adm'))
|
||||
else:
|
||||
abort(403)
|
||||
abort(404)
|
||||
elif id == "all":
|
||||
for n in current_user.notifications:
|
||||
db.session.delete(n)
|
||||
db.session.commit()
|
||||
return redirect(url_for('list_notifications'))
|
||||
# TODO: add something to allow an admin to delete all notifs for a user
|
||||
# with a GET parameter
|
||||
else:
|
||||
abort(404)
|
|
@ -1,15 +1,19 @@
|
|||
from flask import flash, redirect, url_for
|
||||
from flask import flash, redirect, url_for, request
|
||||
from flask_login import current_user
|
||||
from wtforms import BooleanField
|
||||
from app.utils.priv_required import priv_required
|
||||
from app.models.users import Member
|
||||
from app.models.trophies import Trophy
|
||||
from app.models.privs import Group
|
||||
from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm, \
|
||||
AdminAccountEditTrophyForm
|
||||
AdminAccountEditTrophyForm, AdminAccountEditGroupForm
|
||||
from app.utils.render import render
|
||||
from app.utils.notify import notify
|
||||
from app import app, db
|
||||
from config import V5Config
|
||||
|
||||
|
||||
@app.route('/admin/account/<user_id>/edit', methods=['GET', 'POST'])
|
||||
@app.route('/admin/compte/<user_id>/editer', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'edit-account')
|
||||
def adm_edit_account(user_id):
|
||||
user = Member.query.filter_by(id=user_id).first_or_404()
|
||||
|
@ -18,23 +22,32 @@ def adm_edit_account(user_id):
|
|||
|
||||
class TrophyForm(AdminAccountEditTrophyForm):
|
||||
pass
|
||||
class GroupForm(AdminAccountEditGroupForm):
|
||||
pass
|
||||
|
||||
for t in Trophy.query.all():
|
||||
setattr(TrophyForm, f't{t.id}', BooleanField(t.name))
|
||||
setattr(TrophyForm, "user_trophies", [f't{t.id}' for t in user.trophies])
|
||||
trophy_form = TrophyForm(prefix="trophies")
|
||||
|
||||
for g in Group.query.all():
|
||||
setattr(GroupForm, f'g{g.id}', BooleanField(g.name))
|
||||
setattr(GroupForm, "user_groups", [f'g{g.id}' for g in user.groups])
|
||||
group_form = GroupForm(prefix="group")
|
||||
|
||||
print(group_form.__dict__.items())
|
||||
|
||||
if form.submit.data:
|
||||
if form.validate_on_submit():
|
||||
if form.avatar.data:
|
||||
f = form.avatar.data
|
||||
f.save("./app/static/" + user.avatar)
|
||||
|
||||
newname = form.username.data
|
||||
names = list(Member.query.filter(Member.id != user.id).values(Member.name))
|
||||
if newname in names:
|
||||
# TODO: avoid this exception
|
||||
# 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')
|
||||
user.update(
|
||||
avatar=form.avatar.data or None,
|
||||
name=form.username.data or None,
|
||||
email=form.email.data or None,
|
||||
password=form.password.data or None,
|
||||
|
@ -47,36 +60,60 @@ def adm_edit_account(user_id):
|
|||
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')
|
||||
return redirect(request.url)
|
||||
else:
|
||||
flash('Erreur lors de la modification', 'error')
|
||||
|
||||
# Trophies
|
||||
if trophy_form.submit.data:
|
||||
if trophy_form.validate_on_submit():
|
||||
for id, field in trophy_form.__dict__.items():
|
||||
if id[0] == "t":
|
||||
print(f"id: {id[1:]}, name: {field.label}, checked={field.data}", end=" ")
|
||||
if field.data:
|
||||
print(f"Add trophy {id[1:]}")
|
||||
user.add_trophy(int(id[1:]))
|
||||
else:
|
||||
print(f"Del trophy {id[1:]}")
|
||||
user.del_trophy(int(id[1:]))
|
||||
|
||||
db.session.merge(user)
|
||||
db.session.commit()
|
||||
flash('Modifications effectuées', 'ok')
|
||||
return redirect(request.url)
|
||||
else:
|
||||
flash("Erreur lors de l'ajout du trophée", 'error')
|
||||
flash("Erreur lors de la modification des trophées", 'error')
|
||||
|
||||
user_owned = set()
|
||||
# Groups
|
||||
if group_form.submit.data:
|
||||
if group_form.validate_on_submit():
|
||||
for id, field in group_form.__dict__.items():
|
||||
if id[0] == "g":
|
||||
if field.data:
|
||||
user.add_group(int(id[1:]))
|
||||
else:
|
||||
user.del_group(int(id[1:]))
|
||||
|
||||
db.session.merge(user)
|
||||
db.session.commit()
|
||||
flash('Modifications effectuées', 'ok')
|
||||
return redirect(request.url)
|
||||
else:
|
||||
flash("Erreur lors de la modification des groupes", 'error')
|
||||
|
||||
trophies_owned = set()
|
||||
for t in user.trophies:
|
||||
user_owned.add(f"t{t.id}")
|
||||
trophies_owned.add(f"t{t.id}")
|
||||
|
||||
return render('admin/edit_account.html', user=user,
|
||||
form=form, trophy_form=trophy_form, user_owned=user_owned)
|
||||
groups_owned = set()
|
||||
for g in user.groups:
|
||||
groups_owned.add(f"g{g.id}")
|
||||
|
||||
return render('admin/edit_account.html', user=user, form=form,
|
||||
trophy_form=trophy_form, trophies_owned=trophies_owned,
|
||||
group_form=group_form, groups_owned=groups_owned)
|
||||
|
||||
|
||||
@app.route('/admin/account/<user_id>/delete', methods=['GET', 'POST'])
|
||||
@app.route('/admin/compte/<user_id>/supprimer', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'delete-account')
|
||||
def adm_delete_account(user_id):
|
||||
user = Member.query.filter_by(id=user_id).first_or_404()
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
from app.utils.priv_required import priv_required
|
||||
from app.utils.render import render
|
||||
from app.models.forum import Forum
|
||||
from app import app, db
|
||||
|
||||
@app.route('/admin/forums', methods=['GET'])
|
||||
@priv_required('access-admin-panel')
|
||||
def adm_forums():
|
||||
main_forum = Forum.query.filter_by(parent=None).first()
|
||||
|
||||
return render('admin/forums.html', main_forum=main_forum)
|
|
@ -9,7 +9,7 @@ import yaml
|
|||
import os
|
||||
|
||||
|
||||
@app.route('/admin/groups', methods=['GET', 'POST'])
|
||||
@app.route('/admin/groupes', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel')
|
||||
def adm_groups():
|
||||
users = Member.query.all()
|
||||
|
|
|
@ -6,7 +6,7 @@ from app.utils.render import render
|
|||
from app import app, db
|
||||
|
||||
|
||||
@app.route('/admin/trophies', methods=['GET', 'POST'])
|
||||
@app.route('/admin/trophees', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'edit-trophies')
|
||||
def adm_trophies():
|
||||
form = TrophyForm()
|
||||
|
@ -28,7 +28,7 @@ def adm_trophies():
|
|||
form=form)
|
||||
|
||||
|
||||
@app.route('/admin/trophies/<trophy_id>/edit', methods=['GET', 'POST'])
|
||||
@app.route('/admin/trophees/<trophy_id>/editer', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'edit-trophies')
|
||||
def adm_edit_trophy(trophy_id):
|
||||
trophy = Trophy.query.filter_by(id=trophy_id).first_or_404()
|
||||
|
@ -52,7 +52,7 @@ def adm_edit_trophy(trophy_id):
|
|||
return render('admin/edit_trophy.html', trophy=trophy, form=form)
|
||||
|
||||
|
||||
@app.route('/admin/trophies/<trophy_id>/delete', methods=['GET', 'POST'])
|
||||
@app.route('/admin/trophees/<trophy_id>/supprimer', methods=['GET', 'POST'])
|
||||
@priv_required('access-admin-panel', 'edit-trophies')
|
||||
def adm_delete_trophy(trophy_id):
|
||||
trophy = Trophy.query.filter_by(id=trophy_id).first_or_404()
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
from flask_login import current_user
|
||||
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.forms.forum import TopicCreationForm
|
||||
from app.models.forum import Forum
|
||||
from app.models.topic import Topic
|
||||
from app.models.thread import Thread
|
||||
from app.models.comment import Comment
|
||||
|
||||
|
||||
@app.route('/forum/')
|
||||
def forum_index():
|
||||
return render('/forum/index.html')
|
||||
|
||||
@app.route('/forum/<forum:f>/', methods=['GET', 'POST'])
|
||||
def forum_page(f):
|
||||
form = TopicCreationForm()
|
||||
|
||||
# TODO: do not hardcode name of news forums
|
||||
if form.validate_on_submit() and (
|
||||
# User can write anywhere
|
||||
(current_user.is_authenticated and current_user.priv('write-anywhere'))
|
||||
# Forum is news forum TODO: add good condition to check if it's news
|
||||
or ("/actus" in f.url and current_user.is_authenticated
|
||||
and current_user.priv('write-news'))
|
||||
# Forum is not news and is a leaf:
|
||||
or ("/actus" not in f.url and not f.sub_forums)):
|
||||
|
||||
# First create the thread, then the comment, then the topic
|
||||
th = Thread()
|
||||
db.session.add(th)
|
||||
db.session.commit()
|
||||
|
||||
c = Comment(current_user, form.message.data, th)
|
||||
db.session.add(c)
|
||||
db.session.commit()
|
||||
|
||||
th.set_top_comment(c)
|
||||
db.session.merge(th)
|
||||
|
||||
t = Topic(f, current_user, form.title.data, th)
|
||||
db.session.add(t)
|
||||
db.session.commit()
|
||||
|
||||
# Update member's xp and trophies
|
||||
current_user.add_xp(V5Config.XP_POINTS['topic'])
|
||||
current_user.update_trophies('new-post')
|
||||
|
||||
|
||||
flash('Le sujet a bien été créé', 'ok')
|
||||
return redirect(url_for('forum_topic', f=f, t=t))
|
||||
|
||||
return render('/forum/forum.html', f=f, form=form)
|
|
@ -0,0 +1,49 @@
|
|||
from flask_login import current_user
|
||||
from flask import request, redirect, url_for, flash, abort
|
||||
|
||||
from app import app, db
|
||||
from config import V5Config
|
||||
from app.utils.render import render
|
||||
from app.forms.forum import CommentForm
|
||||
from app.models.forum import Forum
|
||||
from app.models.topic import Topic
|
||||
from app.models.thread import Thread
|
||||
from app.models.comment import Comment
|
||||
|
||||
|
||||
@app.route('/forum/<forum:f>/<topicslug:t>', methods=['GET', 'POST'])
|
||||
def forum_topic(f, t):
|
||||
# Quick n' dirty workaround to converters
|
||||
if f != t.forum:
|
||||
abort(404)
|
||||
|
||||
form = CommentForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
c = Comment(current_user, form.message.data, t.thread)
|
||||
db.session.add(c)
|
||||
db.session.commit()
|
||||
|
||||
# Update member's xp and trophies
|
||||
current_user.add_xp(V5Config.XP_POINTS['comment'])
|
||||
current_user.update_trophies('new-post')
|
||||
|
||||
flash('Message envoyé', 'ok')
|
||||
# Redirect to empty the form
|
||||
return redirect(url_for('forum_topic', f=f, t=t, page="last"))
|
||||
|
||||
# Update views
|
||||
t.views += 1
|
||||
db.session.merge(t)
|
||||
db.session.commit()
|
||||
|
||||
if request.args.get('page') == "last":
|
||||
page = (t.thread.comments.count() - 1) \
|
||||
// V5Config.COMMENTS_PER_PAGE + 1
|
||||
else:
|
||||
page = request.args.get('page', 1, type=int)
|
||||
|
||||
comments = t.thread.comments.paginate(page,
|
||||
V5Config.COMMENTS_PER_PAGE, True)
|
||||
|
||||
return render('/forum/topic.html', t=t, form=form, comments=comments)
|
|
@ -3,7 +3,7 @@ from app.forms.search import AdvancedSearchForm
|
|||
from app.utils.render import render
|
||||
|
||||
|
||||
@app.route('/search')
|
||||
@app.route('/rechercher')
|
||||
def search():
|
||||
form = AdvancedSearchForm()
|
||||
return render('search.html', form=form)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
from app import app
|
||||
|
||||
from app.utils.render import render
|
||||
|
||||
|
||||
@app.route('/outils')
|
||||
def tools():
|
||||
return render('tools.html')
|
|
@ -1,16 +1,28 @@
|
|||
from flask import redirect, url_for
|
||||
from flask import redirect, url_for, send_from_directory
|
||||
from werkzeug.utils import secure_filename
|
||||
import os.path
|
||||
from app import app
|
||||
from app.models.users import Member
|
||||
from app.utils import unicode_names
|
||||
from app.utils.render import render
|
||||
from config import V5Config
|
||||
|
||||
|
||||
@app.route('/user/<username>')
|
||||
@app.route('/membre/<username>')
|
||||
def user(username):
|
||||
member = Member.query.filter_by(name=username).first_or_404()
|
||||
return render('user.html', member=member)
|
||||
norm = unicode_names.normalize(username)
|
||||
member = Member.query.filter_by(norm=norm).first_or_404()
|
||||
return render('account/user.html', member=member)
|
||||
|
||||
|
||||
@app.route('/user/id/<int:user_id>')
|
||||
@app.route('/membre/id/<int:user_id>')
|
||||
def user_by_id(user_id):
|
||||
member = Member.query.filter_by(id=user_id).first_or_404()
|
||||
return redirect(url_for('user', username=member.name))
|
||||
|
||||
@app.route('/avatar/<filename>')
|
||||
def avatar(filename):
|
||||
filename = secure_filename(filename) # No h4ckers allowed
|
||||
if os.path.isfile(V5Config.AVATARS_FOLDER + filename):
|
||||
return send_from_directory(V5Config.AVATARS_FOLDER, filename)
|
||||
return redirect(url_for('static', filename='images/default_avatar.png'))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.container {
|
||||
margin-left: 60px;
|
||||
margin-left: 110px;
|
||||
}
|
||||
|
||||
section {
|
||||
|
@ -26,3 +26,26 @@ section .avatar {
|
|||
display: block;
|
||||
width: 128px; height: 128px;
|
||||
}
|
||||
|
||||
|
||||
/* Some grid */
|
||||
.flex-grid {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
.flex-grid > * {
|
||||
min-width: 250px;
|
||||
flex: auto;
|
||||
}
|
||||
/* Two columns */
|
||||
.flex-grid.fg2 > * {
|
||||
width: 50%;
|
||||
}
|
||||
/* Three columns */
|
||||
.flex-grid.fg3 > * {
|
||||
width: 33%;
|
||||
}
|
||||
/* Four columns */
|
||||
.flex-grid.fg4 > * {
|
||||
width: 25%;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
.editor div {
|
||||
display: flex; flex-direction: row;
|
||||
flex-wrap: wrap; align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.editor button {
|
||||
height: 25px; margin: 0 0px; padding: 0 3px;
|
||||
border: var(--border); border-radius: 2px;
|
||||
cursor: pointer;
|
||||
background: var(--background);
|
||||
}
|
||||
.editor button > img {
|
||||
opacity: .7;
|
||||
}
|
||||
.editor button:hover,
|
||||
.editor button:focus {
|
||||
border: var(--border-focused);
|
||||
}
|
||||
.editor button:hover > img,
|
||||
.editor button:focus > img {
|
||||
opacity: 1;
|
||||
}
|
|
@ -6,40 +6,38 @@
|
|||
position: fixed; left: 15%;
|
||||
display: flex; align-items: center;
|
||||
width: 70%; z-index: 10;
|
||||
font-family: NotoSans; font-size: 14px; color: #212121;
|
||||
background: #ffffff;
|
||||
border-bottom: 5px solid #4caf50;
|
||||
border-radius: 1px; box-shadow: 0 1px 12px rgba(0, 0, 0, 0.3);
|
||||
font-family: NotoSans; font-size: 14px; color: var(--text);
|
||||
background: var(--background);
|
||||
border-bottom: 5px solid var(--info);
|
||||
border-radius: 1px; box-shadow: var(--shadow);
|
||||
transition: opacity .15s ease;
|
||||
transition: top .2s ease;
|
||||
}
|
||||
.flash.info {
|
||||
border-color: #2e7aec;
|
||||
border-color: var(--info);
|
||||
}
|
||||
.flash.ok {
|
||||
border-color: #4caf50;
|
||||
border-color: var(--ok);
|
||||
}
|
||||
.flash.warning {
|
||||
border-color: #fbbc26;
|
||||
border-color: var(--warn);
|
||||
}
|
||||
.flash.error {
|
||||
border-color: #f44336;
|
||||
border-color: var(--error);
|
||||
}
|
||||
.flash span {
|
||||
flex-grow: 1; margin: 15px 10px 10px 0;
|
||||
}
|
||||
.flash input[type="button"] {
|
||||
margin: 3px 30px 0 0; padding: 10px 15px;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0); color: #727272;
|
||||
}
|
||||
.flash input[type="button"]:hover {
|
||||
background: rgba(0, 0, 0, .1);
|
||||
}
|
||||
.flash input[type="button"]:focus {
|
||||
background: rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.flash svg {
|
||||
margin: 15px 20px 10px 30px;
|
||||
}
|
||||
|
||||
.flash input[type="button"] {
|
||||
margin: 3px 30px 0 0; padding: 10px 15px;
|
||||
border: none;
|
||||
background: var(--btn-bg); color: var(--btn-text);
|
||||
}
|
||||
.flash input[type="button"]:hover,
|
||||
.flash input[type="button"]:focus {
|
||||
background: var(--btn-bg-active);
|
||||
}
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
*/
|
||||
|
||||
footer {
|
||||
margin: 20px 10% 5px 10%; padding: 10px 0;
|
||||
margin: 20px 0 0 0; padding: 10px 10%;
|
||||
text-align: center; font-size: 11px; font-style: italic;
|
||||
color: #a0a0a0;
|
||||
border-top: 1px solid rgba(0, 0, 0, .1);
|
||||
background: var(--background); color: var(--text);
|
||||
border-top: var(--border);
|
||||
}
|
||||
footer p {
|
||||
margin: 3px 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
}
|
||||
|
||||
.form .avatar + input[type="file"] {
|
||||
margin: 16px 0 0 0;
|
||||
margin: 16px 0 0 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,8 @@
|
|||
.trophies-panel > div {
|
||||
display: block;
|
||||
width: 100%; padding: 6px 8px;
|
||||
border: 1px solid #c8c8c8;
|
||||
background: var(--background); color: var(--text);
|
||||
border: var(--border);
|
||||
|
||||
/* Transitions when resizing with the mouse produces apparent lag */
|
||||
transition: all .15s ease, width 0s, height 0s;
|
||||
|
@ -40,8 +41,7 @@
|
|||
.form input[type='password']:focus,
|
||||
.form input[type='search']:focus,
|
||||
.form textarea:focus {
|
||||
border-color: #7cade0;
|
||||
box-shadow: 0 0 0 3px rgba(87, 143, 228, 0.5);
|
||||
border-color: var(--border-focused);
|
||||
}
|
||||
|
||||
.form textarea {
|
||||
|
@ -60,20 +60,19 @@
|
|||
}
|
||||
|
||||
.form form .msgerror {
|
||||
color: red;
|
||||
color: var(--error);
|
||||
font-weight: 400;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.form .desc {
|
||||
font-size: 80%;
|
||||
color: gray;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.form hr {
|
||||
color: white;
|
||||
height: 3px;
|
||||
border: 0 solid #b0b0b0;
|
||||
border: var(--hr-border);
|
||||
border-width: 1px 0;
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
@ -86,3 +85,10 @@
|
|||
.trophies-panel p label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Editor */
|
||||
|
||||
.editor textarea {
|
||||
font-family: monospace;
|
||||
height: 192px;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
/* Fonts */
|
||||
|
||||
@font-face { font-family: NotoSans; src: url(../fonts/noto_sans.ttf); }
|
||||
@font-face { font-family: Cantarell; font-weight: normal; src: url(../fonts/Cantarell-Regular.otf); }
|
||||
@font-face { font-family: Cantarell; font-weight: bold; src: url(../fonts/Cantarell-Bold.otf); }
|
||||
/* @font-face { font-family: NotoSans; src: url(../fonts/noto_sans.ttf); font-display: swap; }
|
||||
@font-face { font-family: Twemoji; src: url(../fonts/TwitterColorEmoji.ttf); font-display: swap; }
|
||||
@font-face { font-family: Cantarell; font-weight: normal; src: url(../fonts/Cantarell-Regular.otf); font-display: swap; }
|
||||
@font-face { font-family: Cantarell; font-weight: bold; src: url(../fonts/Cantarell-Bold.otf); font-display: swap; } */
|
||||
|
||||
/* Whole page */
|
||||
|
||||
|
@ -15,15 +16,15 @@
|
|||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #ffffff;
|
||||
font-family: 'DejaVu Sans', sans-serif;
|
||||
background: var(--background); color: var(--text);
|
||||
font-family: Twemoji, 'DejaVu Sans', sans-serif;
|
||||
}
|
||||
|
||||
/* General */
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #c61a1a;
|
||||
color: var(--links);
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
|
@ -34,6 +35,7 @@ a:focus {
|
|||
|
||||
section p {
|
||||
line-height: 20px;
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
|
||||
section ul {
|
||||
|
@ -95,6 +97,7 @@ input[type="submit"]:hover,
|
|||
margin: -1px;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 1499px) {
|
||||
.profile-avatar {
|
||||
width: 96px;
|
||||
|
@ -124,47 +127,34 @@ input[type="submit"]:hover,
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.bg-green,
|
||||
.bg-green {
|
||||
background: #149641;
|
||||
color: #ffffff;
|
||||
.bg-ok,
|
||||
.bg-ok {
|
||||
background: var(--ok);
|
||||
color: var(--ok-text);
|
||||
}
|
||||
.bg-green:hover,
|
||||
.bg-green:focus,
|
||||
.bg-green:active {
|
||||
background: #0f7331;
|
||||
.bg-ok:hover,
|
||||
.bg-ok:focus,
|
||||
.bg-ok:active {
|
||||
background: var(--ok-active);
|
||||
}
|
||||
|
||||
.bg-red,
|
||||
.bg-red {
|
||||
background: #d23a2f;
|
||||
color: #ffffff;
|
||||
.bg-error,
|
||||
.bg-error {
|
||||
background: var(--error);
|
||||
color: var(--error-text);
|
||||
}
|
||||
.bg-red:hover,
|
||||
.bg-red:focus,
|
||||
.bg-red:active {
|
||||
background: #b32a20;
|
||||
.bg-error:hover,
|
||||
.bg-error:focus,
|
||||
.bg-error:active {
|
||||
background: var(--error-active);
|
||||
}
|
||||
|
||||
.bg-orange {
|
||||
background: #f59f25;
|
||||
color: #ffffff;
|
||||
.bg-warn {
|
||||
background: var(--warn);
|
||||
color: var(--warn-text);
|
||||
}
|
||||
.bg-orange:hover,
|
||||
.bg-orange:focus,
|
||||
.bg-orange:active {
|
||||
background: #ea9720;
|
||||
}
|
||||
|
||||
.bg-white,
|
||||
.bg-white {
|
||||
border: 1px solid #e5e5e5;
|
||||
background: #ffffff;
|
||||
color: #000000;
|
||||
}
|
||||
.bg-white:hover,
|
||||
.bg-white:focus,
|
||||
.bg-white:active {
|
||||
background: #f0f0f0;
|
||||
border-color: #e3e3e3;
|
||||
.bg-warn:hover,
|
||||
.bg-warn:focus,
|
||||
.bg-warn:active {
|
||||
background: var(--warn-active);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
header {
|
||||
height: 50px; margin: 0; padding: 0 16px;
|
||||
background: #f4f4f6; border-bottom: 1px solid #d0d0d0;
|
||||
background: var(--background); border-bottom: var(--border);
|
||||
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
flex-flow: row wrap;
|
||||
|
@ -33,7 +33,6 @@ header .title a {
|
|||
}
|
||||
header .title h1 {
|
||||
font-family: Cantarell; font-weight: bold; font-size: 18px;
|
||||
color: #181818;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
@ -49,7 +48,7 @@ header svg {
|
|||
transition: .15s ease;
|
||||
}
|
||||
header a:hover > svg, header a:focus > svg {
|
||||
fill: black;
|
||||
fill: var(--text);
|
||||
}
|
||||
header a {
|
||||
fill: #363636;
|
||||
|
@ -66,18 +65,20 @@ header .form {
|
|||
header .form input[type="search"] {
|
||||
display: inline-block; width: 250px;
|
||||
padding: 5px 35px 5px 10px;
|
||||
border-color: #d8d8d8;
|
||||
}
|
||||
header .form input[type="search"] ~ a {
|
||||
position: relative; left: -33px;
|
||||
opacity: .7;
|
||||
}
|
||||
header .form input[type="search"] ~ a > svg > path {
|
||||
fill: #cccccc; transition: .15s ease;
|
||||
fill: var(--text);
|
||||
}
|
||||
header .form input[type="search"]:focus ~ a > svg > path {
|
||||
fill: #333333;
|
||||
header .form input[type="search"] ~ a:hover,
|
||||
header .form input[type="search"]:focus ~ a {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
#spotlight {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
|
|
@ -10,11 +10,8 @@
|
|||
|
||||
/* Menu */
|
||||
|
||||
#spacer-menu {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
#light-menu {
|
||||
position: unset;
|
||||
display: flex; flex-direction: row; align-items: center;
|
||||
width: 100%; height: 60px;
|
||||
overflow-x: auto; overflow-y: hidden;
|
||||
|
@ -24,7 +21,7 @@
|
|||
width: auto; height: 100%; margin-bottom: 0;
|
||||
}
|
||||
#logo img {
|
||||
width: 60px;
|
||||
width: 60px; height: inherit;
|
||||
margin-bottom: -4.5px;
|
||||
}
|
||||
|
||||
|
@ -35,7 +32,7 @@
|
|||
padding: 0 2px;
|
||||
}
|
||||
#light-menu li > a {
|
||||
cursor: pointer;
|
||||
cursor: pointer; margin: 0;
|
||||
}
|
||||
#light-menu li > a:hover {
|
||||
text-decoration: none;
|
||||
|
@ -106,7 +103,7 @@
|
|||
margin: 5px 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 549px) {
|
||||
@media all and (max-width: 500px) {
|
||||
#light-menu, #spacer-menu {
|
||||
height: 40px;
|
||||
}
|
||||
|
@ -122,7 +119,10 @@
|
|||
display: block;
|
||||
margin: 5px 15px; padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
background: #e8e8e8; transition: background .15s ease;
|
||||
transition: .15s ease;
|
||||
}
|
||||
#menu form label {
|
||||
float: left; margin-right: 10px;
|
||||
}
|
||||
#menu form input:first-child {
|
||||
margin-bottom: 0; border-bottom: none;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
nav a {
|
||||
color: #ffffff;
|
||||
opacity: 0.75;
|
||||
opacity: .8;
|
||||
cursor: pointer;
|
||||
}
|
||||
nav a:hover,
|
||||
|
@ -14,11 +13,11 @@ nav a:focus {
|
|||
#light-menu {
|
||||
position: fixed; z-index: 10;
|
||||
list-style: none;
|
||||
width: 60px;
|
||||
width: 110px;
|
||||
height: 100%; overflow-y: auto;
|
||||
margin: 0; padding: 0;
|
||||
text-indent: 0;
|
||||
background: #22292c; box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
||||
background: var(--background); box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
#logo {
|
||||
|
@ -26,96 +25,70 @@ nav a:focus {
|
|||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
opacity: 1;
|
||||
background: -moz-linear-gradient(top, #bf1c11, #ba1203);
|
||||
background: -webkit-linear-gradient(top, #bf1c11, #ba1203);
|
||||
background: #bf1c11;
|
||||
background: var(--logo-bg);
|
||||
transition: .15s ease;
|
||||
}
|
||||
#logo img {
|
||||
width: 100%;
|
||||
margin: 0; padding: 0;
|
||||
margin-bottom: -4.5px;
|
||||
display: block; height: 65px;
|
||||
margin: 0 auto; padding: 0;
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, .0));
|
||||
transition: filter .15s ease;
|
||||
}
|
||||
#logo:hover,
|
||||
#logo:focus {
|
||||
background: #d72411;
|
||||
background: var(--logo-active);
|
||||
}
|
||||
#logo:hover img,
|
||||
#logo:focus img {
|
||||
filter: drop-shadow(0 0 2px rgba(0, 0, 0, .7));
|
||||
filter: drop-shadow(var(--logo-shadow));
|
||||
}
|
||||
|
||||
#light-menu li {
|
||||
width: 100%; height: 45px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
color: var(--text);
|
||||
}
|
||||
#light-menu li > a {
|
||||
display: flex; flex-direction: column; flex-grow: 1;
|
||||
display: flex; flex-direction: column; flex-grow: 0;
|
||||
align-items: center; justify-content: center;
|
||||
width: 100%; height: 100%;
|
||||
margin: 20px 0;
|
||||
color: var(--text);
|
||||
transition: opacity .15s ease; /* because Chrome sucks */
|
||||
}
|
||||
|
||||
#light-menu li > a > svg {
|
||||
display: block; width: 35%; flex-shrink: 0;
|
||||
margin: 0 auto 5px auto;
|
||||
display: block; width: 25px; flex-shrink: 0; flex-grow: 0;
|
||||
margin: 0 7px;
|
||||
}
|
||||
#light-menu li div {
|
||||
display: none;
|
||||
}
|
||||
#light-menu li > a::after {
|
||||
content: attr(label);
|
||||
position: fixed; display: none;
|
||||
padding: 4px 8px; left: 63px;
|
||||
font-family: NotoSans; border-radius: 3px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
#light-menu li:not(.opened) > a:hover::after,
|
||||
#light-menu li:not(.opened) > a:focus::after {
|
||||
display: block;
|
||||
#light-menu li > a > svg > path {
|
||||
fill: var(--icons);
|
||||
}
|
||||
|
||||
/*nav li span[notifications]:not([notifications="0"])::before {
|
||||
content: attr(notifications);
|
||||
display: inline-block; margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
padding: 0 5px 0 4px; border-radius: 5px;
|
||||
font-family: NotoSans;
|
||||
background: #ffffff; color: #000000;
|
||||
}*/
|
||||
#light-menu li div {
|
||||
/*flex-grow: 1;*/
|
||||
}
|
||||
|
||||
|
||||
/* Overlay */
|
||||
#menu {
|
||||
position: fixed; z-index: 5;
|
||||
left: -240px; width: 300px; /* left-to-right animation */
|
||||
/*left: 60px; width: 0;*/ /* scroll animation */
|
||||
left: -190px; width: 300px; /* default: left-to-right animation */
|
||||
height: 100%; overflow-x: hidden; overflow-y: auto;
|
||||
background: #1c2124; box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
|
||||
background: var(--background); color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
transition: .15s ease;
|
||||
}
|
||||
|
||||
#menu.opened {
|
||||
left: 60px; /* left-to-right animation */
|
||||
/*width: 300px;*/ /* scroll animation */
|
||||
left: 110px;
|
||||
}
|
||||
|
||||
|
||||
/* Just apply class="scroll-animation" to menu to change to scroll animation */
|
||||
#menu.scroll-animation {
|
||||
left: 60px; width: 0;
|
||||
left: 110px; width: 0;
|
||||
}
|
||||
#menu.scroll-animation.opened {
|
||||
width: 300px;
|
||||
}
|
||||
#menu.left-to-right-animation {
|
||||
left: -240px; width: 300px;
|
||||
}
|
||||
#menu.left-to-right-animation.opened {
|
||||
left: 60px;
|
||||
}
|
||||
|
||||
|
||||
#menu > div {
|
||||
|
@ -130,7 +103,7 @@ nav a:focus {
|
|||
#menu h2 {
|
||||
margin: 0 0 20px 0;
|
||||
font-family: Cantarell; font-weight: bold; font-size: 18px;
|
||||
color: #ffffff;
|
||||
color: var(--text);
|
||||
display: flex; align-items: center;
|
||||
}
|
||||
#menu h2 a {
|
||||
|
@ -151,7 +124,7 @@ nav a:focus {
|
|||
#menu h3 {
|
||||
margin: 16px 0;
|
||||
font-family: Cantarell; font-weight: bold; font-size: 15px;
|
||||
color: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
#menu hr {
|
||||
margin: 15px 0;
|
||||
|
@ -165,11 +138,9 @@ nav a:focus {
|
|||
#menu a,
|
||||
#menu li {
|
||||
display: block; margin: 10px 0;
|
||||
color: var(--text);
|
||||
transition: opacity .15s ease;
|
||||
}
|
||||
#menu li {
|
||||
color: #b8b8b8;
|
||||
}
|
||||
#menu li > a {
|
||||
display: inline;
|
||||
margin: 0; font-style: normal;
|
||||
|
@ -191,18 +162,17 @@ nav a:focus {
|
|||
#menu form input[type="password"] {
|
||||
margin: 8px 0; padding: 5px 2%;
|
||||
font-size: 14px; color: inherit;
|
||||
border: none; border-color: #141719;
|
||||
border: var(--input-border);
|
||||
background: var(--input-bg); color: var(--input-text); opacity: .8;
|
||||
}
|
||||
#menu form input[type="text"]:focus,
|
||||
#menu form input[type="password"]:focus {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 4px rgba(87, 143, 228, 0.65);
|
||||
border-color: #325871;
|
||||
opacity: 1;
|
||||
}
|
||||
#menu form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin: 16px 0 5px 0;
|
||||
margin: 8px 0 5px 0;
|
||||
}
|
||||
#menu form label {
|
||||
font-size: 13px; color: #FFFFFF; opacity: .7;
|
||||
font-size: 13px; opacity: .8;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.pagination {
|
||||
text-align: center;
|
||||
margin: 5px 0;
|
||||
}
|
|
@ -17,3 +17,63 @@ table th {
|
|||
table td {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
/* Forum and sub-forum listings */
|
||||
|
||||
table.forumlist {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
|
||||
margin: 16px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* table.forumlist th {
|
||||
background: #d05950;
|
||||
border-color: #b04940;
|
||||
color: white;
|
||||
} */
|
||||
|
||||
table.forumlist tr {
|
||||
background: unset;
|
||||
}
|
||||
table.forumlist tr:nth-child(4n+2),
|
||||
table.forumlist tr:nth-child(4n+3) {
|
||||
background: rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
|
||||
/* Topic table */
|
||||
|
||||
table.topiclist {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
table.topiclist tr > *:nth-child(n+2) {
|
||||
/* This matches all children except the first column */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
table.forumlist th > td:last-child,
|
||||
table.forumlist tr > td:last-child,
|
||||
table.topiclist th > td:last-child,
|
||||
table.topiclist tr > td:last-child {
|
||||
width: 20%; text-align: center;
|
||||
}
|
||||
|
||||
|
||||
/* Thread table */
|
||||
|
||||
table.thread {
|
||||
width: 100%;
|
||||
}
|
||||
table.thread td.member {
|
||||
width: 20%;
|
||||
}
|
||||
table.thread td {
|
||||
vertical-align: top;
|
||||
}
|
||||
table.thread td:nth-child(2) {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/* Some colors, variables etc. to be used as theme */
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--text: #000000;
|
||||
|
||||
--links: #c61a1a;
|
||||
|
||||
--ok: #149641;
|
||||
--ok-text: #ffffff;
|
||||
--ok-active: #0f7331;
|
||||
|
||||
--warn: #f59f25;
|
||||
--warn-text: #ffffff;
|
||||
--warn-active: #ea9720;
|
||||
|
||||
--error: #d23a2f;
|
||||
--error-text: #ffffff;
|
||||
--error-active: #b32a20;
|
||||
|
||||
--info: #2e7aec;
|
||||
--info-text: #ffffff;
|
||||
--info-active: #215ab0;
|
||||
|
||||
--hr-border: 1px solid #b0b0b0;
|
||||
}
|
||||
|
||||
.form {
|
||||
--background: #ffffff;
|
||||
--text: #000000;
|
||||
--border: 1px solid #c8c8c8;
|
||||
--border-focused: #7cade0;
|
||||
}
|
||||
|
||||
.editor button {
|
||||
--background: #ffffff;
|
||||
--text: #000000;
|
||||
--border: 1px solid rgba(0, 0, 0, 0);
|
||||
--border-focused: 1px solid rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
#light-menu {
|
||||
--background: #22292c;
|
||||
--text: #ffffff;
|
||||
--icons: #ffffff;
|
||||
--shadow: 0 0 4px rgba(0, 0, 0, 0.3);
|
||||
|
||||
--logo-bg: #bf1c11;
|
||||
--logo-shadow: 0 0 2px rgba(0, 0, 0, .7);
|
||||
--logo-active: #d72411;
|
||||
}
|
||||
|
||||
#menu {
|
||||
--background: #1c2124;
|
||||
--text: #ffffff;
|
||||
--icons: #ffffff;
|
||||
--shadow: 0 0 8px rgba(0, 0, 0, 0.3);
|
||||
|
||||
--input-bg: #22292c;
|
||||
--input-text: #ffffff;
|
||||
--input-border: 1px solid #474747;
|
||||
}
|
||||
|
||||
header {
|
||||
--background: #f4f4f6;
|
||||
--text: #000000;
|
||||
--border: 1px solid #d0d0d0;
|
||||
}
|
||||
|
||||
footer {
|
||||
--background: #ffffff;
|
||||
--text: #a0a0a0;
|
||||
--border: #d0d0d0;
|
||||
}
|
||||
|
||||
.flash {
|
||||
--background: #ffffff;
|
||||
--text: #212121;
|
||||
--shadow: 0 1px 12px rgba(0, 0, 0, 0.3);
|
||||
|
||||
/* Uncomment to inherit :root values
|
||||
--ok: #149641;
|
||||
--warn: #f59f25;
|
||||
--error: #d23a2f;
|
||||
--info: #2e7aec; */
|
||||
|
||||
--btn-bg: rgba(0, 0, 0, 0);
|
||||
--btn-text: #000000;
|
||||
--btn-bg-active: rgba(0, 0, 0, .15);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M8,3A2,2 0 0,0 6,5V9A2,2 0 0,1 4,11H3V13H4A2,2 0 0,1 6,15V19A2,2 0 0,0 8,21H10V19H8V14A2,2 0 0,0 6,12A2,2 0 0,0 8,10V5H10V3M16,3A2,2 0 0,1 18,5V9A2,2 0 0,0 20,11H21V13H20A2,2 0 0,0 18,15V19A2,2 0 0,1 16,21H14V19H16V14A2,2 0 0,1 18,12A2,2 0 0,1 16,10V5H14V3H16Z" /></svg>
|
After Width: | Height: | Size: 555 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M9,22A1,1 0 0,1 8,21V18H4A2,2 0 0,1 2,16V4C2,2.89 2.9,2 4,2H20A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H13.9L10.2,21.71C10,21.9 9.75,22 9.5,22V22H9M10,16V19.08L13.08,16H20V4H4V16H10M13,10H11V6H13V10M13,14H11V12H13V14Z" /></svg>
|
After Width: | Height: | Size: 505 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M13.5,15.5H10V12.5H13.5A1.5,1.5 0 0,1 15,14A1.5,1.5 0 0,1 13.5,15.5M10,6.5H13A1.5,1.5 0 0,1 14.5,8A1.5,1.5 0 0,1 13,9.5H10M15.6,10.79C16.57,10.11 17.25,9 17.25,8C17.25,5.74 15.5,4 13.25,4H7V18H14.04C16.14,18 17.75,16.3 17.75,14.21C17.75,12.69 16.89,11.39 15.6,10.79Z" /></svg>
|
After Width: | Height: | Size: 561 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M14,18V16H16V6.31L13.5,7.75V5.44L16,4H18V16H20V18H14Z" /></svg>
|
After Width: | Height: | Size: 381 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M21,18H15A2,2 0 0,1 13,16C13,15.47 13.2,15 13.54,14.64L18.41,9.41C18.78,9.05 19,8.55 19,8A2,2 0 0,0 17,6A2,2 0 0,0 15,8H13A4,4 0 0,1 17,4A4,4 0 0,1 21,8C21,9.1 20.55,10.1 19.83,10.83L15,16H21V18Z" /></svg>
|
After Width: | Height: | Size: 523 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3,4H5V10H9V4H11V18H9V12H5V18H3V4M15,4H19A2,2 0 0,1 21,6V16A2,2 0 0,1 19,18H15A2,2 0 0,1 13,16V15H15V16H19V12H15V10H19V6H15V7H13V6A2,2 0 0,1 15,4Z" /></svg>
|
After Width: | Height: | Size: 441 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M10,4V7H12.21L8.79,15H6V18H14V15H11.79L15.21,7H18V4H10Z" /></svg>
|
After Width: | Height: | Size: 350 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M7,5H21V7H7V5M7,13V11H21V13H7M4,4.5A1.5,1.5 0 0,1 5.5,6A1.5,1.5 0 0,1 4,7.5A1.5,1.5 0 0,1 2.5,6A1.5,1.5 0 0,1 4,4.5M4,10.5A1.5,1.5 0 0,1 5.5,12A1.5,1.5 0 0,1 4,13.5A1.5,1.5 0 0,1 2.5,12A1.5,1.5 0 0,1 4,10.5M7,19V17H21V19H7M4,16.5A1.5,1.5 0 0,1 5.5,18A1.5,1.5 0 0,1 4,19.5A1.5,1.5 0 0,1 2.5,18A1.5,1.5 0 0,1 4,16.5Z" /></svg>
|
After Width: | Height: | Size: 609 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M7,13V11H21V13H7M7,19V17H21V19H7M7,7V5H21V7H7M3,8V5H2V4H4V8H3M2,17V16H5V20H2V19H4V18.5H3V17.5H4V17H2M4.25,10A0.75,0.75 0 0,1 5,10.75C5,10.95 4.92,11.14 4.79,11.27L3.12,13H5V14H2V13.08L4,11H2V10H4.25Z" /></svg>
|
After Width: | Height: | Size: 494 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M14,17H17L19,13V7H13V13H16M6,17H9L11,13V7H5V13H8L6,17Z" /></svg>
|
After Width: | Height: | Size: 349 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3,14H21V12H3M5,4V7H10V10H14V7H19V4M10,19H14V16H10V19Z" /></svg>
|
After Width: | Height: | Size: 349 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M5,21H19V19H5V21M12,17A6,6 0 0,0 18,11V3H15.5V11A3.5,3.5 0 0,1 12,14.5A3.5,3.5 0 0,1 8.5,11V3H6V11A6,6 0 0,0 12,17Z" /></svg>
|
After Width: | Height: | Size: 410 B |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z" /></svg>
|
After Width: | Height: | Size: 1011 B |
After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 6.5 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 17 KiB |
|
@ -0,0 +1,113 @@
|
|||
/* Add callbacks on text formatting buttons */
|
||||
|
||||
function edit(e, type) {
|
||||
function inline(type, str, repeat, insert) {
|
||||
// Characters used to format inline blocs
|
||||
// repeat: if true, add one more char to the longest suite found
|
||||
// insert: insert <insert> between char and str (before and after)
|
||||
var chars = {
|
||||
'bold': '*',
|
||||
'italic': '/',
|
||||
'underline': '_',
|
||||
'strikethrough': '~',
|
||||
'inline-code': '`',
|
||||
'h1': '===',
|
||||
'h2': '---',
|
||||
'h3': '...',
|
||||
}
|
||||
|
||||
if (repeat) {
|
||||
// Detect longest suite of similar chars
|
||||
var n = 1; var tmp = 1;
|
||||
for(var i = 0; i < str.length; i++) {
|
||||
if(str[i] == chars[type]) tmp++;
|
||||
else tmp = 1;
|
||||
n = (tmp > n) ? tmp : n;
|
||||
}
|
||||
return chars[type].repeat(n) + insert + str + insert + chars[type].repeat(n);
|
||||
}
|
||||
|
||||
return chars[type] + insert + str + insert + chars[type];
|
||||
}
|
||||
|
||||
function list(type, str) {
|
||||
switch(type) {
|
||||
case 'list-bulleted':
|
||||
return '* ' + str.replaceAll('\n', '\n* ');
|
||||
break;
|
||||
case 'list-numbered':
|
||||
return '1. ' + str;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var ta = e.parentNode.parentNode.querySelector('textarea');
|
||||
var start = ta.selectionStart;
|
||||
var end = ta.selectionEnd;
|
||||
|
||||
switch(type) {
|
||||
case 'bold':
|
||||
case 'italic':
|
||||
case 'underline':
|
||||
case 'strikethrough':
|
||||
case 'inline-code':
|
||||
ta.value = ta.value.substring(0, start)
|
||||
+ inline(type, ta.value.substring(start, end), true, '')
|
||||
+ ta.value.substring(end);
|
||||
break;
|
||||
case 'h1':
|
||||
case 'h2':
|
||||
case 'h3':
|
||||
ta.value = ta.value.substring(0, start)
|
||||
+ inline(type, ta.value.substring(start, end), false, ' ')
|
||||
+ ta.value.substring(end);
|
||||
break;
|
||||
case 'list-bulleted':
|
||||
case 'list-numbered':
|
||||
ta.value = ta.value.substring(0, start)
|
||||
+ list(type, ta.value.substring(start, end))
|
||||
+ ta.value.substring(end);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function pre(type, str, multiline) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
function bold(e) {
|
||||
var ta = e.parentNode.parentNode.querySelector('textarea');
|
||||
var indexStart = ta.selectionStart;
|
||||
var indexEnd = ta.selectionEnd;
|
||||
var txt = ta.value.substring(indexStart, indexEnd);
|
||||
ta.value += '\n' + inline('bold', txt);
|
||||
}
|
||||
|
||||
|
||||
// Tab insert some spaces
|
||||
// Ctrl+Enter send the form
|
||||
ta = document.querySelector(".editor textarea");
|
||||
ta.addEventListener('keydown', function(e) {
|
||||
var keyCode = e.keyCode || e.which;
|
||||
if (keyCode == 9) {
|
||||
e.preventDefault();
|
||||
|
||||
var start = e.target.selectionStart;
|
||||
var end = e.target.selectionEnd;
|
||||
// set textarea value to: text before caret + tab + text after caret
|
||||
e.target.value = e.target.value.substring(0, start) + "\t" + e.target.value.substring(end);
|
||||
e.target.selectionEnd = start + 1;
|
||||
}
|
||||
if (e.ctrlKey && keyCode == 13) {
|
||||
var e = e.target;
|
||||
while(! (e instanceof HTMLFormElement)) {
|
||||
e = e.parentNode;
|
||||
}
|
||||
try {
|
||||
e.submit();
|
||||
} catch(exception) {
|
||||
e.submit.click();
|
||||
}
|
||||
}
|
||||
});
|
|
@ -50,59 +50,3 @@ function flash_close(element) {
|
|||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/*
|
||||
Send post ajax request to url defined in action.
|
||||
Callback the function defined in the callback attribute from the submit type.
|
||||
*/
|
||||
/* We don't need Ajax at that time. Maybe later
|
||||
function ajaxWrapper(evt){
|
||||
evt.preventDefault();
|
||||
var elems = evt.target;
|
||||
var params = "";
|
||||
// do not embed submit value (-1)
|
||||
for(i = 0; i < elems.length-1; i++){
|
||||
if(params) params += "&";
|
||||
params += encodeURIComponent(elems[i].name)+"="+encodeURIComponent(elems[i].value);
|
||||
}
|
||||
const req = new XMLHttpRequest();
|
||||
req.open("POST", evt.target.action, true);
|
||||
req.setRequestHeader('Content-Type',"application/x-www-form-urlencoded");
|
||||
req.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
req.onreadystatechange = function(){
|
||||
if(req.readyState == 4 && (req.status == 200 || req.status == 0)){
|
||||
var fn = window[elems[elems.length-1].getAttribute("callback")];
|
||||
if(typeof fn == 'function'){
|
||||
fn(req.responseText);
|
||||
}
|
||||
}
|
||||
}
|
||||
req.send(params);
|
||||
}
|
||||
|
||||
// Add event listener on submit for all form with class with-ajax.
|
||||
|
||||
window.onload = function(){
|
||||
|
||||
var ele;
|
||||
var elems = document.getElementsByClassName('with-ajax');
|
||||
for(i = 0; i < elems.length; i++){
|
||||
ele = elems[i];
|
||||
if(ele.addEventListener){ // Normal people
|
||||
ele.addEventListener("submit", ajaxWrapper, false);
|
||||
}else if(ele.attachEvent){ // Retarded user using IE
|
||||
ele.attachEvent("onsubmit", ajaxWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
if(getCookie('pc_notif') == 'true')
|
||||
document.getElementsByClassName('alert')[0].parentNode.removeChild(document.getElementsByClassName('alert')[0]);
|
||||
if(getCookie('pc_notif_2') == 'true')
|
||||
document.getElementsByClassName('alert')[0].parentNode.removeChild(document.getElementsByClassName('alert')[0]);
|
||||
|
||||
}
|
||||
|
||||
function login(response){
|
||||
alert(response);
|
||||
}
|
||||
//*/
|
|
@ -1,10 +1,11 @@
|
|||
/* Trigger actions for the menu */
|
||||
|
||||
/* Initialization */
|
||||
var b = document.getElementById('light-menu').getElementsByTagName('a')
|
||||
var b = document.querySelectorAll('#light-menu a');
|
||||
for(var i = 1; i < b.length; i++) {
|
||||
b[i].setAttribute('onfocus', "this.setAttribute('f', 'true');");
|
||||
b[i].setAttribute('onblur', "this.setAttribute('f', 'false');");
|
||||
b[i].removeAttribute('href');
|
||||
}
|
||||
|
||||
var trigger_menu = function(active) {
|
||||
|
@ -15,8 +16,8 @@ var trigger_menu = function(active) {
|
|||
element.classList.remove('opened');
|
||||
}
|
||||
|
||||
var menu = document.getElementById('menu');
|
||||
var buttons = document.getElementById('light-menu').getElementsByTagName('li');
|
||||
var menu = document.querySelector('#menu');
|
||||
var buttons = document.querySelectorAll('#light-menu li');
|
||||
var menus = document.querySelectorAll('#menu > div');
|
||||
|
||||
if(active == -1 || buttons[active].classList.contains('opened')) {
|
||||
|
@ -39,8 +40,8 @@ var trigger_menu = function(active) {
|
|||
}
|
||||
|
||||
var mouse_trigger = function(event) {
|
||||
var menu = document.getElementById('menu');
|
||||
var buttons = document.getElementById('light-menu').getElementsByTagName('li');
|
||||
var menu = document.querySelector('#menu');
|
||||
var buttons = document.querySelectorAll('#light-menu li');
|
||||
|
||||
if(!menu.contains(event.target)) {
|
||||
var active = -1;
|
||||
|
@ -48,7 +49,7 @@ var mouse_trigger = function(event) {
|
|||
for(i = 0; i < buttons.length; i++) {
|
||||
if(buttons[i].contains(event.target))
|
||||
active = i;
|
||||
buttons[i].getElementsByTagName('a')[0].blur();
|
||||
buttons[i].querySelector('a').blur();
|
||||
}
|
||||
|
||||
trigger_menu(active);
|
||||
|
@ -57,11 +58,11 @@ var mouse_trigger = function(event) {
|
|||
|
||||
var keyboard_trigger = function(event) {
|
||||
var menu = document.getElementById('menu');
|
||||
var buttons = document.getElementById('light-menu').getElementsByTagName('li');
|
||||
var buttons = document.querySelectorAll('#light-menu li');
|
||||
|
||||
if(event.keyCode == 13) {
|
||||
for(var i = 0; i < buttons.length; i++) {
|
||||
if(buttons[i].getElementsByTagName('a')[0].getAttribute('f') == 'true') {
|
||||
if(buttons[i].querySelector('a').getAttribute('f') == 'true') {
|
||||
trigger_menu(i);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,9 +13,12 @@
|
|||
<div>
|
||||
{{ form.avatar.label }}
|
||||
<div>
|
||||
<img class="avatar" src="{{ url_for('static', filename=current_user.avatar) }}" meta="{{ current_user.avatar }}" />
|
||||
<img class="avatar" src="{{ url_for('avatar', filename=current_user.avatar) }}" meta="{{ current_user.avatar }}" />
|
||||
{{ form.avatar }}
|
||||
</div>
|
||||
{% for error in form.avatar.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.email.label }}
|
||||
|
@ -78,11 +81,11 @@
|
|||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ form.submit(class_="bg-green") }}</div>
|
||||
<div>{{ form.submit(class_="bg-ok") }}</div>
|
||||
</form>
|
||||
|
||||
<h2 style="margin-top:30px;">Supprimer le compte</h2>
|
||||
<a href="{{ url_for('delete_account') }}" class="button bg-red">Supprimer le compte</a>
|
||||
<a href="{{ url_for('delete_account') }}" class="button bg-error">Supprimer le compte</a>
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -20,7 +20,7 @@
|
|||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ del_form.submit(class_="bg-red") }}</div>
|
||||
<div>{{ del_form.submit(class_="bg-error") }}</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -20,8 +20,8 @@
|
|||
{% endfor %}
|
||||
</p>
|
||||
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
|
||||
<p>{{ form.submit(class_="bg-green") }}</p>
|
||||
<p>{{ form.submit(class_="bg-ok") }}</p>
|
||||
</form>
|
||||
<p>Pas encore de compte ? <a href="{{ url_for('register') }}">Créé-en un !</a></p>
|
||||
<p>Pas encore de compte ? <a href="{{ url_for('register') }}">Créé-en un !</a></p>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,33 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<h1>Notifications</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
{% if notifications %}
|
||||
<table style="width: 100%;">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Notification</th>
|
||||
<th><a href="{{ url_for('delete_notification', id='all') }}">Tout supprimer</a></th>
|
||||
</tr>
|
||||
{% for n in notifications|reverse %}
|
||||
<tr>
|
||||
<td>{{ n.date.strftime('Le %Y-%m-%d à %H:%M') }}</td>
|
||||
<td>
|
||||
{% if n.href %}<a href="{{ n.href }}">{% endif %}
|
||||
{{ n.text }}
|
||||
{% if n.href %}</a>{% endif %}
|
||||
</td>
|
||||
<td style="text-align: center;"><a href="{{ url_for('delete_notification', id=n.id)}}">Supprimer</a>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
Aucune notification.
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -50,7 +50,7 @@
|
|||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ form.submit(class_="bg-green") }}</div>
|
||||
<div>{{ form.submit(class_="bg-ok") }}</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
|
@ -9,8 +9,12 @@
|
|||
<section>
|
||||
{{ widget_member.profile(member) }}
|
||||
|
||||
{% if current_user.is_authenticated and current_user.priv('access-admin-panel') %}
|
||||
<div><a href="{{ url_for('adm_edit_account', user_id=member.id) }}">Modifier</a></div>
|
||||
{% if current_user.is_authenticated %}
|
||||
{% if current_user == member %}
|
||||
<div><a href="{{ url_for('edit_account') }}">Modifier le compte</a></div>
|
||||
{% elif current_user.priv('access-admin-panel') %}
|
||||
<div><a href="{{ url_for('adm_edit_account', user_id=member.id) }}">Modifier le compte</a></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<h2>Trophées</h2>
|
|
@ -0,0 +1,18 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<div>
|
||||
<h2>Inscription réussie !</h2>
|
||||
<p>
|
||||
Nous vous avons envoyé un mail de vérification à l'adresse {{mail}}<br>
|
||||
Votre compte sera actif une fois que vous aurez cliqué sur le lien présent dans le mail.<br>
|
||||
Le mail n'est pas arrivé ? Vérifiez bien dans vos messages indésirables(ou spam) si il ne s'y trouve pas.<br>
|
||||
Si le mail ne s'y trouve pas réessayez plus tard, c'est peut-être un problème passager.<br>
|
||||
Sinon, si le problème persiste n'hésitez pas à venir nous le signaler, sur
|
||||
<a href="https://gitea.planet-casio.com/devs/PCv5/issues/new">la page dédié.</a><br>
|
||||
</p>
|
||||
<a href="{{url_for('index')}}">Retour à la page d'accueil</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
|
@ -24,7 +24,7 @@
|
|||
<span class=msgerror>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ del_form.submit(class_="bg-red") }}</div>
|
||||
<div>{{ del_form.submit(class_="bg-error") }}</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<span class=msgerror>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ del_form.submit(class_="bg-red") }}</div>
|
||||
<div>{{ del_form.submit(class_="bg-error") }}</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -14,9 +14,12 @@
|
|||
<div>
|
||||
{{ form.avatar.label }}
|
||||
<div>
|
||||
<img class="avatar" src="{{ url_for('static', filename=user.avatar) }}" meta="{{ user.avatar }}" />
|
||||
<img class="avatar" src="{{ url_for('avatar', filename=user.avatar) }}" meta="{{ user.avatar }}" />
|
||||
{{ form.avatar }}
|
||||
</div>
|
||||
{% for error in form.avatar.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@ -88,7 +91,7 @@
|
|||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ form.submit(class_="bg-green") }}</div>
|
||||
<div>{{ form.submit(class_="bg-ok") }}</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
@ -96,24 +99,43 @@
|
|||
<form action="{{ url_for('adm_edit_account', user_id=user.id) }}" method="post">
|
||||
{{ trophy_form.hidden_tag() }}
|
||||
<h2>Trophées</h2>
|
||||
<div class="trophies-panel">
|
||||
<div class="trophies-panel flex-grid fg3">
|
||||
{% for id, input in trophy_form.__dict__.items() %}
|
||||
{% if id[0] == "t" %}
|
||||
<div>
|
||||
{# TODO: add trophies icons #}
|
||||
{{ input(checked=id in user_owned) }}
|
||||
{{ input(checked=id in trophies_owned) }}
|
||||
{{ input.label }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ trophy_form.submit(class_="bg-green") }}</div>
|
||||
<div>{{ trophy_form.submit(class_="bg-ok") }}</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<form action="{{ url_for('adm_edit_account', user_id=user.id) }}" method="post">
|
||||
{{ group_form.hidden_tag() }}
|
||||
<h2>Groupes</h2>
|
||||
<div class="groups-panel flex-grid fg3">
|
||||
{% for id, input in group_form.__dict__.items() %}
|
||||
{% if id[0] == "g" %}
|
||||
<div>
|
||||
{# TODO: add trophies icons #}
|
||||
{{ input(checked=id in groups_owned) }}
|
||||
{{ input.label }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ group_form.submit(class_="bg-ok") }}</div>
|
||||
</form>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2 style="margin-top:30px;">Supprimer le compte</h2>
|
||||
<a href="{{ url_for('adm_delete_account', user_id=user.id) }}" class="button bg-red">Supprimer le compte</a>
|
||||
<a href="{{ url_for('adm_delete_account', user_id=user.id) }}" class="button bg-error">Supprimer le compte</a>
|
||||
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -33,6 +33,6 @@
|
|||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ form.submit(class_="bg-green") }}</div>
|
||||
<div>{{ form.submit(class_="bg-ok") }}</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{# This macro will allow us to perform recursive HTML generation #}
|
||||
{% macro forumtree(f, level) %}
|
||||
<tr>
|
||||
<td><code>{{ f.url }}</code></td>
|
||||
<td style='padding-left: {{ 6+24*level }}px'>
|
||||
<a href='/forum{{ f.url }}'>{{ f.name }}</a>
|
||||
</td>
|
||||
<td>{{ f.topics | length }}</td>
|
||||
<td>{{ f.post_count() }}</td>
|
||||
</tr>
|
||||
|
||||
{% for subf in f.sub_forums %}
|
||||
{{ forumtree(subf, level+1) }}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block title %}
|
||||
<a href="{{ url_for('adm') }}">Panneau d'administration</a> » <h1>Forums</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<p>Cette page permet de gérer l'arbre des forums.</p>
|
||||
|
||||
<h2>Arbre des forums</h2>
|
||||
|
||||
{% if main_forum == None %}
|
||||
<p>Il n'y a aucun forum.</p>
|
||||
{% else %}
|
||||
<table style='width: 90%; margin: auto'>
|
||||
<tr><th>URL</th><th>Nom</th><th>Sujets</th><th>Messages</th></tr>
|
||||
{{ forumtree(main_forum, 0) }}
|
||||
</table>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -26,7 +26,7 @@
|
|||
<code>{{ priv }}</code>
|
||||
{{- ', ' if not loop.last }}
|
||||
{% endfor %}</td>
|
||||
<td><a href="{{ url_for('adm_edit_account', user_id=user.id) }}">Modifier</a></td>
|
||||
<td style="text-align: center"><a href="{{ url_for('adm_edit_account', user_id=user.id) }}">Modifier</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
|
|
@ -8,8 +8,9 @@
|
|||
<section>
|
||||
<p>Pages générales du panneau d'administration :</p>
|
||||
<ul>
|
||||
<li><a href="{{ url_for('adm_groups') }}">Groupes et privilèges</a></li>
|
||||
<li><a href="{{ url_for('adm_groups') }}">Groupes et privilèges</a></li>
|
||||
<li><a href="{{ url_for('adm_trophies') }}">Titres et trophées</a></li>
|
||||
<li><a href="{{ url_for('adm_forums') }}">Arbre des forums</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -14,13 +14,17 @@
|
|||
|
||||
<table style="width:90%; margin: auto;">
|
||||
<tr><th>ID</th><th>Icône</th><th>Nom</th><th>Titre</th>
|
||||
<th>Style</th><th>Modifier</th><th>Supprimer</th></tr>
|
||||
<th>Style</th><th>Modifier</th><th>Supprimer</th></tr>
|
||||
|
||||
{% for trophy in trophies %}
|
||||
<tr><td>{{ trophy.id }}</td>
|
||||
<td><img src="{{ url_for('static', filename='images/account-circle.svg') }}" alt="{{ trophy.name }}"></td>
|
||||
<td style="{{ trophy.css }}">{{ trophy.name }}</td>
|
||||
<td>{{ trophy | is_title }}</td>
|
||||
{% if trophy | is_title %}
|
||||
<td style="color:green">Oui</td>
|
||||
{% else %}
|
||||
<td style="color:red">Non</td>
|
||||
{% endif %}
|
||||
<td><code>{{ trophy.css }}</code></td>
|
||||
<td style="text-align: center"><a href="{{ url_for('adm_edit_trophy', trophy_id=trophy.id) }}">Modifier</a></td>
|
||||
<td style="text-align: center"><a href="{{ url_for('adm_delete_trophy', trophy_id=trophy.id) }}">Supprimer</a></td>
|
||||
|
@ -56,6 +60,6 @@
|
|||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ form.submit(class_="bg-green") }}</div>
|
||||
<div>{{ form.submit(class_="bg-ok") }}</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% include "base/footer.html" %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
<footer>
|
||||
<p>Planète Casio est un site communautaire non affilié à CASIO. Toute reproduction de Planète Casio, même partielle, est interdite.</p>
|
||||
<p>Les programmes et autres publications présentes sur Planète Casio restent la propriété de leurs auteurs et peuvent être soumis à des licences ou des copyrights.</p>
|
||||
{% if current_user.is_authenticated and current_user.priv('footer-statistics') %}
|
||||
<p>Page générée en {{ g.request_time() }}</p>
|
||||
{% endif %}
|
||||
<p>Ceci est un environnement de test. Tout contenu peut être supprimé sans avertissement préalable.</p>
|
||||
</footer>
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<link rel="icon" href="{{ url_for('static', filename='icons/favicon-96.ico') }}" type="image/x-icon">
|
||||
|
||||
{% for s in styles %}
|
||||
<link rel="stylesheet" type="text/css" href={{url_for('static', filename = s)}}>
|
||||
{% endfor %}
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
<input type="search" name="q" id="q" placeholder="{{search_form.label}}" />
|
||||
<a role=button onclick="this.parentNode.submit();" href=#>
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#adb0b4"d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"></path>
|
||||
<path d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</form>
|
||||
|
||||
<div id="spotlight">
|
||||
<a href="#" class="button bg-red">Jeu du mois : février 2019</a>
|
||||
<a href="#" class="button bg-error">Jeu du mois : février 2019</a>
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
<nav>
|
||||
<ul id="light-menu">
|
||||
<a id="logo" href="{{ url_for('index') }}">
|
||||
<img src="{{ url_for('static',filename= 'images/logo_noshadow.png') }}" alt="logo"/>
|
||||
<img src="{{ url_for('static',filename= 'images/logo_noshadow-small.png') }}" alt="logo"/>
|
||||
</a>
|
||||
|
||||
<li>
|
||||
<a role="button" label="Compte" tabindex="0">
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('user', username=current_user.name) }}" role="button" label="Compte" tabindex="0">
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}" role="button" label="Compte" tabindex="0">
|
||||
{% endif %}
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"></path>
|
||||
<path d="M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z"></path>
|
||||
</svg>
|
||||
<div>Compte</div>
|
||||
</a>
|
||||
|
@ -16,16 +20,16 @@
|
|||
<li>
|
||||
<a role="button" label="Actualités" tabindex="0">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M20,11H4V8H20M20,15H13V13H20M20,19H13V17H20M11,19H4V13H11M20.33,4.67L18.67,3L17,4.67L15.33,3L13.67,4.67L12,3L10.33,4.67L8.67,3L7,4.67L5.33,3L3.67,4.67L2,3V19A2,2 0 0,0 4,21H20A2,2 0 0,0 22,19V3L20.33,4.67Z"></path>
|
||||
<path d="M20,11H4V8H20M20,15H13V13H20M20,19H13V17H20M11,19H4V13H11M20.33,4.67L18.67,3L17,4.67L15.33,3L13.67,4.67L12,3L10.33,4.67L8.67,3L7,4.67L5.33,3L3.67,4.67L2,3V19A2,2 0 0,0 4,21H20A2,2 0 0,0 22,19V3L20.33,4.67Z"></path>
|
||||
</svg>
|
||||
<div>Actualités</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a role="button" label="Forum" tabindex="0">
|
||||
<a href="{{ url_for('forum_index') }}" role="button" label="Forum" tabindex="0">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M17,12V3A1,1 0 0,0 16,2H3A1,1 0 0,0 2,3V17L6,13H16A1,1 0 0,0 17,12M21,6H19V15H6V17A1,1 0 0,0 7,18H18L22,22V7A1,1 0 0,0 21,6Z"></path>
|
||||
<path d="M17,12V3A1,1 0 0,0 16,2H3A1,1 0 0,0 2,3V17L6,13H16A1,1 0 0,0 17,12M21,6H19V15H6V17A1,1 0 0,0 7,18H18L22,22V7A1,1 0 0,0 21,6Z"></path>
|
||||
</svg>
|
||||
<div>Forum</div>
|
||||
</a>
|
||||
|
@ -34,7 +38,7 @@
|
|||
<li>
|
||||
<a role="button" label="Programmes" tabindex="0">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M8,3A2,2 0 0,0 6,5V9A2,2 0 0,1 4,11H3V13H4A2,2 0 0,1 6,15V19A2,2 0 0,0 8,21H10V19H8V14A2,2 0 0,0 6,12A2,2 0 0,0 8,10V5H10V3M16,3A2,2 0 0,1 18,5V9A2,2 0 0,0 20,11H21V13H20A2,2 0 0,0 18,15V19A2,2 0 0,1 16,21H14V19H16V14A2,2 0 0,1 18,12A2,2 0 0,1 16,10V5H14V3H16Z"></path>
|
||||
<path d="M8,3A2,2 0 0,0 6,5V9A2,2 0 0,1 4,11H3V13H4A2,2 0 0,1 6,15V19A2,2 0 0,0 8,21H10V19H8V14A2,2 0 0,0 6,12A2,2 0 0,0 8,10V5H10V3M16,3A2,2 0 0,1 18,5V9A2,2 0 0,0 20,11H21V13H20A2,2 0 0,0 18,15V19A2,2 0 0,1 16,21H14V19H16V14A2,2 0 0,1 18,12A2,2 0 0,1 16,10V5H14V3H16Z"></path>
|
||||
</svg>
|
||||
<div>Programmes</div>
|
||||
</a>
|
||||
|
@ -43,7 +47,7 @@
|
|||
<li>
|
||||
<a role="button" label="Tutoriels" tabindex="0">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M12,3L1,9L12,15L21,10.09V17H23V9M5,13.18V17.18L12,21L19,17.18V13.18L12,17L5,13.18Z"></path>
|
||||
<path d="M12,3L1,9L12,15L21,10.09V17H23V9M5,13.18V17.18L12,21L19,17.18V13.18L12,17L5,13.18Z"></path>
|
||||
</svg>
|
||||
<div>Tutoriels</div>
|
||||
</a>
|
||||
|
@ -52,24 +56,22 @@
|
|||
<li>
|
||||
<a role="button" label="Sprites" tabindex="0">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M20.71,4.63L19.37,3.29C19,2.9 18.35,2.9 17.96,3.29L9,12.25L11.75,15L20.71,6.04C21.1,5.65 21.1,5 20.71,4.63M7,14A3,3 0 0,0 4,17C4,18.31 2.84,19 2,19C2.92,20.22 4.5,21 6,21A4,4 0 0,0 10,17A3,3 0 0,0 7,14Z"></path>
|
||||
<path d="M20.71,4.63L19.37,3.29C19,2.9 18.35,2.9 17.96,3.29L9,12.25L11.75,15L20.71,6.04C21.1,5.65 21.1,5 20.71,4.63M7,14A3,3 0 0,0 4,17C4,18.31 2.84,19 2,19C2.92,20.22 4.5,21 6,21A4,4 0 0,0 10,17A3,3 0 0,0 7,14Z"></path>
|
||||
</svg>
|
||||
<div>Sprites</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a role="button" label="Outils" tabindex="0">
|
||||
<a href="{{ url_for('tools') }}" role="button" label="Outils" tabindex="0">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z"></path>
|
||||
<path d="M22.7,19L13.6,9.9C14.5,7.6 14,4.9 12.1,3C10.1,1 7.1,0.6 4.7,1.7L9,6L6,9L1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1C4.8,14 7.5,14.5 9.8,13.6L18.9,22.7C19.3,23.1 19.9,23.1 20.3,22.7L22.6,20.4C23.1,20 23.1,19.3 22.7,19Z"></path>
|
||||
</svg>
|
||||
<div>Outils</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div id=spacer-menu></div>
|
||||
|
||||
<div id=menu>
|
||||
{% include "base/navbar/account.html" %}
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@
|
|||
<div>
|
||||
<h2>
|
||||
<a href="{{ url_for('user', username=current_user.name) }}">
|
||||
<img src="{{ url_for('static', filename=current_user.avatar) }}"></a>
|
||||
<img src="{{ url_for('avatar', filename=current_user.avatar) }}"></a>
|
||||
<a href="{{ url_for('user', username=current_user.name) }}">
|
||||
{{ current_user.name }}</a>
|
||||
</h2>
|
||||
<a href="#">
|
||||
<a href="{{ url_for('list_notifications') }}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M20,2A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H6L2,22V4C2,2.89 2.9,2 4,2H20M4,4V17.17L5.17,16H20V4H4M6,7H18V9H6V7M6,11H15V13H6V11Z"></path>
|
||||
</svg>Notifications
|
||||
</svg>Notifications{{ " ({})".format(current_user.notifications|length) if current_user.notifications|length }}
|
||||
</a>
|
||||
<a href="#">
|
||||
<svg viewBox="0 0 24 24">
|
||||
|
@ -48,10 +48,10 @@
|
|||
<form method="post" action="{{url_for('login')}}" class="login form">
|
||||
{{ login_form.hidden_tag() }}
|
||||
{{ login_form.username.label }}
|
||||
{{ login_form.username(size=32, placeholder="Identifiant") }}
|
||||
{{ login_form.username(size=32) }}
|
||||
{{ login_form.password.label }}
|
||||
{{ login_form.password(size=32, placeholder="Mot de passe") }}
|
||||
{{ login_form.submit(class_="bg-green") }}
|
||||
{{ login_form.password(size=32) }}
|
||||
{{ login_form.submit(class_="bg-ok") }}
|
||||
{{ login_form.remember_me.label }} {{ login_form.remember_me() }}
|
||||
</form>
|
||||
<hr />
|
||||
|
|
|
@ -5,21 +5,22 @@
|
|||
</svg>
|
||||
Forum
|
||||
</h2>
|
||||
<a href="#">Vie communautaire</a>
|
||||
<a href="#">Projets de programmation</a>
|
||||
<a href="#">Questions et problèmes</a>
|
||||
<a href="#">Discussions</a>
|
||||
<a href="#">Administration</a>
|
||||
<a href="#">CreativeCalc</a>
|
||||
<a href='{{ url_for('forum_index') }}'>Index du forum</a>
|
||||
|
||||
<hr />
|
||||
<hr>
|
||||
|
||||
<h3>Derniers commentaires</h3>
|
||||
{% for f in main_forum.sub_forums %}
|
||||
<a href="{{ url_for('forum_page', f=f) }}">{{ f.name }}</a>
|
||||
{% endfor %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Derniers topics actifs</h3>
|
||||
<ul>
|
||||
<li><a href="#">Legolas</a> sur <a href="#">Bugs de la v5</a></li>
|
||||
<li><a href="#">Dark Storm</a> sur <a href="#">fxSDK support</a></li>
|
||||
<li><a href="#">Gollum</a> sur <a href="#">Le nom de topic qui fout le bordel car il est trop long…</a></li>
|
||||
<li><a href="#">Lephenixnoir</a> sur <a href="#">fxSDK support</a></li>
|
||||
<li><a href="#">Kristaba</a> sur <a href="#">FiXos, le retour</a></li>
|
||||
{% for t in last_active_topics %}
|
||||
<li>
|
||||
<a href="{{ url_for('forum_topic', f=t.forum, t=t, page='last')}}">{{ t.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -5,12 +5,16 @@
|
|||
</svg>
|
||||
Actualités
|
||||
</h2>
|
||||
<a href="#">Casio</a>
|
||||
<a href="#">Arduino</a>
|
||||
<a href="#">Projets communautaires</a>
|
||||
<a href="#">Divers</a>
|
||||
<a href='/forum/news'>Toutes les nouveautés</a>
|
||||
|
||||
<hr />
|
||||
<hr>
|
||||
|
||||
<a href='/forum/news/calc'>Nouveautés Casio</a>
|
||||
<a href='/forum/news/projects'>Projets communutaires</a>
|
||||
<a href='/forum/news/events'>Événements de Planète Casio</a>
|
||||
<a href='/forum/news/other'>Autres nouveautés</a>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Derniers articles</h3>
|
||||
<ul>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<script type="text/javascript" src={{url_for('static', filename = 'scripts/trigger_menu.js')}}></script>
|
||||
<script type="text/javascript" src={{url_for('static', filename = 'scripts/smartphone_patch.js')}}></script>
|
||||
<script type="text/javascript" src={{url_for('static', filename = 'scripts/pc-utils.js')}}></script>
|
||||
{% for s in scripts %}
|
||||
<script type="text/javascript" src={{url_for('static', filename=s)}}></script>
|
||||
{% endfor %}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<h1>403 - Accès non autorisé</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>403 - Accès non autorisé</h1>
|
||||
|
||||
<img src="{{url_for('static', filename = 'images/403.webp')}}" style="display:block;margin:auto"; />
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<h1>404 - Page non trouvée</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>404 - Page non trouvée</h1>
|
||||
|
||||
<img src="{{url_for('static', filename = 'images/404.webp')}}" style="display:block;margin:auto"; />
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{% extends "base/base.html" %}
|
||||
{% import "widgets/editor.html" as widget_editor %}
|
||||
{% import "widgets/member.html" as widget_member %}
|
||||
{% import "widgets/pagination.html" as widget_pagination with context %}
|
||||
|
||||
{% block title %}
|
||||
<a href='/forum'>Forum de Planète Casio</a> » <a href="{{ url_for('forum_page', f=t.forum) }}">{{ t.forum.name }}</a> » <h1>{{ t.title }}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>Édition du topic {{ t.title }}</h1>
|
||||
|
||||
<div class=form>
|
||||
<h3>Commenter le sujet</h3>
|
||||
<form action="" method="post" enctype="multipart/form-data">
|
||||
Un formulaire
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,67 @@
|
|||
{% extends "base/base.html" %}
|
||||
{% import "widgets/editor.html" as widget_editor %}
|
||||
|
||||
{% block title %}
|
||||
<a href='/forum'>Forum de Planète Casio</a> » <h1>{{ f.name }}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<p>{{ f.descr }}</p>
|
||||
|
||||
{% if f.topics %}
|
||||
<h2>Sujets</h2>
|
||||
<table class=topiclist>
|
||||
<tr><th>Sujet</th><th>Auteur</th><th>Date de création</th>
|
||||
<th>Commentaires</th><th>Vues</th></tr>
|
||||
|
||||
{% for t in f.topics %}
|
||||
<tr><td><a href='{{ url_for('forum_topic', f=t.forum, t=t) }}'>{{ t.title }}</a></td>
|
||||
<td><a href='{{ url_for('user', username=t.author.name) }}'>{{ t.author.name }}</a></td>
|
||||
<td>{{ t.date_created | date }}</td>
|
||||
<td>{{ t.thread.comments.count() }}</td>
|
||||
<td>{{ t.views }} </td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% elif not f.sub_forums %}
|
||||
<p>Il n'y a aucun topic sur ce forum ! Animons-le vite !</p>
|
||||
{% endif %}
|
||||
|
||||
{% if f.sub_forums %}
|
||||
<h2>Forums</h2>
|
||||
<table class=forumlist>
|
||||
<tr><th>{{ f.name }}</th><th>Nombre de sujets</th></tr>
|
||||
|
||||
{% for sf in f.sub_forums %}
|
||||
<tr><td><a href='/forum{{ sf.url }}'>{{ sf.name }}</td>
|
||||
<td>{{ sf.topics | length }}</td></tr>
|
||||
<tr><td>{{ sf.descr }}</td><td></td></tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{% if (current_user.is_authenticated and current_user.priv('write-anywhere'))
|
||||
or ("/actus" in f.url and current_user.is_authenticated and current_user.priv('write-news'))
|
||||
or ("/actus" not in f.url and not f.sub_forums) %}
|
||||
<div class=form>
|
||||
<h2>Créer un nouveau sujet</h2>
|
||||
<form action="" method="post" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<div>
|
||||
{{ form.title.label }}
|
||||
{{ form.title() }}
|
||||
{% for error in form.title.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{{ widget_editor.text_editor(form.message) }}
|
||||
|
||||
<div>{{ form.submit(class_='bg-ok') }}</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,43 @@
|
|||
{% extends "base/base.html" %}
|
||||
|
||||
{% block title %}
|
||||
<h1>Forum de Planète Casio</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<p>
|
||||
Bienvenue sur le forum de Planète Casio ! Vous pouvez créer des
|
||||
nouveaux sujets ou poster des réponses avec un compte
|
||||
{% if not current_user.is_authenticated %}
|
||||
ou en postant en tant qu'invité
|
||||
{% endif %}
|
||||
.
|
||||
</p>
|
||||
|
||||
{% if main_forum == None %}
|
||||
<p>Il n'y a aucun forum.</p>
|
||||
{% else %}
|
||||
|
||||
{% for l1 in main_forum.sub_forums %}
|
||||
<table class=forumlist>
|
||||
<tr><th>{{ l1.name }}</th><th>Nombre de sujets</th></tr>
|
||||
|
||||
{% if l1.sub_forums == [] %}
|
||||
<tr><td><a href='/forum{{ l1.url }}'>{{ l1.name }}</td>
|
||||
<td>{{ l1.topics | length }}</td></tr>
|
||||
<tr><td>{{ l1.descr }}</td><td></td></tr>
|
||||
{% endif %}
|
||||
|
||||
{% for l2 in l1.sub_forums %}
|
||||
<tr><td><a href='/forum{{ l2.url }}'>{{ l2.name }}</td>
|
||||
<td>{{ l2.topics | length }}</td></tr>
|
||||
<tr><td>{{ l2.descr }}</td><td></td></tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,58 @@
|
|||
{% extends "base/base.html" %}
|
||||
{% import "widgets/editor.html" as widget_editor %}
|
||||
{% import "widgets/member.html" as widget_member %}
|
||||
{% import "widgets/pagination.html" as widget_pagination with context %}
|
||||
|
||||
{% block title %}
|
||||
<a href='/forum'>Forum de Planète Casio</a> » <a href="{{ url_for('forum_page', f=t.forum) }}">{{ t.forum.name }}</a> » <h1>{{ t.title }}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>{{ t.title }}</h1>
|
||||
<table class="thread"><tr>
|
||||
<td class="member">{{ widget_member.profile(t.author ) }}</td>
|
||||
<td>{{ t.thread.top_comment.text }}</td>
|
||||
</tr></table>
|
||||
|
||||
{{ widget_pagination.paginate(comments, 'forum_topic', {'f': t.forum, 't':t}) }}
|
||||
|
||||
<table class="thread">
|
||||
{% for c in comments.items %}
|
||||
<tr id="{{ c.id }}">
|
||||
{% if c != t.thread.top_comment %}
|
||||
<td class="member">{{ widget_member.profile(c.author ) }}</td>
|
||||
<td>
|
||||
<div>{% if c.date_created != c.date_modified %}
|
||||
Posté le {{ c.date_created|date }} (Modifié le {{ c.date_modified|date }})
|
||||
{% else %}
|
||||
Posté le {{ c.date_created|date }}
|
||||
{% endif %}
|
||||
| <a href="{{ url_for('forum_topic', f=t.forum, t=t, page=comments.page, _anchor=c.id) }}">#</a>
|
||||
| <a href="#">Modifier</a>
|
||||
| <a href="#">Supprimer</a>
|
||||
</div>
|
||||
<!--<hr>-->
|
||||
<p>{{ c.text }}</p>
|
||||
{% elif loop.index0 != 0 %}
|
||||
<div>Ce message est le top comment</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
||||
{{ widget_pagination.paginate(comments, 'forum_topic', {'f': t.forum, 't':t}) }}
|
||||
|
||||
<div class=form>
|
||||
<h3>Commenter le sujet</h3>
|
||||
<form action="" method="post" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{{ widget_editor.text_editor(form.message, label=False) }}
|
||||
|
||||
<div>{{ form.submit(class_='bg-ok') }}</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|