Compare commits

...

41 Commits

Author SHA1 Message Date
Darks 821a853ddb
Pas besoin des versions large sur du SVG 2019-12-02 23:49:11 +01:00
Eragon f7bb7a2f11
Add mrgreen emoji 2019-09-21 12:05:51 +02:00
Darks aebe09de68
Merge branch 'dev' of gitea.planet-casio.com:devs/PCv5 into dev 2019-09-09 14:55:04 +02:00
Darks c5d9b39f06
Correction d'un bug 2019-09-09 14:22:55 +02:00
Eragon e3e38fde6f Supprimer 'package.txt' 2019-09-09 13:38:33 +02:00
Eragon c80398cba3 Supprimer 'Pipfile' 2019-09-09 13:37:22 +02:00
Eragon ac654d0232 Supprimer 'requirements.txt' 2019-09-09 13:37:20 +02:00
Eragon d2365a8444
Merge branch 'dev' of gitea.planet-casio.com:devs/PCv5 into dev 2019-09-09 13:34:57 +02:00
Eragon 75756c3b36
Ignore virtualenv files 2019-09-09 13:31:19 +02:00
Lephe e0dc6944f7
gitignore: add a generic "exclude" folder
For notes, TODOs, whatever at a personal level.
2019-09-09 08:11:39 +02:00
Lephe 3ad3eca470
forum: list subforum topics
Also introduces a 'date' filter that displays date in a readable format.
2019-09-09 08:11:39 +02:00
Lephe 79e5af7924
users: allow norm-equivalent names in profile url 2019-09-09 08:11:38 +02:00
Lephe 8a0ba309e0
forum: restructure models and add topic creation
This changes fixes #25 by restructuring the forum models in a way
compatible with the polymorphic behavior of SQLAlchemy. Incidentally,
the new form turns out to be more appropriate for our use than the
polymorphic one originally used.

The migration for this task is non-trivial because the Thread class was
created with a foreign-key id which thus had no auto-increment or
associated sequence. The most reliable way of getting it back was to
recreate the table because SQLAlchemy ony performs automated sequence
introduction at table creation time. Four separate migration files
perform the whole change.

This commit also adds views and forms to create topics, and the
boilerplate for an advanced markup editor that can be used as a widget.
2019-09-09 08:11:38 +02:00
Lephe d1a8333cae
forum: add forum listing routes
With this change, URLs for forum listings are now available. This
includes URLs like /news or /projects/games. Each of them show a
(currently empty) forum index.

Note that URLs that are not linked to in the forum index, namely URLs
for forums that have children (eg. /forum/news), are still accessible. We
could ban this by raising ValidationError if the forum has a non-empty
[sub_forums] attribute but displaying all subjects feels better.

URLs that point to /forum, but do not name a sub-forum and are not of
the form of a topic URL produce 404 errors.
2019-09-09 08:11:38 +02:00
Lephe 10e3c88bd4
errors: use the title block 2019-09-09 08:11:38 +02:00
Lephe 9f30bd36a0
forum: add the forum index page
Also prepare some functions for topic listings for each forum.
2019-09-09 08:11:38 +02:00
Lephe 35f1335f64
forum: better tree visualization, and topics
Turns the forum tree visualization in the admin panel into a tree-like
table, and exposes the Topic class to the application and database.
2019-09-09 08:11:38 +02:00
Lephe 153f303857
rebase migration scripts from the notifications branch 2019-09-09 08:11:38 +02:00
Lephe aa75ff09a1
forum: implement forum tree generation
This commit adds a forum tree YAML file (URL-based rather than an
actual tree...) and the 'forums' and 'create-forums' commands for
the master script.

