polls: added models

This commit is contained in:
Eldeberen 2021-02-19 22:07:31 +01:00
parent c0bb2f5448
commit d2c5ddd874
Signed by untrusted user: Darks
GPG Key ID: 7515644268BE1433
8 changed files with 203 additions and 18 deletions

View File

@ -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:
# <poll> A poll attached to the comment (of class Poll)
def __init__(self, author, text, thread):
"""

View File

@ -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:
# <answers> 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

View File

@ -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

View File

@ -121,6 +121,7 @@ class Member(User):
# Other fields populated automatically through relations:
# <notifications> List of unseen notifications (of type Notification)
# <polls> Polls created by the member (of class Poll)
def __init__(self, name, email, password):
"""Register a new user."""

View File

@ -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/<int:poll_id>", 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))

View File

@ -0,0 +1,37 @@
{% macro wpoll(p) %}
{% import "widgets/polls/"+p.template as poll_template with context %}
<div class="poll">
<h3>{{ p.title }}</h3>
{# Poll has not begin #}
{% if not p.started %}
<p><i>Le sondage ouvrira le {{ p.start | date }}.</i></p>
{# Poll has ended: display results #}
{% elif p.ended %}
<div>Ce sondage est terminé. Voici les résultats.</div>
{{ poll_template.results(p) }}
{# Current user is a guest #}
{% elif not current_user.is_authenticated %}
<p><i>Seuls les membres peuvent voter</i></p>
{# Current user cannot vote #}
{% elif not p.can_vote(current_user) %}
<p><i>Vous n'avez pas le droit de voter dans ce sondage. Désolé…</i></p>
{# Current user has already voted #}
{% elif p.has_voted(current_user) %}
<p><i>Vous avez déjà voté. Revenez le {{ p.ended | date }} pour voir les résultats</i></p>
{# Current user can vote #}
{% else %}
<form class="poll" action="/poll/{{ p.id }}" method="post" enctype="multipart/form-data">
{{ poll_template.choices(p) }}
<input type="submit" value="Envoyer">
<input id="csrf_token" name="csrf_token" type="hidden" value="{{ csrf_token() }}">
</form>
{% endif %}
</div>
{% endmacro %}

View File

@ -0,0 +1,7 @@
{% macro choices(p) %}
<div>Default choices</div>
{% endmacro %}
{% macro results(p) %}
<div>Default results.</div>
{% endmacro %}

View File

@ -0,0 +1,23 @@
{% macro choices(poll) %}
<fieldset>
{% for choice in poll.choices %}
<input type="radio" id="{{ poll.id }}-{{ choice.id }}" name="pollanwsers" value="{{ choice.title }}" />
<label for="{{ poll.id }}-{{ choice.id }}">{{ choice.title }}</label><br>
{% endfor %}
</fieldset>
{% endmacro %}
{% macro results(poll) %}
<table>
{% for choice, votes in poll.results.most_common() %}
<tr>
<td><label for="{{ poll.id }}-{{ choice.id }}">{{ choice.title }}</label></td>
<td>
<progress id="{{ poll.id }}-{{ choice.id }}" value="{{ votes }}" max="{{ len(poll.answers) }}">
{{ votes / len(poll.answers) if len(poll.answers) else 0 }}% ({{ votes }})
</progress>
</td>
</tr>
{% endfor %}
</table>
{% endmacro %}