programs: program creation + view + comments

This is very much a work in progress, but the main ideas are here.

[MIGRATION] This commit contains a new version of the schema.
This commit is contained in:
Lephe 2022-05-19 20:34:23 +01:00
parent 0e1b434f7d
commit b047ed97af
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
14 changed files with 301 additions and 15 deletions

View File

@ -27,6 +27,7 @@ login.login_message = "Veuillez vous authentifier avant de continuer."
from app.utils.converters import *
app.url_map.converters['forum'] = ForumConverter
app.url_map.converters['topicpage'] = TopicPageConverter
app.url_map.converters['programpage'] = ProgramPageConverter
# Register routes
from app import routes

14
app/forms/programs.py Normal file
View File

@ -0,0 +1,14 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField
from wtforms.validators import InputRequired, Length
import app.utils.validators as vf
from app.utils.antibot_field import AntibotField
from app.forms.forum import CommentForm
class ProgramCreationForm(CommentForm):
name = StringField('Nom du programme',
validators=[InputRequired(), Length(min=3, max=64)])
tags = StringField('Liste de tags', description='Séparés par des virgules')
submit = SubmitField('Soumettre le programme')

View File

@ -9,7 +9,7 @@ class Program(Post):
id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True)
# Program name
title = db.Column(db.Unicode(128))
name = db.Column(db.Unicode(128))
# TODO: Category (games/utilities/lessons)
# TODO: Compatible calculator models
@ -43,4 +43,4 @@ class Program(Post):
db.session.delete(self)
def __repr__(self):
return f'<Program: #{self.id} "{self.title}">'
return f'<Program: #{self.id} "{self.name}">'

View File

@ -12,4 +12,4 @@ class Tag(db.Model):
def __init__(self, post, tag):
self.post = post
self.tag = tag
self.name = tag

View File

@ -7,7 +7,7 @@ from app.routes.admin import index, groups, account, trophies, forums, \
from app.routes.forum import index, topic
from app.routes.polls import vote, delete
from app.routes.posts import edit
from app.routes.programs import index
from app.routes.programs import index, submit, program
from app.routes.api import markdown
try:

View File

@ -4,5 +4,5 @@ from app.utils.render import render
@app.route('/programmes')
def program_index():
programs = Program.query.all()
return render('/programs/index.html')
programs = Program.query.order_by(Program.date_created.desc()).all()
return render('/programs/index.html', programs=programs)

View File

@ -0,0 +1,62 @@
from app import app, db
from app.models.program import Program
from app.models.comment import Comment
from app.models.thread import Thread
from app.utils.render import render
from app.forms.forum import CommentForm, AnonymousCommentForm
from config import V5Config
from flask_login import current_user
from flask import redirect, url_for, flash
@app.route('/programmes/<programpage:page>', methods=['GET','POST'])
def program_view(page):
p, page = page
if current_user.is_authenticated:
form = CommentForm()
else:
form = AnonymousCommentForm()
if form.validate_on_submit() and (
V5Config.ENABLE_GUEST_POST or current_user.is_authenticated):
# Manage author
if current_user.is_authenticated:
author = current_user
else:
author = Guest(form.pseudo.data)
db.session.add(author)
# Create comment
c = Comment(author, form.message.data, p.thread)
db.session.add(c)
db.session.commit()
# Manage files
attachments = []
for file in form.attachments.data:
if file.filename != "":
a = Attachment(file, c)
attachments.append((a, file))
db.session.add(a)
db.session.commit()
for a, file in attachments:
a.set_file(file)
# Update member's xp and trophies
if current_user.is_authenticated:
current_user.add_xp(1)
current_user.update_trophies('new-post')
flash('Message envoyé', 'ok')
# Redirect to empty the form
return redirect(url_for('program_view', page=(p, "fin"), _anchor=c.id))
if page == -1:
page = (p.thread.comments.count() - 1) // Thread.COMMENTS_PER_PAGE + 1
comments = p.thread.comments.order_by(Comment.date_created.asc()) \
.paginate(page, Thread.COMMENTS_PER_PAGE, True)
return render('/programs/program.html', p=p, form=form, comments=comments)

View File

