Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
Darks | e15005a427 |
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -6,12 +6,13 @@ app/static/avatars/
|
|||
## Devlopement files
|
||||
|
||||
# virtualenv
|
||||
requirements.txt
|
||||
venv/
|
||||
.venv/
|
||||
# pipenv
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
# Sublime Text files
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
## Deployment files
|
||||
|
||||
|
@ -21,13 +22,7 @@ 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
Normal file
25
Pipfile
Normal file
|
@ -0,0 +1,25 @@
|
|||
[[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
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.topic import Topic
|
||||
# from app.models.models import Post
|
||||
|
||||
|
||||
@app.shell_context_processor
|
||||
|
|
|
@ -5,15 +5,12 @@ 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():
|
||||
|
@ -26,17 +23,8 @@ 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, notification
|
||||
from app.routes.admin import index, groups, account, trophies, forums
|
||||
from app.routes.forum import index
|
||||
|
||||
from app.routes.account import login, account
|
||||
from app.routes.admin import index, groups, account, trophies
|
||||
from app.utils import pluralize # To use pluralize into the templates
|
||||
from app.utils import date
|
||||
from app.utils import is_title
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
# 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.
|
|
@ -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,10 +11,9 @@
|
|||
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
|
||||
|
@ -24,7 +23,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
|
||||
|
@ -34,7 +33,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
|
||||
|
@ -43,7 +42,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
|
||||
|
@ -52,26 +51,22 @@
|
|||
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,15 +0,0 @@
|
|||
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
|
|
@ -1,9 +0,0 @@
|
|||
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')
|
|
@ -1,40 +0,0 @@
|
|||
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}>'
|
|
@ -1,40 +0,0 @@
|
|||
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)
|
|
@ -1,23 +0,0 @@
|
|||
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})>'
|
|
@ -1,58 +1,42 @@
|
|||
from app import db
|
||||
from app.models.users import User
|
||||
from datetime import datetime
|
||||
from app import db
|
||||
from app.models.users import *
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
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 __init__(self, author, text):
|
||||
""" Create a Post """
|
||||
self.text = text
|
||||
if type(author) == Member:
|
||||
author = author.id
|
||||
self.author_id = author
|
||||
|
||||
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. This is a privileged operation!
|
||||
|
||||
Arguments:
|
||||
new_author -- new post author (User)
|
||||
"""
|
||||
|
||||
self.author = 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
|
||||
|
||||
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
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
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}>'
|
|
@ -1,44 +0,0 @@
|
|||
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}>'
|
|
@ -2,12 +2,11 @@ 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
|
||||
|
@ -15,10 +14,8 @@ 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
|
||||
|
@ -27,7 +24,8 @@ class User(UserMixin, db.Model):
|
|||
# User type (polymorphic discriminator)
|
||||
type = db.Column(db.String(30))
|
||||
|
||||
# Also a [posts] relationship populated from the Post class.
|
||||
# TODO: add good relation
|
||||
posts = db.relationship('Post', backref="author", lazy=False)
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': __tablename__,
|
||||
|
@ -37,10 +35,8 @@ class User(UserMixin, db.Model):
|
|||
def __repr__(self):
|
||||
return f'<User: #{self.id}>'
|
||||
|
||||
|
||||
class Guest(User):
|
||||
""" Unregistered user with minimal privileges """
|
||||
|
||||
# Guest: Unregistered user with minimal privileges
|
||||
class Guest(User, db.Model):
|
||||
__tablename__ = 'guest'
|
||||
__mapper_args__ = {'polymorphic_identity': __tablename__}
|
||||
|
||||
|
@ -56,9 +52,8 @@ class Guest(User):
|
|||
return f'<Guest: {self.username} ({self.ip})>'
|
||||
|
||||
|
||||
class Member(User):
|
||||
""" Registered user with full access to the website's services """
|
||||
|
||||
# Member: Registered user with full access to the website's services
|
||||
class Member(User, db.Model):
|
||||
__tablename__ = 'member'
|
||||
__mapper_args__ = {'polymorphic_identity': __tablename__}
|
||||
|
||||
|
@ -97,7 +92,6 @@ class Member(User):
|
|||
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')
|
||||
|
||||
|
@ -209,14 +203,6 @@ class Member(User):
|
|||
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
|
||||
|
@ -228,7 +214,8 @@ class Member(User):
|
|||
t = Trophy.query.filter_by(name=t).first()
|
||||
if t not in self.trophies:
|
||||
self.trophies.append(t)
|
||||
self.notify(f"Vous avez débloqué le trophée '{t.name}'")
|
||||
# TODO: implement the notification system
|
||||
# self.notify(f"Vous venez de débloquer le trophée '{t.name}'")
|
||||
|
||||
def del_trophy(self, t):
|
||||
"""
|
||||
|
@ -238,7 +225,7 @@ class Member(User):
|
|||
if type(t) == int:
|
||||
t = Trophy.query.get(t)
|
||||
if type(t) == str:
|
||||
t = Trophy.query.filter_by(name=t).first()
|
||||
t = Trophy.query.filter_by(name=name).first()
|
||||
if t in self.trophies:
|
||||
self.trophies.remove(t)
|
||||
|
||||
|
|
|
@ -57,17 +57,15 @@ 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') + "?email=" + form.email.data)
|
||||
return redirect(url_for('validation'))
|
||||
return render('register.html', title='Register', form=form)
|
||||
|
||||
|
||||
@app.route('/register/validation/', methods=['GET', 'POST'])
|
||||
@app.route('/register/validation/')
|
||||
def validation():
|
||||
mail = request.args['email']
|
||||
if current_user.is_authenticated:
|
||||
return redirect(url_for('index'))
|
||||
return render('validation.html', mail=mail)
|
||||
return render('validation.html')
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
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'])
|
||||
|
@ -17,40 +14,16 @@ 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')
|
||||
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)
|
||||
return redirect(request.referrer)
|
||||
login_user(member, remember=form.remember_me.data)
|
||||
member.update_trophies("on-login")
|
||||
|
||||
# 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.args.get('next'):
|
||||
return redirect(request.args.get('next'))
|
||||
if request.referrer:
|
||||
return redirect(request.referrer)
|
||||
return redirect(url_for('index'))
|
||||
|
||||
return render('login.html', form=form)
|
||||
|
||||
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
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)
|
|
@ -1,5 +1,4 @@
|
|||
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
|
||||
|
@ -7,7 +6,6 @@ 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
|
||||
|
||||
|
||||
|
@ -49,7 +47,6 @@ 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')
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
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)
|
|
@ -1,42 +0,0 @@
|
|||
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)
|
|
@ -1,14 +1,12 @@
|
|||
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):
|
||||
norm = unicode_names.normalize(username)
|
||||
member = Member.query.filter_by(norm=norm).first_or_404()
|
||||
member = Member.query.filter_by(name=username).first_or_404()
|
||||
return render('user.html', member=member)
|
||||
|
||||
|
||||
|
|
|
@ -86,10 +86,3 @@
|
|||
.trophies-panel p label {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Editor */
|
||||
|
||||
.editor textarea {
|
||||
font-family: monospace;
|
||||
height: 192px;
|
||||
}
|
||||
|
|
|
@ -122,10 +122,7 @@
|
|||
display: block;
|
||||
margin: 5px 15px; padding: 5px 10px;
|
||||
font-size: 14px;
|
||||
transition: background .15s ease;
|
||||
}
|
||||
#menu form label {
|
||||
float: left; margin-right: 10px;
|
||||
background: #e8e8e8; transition: background .15s ease;
|
||||
}
|
||||
#menu form input:first-child {
|
||||
margin-bottom: 0; border-bottom: none;
|
||||
|
|
|
@ -192,7 +192,6 @@ 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 {
|
||||
|
@ -202,7 +201,7 @@ nav a:focus {
|
|||
}
|
||||
#menu form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin: 8px 0 5px 0;
|
||||
margin: 16px 0 5px 0;
|
||||
}
|
||||
#menu form label {
|
||||
font-size: 13px; color: #FFFFFF; opacity: .7;
|
||||
|
|
|
@ -17,38 +17,3 @@ 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;
|
||||
}
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
<?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>
|
Before Width: | Height: | Size: 2.2 KiB |
|
@ -1,33 +0,0 @@
|
|||
<?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>
|
Before Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
Before Width: | Height: | Size: 17 KiB |
|
@ -1,33 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,38 +0,0 @@
|
|||
{% 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 style="text-align: center"><a href="{{ url_for('adm_edit_account', user_id=user.id) }}">Modifier</a></td>
|
||||
<td><a href="{{ url_for('adm_edit_account', user_id=user.id) }}">Modifier</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
|
|
@ -8,9 +8,8 @@
|
|||
<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 %}
|
||||
|
|
|
@ -4,5 +4,4 @@
|
|||
{% 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>
|
||||
|
|
|
@ -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-small.png') }}" alt="logo"/>
|
||||
<img src="{{ url_for('static',filename= 'images/logo_noshadow.png') }}" alt="logo"/>
|
||||
</a>
|
||||
|
||||
<li>
|
||||
|
|
|
@ -6,10 +6,10 @@
|
|||
<a href="{{ url_for('user', username=current_user.name) }}">
|
||||
{{ current_user.name }}</a>
|
||||
</h2>
|
||||
<a href="{{ url_for('list_notifications') }}">
|
||||
<a href="#">
|
||||
<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{{ " ({})".format(current_user.notifications|length) if current_user.notifications|length }}
|
||||
</svg>Notifications
|
||||
</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) }}
|
||||
{{ login_form.username(size=32, placeholder="Identifiant") }}
|
||||
{{ login_form.password.label }}
|
||||
{{ login_form.password(size=32) }}
|
||||
{{ login_form.password(size=32, placeholder="Mot de passe") }}
|
||||
{{ login_form.submit(class_="bg-green") }}
|
||||
{{ login_form.remember_me.label }} {{ login_form.remember_me() }}
|
||||
</form>
|
||||
|
|
|
@ -5,10 +5,6 @@
|
|||
</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>
|
||||
|
@ -16,7 +12,7 @@
|
|||
<a href="#">Administration</a>
|
||||
<a href="#">CreativeCalc</a>
|
||||
|
||||
<hr>
|
||||
<hr />
|
||||
|
||||
<h3>Derniers commentaires</h3>
|
||||
<ul>
|
||||
|
|
|
@ -5,16 +5,12 @@
|
|||
</svg>
|
||||
Actualités
|
||||
</h2>
|
||||
<a href='/forum/news'>Toutes les nouveautés</a>
|
||||
<a href="#">Casio</a>
|
||||
<a href="#">Arduino</a>
|
||||
<a href="#">Projets communautaires</a>
|
||||
<a href="#">Divers</a>
|
||||
|
||||
<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>
|
||||
<hr />
|
||||
|
||||
<h3>Derniers articles</h3>
|
||||
<ul>
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
{% 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,11 +1,9 @@
|
|||
{% 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 %}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,38 +0,0 @@
|
|||
{% 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 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 %}
|
|
@ -3,15 +3,7 @@
|
|||
{% 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>
|
||||
<p>ici il y aura la page qui demande de checker les mails pour valider</p><br>
|
||||
<a href="{{url_for('index')}}">Retour à la page d'accueil</a>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
{% 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 %}
|
|
@ -1,57 +0,0 @@
|
|||
"""
|
||||
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()
|
|
@ -1,9 +0,0 @@
|
|||
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")
|
|
@ -7,7 +7,7 @@ def is_title(object):
|
|||
"""
|
||||
Check if an object is a title
|
||||
"""
|
||||
if isinstance(object, Title):
|
||||
if type(object) == Title:
|
||||
return "Oui"
|
||||
else:
|
||||
return "Non"
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
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")
|
10
config.py
10
config.py
|
@ -1,12 +1,10 @@
|
|||
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') + ':@/' + DB_NAME
|
||||
'postgresql+psycopg2://' + os.environ.get('USER') + ':@/pcv5'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
UPLOAD_FOLDER = './app/static/avatars'
|
||||
|
||||
|
@ -23,7 +21,5 @@ class V5Config(object):
|
|||
USER_NAME_MAXLEN = 32
|
||||
# Minimum password length for new users and new passwords
|
||||
PASSWORD_MINLEN = 10
|
||||
# Maximum thread name length
|
||||
THREAD_NAME_MAXLEN = 32
|
||||
# Remember-me cookie duration time
|
||||
REMEMBER_COOKIE_DURATION = datetime.timedelta(days=7)
|
||||
# Maximum topic name length
|
||||
TOPIC_NAME_MAXLEN = 32
|
||||
|
|
55
master.py
55
master.py
|
@ -4,7 +4,6 @@ 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
|
||||
|
@ -20,7 +19,6 @@ 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.
|
||||
|
||||
|
@ -38,8 +36,6 @@ 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.
|
||||
"""
|
||||
|
||||
#
|
||||
|
@ -88,19 +84,6 @@ 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
|
||||
#
|
||||
|
@ -141,15 +124,13 @@ def create_groups_and_privs():
|
|||
if g is not None:
|
||||
member.groups.append(g)
|
||||
|
||||
m = Member("PlanèteCasio", "contact@planet-casio.com", "nologin")
|
||||
m = Member("PlanèteCasio", "contact@planet-casio.com", "v5-forever")
|
||||
addgroup(m, "Compte communautaire")
|
||||
addgroup(m, "No login")
|
||||
db.session.add(m)
|
||||
|
||||
m = Member("GLaDOS", "glados@aperture.science", "nologin")
|
||||
m = Member("GLaDOS", "glados@aperture.science", "v5-forever")
|
||||
m.xp = 1338
|
||||
addgroup(m, "Robot")
|
||||
addgroup(m, "No login")
|
||||
db.session.add(m)
|
||||
db.session.commit()
|
||||
|
||||
|
@ -180,36 +161,6 @@ 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 '#'")
|
||||
|
@ -241,10 +192,8 @@ 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,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,32 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,28 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,33 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,48 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,54 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,28 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,34 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,34 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,34 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,34 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,28 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,34 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,37 +0,0 @@
|
|||
"""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 ###
|
|
@ -1,28 +0,0 @@
|
|||
"""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 ###
|
17
package.txt
Normal file
17
package.txt
Normal file
|
@ -0,0 +1,17 @@
|
|||
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
|
9
requirements.txt
Normal file
9
requirements.txt
Normal file
|
@ -0,0 +1,9 @@
|
|||
flask
|
||||
flask-login
|
||||
flask-migrate
|
||||
flask-script
|
||||
flask-sqlalchemy
|
||||
flask-wtf
|
||||
uwsgi
|
||||
psycopg2
|
||||
pyyaml
|
Loading…
Reference in a new issue