Merge branch 'polls' into dev
This commit is contained in:
commit
5efcadb23e
|
@ -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
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, TextAreaField
|
||||
from wtforms.fields.html5 import DateTimeField
|
||||
from wtforms.validators import InputRequired, Optional
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
class PollForm(FlaskForm):
|
||||
title = StringField(
|
||||
'Question',
|
||||
validators=[
|
||||
InputRequired(),
|
||||
]
|
||||
)
|
||||
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'
|
||||
)
|
|
@ -17,8 +17,9 @@ class Comment(Post):
|
|||
thread_id = db.Column(db.Integer, db.ForeignKey('thread.id'),
|
||||
nullable=False)
|
||||
thread = db.relationship('Thread',
|
||||
backref=backref('comments', lazy='dynamic'),
|
||||
foreign_keys=thread_id)
|
||||
backref=backref('comments', lazy='dynamic'),
|
||||
foreign_keys=thread_id)
|
||||
|
||||
|
||||
def __init__(self, author, text, thread):
|
||||
"""
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
from app import db
|
||||
from enum import Enum
|
||||
from sqlalchemy.orm import backref
|
||||
from datetime import datetime, timedelta
|
||||
from collections import Counter
|
||||
|
||||
|
||||
class Poll(db.Model):
|
||||
"""Default class for polls"""
|
||||
|
||||
__tablename__ = 'poll'
|
||||
|
||||
# Names of templates
|
||||
template = 'defaultpoll.html'
|
||||
|
||||
# Unique ID
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
|
||||
# Type
|
||||
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)
|
||||
|
||||
# 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:
|
||||
# <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"""
|
||||
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"""
|
||||
|
||||
__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', backref=backref('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)
|
||||
answer = db.Column(db.PickleType)
|
||||
|
||||
def __init__(self, poll, user, answer):
|
||||
self.poll = poll
|
||||
self.author = user
|
||||
self.answer = answer
|
|
@ -0,0 +1,49 @@
|
|||
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, 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)
|
||||
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):
|
||||
self.id = id
|
||||
self.title = title
|
|
@ -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."""
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
# 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.programs import index
|
||||
from app.routes.polls import vote
|
||||
from app.routes.posts import edit
|
||||
from app.routes.programs import index
|
||||
from app.routes.api import markdown
|
||||
|
||||
try:
|
||||
from app.routes import test
|
||||
except ImportError:
|
||||
pass
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -9,6 +9,5 @@ class API():
|
|||
try:
|
||||
markdown = request.get_json()['text']
|
||||
except BadRequestKeyError:
|
||||
return "Dummy value"
|
||||
abort(400)
|
||||
return str(md(markdown))
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
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("/sondages/<int:poll_id>/voter", methods=['POST'])
|
||||
def poll_vote(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)
|
||||
|
||||
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'))
|
|
@ -0,0 +1,53 @@
|
|||
{% extends "base/base.html" %}
|
||||
{% import "widgets/poll.html" as poll_widget with context %}
|
||||
|
||||
{% block title %}
|
||||
<h1>Gestion des sondages</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form">
|
||||
<h1>Créer un sondage</h1>
|
||||
<form action="" method="post">
|
||||
{{ form.hidden_tag() }}
|
||||
<div>
|
||||
{{ form.title.label }}<br>
|
||||
{{ form.title(size=32) }}<br>
|
||||
{% for error in form.title.errors %}
|
||||
<span style="color: red;">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.choices.label }}
|
||||
<textarea id="{{ form.choices.name }}" name="{{ form.choices.name }}"></textarea>
|
||||
{% for error in form.choices.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.start.label }}
|
||||
{{ form.start() }}
|
||||
{% for error in form.start.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>
|
||||
{{ form.end.label }}
|
||||
{{ form.end() }}
|
||||
{% for error in form.end.errors %}
|
||||
<span class="msgerror">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div>{{ form.submit(class_="bg-ok") }}</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h1>Mes sondages</h1>
|
||||
<div>
|
||||
{% for p in polls %}
|
||||
{{ poll_widget.wpoll(p) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -11,6 +11,11 @@
|
|||
<path fill="#ffffff" d="M20,2A2,2 0 0,1 22,4V16A2,2 0 0,1 20,18H6L2,22V4C2,2.89 2.9,2 4,2H20M4,4V17.17L5.17,16H20V4H4M6,7H18V9H6V7M6,11H15V13H6V11Z"></path>
|
||||
</svg>Notifications{{ " ({})".format(current_user.notifications|length) if current_user.notifications|length }}
|
||||
</a>
|
||||
<a href="{{ url_for('account_polls') }}">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M9 17H7V10H9M13 17H11V7H13M17 17H15V13H17M19.5 19.1H4.5V5H19.5M19.5 3H4.5C3.4 3 2.5 3.9 2.5 5V19C2.5 20.1 3.4 21 4.5 21H19.5C20.6 21 21.5 20.1 21.5 19V5C21.5 3.9 20.6 3 19.5 3Z" />
|
||||
</svg>Sondages
|
||||
</a>
|
||||
<a href="#">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path fill="#ffffff" d="M2,2V11C2,12 3,13 4,13H6.2C6.6,15 7.9,16.7 11,17V19.1C8.8,19.3 8,20.4 8,21.7V22H16V21.7C16,20.4 15.2,19.3 13,19.1V17C16.1,16.7 17.4,15 17.8,13H20C21,13 22,12 22,11V2H18C17.1,2 16,3 16,4H8C8,3 6.9,2 6,2H2M4,4H6V6L6,11H4V4M18,4H20V11H18V6L18,4M8,6H16V11.5C16,13.43 15.42,15 12,15C8.59,15 8,13.43 8,11.5V6Z"></path>
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
{% macro wpoll(poll) %}
|
||||
|
||||
{% set n_answers = len(poll.answers) %}
|
||||
|
||||
{% import "widgets/polls/"+poll.template as poll_template with context %}
|
||||
|
||||
<div class="poll">
|
||||
<h3>{{ poll.title }}</h3>
|
||||
{# Poll has not begin #}
|
||||
{% if not poll.started %}
|
||||
<p><i>Le sondage ouvrira le {{ poll.start | date }}.</i></p>
|
||||
|
||||
{# Poll has ended: display results #}
|
||||
{% elif poll.ended %}
|
||||
<div>Ce sondage est terminé. Voici les résultats des {{ n_answers }} participation{{ n_answers | pluralize }}.</div>
|
||||
{{ poll_template.results(poll) }}
|
||||
|
||||
{# 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 poll.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 poll.has_voted(current_user) %}
|
||||
<p><i>Vous avez déjà voté. Revenez le {{ poll.end | date }} pour voir les résultats</i></p>
|
||||
|
||||
{# Current user can vote #}
|
||||
{% else %}
|
||||
<form class="poll" action="{{ url_for('poll_vote', poll_id=poll.id) }}" method="post" enctype="multipart/form-data">
|
||||
{{ poll_template.choices(poll) }}
|
||||
<input type="submit" value="Envoyer">
|
||||
<input id="csrf_token" name="csrf_token" type="hidden" value="{{ csrf_token() }}">
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{{ wpoll(poll) if poll }}
|
|
@ -0,0 +1,7 @@
|
|||
{% macro choices(p) %}
|
||||
<div>Default choices</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro results(p) %}
|
||||
<div>Default results.</div>
|
||||
{% endmacro %}
|
|
@ -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.id }}" />
|
||||
<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="{{ n_answers }}">
|
||||
{{ votes / n_answers if n_answers else 0 }} % ({{ votes }})
|
||||
</progress>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endmacro %}
|
|
@ -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):
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
'''
|
||||
PClinks Extension for Python-Markdown
|
||||
======================================
|
||||
|
||||
Converts [[type:id]] to relative links.
|
||||
|
||||
Based on <https://Python-Markdown.github.io/extensions/wikilinks>.
|
||||
|
||||
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.utils.unicode_names import normalize
|
||||
from app.models.poll import Poll
|
||||
from app.models.user import Member
|
||||
|
||||
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+)\]\]'
|
||||
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,
|
||||
'user': handleUser,
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# 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):
|
||||
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 <br> 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
|
|
@ -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 ###
|
Loading…
Reference in New Issue