A page /admin/forums is also used to currently display the forum
tree, although this will probably be turned into a full table with
forum descriptions, and a form with edition capabilities.
2019-09-09 08:11:37 +02:00
Darks de83f09024
Ajout d'un logo plus petit pour ne pas charger le gros en permanence
Dans l'idéal faudrait même faire une version svg
2019-09-08 23:10:28 +02:00
Darks a29657da24
Merge branch 'dev' of gitea.planet-casio.com:devs/PCv5 into dev 2019-09-08 22:38:57 +02:00
Darks 035e4f9062
Corrections de style
Cf post 
https://www.planet-casio.com/Fr/forums/lecture_sujet.php?id=15836&page=last#168760
2019-09-08 22:38:19 +02:00
Eragon b628510455
Merge branch 'dev' of gitea.planet-casio.com:devs/PCv5 into dev 2019-09-08 16:45:11 +02:00
Eragon 2b8a78fe20
Issue #3, remplir /register/validation 2019-09-08 16:44:10 +02:00
Darks 0a33161af0
Corrige une erreur 500 si l'on se connecte sur un compte inexistant 2019-09-08 12:28:39 +02:00
Darks 7cad3d4345
Correction préférence de newsletter
À l'inscription, la préférence de la newsletter n'était pas prise en 
compte. Merci Hackcell pour la remontée du bug.
2019-09-08 11:38:26 +02:00
Darks 5bf90f9d05
Corrections de CSS
Corrige #26 et améliore un poil le bouton « se souvenir de moi » en mode 
light.
2019-09-07 21:54:59 +02:00
Darks ab6275c08f
Correction de bug
Il manquait un import menant à une erreur 500 lors d'une redirection 
avec `?next=some_url`
2019-09-07 14:58:16 +02:00
Eragon dbef50cb86
Ajout du temps maximum d'inactivité pour une connexion
Issue n° #23 Fixer le temps d'une session par cookie (Remember me)
Corrigé, par l'ajout d'une option dans le fichier de configuration et
du code pour gèrer ça.
2019-09-07 14:15:31 +02:00
Darks 2e80a56596
Ajout d'un groupe `nologin` (corrige #22)
Les comptes GLaDOS et PlanèteCasio sont automatiquement ajoutés au 
groupe "No login", qui empêche l'utilisateur de se connecter, et ce même 
si les identifiants sont corrects.
2019-09-03 09:28:07 +02:00
Darks 6d43d742c8
Mieux quand les notifs sont dans l'ordre anti-chronologique 2019-09-01 22:54:53 +02:00
Darks 7971e47522
Debug sur notifs, ajout du nombre dans le menu latéral
L'affichage du nombre de notifs méritera peut être un coup de peinture… 
À voir.
2019-09-01 22:33:00 +02:00
Darks c2fbef7ace
Petite modif, test de CI sur dev 2019-09-01 21:59:40 +02:00
Darks 15a4d38ea0
Ajout des notifications 2019-09-01 12:30:41 +02:00
Darks 0c7c408e40
Ajout d'un fichier de configuration local, non tracé par git 2019-09-01 10:35:37 +02:00
Darks 4868774b96
Test d'intégration continue 2019-09-01 00:32:20 +02:00
Darks f508536805
Détaché le nom de la bdd du fichier de config 2019-08-31 23:00:43 +02:00
Lephe eeaab86d0a forum: improve model relationships (so that it works) 2019-08-24 19:17:13 +02:00
Lephe 11b19af199 forum: provide suitable migrations for the database
First migrate without the foreign key to create the tables, then add the
foreign key in a second migration.

Also removed unneeded imports that caused dependency cycles.

Minor "style" edits with the ambiguous use of super and unnecessary
db.Model inheritance.
2019-08-21 16:50:23 +02:00
Darks 201e961ba2
Ajout des stats sur la durée de chargement 2019-08-20 18:07:16 +02:00
Darks 81c910832b
Ajout des post/thread/comment/etc. 2019-08-20 17:34:00 +02:00
69 changed files with 1566 additions and 138 deletions

11
.gitignore vendored
View File

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

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

2
V5.py
View File

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

View File

@ -1,21 +1,42 @@
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)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
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)
login = LoginManager(app)
login.login_view = 'login'
login.login_message = "Veuillez vous authentifier avant de continuer."
from app import models # IDK why this is here, but it works
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 # To load routes at initialization
from app.routes.account import login, account
from app.routes.admin import index, groups, account, trophies
from app.routes.account import login, account, notification
from app.routes.admin import index, groups, account, trophies, forums
from app.routes.forum import index
from app.utils import pluralize # To use pluralize into the templates
from app.utils import date
from app.utils import is_title

97
app/data/forums.yaml Normal file
View File

@ -0,0 +1,97 @@
# 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
/news:
name: Actualités
prefix: news
/news/projects:
name: Actualités des projets
prefix: projectnews
descr: Nouveautés des projets de la communauté.
/news/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.
/news/events:
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é.
/news/other:
name: Autres nouveautés
prefix: othernews
descr: Actualités non catégorisées.
# Help
/help:
name: Aide et questions
prefix: help
/help/transfers:
name: Questions sur les tranferts
prefix: transferhelp
descr: Questions sur le transfert de fichiers et l'installation de programmes
sur la calculatrice.
/help/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...
/help/prog:
name: Questions de programmation
prefix: proghelp
descr: Questions sur le développement et le debuggage de programmes.
/help/other:
name: Autres questions
prefix: otherhelp
descr: Questions non catégorisées.
# Projects
/projects:
name: Forum des projets
prefix: projects
/projects/games:
name: Projets de jeux
prefix: gameprojects
descr: Projets de jeux pour calculatrices, tous langages confondus et tous
modèles de calculatrices confondus.
/projects/apps:
name: Projets d'applications, utilitaires, outils pour calculatrice
prefix: appprojects
descr: Projets d'applications (hors jeux) pour calculatrice, tous langages et
modèles confondus.
/projects/tools:
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.
# Discussion
/discussion:
name: Discussion
prefix: discussion
descr: Sujets hors-sujet et discussion libre.

View File

@ -1,6 +1,6 @@
-
name: Administrateur
css: "color: #ee0000"
css: "color: #ee0000;"
descr: "Vous voyez Chuck Norris? Pareil."
privs: access-admin-board access-assoc-board write-news
upload-shared-files delete-shared-files
@ -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é."

15
app/forms/editor.py Normal file
View File

@ -0,0 +1,15 @@
from flask_wtf import FlaskForm
from wtforms import TextAreaField
class EditorForm(FlaskForm):
"""
A text editor with formatting buttons and help. A rendering macro is
defined in the template widgets/editor.html.
"""
# TODO: How to set DataRequired() dynamically?
contents = TextAreaField()
@property
def value(self):
return self.contents.data

9
app/forms/forum.py Normal file
View File

