From d2c5ddd8747fcdafbd72304d65c4731726834e2b Mon Sep 17 00:00:00 2001 From: Eldeberen Date: Fri, 19 Feb 2021 22:07:31 +0100 Subject: [PATCH] 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 %}