From 4aa1802afbbd004d34d659e2b0e651e73d01da0e Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 12 Nov 2020 00:11:33 +0100 Subject: [PATCH 01/11] polls: created backbone of models (#72) --- app/models/comment.py | 3 +++ app/models/poll.py | 63 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 app/models/poll.py diff --git a/app/models/comment.py b/app/models/comment.py index 8c7be13..5c1d0e7 100644 --- a/app/models/comment.py +++ b/app/models/comment.py @@ -19,6 +19,9 @@ class Comment(Post): backref=backref('comments', lazy='dynamic'), foreign_keys=thread_id) + # Other fields populated automatically through relations: + # A poll attached to the comment (of class Poll) + def __init__(self, author, text, thread): """ Create a new Comment in a thread. diff --git a/app/models/poll.py b/app/models/poll.py new file mode 100644 index 0000000..3b11930 --- /dev/null +++ b/app/models/poll.py @@ -0,0 +1,63 @@ +from app import db +from enum import Enum +from sqlalchemy.orm import backref + + +class PollType(Enum): + """Polls types: single/multiple answers. Easier than inheritance""" + SINGLE = 1 + MULTIPLE = 2 + + +class Poll(db.Model): + """Some poll, with different options""" + + __tablename__ = 'poll' + + # Unique ID + id = db.Column(db.Integer, primary_key=True) + + # Owner comment + comment_id = db.Column(db.Integer, db.ForeignKey('comment.id')) + comment = db.relationship('Comment', uselist=False, back_populates="poll", + foreign_keys=thread_id) + + # Type + type = db.Column(db.Enum(PollType)) + + # Title/question + title = db.Column(db.UnicodeText) + + # End datetime + end = db.Column(db.DateTime) + + # Choices + # We want a size-variable list of strings, or a dictionnary with + # key/values, depending on the poll type. + # As the data is likely to be adapted to the poll type, the PickleType + # seems to be appropriate. Same applies for PollAnswer. + choices = db.Column(db.PickleType) + + # Other fields populated automatically through relations: + # The list of answers (of type PollAnswer) + + +class PollAnswer(db.Model): + """An answer to a poll""" + + __tablename__ = 'pollanswer' + + # Unique ID + id = db.Column(db.Integer, primary_key=True) + + # Poll + poll_id = db.Column(db.Integer, db.ForeignKey('poll.id')) + poll = db.relationship('Poll', back_populates="answers", + foreign_keys=poll_id) + + # Author. Must be Member + author_id = db.Column(db.Integer, db.ForeignKey('member.id')) + author = db.relationship('Member', foreign_keys=author_id) + + # Choice(s) + choices = db.Column(db.PickleType) From 243ae437835c57fa42b6dbed0d8b5d8d8e79aca3 Mon Sep 17 00:00:00 2001 From: Darks Date: Fri, 13 Nov 2020 01:45:55 +0100 Subject: [PATCH 02/11] dev: added support for local development test files --- .gitignore | 4 +++- app/routes/__init__.py | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b7d5fc6..a5d453a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ app/static/avatars/ app/static/images/trophies/ -# Development files +## Development files # Flask env .env @@ -17,6 +17,8 @@ venv/ # pipenv Pipfile Pipfile.lock +# Tests files +test.* ## Deployment files diff --git a/app/routes/__init__.py b/app/routes/__init__.py index 32449c0..dd2283c 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -8,3 +8,8 @@ from app.routes.forum import index, topic from app.routes.programs import index from app.routes.posts import edit from app.routes.api import markdown + +try: + from app.routes import test +except ImportError: + pass From c0bb2f5448523b9640ab0aab95dcc845e481482a Mon Sep 17 00:00:00 2001 From: Eldeberen Date: Thu, 18 Feb 2021 00:04:28 +0100 Subject: [PATCH 03/11] misc: small fixes --- app/routes/account/account.py | 1 + app/routes/api/markdown.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/account/account.py b/app/routes/account/account.py index 532c472..241f91a 100644 --- a/app/routes/account/account.py +++ b/app/routes/account/account.py @@ -144,6 +144,7 @@ def activate_account(token): ts = URLSafeTimedSerializer(app.config["SECRET_KEY"]) email = ts.loads(token, salt="email-confirm-key", max_age=86400) except Exception as e: + # TODO: add proper login print(f"Error: {e}") abort(404) diff --git a/app/routes/api/markdown.py b/app/routes/api/markdown.py index 87a00e6..105111c 100644 --- a/app/routes/api/markdown.py +++ b/app/routes/api/markdown.py @@ -9,6 +9,5 @@ class API(): try: markdown = request.get_json()['text'] except BadRequestKeyError: - return "Dummy value" abort(400) return str(md(markdown)) From d2c5ddd8747fcdafbd72304d65c4731726834e2b Mon Sep 17 00:00:00 2001 From: Eldeberen Date: Fri, 19 Feb 2021 22:07:31 +0100 Subject: [PATCH 04/11] polls: added models --- app/models/comment.py | 4 +- app/models/poll.py | 90 ++++++++++++++++---- app/models/polls/simple.py | 39 +++++++++ app/models/user.py | 1 + app/routes/polls/submit.py | 20 +++++ app/templates/widgets/poll.html | 37 ++++++++ app/templates/widgets/polls/defaultpoll.html | 7 ++ app/templates/widgets/polls/simplepoll.html | 23 +++++ 8 files changed, 203 insertions(+), 18 deletions(-) create mode 100644 app/models/polls/simple.py create mode 100644 app/routes/polls/submit.py create mode 100644 app/templates/widgets/poll.html create mode 100644 app/templates/widgets/polls/defaultpoll.html create mode 100644 app/templates/widgets/polls/simplepoll.html diff --git a/app/models/comment.py b/app/models/comment.py index 5c1d0e7..385507e 100644 --- a/app/models/comment.py +++ b/app/models/comment.py @@ -18,9 +18,7 @@ class Comment(Post): thread = db.relationship('Thread', backref=backref('comments', lazy='dynamic'), foreign_keys=thread_id) - - # Other fields populated automatically through relations: - # A poll attached to the comment (of class Poll) + def __init__(self, author, text, thread): """ diff --git a/app/models/poll.py b/app/models/poll.py index 3b11930..e5bb0e9 100644 --- a/app/models/poll.py +++ b/app/models/poll.py @@ -1,33 +1,35 @@ from app import db from enum import Enum from sqlalchemy.orm import backref - - -class PollType(Enum): - """Polls types: single/multiple answers. Easier than inheritance""" - SINGLE = 1 - MULTIPLE = 2 +from datetime import datetime, timedelta +from collections import Counter class Poll(db.Model): - """Some poll, with different options""" + """Default class for polls""" __tablename__ = 'poll' + # Names of templates + template = 'defaultpoll.html' + # Unique ID id = db.Column(db.Integer, primary_key=True) - # Owner comment - comment_id = db.Column(db.Integer, db.ForeignKey('comment.id')) - comment = db.relationship('Comment', uselist=False, back_populates="poll", - foreign_keys=thread_id) - # Type - type = db.Column(db.Enum(PollType)) + type = db.Column(db.String(20)) + + # Author + author_id = db.Column(db.Integer, db.ForeignKey('member.id')) + author = db.relationship('Member', backref=backref('polls'), + foreign_keys=author_id) # Title/question title = db.Column(db.UnicodeText) + # Start datetime + start = db.Column(db.DateTime, default=datetime.now()) + # End datetime end = db.Column(db.DateTime) @@ -41,6 +43,60 @@ class Poll(db.Model): # Other fields populated automatically through relations: # The list of answers (of type PollAnswer) + __mapper_args__ = { + 'polymorphic_identity': __tablename__, + 'polymorphic_on':type + } + + def __init__(self, author, title, choices, start=datetime.now(), end=datetime.now()): + self.author = author + self.title = title + self.choices = choices + self.start = start + self.end = end + + def delete(self): + """Deletes a poll and its answers""" + # TODO: move this out of class definition? + for answer in SpecialPrivilege.query.filter_by(poll_id=self.id).all(): + db.session.delete(answer) + db.session.commit() + + db.session.delete(self) + db.session.commit() + + # Common properties and methods + @property + def started(self): + """Returns whether the poll is open""" + print(self.start, datetime.now()) + return self.start <= datetime.now() + + @property + def ended(self): + """Returns whether the poll is closed""" + return self.end < datetime.now() + + def has_voted(self, user): + """Returns wheter the user has voted""" + # TODO: use ORM for this dirty request + return user in [a.author for a in self.answers] + + def can_vote(self, user): + """Returns true if the current user can vote. + More conditions may be added in the future""" + return user.is_authenticated + + # Poll-specific methods. Must be overrided per-poll definition + def vote(self, user, data): + """Return a PollAnswer object from specified user and data""" + return None + + @property + def results(self): + """Returns an easy-to-use object with answers of the poll.""" + return None + class PollAnswer(db.Model): """An answer to a poll""" @@ -52,7 +108,7 @@ class PollAnswer(db.Model): # Poll poll_id = db.Column(db.Integer, db.ForeignKey('poll.id')) - poll = db.relationship('Poll', back_populates="answers", + poll = db.relationship('Poll', backref=backref('answers'), foreign_keys=poll_id) # Author. Must be Member @@ -60,4 +116,8 @@ class PollAnswer(db.Model): author = db.relationship('Member', foreign_keys=author_id) # Choice(s) - choices = db.Column(db.PickleType) + answer = db.Column(db.PickleType) + + def __init__(self, user, answer): + self.author = user + self.answer = answer diff --git a/app/models/polls/simple.py b/app/models/polls/simple.py new file mode 100644 index 0000000..87d0713 --- /dev/null +++ b/app/models/polls/simple.py @@ -0,0 +1,39 @@ +from app import db +from app.models.poll import Poll, PollAnswer +from collections import Counter + +class SimplePoll(Poll): + """Poll with only one answer allowed""" + + __tablename__ = 'simplepoll' + + # Names of templates + template = 'simplepoll.html' + + __mapper_args__ = { + 'polymorphic_identity': __tablename__, + } + + def __init__(self, author, title, choices, **kwargs): + choices = [Choice(i, t) for i, t in enumerate(choices)] + super().__init__(author, title, choices, **kwargs) + + # Mandatory methods + def vote(self, user, data): + data = [data] # TODO + answer = PollAnswer(user, data) + return answer + + @property + def results(self): + values = {c: 0 for c in self.choices} + counter = Counter(values) + for answer in self.answers: + counter.update([answer.answer]) + return counter + + +class Choice(): + def __init__(self, id, title): + self.id = id + self.title = title diff --git a/app/models/user.py b/app/models/user.py index 13c8ee2..9b77b09 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -121,6 +121,7 @@ class Member(User): # Other fields populated automatically through relations: # List of unseen notifications (of type Notification) + # Polls created by the member (of class Poll) def __init__(self, name, email, password): """Register a new user.""" diff --git a/app/routes/polls/submit.py b/app/routes/polls/submit.py new file mode 100644 index 0000000..25aaeda --- /dev/null +++ b/app/routes/polls/submit.py @@ -0,0 +1,20 @@ +from app import app, db +from flask import request +from flask_login import current_user + +from app.models.poll import Poll + +@app.route("/poll/", methods=['POST']) +def poll_submit(poll_id): + p = Poll.query.first_or_404() + + if not current_user.is_authenticated: + return 401 + if p.has_voted(current_user): + return 403 + + try: + resp = request.get_json()['text'] + except BadRequestKeyError: + abort(400) + return str(md(markdown)) diff --git a/app/templates/widgets/poll.html b/app/templates/widgets/poll.html new file mode 100644 index 0000000..5fef9c2 --- /dev/null +++ b/app/templates/widgets/poll.html @@ -0,0 +1,37 @@ +{% macro wpoll(p) %} + +{% import "widgets/polls/"+p.template as poll_template with context %} + +
+

{{ p.title }}

+{# Poll has not begin #} + {% if not p.started %} +

Le sondage ouvrira le {{ p.start | date }}.

+ +{# Poll has ended: display results #} + {% elif p.ended %} +
Ce sondage est terminé. Voici les résultats.
+ {{ poll_template.results(p) }} + +{# Current user is a guest #} + {% elif not current_user.is_authenticated %} +

Seuls les membres peuvent voter

+ +{# Current user cannot vote #} + {% elif not p.can_vote(current_user) %} +

Vous n'avez pas le droit de voter dans ce sondage. Désolé…

+ +{# Current user has already voted #} + {% elif p.has_voted(current_user) %} +

Vous avez déjà voté. Revenez le {{ p.ended | date }} pour voir les résultats

+ +{# Current user can vote #} + {% else %} +
+ {{ poll_template.choices(p) }} + + +
+ {% endif %} +
+{% endmacro %} diff --git a/app/templates/widgets/polls/defaultpoll.html b/app/templates/widgets/polls/defaultpoll.html new file mode 100644 index 0000000..51990c9 --- /dev/null +++ b/app/templates/widgets/polls/defaultpoll.html @@ -0,0 +1,7 @@ +{% macro choices(p) %} +
Default choices
+{% endmacro %} + +{% macro results(p) %} +
Default results.
+{% endmacro %} diff --git a/app/templates/widgets/polls/simplepoll.html b/app/templates/widgets/polls/simplepoll.html new file mode 100644 index 0000000..2347200 --- /dev/null +++ b/app/templates/widgets/polls/simplepoll.html @@ -0,0 +1,23 @@ +{% macro choices(poll) %} +
+{% for choice in poll.choices %} + +
+{% endfor %} +
+{% endmacro %} + +{% macro results(poll) %} + + {% for choice, votes in poll.results.most_common() %} + + + + + {% endfor %} +
+ + {{ votes / len(poll.answers) if len(poll.answers) else 0 }} % ({{ votes }}) + +
+{% endmacro %} From 169aca830493282a4c9a1fa391583dd13851f13e Mon Sep 17 00:00:00 2001 From: Eldeberen Date: Fri, 19 Feb 2021 22:07:43 +0100 Subject: [PATCH 05/11] polls: added migration --- .../versions/cfb91e6aa9fc_added_polls.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 migrations/versions/cfb91e6aa9fc_added_polls.py diff --git a/migrations/versions/cfb91e6aa9fc_added_polls.py b/migrations/versions/cfb91e6aa9fc_added_polls.py new file mode 100644 index 0000000..6d6986d --- /dev/null +++ b/migrations/versions/cfb91e6aa9fc_added_polls.py @@ -0,0 +1,48 @@ +"""Added polls + +Revision ID: cfb91e6aa9fc +Revises: cd4868f312c5 +Create Date: 2021-02-19 21:08:25.065628 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'cfb91e6aa9fc' +down_revision = 'cd4868f312c5' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('poll', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('type', sa.String(length=20), nullable=True), + sa.Column('author_id', sa.Integer(), nullable=True), + sa.Column('title', sa.UnicodeText(), nullable=True), + sa.Column('start', sa.DateTime(), nullable=True), + sa.Column('end', sa.DateTime(), nullable=True), + sa.Column('choices', sa.PickleType(), nullable=True), + sa.ForeignKeyConstraint(['author_id'], ['member.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('pollanswer', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('poll_id', sa.Integer(), nullable=True), + sa.Column('author_id', sa.Integer(), nullable=True), + sa.Column('answer', sa.PickleType(), nullable=True), + sa.ForeignKeyConstraint(['author_id'], ['member.id'], ), + sa.ForeignKeyConstraint(['poll_id'], ['poll.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('pollanswer') + op.drop_table('poll') + # ### end Alembic commands ### From ec3f33ead0c9c7931b04d23eeae32735427b6497 Mon Sep 17 00:00:00 2001 From: Eldeberen Date: Fri, 19 Feb 2021 23:26:10 +0100 Subject: [PATCH 06/11] polls: updated templates --- app/templates/widgets/poll.html | 28 +++++++++++---------- app/templates/widgets/polls/simplepoll.html | 4 +-- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/templates/widgets/poll.html b/app/templates/widgets/poll.html index 5fef9c2..4097b77 100644 --- a/app/templates/widgets/poll.html +++ b/app/templates/widgets/poll.html @@ -1,34 +1,36 @@ -{% macro wpoll(p) %} +{% macro wpoll(poll) %} -{% import "widgets/polls/"+p.template as poll_template with context %} +{% set n_answers = len(poll.answers) %} + +{% import "widgets/polls/"+poll.template as poll_template with context %}
-

{{ p.title }}

+

{{ poll.title }}

{# Poll has not begin #} - {% if not p.started %} -

Le sondage ouvrira le {{ p.start | date }}.

+ {% if not poll.started %} +

Le sondage ouvrira le {{ poll.start | date }}.

{# Poll has ended: display results #} - {% elif p.ended %} -
Ce sondage est terminé. Voici les résultats.
- {{ poll_template.results(p) }} + {% elif poll.ended %} +
Ce sondage est terminé. Voici les résultats des {{ n_answers }} participation{{ n_answers | pluralize }}.
+ {{ poll_template.results(poll) }} {# Current user is a guest #} {% elif not current_user.is_authenticated %}

Seuls les membres peuvent voter

{# Current user cannot vote #} - {% elif not p.can_vote(current_user) %} + {% elif not poll.can_vote(current_user) %}

Vous n'avez pas le droit de voter dans ce sondage. Désolé…

{# Current user has already voted #} - {% elif p.has_voted(current_user) %} -

Vous avez déjà voté. Revenez le {{ p.ended | date }} pour voir les résultats

+ {% elif poll.has_voted(current_user) %} +

Vous avez déjà voté. Revenez le {{ poll.ended | date }} pour voir les résultats

{# Current user can vote #} {% else %} -
- {{ poll_template.choices(p) }} + + {{ poll_template.choices(poll) }}
diff --git a/app/templates/widgets/polls/simplepoll.html b/app/templates/widgets/polls/simplepoll.html index 2347200..3225e44 100644 --- a/app/templates/widgets/polls/simplepoll.html +++ b/app/templates/widgets/polls/simplepoll.html @@ -13,8 +13,8 @@ - - {{ votes / len(poll.answers) if len(poll.answers) else 0 }} % ({{ votes }}) + + {{ votes / n_answers if n_answers else 0 }} % ({{ votes }}) From 473448ab5b09e9d71c73a50ac43c7a9cffd9b726 Mon Sep 17 00:00:00 2001 From: Eldeberen Date: Sat, 20 Feb 2021 00:33:34 +0100 Subject: [PATCH 07/11] poll: add ability to vote Fixed some bugs too --- app/models/poll.py | 4 +-- app/models/polls/simple.py | 20 +++++++++--- app/routes/__init__.py | 3 +- app/routes/polls/create.py | 9 ++++++ app/routes/polls/submit.py | 20 ------------ app/routes/polls/vote.py | 34 +++++++++++++++++++++ app/templates/widgets/poll.html | 4 +-- app/templates/widgets/polls/simplepoll.html | 2 +- 8 files changed, 65 insertions(+), 31 deletions(-) create mode 100644 app/routes/polls/create.py delete mode 100644 app/routes/polls/submit.py create mode 100644 app/routes/polls/vote.py diff --git a/app/models/poll.py b/app/models/poll.py index e5bb0e9..9a6d0b2 100644 --- a/app/models/poll.py +++ b/app/models/poll.py @@ -69,7 +69,6 @@ class Poll(db.Model): @property def started(self): """Returns whether the poll is open""" - print(self.start, datetime.now()) return self.start <= datetime.now() @property @@ -118,6 +117,7 @@ class PollAnswer(db.Model): # Choice(s) answer = db.Column(db.PickleType) - def __init__(self, user, answer): + def __init__(self, poll, user, answer): + self.poll = poll self.author = user self.answer = answer diff --git a/app/models/polls/simple.py b/app/models/polls/simple.py index 87d0713..04c285c 100644 --- a/app/models/polls/simple.py +++ b/app/models/polls/simple.py @@ -19,19 +19,29 @@ class SimplePoll(Poll): super().__init__(author, title, choices, **kwargs) # Mandatory methods - def vote(self, user, data): - data = [data] # TODO - answer = PollAnswer(user, data) + def vote(self, user, request): + try: + choice_id = int(request.form['pollanwsers']) + except (KeyError, ValueError): + return None + + answer = PollAnswer(self, user, choice_id) return answer @property def results(self): values = {c: 0 for c in self.choices} counter = Counter(values) - for answer in self.answers: - counter.update([answer.answer]) + answers = [self.choice_from_id(a.answer) for a in self.answers] + counter.update(answers) return counter + # Custom method + def choice_from_id(self, id): + for c in self.choices: + if c.id == id: + return c + return None class Choice(): def __init__(self, id, title): diff --git a/app/routes/__init__.py b/app/routes/__init__.py index dd2283c..c8d40ae 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -5,8 +5,9 @@ from app.routes.account import login, account, notification from app.routes.admin import index, groups, account, trophies, forums, \ attachments, config, members from app.routes.forum import index, topic -from app.routes.programs import index +from app.routes.polls import create, vote from app.routes.posts import edit +from app.routes.programs import index from app.routes.api import markdown try: diff --git a/app/routes/polls/create.py b/app/routes/polls/create.py new file mode 100644 index 0000000..4bc7c22 --- /dev/null +++ b/app/routes/polls/create.py @@ -0,0 +1,9 @@ +from app import app, db +from flask import abort, redirect, request, url_for +from flask_login import current_user + +from app.models.poll import Poll + +@app.route("/poll/new", methods=['GET', 'POST']) +def poll_create(poll_id): + return redirect(url_for('index')) diff --git a/app/routes/polls/submit.py b/app/routes/polls/submit.py deleted file mode 100644 index 25aaeda..0000000 --- a/app/routes/polls/submit.py +++ /dev/null @@ -1,20 +0,0 @@ -from app import app, db -from flask import request -from flask_login import current_user - -from app.models.poll import Poll - -@app.route("/poll/", methods=['POST']) -def poll_submit(poll_id): - p = Poll.query.first_or_404() - - if not current_user.is_authenticated: - return 401 - if p.has_voted(current_user): - return 403 - - try: - resp = request.get_json()['text'] - except BadRequestKeyError: - abort(400) - return str(md(markdown)) diff --git a/app/routes/polls/vote.py b/app/routes/polls/vote.py new file mode 100644 index 0000000..08a7911 --- /dev/null +++ b/app/routes/polls/vote.py @@ -0,0 +1,34 @@ +from app import app, db +from flask import abort, flash, redirect, request, url_for +from flask_login import current_user + +from app.models.poll import Poll + +@app.route("/poll//vote", methods=['POST']) +def poll_vote(poll_id): + poll = Poll.query.first_or_404(poll_id) + + if not current_user.is_authenticated: + abort(401) + if not poll.can_vote(current_user): + abort(403) + if poll.has_voted(current_user): + abort(403) + if not poll.started: + abort(403) + if poll.ended: + abort(403) + + answer = poll.vote(current_user, request) + + if answer is None: + abort(400) + + db.session.add(answer) + db.session.commit() + + flash('Le vote a été pris en compte', 'info') + + if request.referrer: + return redirect(request.referrer) + return redirect(url_for('index')) diff --git a/app/templates/widgets/poll.html b/app/templates/widgets/poll.html index 4097b77..d63c595 100644 --- a/app/templates/widgets/poll.html +++ b/app/templates/widgets/poll.html @@ -25,11 +25,11 @@ {# Current user has already voted #} {% elif poll.has_voted(current_user) %} -

Vous avez déjà voté. Revenez le {{ poll.ended | date }} pour voir les résultats

+

Vous avez déjà voté. Revenez le {{ poll.end | date }} pour voir les résultats

{# Current user can vote #} {% else %} -
+ {{ poll_template.choices(poll) }} diff --git a/app/templates/widgets/polls/simplepoll.html b/app/templates/widgets/polls/simplepoll.html index 3225e44..2cbb3e1 100644 --- a/app/templates/widgets/polls/simplepoll.html +++ b/app/templates/widgets/polls/simplepoll.html @@ -1,7 +1,7 @@ {% macro choices(poll) %}
{% for choice in poll.choices %} - +
{% endfor %}
From 0801b8ec160960407d099b3a10cb8f508efc515d Mon Sep 17 00:00:00 2001 From: Eldeberen Date: Sat, 20 Feb 2021 00:39:22 +0100 Subject: [PATCH 08/11] poll: add poll creation form --- app/forms/poll.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/forms/poll.py diff --git a/app/forms/poll.py b/app/forms/poll.py new file mode 100644 index 0000000..988b598 --- /dev/null +++ b/app/forms/poll.py @@ -0,0 +1,22 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, SubmitField, TextAreaField +from wtforms.fields.html5 import DateTimeField +from wtforms.validators import InputRequired + + +class FPollForm(FlaskForm): + title = StringField( + 'Question', + validators=[ + InputRequired(), + ], + ) + choices = PasswordField( + 'Choix (un par ligne)', + validators=[ + InputRequired(), + ], + ) + submit = SubmitField( + 'Créer le sondage', + ) From fabad329550414f5253f1d706a4cd26df285f1e5 Mon Sep 17 00:00:00 2001 From: Eldeberen Date: Sat, 20 Feb 2021 01:28:08 +0100 Subject: [PATCH 09/11] polls: add a panel to manage own polls --- app/forms/poll.py | 28 +++++++++++--- app/routes/__init__.py | 4 +- app/routes/account/polls.py | 23 +++++++++++ app/routes/polls/create.py | 9 ----- app/routes/polls/vote.py | 11 +++++- app/templates/account/polls.html | 53 ++++++++++++++++++++++++++ app/templates/base/navbar/account.html | 5 +++ 7 files changed, 114 insertions(+), 19 deletions(-) create mode 100644 app/routes/account/polls.py delete mode 100644 app/routes/polls/create.py create mode 100644 app/templates/account/polls.html diff --git a/app/forms/poll.py b/app/forms/poll.py index 988b598..4a003ff 100644 --- a/app/forms/poll.py +++ b/app/forms/poll.py @@ -1,22 +1,38 @@ from flask_wtf import FlaskForm from wtforms import StringField, SubmitField, TextAreaField from wtforms.fields.html5 import DateTimeField -from wtforms.validators import InputRequired +from wtforms.validators import InputRequired, Optional +from datetime import datetime, timedelta -class FPollForm(FlaskForm): +class PollForm(FlaskForm): title = StringField( 'Question', validators=[ InputRequired(), - ], + ] ) - choices = PasswordField( + choices = TextAreaField( 'Choix (un par ligne)', validators=[ InputRequired(), - ], + # TODO: add a validator to check if there is at least one choice + ] + ) + start = DateTimeField( + 'Début', + default=datetime.now(), + validators=[ + Optional() + ] + ) + end = DateTimeField( + 'Fin', + default=datetime.now() + timedelta(days=1), + validators=[ + Optional() + ] ) submit = SubmitField( - 'Créer le sondage', + 'Créer le sondage' ) diff --git a/app/routes/__init__.py b/app/routes/__init__.py index c8d40ae..28a675d 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -1,11 +1,11 @@ # Register routes here from app.routes import index, search, users, tools, development -from app.routes.account import login, account, notification +from app.routes.account import login, account, notification, polls from app.routes.admin import index, groups, account, trophies, forums, \ attachments, config, members from app.routes.forum import index, topic -from app.routes.polls import create, vote +from app.routes.polls import vote from app.routes.posts import edit from app.routes.programs import index from app.routes.api import markdown diff --git a/app/routes/account/polls.py b/app/routes/account/polls.py new file mode 100644 index 0000000..665566f --- /dev/null +++ b/app/routes/account/polls.py @@ -0,0 +1,23 @@ +from app import app, db +from flask import abort, flash, redirect, request, url_for +from flask_login import current_user + +from app.models.polls.simple import SimplePoll +from app.forms.poll import PollForm +from app.utils.render import render + +@app.route("/compte/sondages", methods=['GET', 'POST']) +def account_polls(): + form = PollForm() + polls = current_user.polls + + if form.validate_on_submit(): + choices = list(filter(None, form.choices.data.split('\n'))) + p = SimplePoll(current_user, form.title.data, choices, + start=form.start.data, end=form.end.data) + db.session.add(p) + db.session.commit() + + flash(f"Le sondage {p.id} a été créé", "info") + + return render("account/polls.html", polls=polls, form=form) diff --git a/app/routes/polls/create.py b/app/routes/polls/create.py deleted file mode 100644 index 4bc7c22..0000000 --- a/app/routes/polls/create.py +++ /dev/null @@ -1,9 +0,0 @@ -from app import app, db -from flask import abort, redirect, request, url_for -from flask_login import current_user - -from app.models.poll import Poll - -@app.route("/poll/new", methods=['GET', 'POST']) -def poll_create(poll_id): - return redirect(url_for('index')) diff --git a/app/routes/polls/vote.py b/app/routes/polls/vote.py index 08a7911..7079bdb 100644 --- a/app/routes/polls/vote.py +++ b/app/routes/polls/vote.py @@ -4,19 +4,26 @@ from flask_login import current_user from app.models.poll import Poll -@app.route("/poll//vote", methods=['POST']) +@app.route("/sondages//voter", methods=['POST']) def poll_vote(poll_id): - poll = Poll.query.first_or_404(poll_id) + poll = Poll.query.get(poll_id) + if poll is None: + abort(404) if not current_user.is_authenticated: + flash("Seuls les membres connectés peuvent voter", 'error') abort(401) if not poll.can_vote(current_user): + flash("Vous n'avez pas le droit de voter", 'error') abort(403) if poll.has_voted(current_user): + flash("Vous avez déjà voté", 'error') abort(403) if not poll.started: + flash("Le sondage n'a pas débuté", 'error') abort(403) if poll.ended: + flash("Le sondage est terminé", 'error') abort(403) answer = poll.vote(current_user, request) diff --git a/app/templates/account/polls.html b/app/templates/account/polls.html new file mode 100644 index 0000000..bac4c4a --- /dev/null +++ b/app/templates/account/polls.html @@ -0,0 +1,53 @@ +{% extends "base/base.html" %} +{% import "widgets/poll.html" as poll_widget with context %} + +{% block title %} +

Gestion des sondages

+{% endblock %} + +{% block content %} +
+

Créer un sondage

+ + {{ form.hidden_tag() }} +
+ {{ form.title.label }}
+ {{ form.title(size=32) }}
+ {% for error in form.title.errors %} + {{ error }} + {% endfor %} +
+
+ {{ form.choices.label }} + + {% for error in form.choices.errors %} + {{ error }} + {% endfor %} +
+
+ {{ form.start.label }} + {{ form.start() }} + {% for error in form.start.errors %} + {{ error }} + {% endfor %} +
+
+ {{ form.end.label }} + {{ form.end() }} + {% for error in form.end.errors %} + {{ error }} + {% endfor %} +
+
{{ form.submit(class_="bg-ok") }}
+ +
+ +
+

Mes sondages

+
+ {% for p in polls %} + {{ poll_widget.wpoll(p) }} + {% endfor %} +
+
+{% endblock %} diff --git a/app/templates/base/navbar/account.html b/app/templates/base/navbar/account.html index 000e0a7..de139f7 100644 --- a/app/templates/base/navbar/account.html +++ b/app/templates/base/navbar/account.html @@ -11,6 +11,11 @@ Notifications{{ " ({})".format(current_user.notifications|length) if current_user.notifications|length }} + + + + Sondages + From 9d08f81342e5fc498c1fba70246c9b655e1109c0 Mon Sep 17 00:00:00 2001 From: Eldeberen Date: Sat, 20 Feb 2021 17:17:33 +0100 Subject: [PATCH 10/11] poll,md: adds pclink for polls --- app/templates/widgets/poll.html | 2 + app/utils/filters/markdown.py | 3 + app/utils/markdown_extensions/pclinks.py | 74 ++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 app/utils/markdown_extensions/pclinks.py diff --git a/app/templates/widgets/poll.html b/app/templates/widgets/poll.html index d63c595..fc18dad 100644 --- a/app/templates/widgets/poll.html +++ b/app/templates/widgets/poll.html @@ -37,3 +37,5 @@ {% endif %}
{% endmacro %} + +{{ wpoll(poll) if poll }} diff --git a/app/utils/filters/markdown.py b/app/utils/filters/markdown.py index efddb79..f7455a9 100644 --- a/app/utils/filters/markdown.py +++ b/app/utils/filters/markdown.py @@ -5,6 +5,8 @@ from markdown.extensions.codehilite import CodeHiliteExtension from markdown.extensions.footnotes import FootnoteExtension from markdown.extensions.toc import TocExtension +from app.utils.markdown_extensions.pclinks import PCLinkExtension + @app.template_filter('md') def md(text): @@ -22,6 +24,7 @@ def md(text): CodeHiliteExtension(linenums=True, use_pygments=True), FootnoteExtension(UNIQUE_IDS=True), TocExtension(baselevel=2), + PCLinkExtension(), ] def escape(text): diff --git a/app/utils/markdown_extensions/pclinks.py b/app/utils/markdown_extensions/pclinks.py new file mode 100644 index 0000000..fb533ad --- /dev/null +++ b/app/utils/markdown_extensions/pclinks.py @@ -0,0 +1,74 @@ +''' +PClinks Extension for Python-Markdown +====================================== + +Converts [[type:id]] to relative links. + +Based on . + +Original code Copyright [Waylan Limberg](http://achinghead.com/). + +License: [BSD](https://opensource.org/licenses/bsd-license.php) +''' + +from markdown.extensions import Extension +from markdown.inlinepatterns import InlineProcessor +import xml.etree.ElementTree as etree +from flask import url_for, render_template +from app.models.poll import Poll + + +class PCLinkExtension(Extension): + def __init__(self, **kwargs): + self.config = { + # 'base_url': ['/', 'String to append to beginning or URL.'], + # 'end_url': ['/', 'String to append to end of URL.'], + # 'html_class': ['pclink', 'CSS hook. Leave blank for none.'], + } + super().__init__(**kwargs) + + def extendMarkdown(self, md): + self.md = md + + # append to end of inline patterns + PCLINK_RE = r'\[\[([a-z]+):\W?(\w+)\]\]' + pclinkPattern = PCLinksInlineProcessor(PCLINK_RE, self.getConfigs()) + pclinkPattern.md = md + md.inlinePatterns.register(pclinkPattern, 'pclink', 75) + + +class PCLinksInlineProcessor(InlineProcessor): + def __init__(self, pattern, config): + super().__init__(pattern) + self.config = config + self.handles = { + 'poll': handlePoll, + } + + def handleMatch(self, m, data): + link_type = m.group(1).strip() + if link_type in self.handles: + content_id = m.group(2).strip() + a = self.handles[link_type](content_id, data) + else: + a = '' + return a, m.start(0), m.end(0) + + +def handlePoll(content_id, context): + print(context) + if not context.startswith("[[") or not context.endswith("]]"): + return "[Sondage invalide]" + try: + id = int(content_id) + except ValueError: + return "[ID du sondage invalide]" + + poll = Poll.query.get(content_id) + + if poll is None: + return "[Sondage non trouvé]" + + html = render_template('widgets/poll.html', poll=poll) + html = html.replace('\n', '') # Needed to avoid lots of
due to etree + return etree.fromstring(html) From b5630e007993e9ff5218ec920286f2ea29763090 Mon Sep 17 00:00:00 2001 From: Eldeberen Date: Sat, 20 Feb 2021 17:36:36 +0100 Subject: [PATCH 11/11] md: add pclink for users --- app/utils/markdown_extensions/pclinks.py | 34 +++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/app/utils/markdown_extensions/pclinks.py b/app/utils/markdown_extensions/pclinks.py index fb533ad..6a68376 100644 --- a/app/utils/markdown_extensions/pclinks.py +++ b/app/utils/markdown_extensions/pclinks.py @@ -15,8 +15,9 @@ from markdown.extensions import Extension from markdown.inlinepatterns import InlineProcessor import xml.etree.ElementTree as etree from flask import url_for, render_template +from app.utils.unicode_names import normalize from app.models.poll import Poll - +from app.models.user import Member class PCLinkExtension(Extension): def __init__(self, **kwargs): @@ -31,7 +32,7 @@ class PCLinkExtension(Extension): self.md = md # append to end of inline patterns - PCLINK_RE = r'\[\[([a-z]+):\W?(\w+)\]\]' + PCLINK_RE = r'\[\[([a-z]+): ?(\w+)\]\]' pclinkPattern = PCLinksInlineProcessor(PCLINK_RE, self.getConfigs()) pclinkPattern.md = md md.inlinePatterns.register(pclinkPattern, 'pclink', 75) @@ -43,6 +44,7 @@ class PCLinksInlineProcessor(InlineProcessor): self.config = config self.handles = { 'poll': handlePoll, + 'user': handleUser, } def handleMatch(self, m, data): @@ -55,8 +57,16 @@ class PCLinksInlineProcessor(InlineProcessor): return a, m.start(0), m.end(0) +# pclinks are links defined as [[type:content_id]] +# To add a custom handle, create a function and add it to processor's handles +# A custom handle takes two arguments: +# - content_id: as defined +# - context: the block in which the link has been found +# It should return: +# - either a string, which will be html-escaped +# - either an xml.etree.ElementTree + def handlePoll(content_id, context): - print(context) if not context.startswith("[[") or not context.endswith("]]"): return "[Sondage invalide]" try: @@ -72,3 +82,21 @@ def handlePoll(content_id, context): html = render_template('widgets/poll.html', poll=poll) html = html.replace('\n', '') # Needed to avoid lots of
due to etree return etree.fromstring(html) + +def handleUser(content_id, context): + try: + norm = normalize(content_id) + except ValueError: + return "[Nom d'utilisateur invalide]" + + member = Member.query.filter_by(norm=norm).first() + + if member is None: + return "[Utilisateur non trouvé]" + + a = etree.Element('a') + a.text = member.name + a.set('href', url_for('user_by_id', user_id=member.id)) + a.set('class', 'profile-link') + + return a