@ -0,0 +1,59 @@
from app import app, db
from app.models.program import Program
from app.models.thread import Thread
from app.models.comment import Comment
from app.models.tag import Tag
from app.utils.render import render
from app.forms.programs import ProgramCreationForm
from flask_login import current_user
from flask import redirect, url_for, flash
@app.route('/programmes/soumettre', methods=['GET', 'POST'])
def program_submit():
if current_user.is_authenticated:
form = ProgramCreationForm()
if form.validate_on_submit():
# First create a new thread
# TODO: Reuse a thread when performing topic promotion
th = Thread()
db.session.add(th)
db.session.commit()
# Create its top comment
c = Comment(current_user, form.message.data, th)
db.session.add(c)
db.session.commit()
th.set_top_comment(c)
db.session.merge(th)
# Then build the actual program
p = Program(current_user, form.name.data, th)
db.session.add(p)
db.session.commit()
# Add tags
# TODO: Check tags against a predefined set
for tag in form.tags.data.split(","):
db.session.add(Tag(p, tag.strip()))
db.session.commit()
# Manage files
attachments = []
for file in form.attachments.data:
if file.filename != "":
a = Attachment(file, c)
attachments.append((a, file))
db.session.add(a)
db.session.commit()
for a, file in attachments:
a.set_file(file)
current_user.add_xp(20)
current_user.update_trophies('new-program')
flash('Le programme a bien été soumis', 'ok')
return redirect(url_for('program_index'))
return render('/programs/submit.html', form=form)

View File

@ -4,6 +4,9 @@
<path fill="#ffffff" d="M8,3A2,2 0 0,0 6,5V9A2,2 0 0,1 4,11H3V13H4A2,2 0 0,1 6,15V19A2,2 0 0,0 8,21H10V19H8V14A2,2 0 0,0 6,12A2,2 0 0,0 8,10V5H10V3M16,3A2,2 0 0,1 18,5V9A2,2 0 0,0 20,11H21V13H20A2,2 0 0,0 18,15V19A2,2 0 0,1 16,21H14V19H16V14A2,2 0 0,1 18,12A2,2 0 0,1 16,10V5H14V3H16Z"></path>
</svg>Programmes
</h2>
<a href="{{ url_for('program_index') }}">Index des programmes</a>
<hr>
<a href="#">
<svg viewBox="0 0 24 24">
<path fill="#ffffff" d="M7,6H17A6,6 0 0,1 23,12A6,6 0 0,1 17,18C15.22,18 13.63,17.23 12.53,16H11.47C10.37,17.23 8.78,18 7,18A6,6 0 0,1 1,12A6,6 0 0,1 7,6M6,9V11H4V13H6V15H8V13H10V11H8V9H6M15.5,12A1.5,1.5 0 0,0 14,13.5A1.5,1.5 0 0,0 15.5,15A1.5,1.5 0 0,0 17,13.5A1.5,1.5 0 0,0 15.5,12M18.5,9A1.5,1.5 0 0,0 17,10.5A1.5,1.5 0 0,0 18.5,12A1.5,1.5 0 0,0 20,10.5A1.5,1.5 0 0,0 18.5,9Z"></path>

View File

@ -6,15 +6,24 @@
{% block content %}
<section>
<h2>Tous les programmes</h2>
<h2>Publications récentes</h2>
<p><i>[Ici quelques "cartes" de programmes récents]</i></p>
<h2>Populaires</h2>
<p><i>[Ici quelques "cartes" de programmes populaires aléatoires]</i></p>
<h2>Poster un programme</h2>
<p><a href='{{ url_for("program_submit") }}'>Poster un nouveau programme sur Planète Casio</a></p>
<h2>Tous les programmes</h2>
<table class=programlist>
<tr><th>ID</th><th>Nom</th><th>Auteur</th><th>Publié le</th></tr>
<tr><th>ID</th><th>Nom</th><th>Auteur</th><th>Publié le</th><th>Tags</th></tr>
{% for p in programs %}
<tr><td>{{ p.id }}</td>
<td><a href='#'>{{ p.name }}</a></td>
<td><a href='{{ url_for("program_view", page=(p,1)) }}'>{{ p.name }}</a></td>
<td>{{ p.author.name }}</td>
<td>{{ p.date_created }}</td></tr>
<td>{{ p.date_created | dyndate }}</td>
<td>{% for tag in p.tags %}{{ tag.name }} {% endfor %}</td></tr>
{% endfor %}
</table>
</section>

View File