@ -0,0 +1,9 @@
from flask_wtf import FlaskForm
from wtforms import StringField, FormField, SubmitField
from wtforms.validators import DataRequired
from app.forms.editor import EditorForm
class TopicCreationForm(FlaskForm):
title = StringField('Nom du sujet', validators=[DataRequired()])
message = FormField(EditorForm, 'Premier post')
submit = SubmitField('Créer le sujet')

40
app/models/comment.py Normal file
View File

@ -0,0 +1,40 @@
from app import db
from app.models.post import Post
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='comments',
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
def edit(self, new_text):
"""Edit a Comment's contents."""
self.text = new_text
self.touch()
def __repr__(self):
return f'<Comment: #{self.id}>'

40
app/models/forum.py Normal file
View File

@ -0,0 +1,40 @@
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)
# Also [topics] which is provided by a backref from the Topic class
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 __repr__(self):
return f'<Forum: {self.name}>'
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)

View File

@ -0,0 +1,23 @@
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)
def __init__(self, owner_id, text, href=None):
""" Check weather or not the id is valid before creating the notif! """
self.text = text
self.href = href
self.owner_id = owner_id
def __repr__(self):
return f'<Notification to {self.owner.name}: {self.text} ({self.href})>'

View File

@ -1,42 +1,58 @@
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 = datetime.now()
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}>'

View File

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

40
app/models/thread.py Normal file
View File

@ -0,0 +1,40 @@
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)
# Also a relation [comments] populated from the Comment class.
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 become
visible in the flow of posts?
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}>'

44
app/models/topic.py Normal file
View File

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

View File

