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:
parent
0e1b434f7d
commit
b047ed97af
|
@ -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
|
||||
|
|
|
@ -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')
|
|
@ -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}">'
|
||||
|
|
|
@ -12,4 +12,4 @@ class Tag(db.Model):
|
|||
|
||||
def __init__(self, post, tag):
|
||||
self.post = post
|
||||
self.tag = tag
|
||||
self.name = tag
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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"]
|
||||
|
|
|
@ -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')
|
Loading…
Reference in New Issue