mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
poll ui, wip #181
This commit is contained in:
parent
9e003a5d8b
commit
526ac5260f
11 changed files with 259 additions and 40 deletions
|
@ -187,6 +187,41 @@ class CreateImageForm(FlaskForm):
|
|||
return True
|
||||
|
||||
|
||||
class CreatePollForm(FlaskForm):
|
||||
communities = SelectField(_l('Community'), validators=[DataRequired()], coerce=int, render_kw={'class': 'form-select'})
|
||||
poll_title = StringField(_l('Title'), validators=[DataRequired(), Length(min=3, max=255)])
|
||||
poll_body = TextAreaField(_l('Body'), validators=[Optional(), Length(min=3, max=5000)], render_kw={'rows': 5})
|
||||
mode = SelectField(_('Mode'), validators=[DataRequired()], choices=[('single', _l('Single choice')), ('multiple', _l('Multiple choices'))])
|
||||
finish_choices=[
|
||||
('30m', _l('30 minutes')),
|
||||
('1h', _l('1 hour')),
|
||||
('6h', _l('6 hours')),
|
||||
('12h', _l('12 hours')),
|
||||
('1d', _l('1 day')),
|
||||
('3d', _l('3 days')),
|
||||
('7d', _l('7 days')),
|
||||
]
|
||||
finish_in = SelectField(_('End voting in'), validators=[DataRequired()], choices=finish_choices)
|
||||
local_only = BooleanField(_l('Accept votes from this instance only'))
|
||||
choice_1 = StringField('Choice') # intentionally left out of internationalization (no _l()) as this label is not used
|
||||
choice_2 = StringField('Choice')
|
||||
choice_3 = StringField('Choice')
|
||||
choice_4 = StringField('Choice')
|
||||
choice_5 = StringField('Choice')
|
||||
choice_6 = StringField('Choice')
|
||||
choice_7 = StringField('Choice')
|
||||
choice_8 = StringField('Choice')
|
||||
choice_9 = StringField('Choice')
|
||||
choice_10 = StringField('Choice')
|
||||
tags = StringField(_l('Tags'), validators=[Optional(), Length(min=3, max=5000)])
|
||||
sticky = BooleanField(_l('Sticky'))
|
||||
nsfw = BooleanField(_l('NSFW'))
|
||||
nsfl = BooleanField(_l('Gore/gross'))
|
||||
notify_author = BooleanField(_l('Notify about replies'))
|
||||
language_id = SelectField(_l('Language'), validators=[DataRequired()], coerce=int, render_kw={'class': 'form-select'})
|
||||
submit = SubmitField(_l('Save'))
|
||||
|
||||
|
||||
class ReportCommunityForm(FlaskForm):
|
||||
reason_choices = [('1', _l('Breaks instance rules')),
|
||||
('2', _l('Abandoned by moderators')),
|
||||
|
|
|
@ -15,7 +15,7 @@ from app.chat.util import send_message
|
|||
from app.community.forms import SearchRemoteCommunity, CreateDiscussionForm, CreateImageForm, CreateLinkForm, \
|
||||
ReportCommunityForm, \
|
||||
DeleteCommunityForm, AddCommunityForm, EditCommunityForm, AddModeratorForm, BanUserCommunityForm, \
|
||||
EscalateReportForm, ResolveReportForm, CreateVideoForm
|
||||
EscalateReportForm, ResolveReportForm, CreateVideoForm, CreatePollForm
|
||||
from app.community.util import search_for_community, actor_to_community, \
|
||||
opengraph_parse, url_to_thumbnail_file, save_post, save_icon_file, save_banner_file, send_to_remote_instance, \
|
||||
delete_post_from_community, delete_post_reply_from_community, community_in_list
|
||||
|
@ -769,6 +769,73 @@ def add_video_post(actor):
|
|||
)
|
||||
|
||||
|
||||
@bp.route('/<actor>/submit_poll', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@validation_required
|
||||
def add_poll_post(actor):
|
||||
if current_user.banned:
|
||||
return show_ban_message()
|
||||
community = actor_to_community(actor)
|
||||
|
||||
form = CreatePollForm()
|
||||
|
||||
if g.site.enable_nsfl is False:
|
||||
form.nsfl.render_kw = {'disabled': True}
|
||||
if community.nsfw:
|
||||
form.nsfw.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if community.nsfl:
|
||||
form.nsfl.data = True
|
||||
form.nsfw.render_kw = {'disabled': True}
|
||||
if not(community.is_moderator() or community.is_owner() or current_user.is_admin()):
|
||||
form.sticky.render_kw = {'disabled': True}
|
||||
|
||||
form.communities.choices = [(c.id, c.display_name()) for c in current_user.communities()]
|
||||
if not community_in_list(community.id, form.communities.choices):
|
||||
form.communities.choices.append((community.id, community.display_name()))
|
||||
|
||||
form.language_id.choices = languages_for_form()
|
||||
|
||||
if not can_create_post(current_user, community):
|
||||
abort(401)
|
||||
|
||||
if form.validate_on_submit():
|
||||
community = Community.query.get_or_404(form.communities.data)
|
||||
if not can_create_post(current_user, community):
|
||||
abort(401)
|
||||
post = Post(user_id=current_user.id, community_id=form.communities.data, instance_id=1)
|
||||
save_post(form, post, 'poll')
|
||||
community.post_count += 1
|
||||
community.last_active = g.site.last_active = utcnow()
|
||||
db.session.commit()
|
||||
post.ap_id = f"https://{current_app.config['SERVER_NAME']}/post/{post.id}"
|
||||
db.session.commit()
|
||||
|
||||
upvote_own_post(post)
|
||||
|
||||
notify_about_post(post)
|
||||
|
||||
if not community.local_only:
|
||||
federate_post(community, post)
|
||||
federate_post_to_user_followers(post)
|
||||
|
||||
return redirect(f"/post/{post.id}")
|
||||
else:
|
||||
form.communities.data = community.id
|
||||
form.notify_author.data = True
|
||||
form.language_id.data = current_user.language_id if current_user.is_authenticated and current_user.language_id else english_language_id()
|
||||
form.finish_in.data = '3d'
|
||||
if community.posting_warning:
|
||||
flash(community.posting_warning)
|
||||
|
||||
return render_template('community/add_poll_post.html', title=_('Add poll to community'), form=form, community=community,
|
||||
markdown_editor=current_user.markdown_editor, low_bandwidth=False, actor=actor,
|
||||
moderating_communities=moderating_communities(current_user.get_id()),
|
||||
joined_communities=joined_communities(current_user.id),
|
||||
inoculation=inoculation[randint(0, len(inoculation) - 1)]
|
||||
)
|
||||
|
||||
|
||||
def federate_post(community, post):
|
||||
page = {
|
||||
'type': 'Page',
|
||||
|
|
|
@ -12,9 +12,10 @@ from app import db, cache, celery
|
|||
from app.activitypub.signature import post_request, default_context
|
||||
from app.activitypub.util import find_actor_or_create, actor_json_to_model, post_json_to_model, ensure_domains_match, \
|
||||
find_hashtag_or_create
|
||||
from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_VIDEO, NOTIF_POST
|
||||
from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_VIDEO, NOTIF_POST, \
|
||||
POST_TYPE_POLL
|
||||
from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site, \
|
||||
Instance, Notification, User, ActivityPubLog, NotificationSubscription, Language, Tag
|
||||
Instance, Notification, User, ActivityPubLog, NotificationSubscription, Language, Tag, PollChoice, Poll
|
||||
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, allowlist_html, \
|
||||
is_image_url, ensure_directory_exists, inbox_domain, post_ranking, shorten_string, parse_page, \
|
||||
remove_tracking_from_link, ap_datetime, instance_banned, blocked_phrases
|
||||
|
@ -360,7 +361,10 @@ def save_post(form, post: Post, type: str):
|
|||
db.session.add(file)
|
||||
|
||||
elif type == 'poll':
|
||||
...
|
||||
post.title = form.poll_title.data
|
||||
post.body = form.poll_body.data
|
||||
post.body_html = markdown_to_html(post.body)
|
||||
post.type = POST_TYPE_POLL
|
||||
else:
|
||||
raise Exception('invalid post type')
|
||||
|
||||
|
@ -386,10 +390,29 @@ def save_post(form, post: Post, type: str):
|
|||
|
||||
db.session.add(post)
|
||||
else:
|
||||
db.session.execute(text('DELETE FROM "post_tag" WHERE post_id = :post_id'), { 'post_id': post.id})
|
||||
db.session.execute(text('DELETE FROM "post_tag" WHERE post_id = :post_id'), {'post_id': post.id})
|
||||
post.tags = tags_from_string(form.tags.data)
|
||||
db.session.commit()
|
||||
|
||||
# Save poll choices. NB this will delete all votes whenever a poll is edited. Partially because it's easier to code but also to stop malicious alterations to polls after people have already voted
|
||||
if type == 'poll':
|
||||
db.session.execute(text('DELETE FROM "poll_choice_vote" WHERE post_id = :poll_id'), {'post_id': post.id})
|
||||
db.session.execute(text('DELETE FROM "poll_choice" WHERE post_id = :poll_id'), {'post_id': post.id})
|
||||
for i in range(1, 10):
|
||||
choice_data = getattr(form, f"choice_{i}").data.strip()
|
||||
if choice_data != '':
|
||||
db.session.add(PollChoice(post_id=post.id, choice_text=choice_data, sort_order=i))
|
||||
|
||||
poll = Poll.query.filter_by(post_id=post.id).first()
|
||||
if poll is None:
|
||||
poll = Poll(post_id=post.id)
|
||||
db.session.add(poll)
|
||||
poll.mode = form.mode.data
|
||||
poll.end_poll = end_poll_date(form.finish_in.data)
|
||||
poll.local_only = form.local_only.data
|
||||
poll.latest_vote = None
|
||||
db.session.commit()
|
||||
|
||||
# Notify author about replies
|
||||
# Remove any subscription that currently exists
|
||||
existing_notification = NotificationSubscription.query.filter(NotificationSubscription.entity_id == post.id,
|
||||
|
@ -408,6 +431,23 @@ def save_post(form, post: Post, type: str):
|
|||
db.session.commit()
|
||||
|
||||
|
||||
def end_poll_date(end_choice):
|
||||
delta_mapping = {
|
||||
'30m': timedelta(minutes=30),
|
||||
'1h': timedelta(hours=1),
|
||||
'6h': timedelta(hours=6),
|
||||
'12h': timedelta(hours=12),
|
||||
'1d': timedelta(days=1),
|
||||
'3d': timedelta(days=3),
|
||||
'7d': timedelta(days=7)
|
||||
}
|
||||
|
||||
if end_choice in delta_mapping:
|
||||
return datetime.utcnow() + delta_mapping[end_choice]
|
||||
else:
|
||||
raise ValueError("Invalid choice")
|
||||
|
||||
|
||||
def tags_from_string(tags: str) -> List[Tag]:
|
||||
return_value = []
|
||||
tags = tags.strip()
|
||||
|
|
|
@ -1372,6 +1372,7 @@ class NotificationSubscription(db.Model):
|
|||
class Poll(db.Model):
|
||||
post_id = db.Column(db.Integer, db.ForeignKey('post.id'), primary_key=True)
|
||||
end_poll = db.Column(db.DateTime)
|
||||
mode = db.Column(db.String(10)) # 'single' or 'multiple' determines whether people can vote for one or multiple options
|
||||
local_only = db.Column(db.Boolean)
|
||||
latest_vote = db.Column(db.DateTime)
|
||||
|
||||
|
|
9
app/templates/community/_add_post_types.html
Normal file
9
app/templates/community/_add_post_types.html
Normal file
|
@ -0,0 +1,9 @@
|
|||
<div id="type_of_post" class="btn-group flex-wrap" role="navigation">
|
||||
<a href="{{ url_for('community.add_discussion_post', actor=actor) }}" class="btn {{ 'btn-primary' if request.path.endswith('submit') else 'btn-outline-secondary' }}" aria-label="{{ _('Start a discussion') }}">{{ _('Discussion') }}</a>
|
||||
<a href="{{ url_for('community.add_link_post', actor=actor) }}" class="btn {{ 'btn-primary' if request.path.endswith('submit_link') else 'btn-outline-secondary' }}" aria-label="{{ _('Share a link') }}">{{ _('Link') }}</a>
|
||||
<a href="{{ url_for('community.add_image_post', actor=actor) }}" class="btn {{ 'btn-primary' if request.path.endswith('submit_image') else 'btn-outline-secondary' }}" aria-label="{{ _('Share an image') }}">{{ _('Image') }}</a>
|
||||
<a href="{{ url_for('community.add_video_post', actor=actor) }}" class="btn {{ 'btn-primary' if request.path.endswith('submit_video') else 'btn-outline-secondary' }}" aria-label="{{ _('Share a video') }}">{{ _('Video') }}</a>
|
||||
<a href="{{ url_for('community.add_poll_post', actor=actor) }}" class="btn {{ 'btn-primary' if request.path.endswith('submit_poll') else 'btn-outline-secondary' }}" aria-label="{{ _('Create a poll') }}">{{ _('Poll') }}</a>
|
||||
<!--
|
||||
<a href="#" class="btn" aria-label="{{ _('Create an event') }}">{{ _('Event') }}</a> -->
|
||||
</div>
|
|
@ -15,14 +15,7 @@
|
|||
<label class="form-control-label" for="type_of_post">
|
||||
{{ _('Type of post') }}
|
||||
</label>
|
||||
<div id="type_of_post" class="btn-group flex-wrap" role="navigation">
|
||||
<a href="{{ url_for('community.add_discussion_post', actor=actor) }}" class="btn btn-primary" aria-label="{{ _('Start a discussion') }}">{{ _('Discussion') }}</a>
|
||||
<a href="{{ url_for('community.add_link_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a link') }}">{{ _('Link') }}</a>
|
||||
<a href="{{ url_for('community.add_image_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share an image') }}">{{ _('Image') }}</a>
|
||||
<a href="{{ url_for('community.add_video_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a video') }}">{{ _('Video') }}</a>
|
||||
<!-- <a href="#" class="btn" aria-label="{{ _('Create a poll') }}">{{ _('Poll') }}</a>
|
||||
<a href="#" class="btn" aria-label="{{ _('Create an event') }}">{{ _('Event') }}</a> -->
|
||||
</div>
|
||||
{% include 'community/_add_post_types.html' %}
|
||||
</div>
|
||||
|
||||
{{ render_field(form.communities) }}
|
||||
|
|
|
@ -15,14 +15,7 @@
|
|||
<label class="form-control-label" for="type_of_post">
|
||||
{{ _('Type of post') }}
|
||||
</label>
|
||||
<div id="type_of_post" class="btn-group flex-wrap" role="navigation">
|
||||
<a href="{{ url_for('community.add_discussion_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Start a discussion') }}">{{ _('Discussion') }}</a>
|
||||
<a href="{{ url_for('community.add_link_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a link') }}">{{ _('Link') }}</a>
|
||||
<a href="{{ url_for('community.add_image_post', actor=actor) }}" class="btn btn-primary" aria-label="{{ _('Share an image') }}">{{ _('Image') }}</a>
|
||||
<a href="{{ url_for('community.add_video_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a video') }}">{{ _('Video') }}</a>
|
||||
<!-- <a href="#" class="btn" aria-label="{{ _('Create a poll') }}">{{ _('Poll') }}</a>
|
||||
<a href="#" class="btn" aria-label="{{ _('Create an event') }}">{{ _('Event') }}</a> -->
|
||||
</div>
|
||||
{% include 'community/_add_post_types.html' %}
|
||||
</div>
|
||||
{{ render_field(form.communities) }}
|
||||
{{ render_field(form.image_title) }}
|
||||
|
|
|
@ -15,14 +15,7 @@
|
|||
<label class="form-control-label" for="type_of_post">
|
||||
{{ _('Type of post') }}
|
||||
</label>
|
||||
<div id="type_of_post" class="btn-group flex-wrap" role="navigation">
|
||||
<a href="{{ url_for('community.add_discussion_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Start a discussion') }}">{{ _('Discussion') }}</a>
|
||||
<a href="{{ url_for('community.add_link_post', actor=actor) }}" class="btn btn-primary" aria-label="{{ _('Share a link') }}">{{ _('Link') }}</a>
|
||||
<a href="{{ url_for('community.add_image_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share an image') }}">{{ _('Image') }}</a>
|
||||
<a href="{{ url_for('community.add_video_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a video') }}">{{ _('Video') }}</a>
|
||||
<!-- <a href="#" class="btn" aria-label="{{ _('Create a poll') }}">{{ _('Poll') }}</a>
|
||||
<a href="#" class="btn" aria-label="{{ _('Create an event') }}">{{ _('Event') }}</a> -->
|
||||
</div>
|
||||
{% include 'community/_add_post_types.html' %}
|
||||
</div>
|
||||
{{ render_field(form.communities) }}
|
||||
|
||||
|
|
94
app/templates/community/add_poll_post.html
Normal file
94
app/templates/community/add_poll_post.html
Normal file
|
@ -0,0 +1,94 @@
|
|||
{% if theme() and file_exists('app/templates/themes/' + theme() + '/base.html') %}
|
||||
{% extends 'themes/' + theme() + '/base.html' %}
|
||||
{% else %}
|
||||
{% extends "base.html" %}
|
||||
{% endif %} %}
|
||||
{% from 'bootstrap/form.html' import render_field %}
|
||||
|
||||
{% block app_content %}
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8 position-relative main_pane">
|
||||
<h1>{{ _('Create post') }}</h1>
|
||||
<form method="post" enctype="multipart/form-data" role="form">
|
||||
{{ form.csrf_token() }}
|
||||
<div class="form-group">
|
||||
<label class="form-control-label" for="type_of_post">
|
||||
{{ _('Type of post') }}
|
||||
</label>
|
||||
{% include 'community/_add_post_types.html' %}
|
||||
</div>
|
||||
|
||||
{{ render_field(form.communities) }}
|
||||
{{ render_field(form.poll_title) }}
|
||||
{{ render_field(form.poll_body) }}
|
||||
{% if not low_bandwidth %}
|
||||
{% if markdown_editor %}
|
||||
<script nonce="{{ session['nonce'] }}">
|
||||
window.addEventListener("load", function () {
|
||||
var downarea = new DownArea({
|
||||
elem: document.querySelector('#poll_body'),
|
||||
resize: DownArea.RESIZE_VERTICAL,
|
||||
hide: ['heading', 'bold-italic'],
|
||||
});
|
||||
setupAutoResize('discussion_body');
|
||||
});
|
||||
</script>
|
||||
{% else %}
|
||||
<a href="#" aria-hidden="true" class="markdown_editor_enabler create_post_markdown_editor_enabler" data-id="discussion_body">{{ _('Enable markdown editor') }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ render_field(form.mode) }}
|
||||
{{ render_field(form.finish_in) }}
|
||||
{{ render_field(form.local_only) }}
|
||||
{{ render_field(form.tags) }}
|
||||
<small class="field_hint">{{ _('Separate each tag with a comma.') }}</small>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-3">
|
||||
{{ render_field(form.notify_author) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.sticky) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfw) }}
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
{{ render_field(form.nsfl) }}
|
||||
</div>
|
||||
<div class="col post_language_chooser">
|
||||
{{ render_field(form.language_id) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ render_field(form.submit) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<aside id="side_pane" class="col-12 col-md-4 side_pane" role="complementary">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h2>{{ community.title }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>{{ community.description_html|safe if community.description_html else '' }}</p>
|
||||
<p>{{ community.rules_html|safe if community.rules_html else '' }}</p>
|
||||
{% if len(mods) > 0 and not community.private_mods %}
|
||||
<h3>Moderators</h3>
|
||||
<ul class="moderator_list">
|
||||
{% for mod in mods %}
|
||||
<li>{{ render_username(mod) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if rss_feed %}
|
||||
<p class="mt-4">
|
||||
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "_inoculation_links.html" %}
|
||||
</aside>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -15,14 +15,7 @@
|
|||
<label class="form-control-label" for="type_of_post">
|
||||
{{ _('Type of post') }}
|
||||
</label>
|
||||
<div id="type_of_post" class="btn-group flex-wrap" role="navigation">
|
||||
<a href="{{ url_for('community.add_discussion_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Start a discussion') }}">{{ _('Discussion') }}</a>
|
||||
<a href="{{ url_for('community.add_link_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share a link') }}">{{ _('Link') }}</a>
|
||||
<a href="{{ url_for('community.add_image_post', actor=actor) }}" class="btn btn-outline-secondary" aria-label="{{ _('Share an image') }}">{{ _('Image') }}</a>
|
||||
<a href="{{ url_for('community.add_video_post', actor=actor) }}" class="btn btn-primary" aria-label="{{ _('Share a video') }}">{{ _('Video') }}</a>
|
||||
<!-- <a href="#" class="btn" aria-label="{{ _('Create a poll') }}">{{ _('Poll') }}</a>
|
||||
<a href="#" class="btn" aria-label="{{ _('Create an event') }}">{{ _('Event') }}</a> -->
|
||||
</div>
|
||||
{% include 'community/_add_post_types.html' %}
|
||||
</div>
|
||||
{{ render_field(form.communities) }}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
"""polls
|
||||
|
||||
Revision ID: 92bdb39f6c72
|
||||
Revision ID: 05e3a7023a1e
|
||||
Revises: 9752fb47d7a6
|
||||
Create Date: 2024-05-16 20:42:05.491951
|
||||
Create Date: 2024-05-16 20:47:35.566151
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
|
@ -10,7 +10,7 @@ import sqlalchemy as sa
|
|||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '92bdb39f6c72'
|
||||
revision = '05e3a7023a1e'
|
||||
down_revision = '9752fb47d7a6'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
@ -21,6 +21,7 @@ def upgrade():
|
|||
op.create_table('poll',
|
||||
sa.Column('post_id', sa.Integer(), nullable=False),
|
||||
sa.Column('end_poll', sa.DateTime(), nullable=True),
|
||||
sa.Column('mode', sa.String(length=10), nullable=True),
|
||||
sa.Column('local_only', sa.Boolean(), nullable=True),
|
||||
sa.Column('latest_vote', sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['post_id'], ['post.id'], ),
|
Loading…
Reference in a new issue