@ -2,11 +2,12 @@ from datetime import date
from app import db
from flask import flash
from flask_login import UserMixin
from app.models.post import Post
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
from config import V5Config
import werkzeug.security
@ -14,8 +15,10 @@ import re
import math
import app
# 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 +27,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 +37,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 +56,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__}
@ -92,6 +97,7 @@ class Member(User, db.Model):
newsletter = db.Column(db.Boolean, default=False)
# Relations
notifications = db.relationship('Notification', backref="owner", lazy=True)
trophies = db.relationship('Trophy', secondary=TrophyMember,
back_populates='owners')
@ -203,6 +209,14 @@ class Member(User, db.Model):
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 """
n = Notification(self.id, message, href=href)
db.session.add(n)
db.session.commit()
def add_trophy(self, t):
"""
Add a trophy to the current user. Check whether the request sender has
@ -214,8 +228,7 @@ 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):
"""
@ -225,7 +238,7 @@ class Member(User, db.Model):
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)

View File

@ -57,15 +57,17 @@ def register():
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()
flash('Inscription réussie', 'ok')
return redirect(url_for('validation'))
return redirect(url_for('validation') + "?email=" + form.email.data)
return render('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('validation.html', mail=mail)

View File

@ -1,9 +1,12 @@
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
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'])
@ -14,16 +17,40 @@ def login():
form = LoginForm()
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)

View File

@ -0,0 +1,50 @@
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/delete/<id>', methods=['GET'])
@login_required
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)

View File

@ -1,4 +1,5 @@
from flask import flash, redirect, url_for
from flask_login import current_user
from wtforms import BooleanField
from app.utils.priv_required import priv_required
from app.models.users import Member
@ -6,6 +7,7 @@ from app.models.trophies import Trophy
from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm, \
AdminAccountEditTrophyForm
from app.utils.render import render
from app.utils.notify import notify
from app import app, db
@ -47,6 +49,7 @@ 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')
else:
flash('Erreur lors de la modification', 'error')

View File

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

42
app/routes/forum/index.py Normal file
View File

@ -0,0 +1,42 @@
from flask_login import current_user
from flask import request
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
from app import app, db
@app.route('/forum/')
def forum_index():
main_forum = Forum.query.filter_by(parent=None).first()
return render('/forum/index.html', main_forum=main_forum)
@app.route('/forum/<forum:f>/', methods=['GET', 'POST'])
def forum_page(f):
form = TopicCreationForm()
if form.validate_on_submit():
# TODO: Check user privileges for this specific forum!
# 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.value, th)
th.set_top_comment(c)
t = Topic(f, current_user, form.title.data, th)
db.session.add(th)
db.session.add(c)
db.session.add(t)
db.session.commit()
return render('/forum/forum.html', f=f, form=form)
@app.route('/forum/<forum:f>/<topicslug:t>')
def forum_topic(f, t):
return render('/forum/topic.html', f=f, t=t)

View File

@ -1,12 +1,14 @@
from flask import redirect, url_for
from app import app
from app.models.users import Member
from app.utils import unicode_names
from app.utils.render import render
@app.route('/user/<username>')
def user(username):
member = Member.query.filter_by(name=username).first_or_404()
norm = unicode_names.normalize(username)
member = Member.query.filter_by(norm=norm).first_or_404()
return render('user.html', member=member)

View File

@ -86,3 +86,10 @@
.trophies-panel p label {
margin: 0;
}
/* Editor */
.editor textarea {
font-family: monospace;
height: 192px;
}

View File

@ -122,7 +122,10 @@
display: block;
margin: 5px 15px; padding: 5px 10px;
font-size: 14px;
background: #e8e8e8; transition: background .15s ease;
transition: background .15s ease;
}
#menu form label {
float: left; margin-right: 10px;
}
#menu form input:first-child {
margin-bottom: 0; border-bottom: none;

View File

@ -192,6 +192,7 @@ nav a:focus {
margin: 8px 0; padding: 5px 2%;
font-size: 14px; color: inherit;
border: none; border-color: #141719;
border-radius: 2px;
}
#menu form input[type="text"]:focus,
#menu form input[type="password"]:focus {
@ -201,7 +202,7 @@ nav a:focus {
}
#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;

View File

@ -17,3 +17,38 @@ 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: 90%;
margin: auto;
}
table.topiclist tr > *:nth-child(n+2) {
/* This matches all children except the first column */
text-align: center;
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 45 45" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.25 0 0 -1.25 .0010481 45.005)">
<!-- Tête -->
<g transform="translate(36,18)">
<path d="m0 0c0-9.941-8.059-18-18-18-9.94 0-18 8.059-18 18 0 9.94 8.06 18 18 18 9.941 0 18-8.06 18-18" style="fill:#00cea6"/>
</g>
<!--Oeil de droite(à gauche sur l'écran)-->
<g transform="translate(16,18)">
<path d="M 0,0 C -0.419,0 -0.809,0.265 -0.949,0.684 -1.152,1.283 -1.966,3 -3,3 -4.062,3 -4.888,1.173 -5.051,0.684 -5.226,0.16 -5.79,-0.124 -6.316,0.052 -6.84,0.226 -7.124,0.792 -6.949,1.316 -6.823,1.693 -5.645,5 -3,5 -0.355,5 0.823,1.693 0.949,1.316 1.124,0.792 0.84,0.226 0.316,0.052 0.211,0.017 0.105,0 0,0"/>
</g>
<!--Oeil de gauche(à droite sur l'écran)-->
<g transform="translate(26,18)">
<path d="M 0,0 C -0.419,0 -0.809,0.265 -0.948,0.684 -1.151,1.283 -1.967,3 -3,3 -4.062,3 -4.889,1.173 -5.052,0.684 -5.227,0.16 -5.788,-0.124 -6.316,0.052 -6.84,0.226 -7.123,0.792 -6.948,1.316 -6.823,1.693 -5.645,5 -3,5 -0.355,5 0.823,1.693 0.948,1.316 1.123,0.792 0.84,0.226 0.316,0.052 0.211,0.017 0.105,0 0,0"/>
</g>
<!--Contour noir de la bouche-->
<g transform="translate(18,14)">
<path d="m0 0c-3.623 0-6.027 0.422-9 1-0.679 0.131-2 0-2-2 0-4 4.595-9 11-9 6.404 0 11 5 11 9 0 2-1.321 2.132-2 2-2.973-0.578-5.377-1-9-1"/>
</g>
<!-- Fond blanc de la bouche -->
<g transform="translate(9,13)">
<path d="m0 0s3-1 9-1 9 1 9 1-1.344-6.75-9-6.75-9 6.75-9 6.75" style="fill:#fff"/>
</g>
<!--Barre entre les dents-->
<!-- <g transform="translate(18,8.4062)">
<path d="M 0,0 C -3.596,0 -6.272,0.372 -7.937,0.745 L -8.763,2.616 C -7.939,2.305 -4.874,1.719 0,1.719 c 4.954,0 8.037,0.616 8.864,0.937 L 8.163,0.814 C 6.53,0.435 3.745,0 0,0"/>
</g> -->
</g>
<!-- Dents -->
<rect x="14.5" y="28" width="1.98" height="8" style="fill-rule:evenodd;fill:#000"/>
<rect x="21.5" y="28" width="1.98" height="10" style="fill-rule:evenodd;fill:#000"/>
<rect x="28.5" y="28" width="1.98" height="8" style="fill-rule:evenodd;fill:#000"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 45 45" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.25 0 0 -1.25 .0010481 45.005)">
<!-- Tête -->
<g transform="translate(36,18)">
<path d="m0 0c0-9.941-8.059-18-18-18-9.94 0-18 8.059-18 18 0 9.94 8.06 18 18 18 9.941 0 18-8.06 18-18" style="fill:#3ed472"/>
</g>
<!--Oeil de droite(à gauche sur l'écran)-->
<g transform="translate(16,18)">
<path d="M 0,0 C -0.419,0 -0.809,0.265 -0.949,0.684 -1.152,1.283 -1.966,3 -3,3 -4.062,3 -4.888,1.173 -5.051,0.684 -5.226,0.16 -5.79,-0.124 -6.316,0.052 -6.84,0.226 -7.124,0.792 -6.949,1.316 -6.823,1.693 -5.645,5 -3,5 -0.355,5 0.823,1.693 0.949,1.316 1.124,0.792 0.84,0.226 0.316,0.052 0.211,0.017 0.105,0 0,0"/>
</g>
<!--Oeil de gauche(à droite sur l'écran)-->
<g transform="translate(26,18)">
<path d="M 0,0 C -0.419,0 -0.809,0.265 -0.948,0.684 -1.151,1.283 -1.967,3 -3,3 -4.062,3 -4.889,1.173 -5.052,0.684 -5.227,0.16 -5.788,-0.124 -6.316,0.052 -6.84,0.226 -7.123,0.792 -6.948,1.316 -6.823,1.693 -5.645,5 -3,5 -0.355,5 0.823,1.693 0.948,1.316 1.123,0.792 0.84,0.226 0.316,0.052 0.211,0.017 0.105,0 0,0"/>
</g>
<!--Contour noir de la bouche-->
<g transform="translate(18,14)">
<path d="m0 0c-3.623 0-6.027 0.422-9 1-0.679 0.131-2 0-2-2 0-4 4.595-9 11-9 6.404 0 11 5 11 9 0 2-1.321 2.132-2 2-2.973-0.578-5.377-1-9-1"/>
</g>
<!-- Fond blanc de la bouche -->
<g transform="translate(9,13)">
<path d="m0 0s3-1 9-1 9 1 9 1-1.344-6.75-9-6.75-9 6.75-9 6.75" style="fill:#fff"/>
</g>
<!--Barre entre les dents-->
<!-- <g transform="translate(18,8.4062)">
<path d="M 0,0 C -3.596,0 -6.272,0.372 -7.937,0.745 L -8.763,2.616 C -7.939,2.305 -4.874,1.719 0,1.719 c 4.954,0 8.037,0.616 8.864,0.937 L 8.163,0.814 C 6.53,0.435 3.745,0 0,0"/>
</g> -->
</g>
<!-- Dents -->
<rect x="14.5" y="28" width="1.98" height="8" style="fill-rule:evenodd;fill:#000"/>
<rect x="21.5" y="28" width="1.98" height="10" style="fill-rule:evenodd;fill:#000"/>
<rect x="28.5" y="28" width="1.98" height="8" style="fill-rule:evenodd;fill:#000"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,10 +6,10 @@
<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,9 +48,9 @@
<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.password(size=32) }}
{{ login_form.submit(class_="bg-green") }}
{{ login_form.remember_me.label }} {{ login_form.remember_me() }}
</form>

View File

@ -5,6 +5,10 @@
</svg>
Forum
</h2>
<a href='/forum'>Index du forum</a>
<hr>
<a href="#">Vie communautaire</a>
<a href="#">Projets de programmation</a>
<a href="#">Questions et problèmes</a>
@ -12,7 +16,7 @@
<a href="#">Administration</a>
<a href="#">CreativeCalc</a>
<hr />
<hr>
<h3>Derniers commentaires</h3>
<ul>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,49 @@
{% 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 %}
<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>{{ t.title }}</td>
<td><a href='/user/{{ t.author.norm }}'>{{ t.author.name }}</a></td>
<td>{{ t.date_created | date }}</td>
<td>{{ t.comments | length }}</td>
<td>{{ t.views }} </td></tr>
{% endfor %}
</table>
{% else %}
<p>Il n'y a aucun topic sur ce forum ! Animons-le vite !</p>
{% endif %}
<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.editor(form.message) }}
<div>{{ form.submit(class_='bg-green') }}</div>
</form>
</div>
</section>
{% endblock %}

View File

@ -0,0 +1,38 @@
{% 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&nbsp;! Vous pouvez créer des
nouveaux sujets ou poster des réponses avec un compte ou en postant en
tant qu'invité.</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 %}

View File

@ -3,7 +3,15 @@
{% block content %}
<section>
<div>
<p>ici il y aura la page qui demande de checker les mails pour valider</p><br>
<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>

View File

@ -0,0 +1,10 @@
{% macro editor(form) %}
<div class=editor>
{{ form.hidden_tag() }}
{{ form.label }}
{{ form.contents() }}
{% for error in form.contents.errors %}
<span class=msgerror>{{ error }}</span>
{% endfor %}
</div>
{% endmacro %}

57
app/utils/converters.py Normal file
View File

@ -0,0 +1,57 @@
"""
utils.converter: Custom URL converters to match patterns in @app.route()
The Flask documentation is elusive on this topic. To add a new converter,
proceed as follows:
1. Define a new converter class.
2. Set the [regex] attribute to decide which portion of the URL will be
considered for conversion (apparently the default is everything until next
slash or end of string).
3. Define the to_python() and to_url() methods to actually convert.
4. Add the class to __all__ at the bottom of this file.
5. In app/__init__.py, add a dictionary entry to [app.url_map.converters].
For more information, see the Werkzeug documentation:
<https://werkzeug.palletsprojects.com/en/0.15.x/routing/#custom-converters>
"""
from werkzeug.routing import BaseConverter, ValidationError
from app.models.forum import Forum
import re
import sys
class ForumConverter(BaseConverter):
# This regex will decide which portion of the URL is matched by the curtom
# converter. By default, slashes are not included, so we must add them.
regex = r'[a-z/]+'
def to_python(self, url):
url = '/' + url
f = Forum.query.filter_by(url=url).first()
if f is None:
raise ValidationError(f"ForumConverter: no forum with url {url}")
return f
def to_url(self, forum):
return forum.url[1:]
class TopicSlugConverter(BaseConverter):
# Only catch integers followed by an optional slug string
regex = r'(\d+)(?:-[\w-]*)?'
def to_python(self, url):
"""Convert an URL pattern to a Python object, or raise an exception."""
m = re.fullmatch(TopicSlugConverter.regex, url)
if m is None:
raise Exception(f"TopicSlugConverter: conversation failed")
return int(m[1], 10)
def to_url(self, topic_id):
return str(topic_id)
# Export only the converter classes
__all__ = "ForumConverter TopicSlugConverter".split()

9
app/utils/date.py Normal file
View File

@ -0,0 +1,9 @@
from app import app
@app.template_filter('date')
def filter_date(date):
"""
Print a date in a human-readable format.
"""
return date.strftime("%d %b %Y à %H:%M")

View File

@ -7,7 +7,7 @@ def is_title(object):
"""
Check if an object is a title
"""
if type(object) == Title:
if isinstance(object, Title):
return "Oui"
else:
return "Non"

20
app/utils/notify.py Normal file
View File

@ -0,0 +1,20 @@
from app import db
from app.models.notification import Notification
# from app.models.users import Member
def notify(user, message, href=None):
""" Notify a user (by id, name or object reference) with a message.
An hyperlink can be added to redirect to the notification source """
# Cuz' duck typing is quite cool
# TODO: maybe abort if no user is found
if type(user) == str:
user = Member.query.filter_by(name=user).first()
if isinstance(user, Member):
user = user.id
if user and Member.query.get(user):
n = Notification(user, message, href=href)
db.session.add(n)
db.session.commit()
else:
print("User not found")

View File

@ -1,10 +1,12 @@
import os
import datetime
from local_config import DB_NAME
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'a-random-secret-key'
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'postgresql+psycopg2://' + os.environ.get('USER') + ':@/pcv5'
'postgresql+psycopg2://' + os.environ.get('USER') + ':@/' + DB_NAME
SQLALCHEMY_TRACK_MODIFICATIONS = False
UPLOAD_FOLDER = './app/static/avatars'
@ -21,5 +23,7 @@ class V5Config(object):
USER_NAME_MAXLEN = 32
# Minimum password length for new users and new passwords
PASSWORD_MINLEN = 10
# Maximum topic name length
TOPIC_NAME_MAXLEN = 32
# Maximum thread name length
THREAD_NAME_MAXLEN = 32
# Remember-me cookie duration time
REMEMBER_COOKIE_DURATION = datetime.timedelta(days=7)

View File

@ -4,6 +4,7 @@ from app import app, db
from app.models.users import Member, Group, GroupPrivilege
from app.models.privs import SpecialPrivilege
from app.models.trophies import Trophy, Title, TrophyMember
from app.models.forum import Forum
from app.utils import unicode_names
import os
import sys
@ -19,6 +20,7 @@ Type a category name to see a list of elements. Available categories are:
'groups' Privilege groups
'trophies' Trophies
'trophy-members' Trophies owned by members
'forums' Forum tree
Type a category name followed by 'clear' to remove all entries in the category.
@ -36,6 +38,8 @@ the database.
Type 'add-group <member> #<group-id>' to add a new member to a group.
Type 'create-trophies' to reset trophies and titles.
Type 'create-forums' to reset the forum tree.
"""
#
@ -84,6 +88,19 @@ def trophy_members(*args):
for m in t.owners:
print(f" {m}")
def forums(*args):
if args == ("clear",):
for f in Forum.query.all():
db.session.delete(f)
db.session.commit()
print("Removed all forums.")
return
for f in Forum.query.all():
parent = f"in {f.parent.url}" if f.parent is not None else "root"
print(f"{f.url} ({parent}) [{f.prefix}]: {f.name}")
print(f" {f.descr}")
#
# Creation and edition
#
@ -124,13 +141,15 @@ def create_groups_and_privs():
if g is not None:
member.groups.append(g)
m = Member("PlanèteCasio", "contact@planet-casio.com", "v5-forever")
m = Member("PlanèteCasio", "contact@planet-casio.com", "nologin")
addgroup(m, "Compte communautaire")
addgroup(m, "No login")
db.session.add(m)
m = Member("GLaDOS", "glados@aperture.science", "v5-forever")
m = Member("GLaDOS", "glados@aperture.science", "nologin")
m.xp = 1338
addgroup(m, "Robot")
addgroup(m, "No login")
db.session.add(m)
db.session.commit()
@ -161,6 +180,36 @@ def create_trophies():
print(f"Created {len(tr)} trophies.")
def create_forums():
# Clean up forums
forums("clear")
# Create the forum tree
fr = []
success = 0
with open(os.path.join(app.root_path, "data", "forums.yaml")) as fp:
fr = yaml.safe_load(fp.read())
for url, f in fr.items():
if url == "/":
parent = None
else:
parent_url = url.rsplit('/', 1)[0]
if parent_url == "":
parent_url = "/"
parent = Forum.query.filter_by(url=parent_url).first()
if parent is None:
print(f"error: no parent with url {parent_url} for {url}")
continue
f = Forum(url, f['name'], f['prefix'], f.get('descr', ''), parent)
db.session.add(f)
success += 1
db.session.commit()
print(f"Created {success} forums.")
def add_group(member, group):
if group[0] != "#":
print(f"error: group id {group} should start with '#'")
@ -192,8 +241,10 @@ commands = {
"groups": groups,
"trophies": trophies,
"trophy-members": trophy_members,
"forums": forums,
"create-groups-and-privs": create_groups_and_privs,
"create-trophies": create_trophies,
"create-forums": create_forums,
"add-group": add_group,
}

