From b047ed97afcd3f0ec92be684814ca81932117a2a Mon Sep 17 00:00:00 2001 From: Lephe Date: Thu, 19 May 2022 20:34:23 +0100 Subject: [PATCH] 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. --- app/__init__.py | 1 + app/forms/programs.py | 14 +++++ app/models/program.py | 4 +- app/models/tag.py | 2 +- app/routes/__init__.py | 2 +- app/routes/programs/index.py | 4 +- app/routes/programs/program.py | 62 +++++++++++++++++++ app/routes/programs/submit.py | 59 ++++++++++++++++++ app/templates/base/navbar/programs.html | 3 + app/templates/programs/index.html | 17 +++-- app/templates/programs/program.html | 56 ++++++++++++++++- app/templates/programs/submit.html | 56 +++++++++++++++++ app/utils/converters.py | 11 +++- ...43c24_rename_program_title_program_name.py | 25 ++++++++ 14 files changed, 301 insertions(+), 15 deletions(-) create mode 100644 app/forms/programs.py create mode 100644 app/routes/programs/program.py create mode 100644 app/routes/programs/submit.py create mode 100644 app/templates/programs/submit.html create mode 100644 migrations/versions/fa34c9f43c24_rename_program_title_program_name.py diff --git a/app/__init__.py b/app/__init__.py index 1049e63..e6b64da 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -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 diff --git a/app/forms/programs.py b/app/forms/programs.py new file mode 100644 index 0000000..1f462e7 --- /dev/null +++ b/app/forms/programs.py @@ -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') diff --git a/app/models/program.py b/app/models/program.py index 5a0569f..92c43db 100644 --- a/app/models/program.py +++ b/app/models/program.py @@ -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'' + return f'' diff --git a/app/models/tag.py b/app/models/tag.py index 25cfd20..5d96089 100644 --- a/app/models/tag.py +++ b/app/models/tag.py @@ -12,4 +12,4 @@ class Tag(db.Model): def __init__(self, post, tag): self.post = post - self.tag = tag + self.name = tag diff --git a/app/routes/__init__.py b/app/routes/__init__.py index b39260d..b2d87e5 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -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: diff --git a/app/routes/programs/index.py b/app/routes/programs/index.py index 7d267c1..333a485 100644 --- a/app/routes/programs/index.py +++ b/app/routes/programs/index.py @@ -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) diff --git a/app/routes/programs/program.py b/app/routes/programs/program.py new file mode 100644 index 0000000..f02294c --- /dev/null +++ b/app/routes/programs/program.py @@ -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/', 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) diff --git a/app/routes/programs/submit.py b/app/routes/programs/submit.py new file mode 100644 index 0000000..0675a84 --- /dev/null +++ b/app/routes/programs/submit.py @@ -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) diff --git a/app/templates/base/navbar/programs.html b/app/templates/base/navbar/programs.html index 75953ee..3e74cdd 100644 --- a/app/templates/base/navbar/programs.html +++ b/app/templates/base/navbar/programs.html @@ -4,6 +4,9 @@ Programmes + Index des programmes +
+ diff --git a/app/templates/programs/index.html b/app/templates/programs/index.html index f141ddf..17ba396 100644 --- a/app/templates/programs/index.html +++ b/app/templates/programs/index.html @@ -6,15 +6,24 @@ {% block content %}
-

Tous les programmes

+

Publications récentes

+

[Ici quelques "cartes" de programmes récents]

+

Populaires

+

[Ici quelques "cartes" de programmes populaires aléatoires]

+ +

Poster un programme

+

Poster un nouveau programme sur Planète Casio

+ +

Tous les programmes

- + {% for p in programs %} - + - + + {% endfor %}
IDNomAuteurPublié le
IDNomAuteurPublié leTags
{{ p.id }}{{ p.name }}{{ p.name }} {{ p.author.name }}{{ p.date_created }}
{{ p.date_created | dyndate }}{% for tag in p.tags %}{{ tag.name }} {% endfor %}
diff --git a/app/templates/programs/program.html b/app/templates/programs/program.html index a6ca9fe..2c83c47 100644 --- a/app/templates/programs/program.html +++ b/app/templates/programs/program.html @@ -7,18 +7,18 @@ {% block title %} -

Programme {{ program.name }}

+

Programme: {{ p.name }}

{% endblock %} {% block content %}
- {{ widget_user.profile(program.author) }} + {{ widget_user.profile(p.author) }}
- {{ program.title }} + {{ p.title }}

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae @@ -78,4 +78,54 @@ auctor a. Praesent sit amet libero risus.

+ {% if p.thread.top_comment %} + {% call widget_thread.thread_leader(p.thread.top_comment) %} +
+
Posté le {{ p.date_created | dyndate }}
+ {{ widget_thread.post_actions(p) }} +
+ {{ 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 %} +
+

Commenter le programme

+
+ {{ form.hidden_tag() }} + + {% if form.pseudo %} +
+ {{ form.pseudo.label }} + {{ form.pseudo }} + {% for error in form.pseudo.errors %} + {{ error }} + {% endfor %} + {{ form.ab }} +
+ {% endif %} + + {{ widget_editor.text_editor(form.message, label=False) }} + +
+ {{ form.attachments.label }} +
+ {{ form.attachments }} + {% for error in form.attachments.errors %} + {{ error }} + {% endfor %} +
+
+ +
{{ form.submit(class_='bg-ok') }}
+
+
+ {% endif %} {% endblock %} diff --git a/app/templates/programs/submit.html b/app/templates/programs/submit.html new file mode 100644 index 0000000..0ac58f7 --- /dev/null +++ b/app/templates/programs/submit.html @@ -0,0 +1,56 @@ +{% extends "base/base.html" %} +{% import "widgets/editor.html" as widget_editor %} + +{% block title %} +

Programmes de Planète Casio

+{% endblock %} + +{% block content %} +
+ + {% if current_user.is_authenticated %} +
+

Soumettre un programme

+
+ {{ form.hidden_tag() }} + +
+ {{ form.name.label }} + {{ form.name() }} + {% for error in form.name.errors %} + {{ error }} + {% endfor %} +
+ +
+ {{ form.tags.label }} +
{{ form.tags.description }}
+ {{ form.tags() }} + {% for error in form.tags.errors %} + {{ error }} + {% endfor %} +
+ + {{ widget_editor.text_editor(form.message) }} + +
+ {{ form.attachments.label }} +
+ {{ form.attachments }} + {% for error in form.attachments.errors %} + {{ error }} + {% endfor %} +
+
+ +
{{ form.submit(class_='bg-ok') }}
+
+
+ {% else %} +
+

Vous devez être connecté·e pour poster un programme (pour que le programme puisse être modifié ensuite). Si vous n'avez pas de compte, vous pouvez vous inscrire ici.

+
+ {% endif %} + +
+{% endblock %} diff --git a/app/utils/converters.py b/app/utils/converters.py index e887f9b..6936a61 100644 --- a/app/utils/converters.py +++ b/app/utils/converters.py @@ -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"] diff --git a/migrations/versions/fa34c9f43c24_rename_program_title_program_name.py b/migrations/versions/fa34c9f43c24_rename_program_title_program_name.py new file mode 100644 index 0000000..9a42430 --- /dev/null +++ b/migrations/versions/fa34c9f43c24_rename_program_title_program_name.py @@ -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')