@ -7,18 +7,18 @@
{% block title %}
<h1>Programme {{ program.name }}</h1>
<h1>Programme: {{ p.name }}</h1>
{% endblock %}
{% block content %}
<section>
<div style="display:flex;flex-wrap:wrap;align-items:center;">
<div>
{{ widget_user.profile(program.author) }}
{{ widget_user.profile(p.author) }}
</div>
<div style="padding:30px;">
<div style="font-size:115%;font-style:italic;margin-bottom:15px;">
{{ program.title }}
{{ p.title }}
</div>
</div>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae
@ -78,4 +78,54 @@
auctor a. Praesent sit amet libero risus.</p>
</div>
{% if p.thread.top_comment %}
{% call widget_thread.thread_leader(p.thread.top_comment) %}
<div class="info">
<div>Posté le {{ p.date_created | dyndate }}</div>
{{ widget_thread.post_actions(p) }}
</div>
{{ p.thread.top_comment.text | md }}
{{ widget_attachments.attachments(p.thread.top_comment) }}
{% endcall %}
{% endif %}
{{ widget_pagination.paginate(comments, 'program_view', p) }}
{{ widget_thread.thread(comments.items, p.thread.top_comment) }}
{{ widget_pagination.paginate(comments, 'program_view', p) }}
{% if V5Config.ENABLE_GUEST_POST or current_user.is_authenticated %}
<div class=form>
<h3>Commenter le programme</h3>
<form action="" method="post" enctype="multipart/form-data">
{{ form.hidden_tag() }}
{% if form.pseudo %}
<div>
{{ form.pseudo.label }}
{{ form.pseudo }}
{% for error in form.pseudo.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
{{ form.ab }}
</div>
{% endif %}
{{ widget_editor.text_editor(form.message, label=False) }}
<div>
{{ form.attachments.label }}
<div>
{{ form.attachments }}
{% for error in form.attachments.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
</div>
<div>{{ form.submit(class_='bg-ok') }}</div>
</form>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,56 @@
{% extends "base/base.html" %}
{% import "widgets/editor.html" as widget_editor %}
{% block title %}
<h1>Programmes de Planète Casio</h1>
{% endblock %}
{% block content %}
<section>
{% if current_user.is_authenticated %}
<div class="form">
<h2>Soumettre un programme</h2>
<form action="" method="post" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div>
{{ form.name.label }}
{{ form.name() }}
{% for error in form.name.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
<div>
{{ form.tags.label }}
<div class=desc>{{ form.tags.description }}</div>
{{ form.tags() }}
{% for error in form.tags.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
{{ widget_editor.text_editor(form.message) }}
<div>
{{ form.attachments.label }}
<div>
{{ form.attachments }}
{% for error in form.attachments.errors %}
<span class="msgerror">{{ error }}</span>
{% endfor %}
</div>
</div>
<div>{{ form.submit(class_='bg-ok') }}</div>
</form>
</div>
{% else %}
<div>
<p>Vous devez <a href='{{ url_for("login") }}'>être connecté·e</a> pour poster un programme (pour que le programme puisse être modifié ensuite). Si vous n'avez pas de compte, vous pouvez vous <a href='{{ url_for("register") }}'>inscrire ici</a>.</p>
</div>
{% endif %}
</section>
{% endblock %}

View File

@ -19,6 +19,7 @@ For more information, see the Werkzeug documentation:
from werkzeug.routing import BaseConverter, ValidationError
from app.models.forum import Forum
from app.models.topic import Topic
from app.models.program import Program
from slugify import slugify
@ -44,6 +45,7 @@ class PageConverter(BaseConverter):
# number, a slug, or a page number followed by a slug
regex = r'(\d+)(?:/(\d+)|/fin)?(?:/[\w-]+)?'
object = None
get_title = lambda o: "empty-title"
def to_python(self, url):
tid, *args = url.split('/')
@ -70,11 +72,16 @@ class PageConverter(BaseConverter):
def to_url(self, object_and_page):
o, page = object_and_page
page = str(page) if page != -1 else "fin"
slug = slugify(o.title)
slug = slugify(self.get_title(o))
return f'{o.id}/{page}/{slug}'
class TopicPageConverter(PageConverter):
object = Topic
get_title = lambda self, t: t.title
class ProgramPageConverter(PageConverter):
object = Program
get_title = lambda self, p: p.name
# Export only the converter classes
__all__ = "ForumConverter TopicPageConverter".split()
__all__ = ["ForumConverter", "TopicPageConverter", "ProgramPageConverter"]

View File

@ -0,0 +1,25 @@
"""rename Program.title -> Program.name
Revision ID: fa34c9f43c24
Revises: 1de8b6b6aed8
Create Date: 2022-05-19 20:16:47.855756
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fa34c9f43c24'
down_revision = '1de8b6b6aed8'
branch_labels = None
depends_on = None
def upgrade():
# Once again modified by hand - Lephe'
op.alter_column('program', 'title', new_column_name='name')
def downgrade():
op.alter_column('program', 'name', new_column_name='title')