View File

@ -0,0 +1,42 @@
"""restructure forum models
Revision ID: 2a1165f6ad0a
Revises: a3fb8937ae16
Create Date: 2019-09-07 19:38:57.472404
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2a1165f6ad0a'
down_revision = 'a3fb8937ae16'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('thread_id_fkey', 'thread', type_='foreignkey')
op.drop_column('thread', 'title')
op.drop_column('thread', 'thread_type')
op.add_column('topic', sa.Column('thread_id', sa.Integer(), nullable=False))
op.add_column('topic', sa.Column('title', sa.Unicode(length=32), nullable=True))
op.drop_constraint('topic_id_fkey', 'topic', type_='foreignkey')
op.create_foreign_key(None, 'topic', 'post', ['id'], ['id'])
op.create_foreign_key(None, 'topic', 'thread', ['thread_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'topic', type_='foreignkey')
op.drop_constraint(None, 'topic', type_='foreignkey')
op.create_foreign_key('topic_id_fkey', 'topic', 'thread', ['id'], ['id'])
op.drop_column('topic', 'title')
op.drop_column('topic', 'thread_id')
op.add_column('thread', sa.Column('thread_type', sa.VARCHAR(length=20), autoincrement=False, nullable=True))
op.add_column('thread', sa.Column('title', sa.VARCHAR(length=32), autoincrement=False, nullable=True))
op.create_foreign_key('thread_id_fkey', 'thread', 'post', ['id'], ['id'])
# ### end Alembic commands ###

View File

@ -0,0 +1,32 @@
"""Remove Thread table
Revision ID: 2dbb614a7236
Revises: b890d9bb207b
Create Date: 2019-09-08 12:32:58.869143
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2dbb614a7236'
down_revision = 'b890d9bb207b'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('thread')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('thread',
sa.Column('top_comment_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.Column('id', sa.INTEGER(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['top_comment_id'], ['comment.id'], name='thread_top_comment_id_fkey')
)
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""add forum urls
Revision ID: 49427f8eb285
Revises: a7aac1469393
Create Date: 2019-09-02 21:16:06.971807
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '49427f8eb285'
down_revision = 'a7aac1469393'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('forum', sa.Column('url', sa.String(length=64), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('forum', 'url')
# ### end Alembic commands ###

View File

@ -0,0 +1,33 @@
"""Recreate Thread table
Revision ID: 4e05b43b18b1
Revises: 2dbb614a7236
Create Date: 2019-09-08 12:33:30.647277
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4e05b43b18b1'
down_revision = '2dbb614a7236'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('thread',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('top_comment_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['top_comment_id'], ['comment.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('thread')
# ### end Alembic commands ###

View File

@ -0,0 +1,48 @@
"""Ajout des posts
Revision ID: 611667e86261
Revises: 87b039db71a5
Create Date: 2019-08-20 11:57:56.053453
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '611667e86261'
down_revision = '87b039db71a5'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('post',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('type', sa.String(length=20), nullable=True),
sa.Column('text', sa.Text(_expect_unicode=True), nullable=True),
sa.Column('date_created', sa.DateTime(), nullable=True),
sa.Column('date_modified', sa.DateTime(), nullable=True),
sa.Column('author_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['author_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.drop_table('content')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('content',
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
sa.Column('type', sa.VARCHAR(length=20), autoincrement=False, nullable=True),
sa.Column('data', sa.TEXT(), autoincrement=False, nullable=True),
sa.Column('date_created', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
sa.Column('date_modified', postgresql.TIMESTAMP(), autoincrement=False, nullable=True),
sa.Column('author_id', sa.INTEGER(), autoincrement=False, nullable=True),
sa.ForeignKeyConstraint(['author_id'], ['user.id'], name='content_author_id_fkey'),
sa.PrimaryKeyConstraint('id', name='content_pkey')
)
op.drop_table('post')
# ### end Alembic commands ###

View File

@ -0,0 +1,54 @@
"""ajout des classes Comment Thread Forum
Revision ID: 6498631e62c5
Revises: f3f6d7f7fa81
Create Date: 2019-08-21 16:47:15.557948
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6498631e62c5'
down_revision = 'f3f6d7f7fa81'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('forum',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.Unicode(length=64), nullable=True),
sa.Column('slug', sa.Unicode(length=64), nullable=True),
sa.Column('description', sa.UnicodeText(), nullable=True),
sa.Column('parent_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['parent_id'], ['forum.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('thread',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('thread_type', sa.String(length=20), nullable=True),
sa.Column('title', sa.Unicode(length=32), nullable=True),
sa.Column('top_comment', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['id'], ['post.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('comment',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('text', sa.UnicodeText(), nullable=True),
sa.Column('thread_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['id'], ['post.id'], ),
sa.ForeignKeyConstraint(['thread_id'], ['thread.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('comment')
op.drop_table('thread')
op.drop_table('forum')
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""add-foreign-key-on-Thread
Revision ID: 794d44c2bef8
Revises: 6498631e62c5
Create Date: 2019-08-21 16:48:06.623266
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '794d44c2bef8'
down_revision = '6498631e62c5'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_foreign_key(None, 'thread', 'comment', ['top_comment'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'thread', type_='foreignkey')
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""add topics
Revision ID: a3fb8937ae16
Revises: ebca7362eb22
Create Date: 2019-09-05 21:35:48.260827
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a3fb8937ae16'
down_revision = 'ebca7362eb22'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('topic',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('forum_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['forum_id'], ['forum.id'], ),
sa.ForeignKeyConstraint(['id'], ['thread.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('topic')
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""forum editions
Revision ID: a7aac1469393
Revises: e3b140752719
Create Date: 2019-09-02 21:12:52.236043
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a7aac1469393'
down_revision = 'e3b140752719'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('forum', sa.Column('descr', sa.UnicodeText(), nullable=True))
op.add_column('forum', sa.Column('prefix', sa.Unicode(length=64), nullable=True))
op.drop_column('forum', 'slug')
op.drop_column('forum', 'description')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('forum', sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True))
op.add_column('forum', sa.Column('slug', sa.VARCHAR(length=64), autoincrement=False, nullable=True))
op.drop_column('forum', 'prefix')
op.drop_column('forum', 'descr')
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""Recreate Thread references
Revision ID: abd5f8de0106
Revises: 4e05b43b18b1
Create Date: 2019-09-08 12:34:12.574672
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'abd5f8de0106'
down_revision = '4e05b43b18b1'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('comment', sa.Column('thread_id', sa.Integer(), nullable=False))
op.create_foreign_key(None, 'comment', 'thread', ['thread_id'], ['id'])
op.add_column('topic', sa.Column('thread_id', sa.Integer(), nullable=False))
op.create_foreign_key(None, 'topic', 'thread', ['thread_id'], ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'topic', type_='foreignkey')
op.drop_column('topic', 'thread_id')
op.drop_constraint(None, 'comment', type_='foreignkey')
op.drop_column('comment', 'thread_id')
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""Remove Thread references
Revision ID: b890d9bb207b
Revises: 2a1165f6ad0a
Create Date: 2019-09-08 12:29:38.650491
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b890d9bb207b'
down_revision = '2a1165f6ad0a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('comment_thread_id_fkey', 'comment', type_='foreignkey')
op.drop_column('comment', 'thread_id')
op.drop_constraint('topic_thread_id_fkey', 'topic', type_='foreignkey')
op.drop_column('topic', 'thread_id')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('topic', sa.Column('thread_id', sa.INTEGER(), autoincrement=False, nullable=False))
op.create_foreign_key('topic_thread_id_fkey', 'topic', 'thread', ['thread_id'], ['id'])
op.add_column('comment', sa.Column('thread_id', sa.INTEGER(), autoincrement=False, nullable=False))
op.create_foreign_key('comment_thread_id_fkey', 'comment', 'thread', ['thread_id'], ['id'])
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""Add number of views to Topic
Revision ID: c665488fc26e
Revises: abd5f8de0106
Create Date: 2019-09-08 16:49:50.448779
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c665488fc26e'
down_revision = 'abd5f8de0106'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('topic', sa.Column('views', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('topic', 'views')
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""improve relations for threads and comments
Revision ID: e3b140752719
Revises: 794d44c2bef8
Create Date: 2019-08-24 19:09:46.981771
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e3b140752719'
down_revision = '794d44c2bef8'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('thread', sa.Column('top_comment_id', sa.Integer(), nullable=True))
op.drop_constraint('thread_top_comment_fkey', 'thread', type_='foreignkey')
op.create_foreign_key(None, 'thread', 'comment', ['top_comment_id'], ['id'])
op.drop_column('thread', 'top_comment')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('thread', sa.Column('top_comment', sa.INTEGER(), autoincrement=False, nullable=True))
op.drop_constraint(None, 'thread', type_='foreignkey')
op.create_foreign_key('thread_top_comment_fkey', 'thread', 'comment', ['top_comment'], ['id'])
op.drop_column('thread', 'top_comment_id')
# ### end Alembic commands ###

View File

@ -0,0 +1,37 @@
"""Ajout des notifications
Revision ID: ebca7362eb22
Revises: e3b140752719
Rebased on: 49427f8eb285 (Lephenixnoir)
Create Date: 2019-09-01 11:36:25.962212
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ebca7362eb22'
down_revision = '49427f8eb285'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('notification',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('text', sa.UnicodeText(), nullable=True),
sa.Column('href', sa.UnicodeText(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('owner_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['owner_id'], ['member.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('notification')
# ### end Alembic commands ###

View File

@ -0,0 +1,28 @@
"""Ajout des topics/comments/autres
Revision ID: f3f6d7f7fa81
Revises: 611667e86261
Create Date: 2019-08-20 17:21:10.330435
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f3f6d7f7fa81'
down_revision = '611667e86261'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('post', 'text')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('post', sa.Column('text', sa.TEXT(), autoincrement=False, nullable=True))
# ### end Alembic commands ###

View File

@ -1,17 +0,0 @@
alembic==0.9.8
click==6.7
Flask==0.12.2
Flask-Login==0.4.1
Flask-Migrate==2.1.1
Flask-SQLAlchemy==2.3.2
Flask-WTF==0.14.2
itsdangerous==0.24
Jinja2==2.10
Mako==1.0.7
MarkupSafe==1.0
python-dateutil==2.6.1
python-editor==1.0.3
six==1.11.0
SQLAlchemy==1.2.3
Werkzeug==0.14.1
WTForms==2.1

View File

@ -1,9 +0,0 @@
flask
flask-login
flask-migrate
flask-script
flask-sqlalchemy
flask-wtf
uwsgi
psycopg2
pyyaml