Merge branch 'dev' of gitea.planet-casio.com:devs/PCv5 into dev

This commit is contained in:
Eragon 2019-09-09 13:34:57 +02:00
commit d2365a8444
No known key found for this signature in database
GPG Key ID: B2B1BF4DA61BBB85
43 changed files with 957 additions and 98 deletions

4
.gitignore vendored
View File

@ -27,3 +27,7 @@ local_config.py
## Wiki
wiki/
## Personal folder
exclude/

View File

@ -5,12 +5,15 @@ 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,9 +29,14 @@ 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
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.

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

View File

@ -5,22 +5,36 @@ class Comment(Post):
__tablename__ = 'comment'
__mapper_args__ = {'polymorphic_identity': __tablename__}
# ID of the associated Post object
# ID of the underlying Post object
id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True)
# Standalone properties
# Comment contents
text = db.Column(db.UnicodeText)
# Relations
# Parent thread
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'),
nullable=False)
thread = db.relationship('Thread', backref='comments',
foreign_keys=thread_id)
# attachement = db.relationship('Attachement', backref='comment')
def __init__(self, author, text, thread):
Post.__init__(author, text)
if isinstance(thread, Thread):
thread = thread.id
self.thread_id = 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}>'

View File

@ -5,22 +5,36 @@ class Forum(db.Model):
__tablename__ = 'forum'
id = db.Column(db.Integer, primary_key=True)
# Standalone properties
# Forum name, as displayed on the site (eg. "Problèmes de transfert")
name = db.Column(db.Unicode(64))
slug = db.Column(db.Unicode(64))
description = db.Column(db.UnicodeText)
# 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)
lazy=True, foreign_keys=parent_id)
# Also [topics] which is provided by a backref from the Topic class
def __init__(self, name, description, priv_prefix):
def __init__(self, url, name, prefix, descr="", parent=None):
self.url = url
self.name = name
self.description = description
self.priv_prefix = priv_prefix
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(thread.comments) for thread in self.topics)

View File

@ -1,47 +1,58 @@
from datetime import datetime
from app import db
from app.models.users import User
from datetime import datetime
class Post(db.Model):
""" Content a User can create and publish """
__tablename__ = 'post'
id = db.Column(db.Integer, primary_key=True)
"""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
date_created = db.Column(db.DateTime)
date_modified = db.Column(db.DateTime)
def __init__(self, author):
"""
Create a new Post.
# Relationships
author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
Arguments:
author -- post author (User)
"""
def __init__(self, author, text):
""" Create a Post """
self.text = text
if isinstance(author, User):
author = author.id
self.author_id = author
self.author = author
self.date_created = datetime.now()
self.date_modified = datetime.now()
def update(self, text):
""" Update a post. Check whether the request sender has the right to do
this! """
self.text = text
def touch(self):
"""Touch a Post when it is edited."""
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 isinstance(new_author, User):
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

@ -1,35 +1,40 @@
from app import db
from app.models.post import Post
from app.models.comment import Comment
from config import V5Config
class Thread(Post):
""" Some thread, such as a topic, program, tutorial """
class Thread(db.Model):
"""Some thread, such as a topic, program, tutorial."""
# Foreign Post object ID
__tablename__ = 'thread'
id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True)
# Identify threads as a type of posts using the table name, and add a
# column to further discriminate types of threads
thread_type = db.Column(db.String(20))
__mapper_args__ = {
'polymorphic_identity': __tablename__,
'polymorphic_on': thread_type
}
# Unique ID
id = db.Column(db.Integer, primary_key=True)
# Properties
title = db.Column(db.Unicode(V5Config.THREAD_NAME_MAXLEN))
# Also a relation [comments] populated from the Comment class.
# Relations
# Top comment
top_comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'))
top_comment = db.relationship('Comment', foreign_keys=top_comment_id)
def __init__(self, author, text, title):
""" Create a Thread """
Post.__init__(author, text)
self.title = title
# 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}'
return f'<Thread: #{self.id}>'

View File

@ -1,22 +1,44 @@
from app import db
from app.models.thread import Thread
from app.models.post import Post
from config import V5Config
class Topic(Thread):
class Topic(Post):
__tablename__ = 'topic'
id = db.Column(db.Integer, db.ForeignKey('thread.id'), primary_key=True)
__mapper_args__ = {'polymorphic_identity': __tablename__}
# Relationships
# 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)
def __init__(self, author, text, title, forum):
""" Create a Topic """
Post.__init__(author, text, title)
if isinstance(forum, Forum):
forum = forum.id
self.forum_id = forum
# 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}'
return f'<Topic: #{self.id}>'

View File

@ -27,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=True)
# Also a [posts] relationship populated from the Post class.
__mapper_args__ = {
'polymorphic_identity': __tablename__,

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

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

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

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

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

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

@ -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
#
@ -163,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 '#'")
@ -194,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,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

@ -2,6 +2,7 @@
Revision ID: ebca7362eb22
Revises: e3b140752719
Rebased on: 49427f8eb285 (Lephenixnoir)
Create Date: 2019-09-01 11:36:25.962212
"""
@ -11,7 +12,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ebca7362eb22'
down_revision = 'e3b140752719'
down_revision = '49427f8eb285'
branch_labels = None
depends_on = None