WIP poll support.
This commit is contained in:
parent
aea03db8e4
commit
f3a2285509
16 changed files with 399 additions and 8 deletions
|
@ -4,6 +4,7 @@
|
|||
@import "confirm";
|
||||
@import "header";
|
||||
@import "post";
|
||||
@import "poll";
|
||||
@import "status";
|
||||
@import "topic";
|
||||
@import "topics";
|
||||
|
|
41
assets/less/forum/poll.less
Normal file
41
assets/less/forum/poll.less
Normal file
|
@ -0,0 +1,41 @@
|
|||
.forum__poll {
|
||||
margin: 2px 0;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&__options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 500px;
|
||||
min-width: 100%;
|
||||
|
||||
@media (min-width: 400px) {
|
||||
min-width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
&__option {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
&__remaining,
|
||||
&__expires {
|
||||
line-height: 1.5em;
|
||||
|
||||
&__num,
|
||||
&__datetime {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
margin: 0 2px;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
.input__checkbox {
|
||||
display: inline-flex;
|
||||
margin: 1px 0;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(&--disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&--radio &__display,
|
||||
&--radio &__display__icon {
|
||||
|
@ -12,6 +15,7 @@
|
|||
display: inline-block;
|
||||
position: absolute;
|
||||
z-index: -1000;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&__display {
|
||||
|
@ -43,8 +47,8 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover &__display,
|
||||
&__input:focus ~ &__display {
|
||||
&:not(&--disabled):hover &__display,
|
||||
&:not(&--disabled) &__input:focus ~ &__display {
|
||||
border-color: var(--accent-colour);
|
||||
}
|
||||
|
||||
|
@ -52,4 +56,8 @@
|
|||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,6 +96,12 @@ html {
|
|||
}
|
||||
}
|
||||
|
||||
// Just in case.
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
// Misc
|
||||
@import "animations";
|
||||
@import "link";
|
||||
|
|
56
assets/typescript/Forum/Polls.ts
Normal file
56
assets/typescript/Forum/Polls.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
function forumPollsInit(): void {
|
||||
const polls: NodeListOf<HTMLFormElement> = document.getElementsByClassName('js-forum-poll');
|
||||
|
||||
if (polls.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < polls.length; i++) {
|
||||
forumPollInit(polls[i]);
|
||||
}
|
||||
}
|
||||
|
||||
function forumPollInit(poll: HTMLFormElement): void {
|
||||
console.log(poll);
|
||||
|
||||
const options: HTMLNodeListOf<HTMLInputElement> = poll.getElementsByClassName('input__checkbox__input'),
|
||||
votesRemaining: HTMLDivElement = poll.querySelector('.js-forum-poll-remaining'),
|
||||
votesRemainingCount: HTMLSpanElement = poll.querySelector('.js-forum-poll-remaining-count'),
|
||||
votesRemainingPlural: HTMLSpanElement = poll.querySelector('.js-forum-poll-remaining-plural'),
|
||||
maxVotes: number = parseInt(poll.dataset.pollMaxVotes);
|
||||
|
||||
if (maxVotes > 1) {
|
||||
let votes: number = maxVotes;
|
||||
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
if (options[i].checked) {
|
||||
if (votes < 1) {
|
||||
options[i].checked = false;
|
||||
} else {
|
||||
votes--;
|
||||
}
|
||||
}
|
||||
|
||||
options[i].addEventListener('change', ev => {
|
||||
if (ev.target.checked) {
|
||||
if (votes < 1) {
|
||||
ev.target.checked = false;
|
||||
ev.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
votes--;
|
||||
} else {
|
||||
votes++;
|
||||
}
|
||||
|
||||
votesRemainingCount.textContent = votes;
|
||||
votesRemainingPlural.hidden = votes == 1;
|
||||
});
|
||||
}
|
||||
|
||||
votesRemaining.hidden = false;
|
||||
votesRemainingCount.textContent = votes;
|
||||
votesRemainingPlural.hidden = votes == 1;
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
/// <reference path="UserRelations.ts" />
|
||||
/// <reference path="Forum/Posting.ts" />
|
||||
/// <reference path="UrlRegistry.ts" />
|
||||
/// <reference path="Forum/Polls.ts" />
|
||||
|
||||
declare const timeago: any;
|
||||
declare const hljs: any;
|
||||
|
@ -63,6 +64,7 @@ window.addEventListener('load', () => {
|
|||
|
||||
commentsInit();
|
||||
forumPostingInit();
|
||||
forumPollsInit();
|
||||
});
|
||||
|
||||
function loginFormUpdateAvatar(avatarElement: HTMLElement, usernameElement: HTMLInputElement, force: boolean = false): void {
|
||||
|
|
83
database/2019_04_16_123231_create_polls_tables.php
Normal file
83
database/2019_04_16_123231_create_polls_tables.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
namespace Misuzu\DatabaseMigrations\CreatePollsTables;
|
||||
|
||||
use PDO;
|
||||
|
||||
function migrate_up(PDO $conn): void
|
||||
{
|
||||
$conn->exec("
|
||||
CREATE TABLE `msz_forum_polls` (
|
||||
`poll_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`poll_max_votes` TINYINT(3) UNSIGNED NOT NULL DEFAULT '1',
|
||||
`poll_expires` TIMESTAMP NULL DEFAULT NULL,
|
||||
`poll_preview_results` TINYINT(3) UNSIGNED NOT NULL DEFAULT '1',
|
||||
PRIMARY KEY (`poll_id`)
|
||||
) COLLATE='utf8mb4_bin' ENGINE=InnoDB
|
||||
");
|
||||
|
||||
$conn->exec("
|
||||
CREATE TABLE `msz_forum_polls_options` (
|
||||
`option_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`poll_id` INT(10) UNSIGNED NOT NULL,
|
||||
`option_text` VARCHAR(255) NOT NULL COLLATE 'utf8mb4_bin',
|
||||
PRIMARY KEY (`option_id`),
|
||||
INDEX `polls_options_poll_foreign` (`poll_id`),
|
||||
CONSTRAINT `polls_options_poll_foreign`
|
||||
FOREIGN KEY (`poll_id`)
|
||||
REFERENCES `msz_forum_polls` (`poll_id`)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
) COLLATE='utf8mb4_bin' ENGINE=InnoDB
|
||||
");
|
||||
|
||||
$conn->exec("
|
||||
CREATE TABLE `msz_forum_polls_answers` (
|
||||
`user_id` INT(10) UNSIGNED NOT NULL,
|
||||
`poll_id` INT(10) UNSIGNED NOT NULL,
|
||||
`option_id` INT(10) UNSIGNED NOT NULL,
|
||||
UNIQUE INDEX `polls_answers_unique` (`user_id`, `poll_id`, `option_id`),
|
||||
INDEX `polls_answers_user_foreign` (`user_id`),
|
||||
INDEX `polls_answers_poll_foreign` (`poll_id`),
|
||||
INDEX `polls_answers_option_foreign` (`option_id`),
|
||||
CONSTRAINT `polls_answers_option_foreign`
|
||||
FOREIGN KEY (`option_id`)
|
||||
REFERENCES `msz_forum_polls_options` (`option_id`)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT `polls_answers_poll_foreign`
|
||||
FOREIGN KEY (`poll_id`)
|
||||
REFERENCES `msz_forum_polls` (`poll_id`)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
CONSTRAINT `polls_answers_user_foreign`
|
||||
FOREIGN KEY (`user_id`)
|
||||
REFERENCES `msz_users` (`user_id`)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
) COLLATE='utf8mb4_bin' ENGINE=InnoDB
|
||||
");
|
||||
|
||||
$conn->exec("
|
||||
ALTER TABLE `msz_forum_topics`
|
||||
ADD COLUMN `poll_id` INT(10) UNSIGNED NULL DEFAULT NULL AFTER `user_id`,
|
||||
ADD INDEX `posts_poll_id_foreign` (`poll_id`),
|
||||
ADD CONSTRAINT `posts_poll_id_foreign`
|
||||
FOREIGN KEY (`poll_id`)
|
||||
REFERENCES `msz_forum_polls` (`poll_id`)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE SET NULL;
|
||||
");
|
||||
}
|
||||
|
||||
function migrate_down(PDO $conn): void
|
||||
{
|
||||
$conn->exec("
|
||||
ALTER TABLE `msz_forum_topics`
|
||||
DROP COLUMN `poll_id`,
|
||||
DROP INDEX `posts_poll_id_foreign`,
|
||||
DROP FOREIGN KEY `posts_poll_id_foreign`;
|
||||
");
|
||||
$conn->exec("DROP TABLE `msz_forum_polls_answers`");
|
||||
$conn->exec("DROP TABLE `msz_forum_polls_options`");
|
||||
$conn->exec("DROP TABLE `msz_forum_polls`");
|
||||
}
|
|
@ -52,6 +52,7 @@ require_once 'src/zalgo.php';
|
|||
require_once 'src/Forum/forum.php';
|
||||
require_once 'src/Forum/leaderboard.php';
|
||||
require_once 'src/Forum/perms.php';
|
||||
require_once 'src/Forum/poll.php';
|
||||
require_once 'src/Forum/post.php';
|
||||
require_once 'src/Forum/topic.php';
|
||||
require_once 'src/Forum/validate.php';
|
||||
|
|
50
public/forum/poll.php
Normal file
50
public/forum/poll.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
require_once '../../misuzu.php';
|
||||
|
||||
$redirect = !empty($_SERVER['HTTP_REFERER']) && empty($_SERVER['HTTP_X_MISUZU_XHR']) ? $_SERVER['HTTP_REFERER'] : '';
|
||||
$isXHR = !$redirect;
|
||||
|
||||
if ($isXHR) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
} elseif (!is_local_url($redirect)) {
|
||||
echo render_info('Possible request forgery detected.', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!csrf_verify('forum_poll', $_REQUEST['csrf'] ?? '')) {
|
||||
echo render_info_or_json($isXHR, "Couldn't verify this request, please refresh the page and try again.", 403);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user_session_active()) {
|
||||
echo render_info_or_json($isXHR, 'You must be logged in to vote on polls.', 401);
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = user_session_current('user_id', 0);
|
||||
|
||||
if (user_warning_check_expiration($currentUserId, MSZ_WARN_BAN) > 0) {
|
||||
echo render_info_or_json($isXHR, 'You have been banned, check your profile for more information.', 403);
|
||||
return;
|
||||
}
|
||||
if (user_warning_check_expiration($currentUserId, MSZ_WARN_SILENCE) > 0) {
|
||||
echo render_info_or_json($isXHR, 'You have been silenced, check your profile for more information.', 403);
|
||||
return;
|
||||
}
|
||||
|
||||
header(csrf_http_header('forum_poll'));
|
||||
|
||||
if (empty($_POST['polls']) || !is_array($_POST['polls'])) {
|
||||
echo render_info_or_json($isXHR, "Invalid request.", 400);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($_POST['polls'] as $pollId => $answerId) {
|
||||
if (!is_int($pollId) || !is_string($answerId) || !ctype_digit($answerId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$answerId = (int)$answerId;
|
||||
|
||||
var_dump($pollId, $answerId);
|
||||
}
|
|
@ -38,6 +38,11 @@ if (!perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM)) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!empty($topic['poll_id'])) {
|
||||
$pollOptions = forum_poll_options($topic['poll_id']);
|
||||
$pollAnswers = [];
|
||||
}
|
||||
|
||||
$topicIsLocked = !empty($topic['topic_locked']);
|
||||
$topicIsArchived = !empty($topic['topic_archived']);
|
||||
$topicPostsTotal = (int)($topic['topic_count_posts'] + $topic['topic_count_posts_deleted']);
|
||||
|
@ -363,4 +368,5 @@ echo tpl_render('forum.topic', [
|
|||
'topic_can_nuke_or_restore' => $canNukeOrRestore,
|
||||
'topic_can_bump' => $canBumpTopic,
|
||||
'topic_can_lock' => $canLockTopic,
|
||||
'topic_poll_options' => $pollOptions ?? [],
|
||||
]);
|
||||
|
|
83
src/Forum/poll.php
Normal file
83
src/Forum/poll.php
Normal file
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
function forum_poll_create(int $maxVotes = 1): int
|
||||
{
|
||||
if ($maxVotes < 1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
$createPoll = db_prepare("
|
||||
INSERT INTO `msz_forum_polls`
|
||||
(`poll_max_votes`)
|
||||
VALUES
|
||||
(:max_votes)
|
||||
");
|
||||
$createPoll->bindValue('max_votes', $maxVotes);
|
||||
return (int)($createPoll->execute() ? db_last_insert_id() : -1);
|
||||
}
|
||||
|
||||
function forum_poll_options(int $poll): array
|
||||
{
|
||||
if($poll < 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
static $polls = [];
|
||||
|
||||
if(array_key_exists($poll, $polls)) {
|
||||
return $polls[$poll];
|
||||
}
|
||||
|
||||
$getOptions = db_prepare('
|
||||
SELECT `option_id`, `option_text`
|
||||
FROM `msz_forum_polls_options`
|
||||
WHERE `poll_id` = :poll
|
||||
');
|
||||
$getOptions->bindValue('poll', $poll);
|
||||
|
||||
return $polls[$poll] = db_fetch_all($getOptions);
|
||||
}
|
||||
|
||||
function forum_poll_reset_answers(int $poll): void
|
||||
{
|
||||
if ($poll < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$resetAnswers = db_prepare("
|
||||
DELETE FROM `msz_forum_polls_answers`
|
||||
WHERE `poll_id` = :poll
|
||||
");
|
||||
$resetAnswers->bindValue('poll', $poll);
|
||||
$resetAnswers->execute();
|
||||
}
|
||||
|
||||
function forum_poll_option_add(int $poll, string $text): int
|
||||
{
|
||||
if ($poll < 1 || empty($text) || strlen($text) > 0xFF) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
$addOption = db_prepare("
|
||||
INSERT INTO `msz_forum_polls_options`
|
||||
(`poll_id`, `option_text`)
|
||||
VALUES
|
||||
(:poll, :text)
|
||||
");
|
||||
$addOption->bindValue('poll', $poll);
|
||||
$addOption->bindValue('text', $text);
|
||||
return (int)($createPoll->execute() ? db_last_insert_id() : -1);
|
||||
}
|
||||
|
||||
function forum_poll_option_remove(int $option): void
|
||||
{
|
||||
if ($option < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$removeOption = db_prepare("
|
||||
DELETE FROM `msz_forum_polls_options`
|
||||
WHERE `option_id` = :option
|
||||
");
|
||||
$removeOption->bindValue('option', $option);
|
||||
$removeOption->execute();
|
||||
}
|
|
@ -76,6 +76,8 @@ function forum_topic_get(int $topicId, bool $allowDeleted = false): array
|
|||
SELECT
|
||||
t.`topic_id`, t.`forum_id`, t.`topic_title`, t.`topic_type`, t.`topic_locked`, t.`topic_created`,
|
||||
f.`forum_archived` as `topic_archived`, t.`topic_deleted`, t.`topic_bumped`,
|
||||
tp.`poll_id`, tp.`poll_max_votes`, tp.`poll_expires`, tp.`poll_preview_results`,
|
||||
(tp.`poll_expires` < CURRENT_TIMESTAMP) AS `poll_expired`,
|
||||
fp.`topic_id` as `author_post_id`, fp.`user_id` as `author_user_id`,
|
||||
(
|
||||
SELECT COUNT(`post_id`)
|
||||
|
@ -98,6 +100,8 @@ function forum_topic_get(int $topicId, bool $allowDeleted = false): array
|
|||
FROM `msz_forum_posts`
|
||||
WHERE `topic_id` = t.`topic_id`
|
||||
)
|
||||
LEFT JOIN `msz_forum_polls` AS tp
|
||||
ON tp.`poll_id` = t.`poll_id`
|
||||
WHERE t.`topic_id` = :topic_id
|
||||
%s
|
||||
',
|
||||
|
|
|
@ -60,6 +60,7 @@ define('MSZ_URLS', [
|
|||
'forum-post-nuke' => ['/forum/post.php', ['p' => '<post>', 'm' => 'nuke']],
|
||||
'forum-post-quote' => ['/forum/posting.php', ['q' => '<post>']],
|
||||
'forum-post-edit' => ['/forum/posting.php', ['p' => '<post>', 'm' => 'edit']],
|
||||
'forum-poll-vote' => ['/forum/poll.php'],
|
||||
|
||||
'user-list' => ['/members.php', ['r' => '<role>', 'ss' => '<sort>', 'sd' => '<direction>', 'p' => '<page>']],
|
||||
|
||||
|
|
|
@ -23,11 +23,12 @@
|
|||
{% endspaceless %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro input_checkbox_raw(name, checked, class, value, radio, attributes) %}
|
||||
{% macro input_checkbox_raw(name, checked, class, value, radio, attributes, disabled) %}
|
||||
{% spaceless %}
|
||||
<input type="{{ radio ? 'radio' : 'checkbox' }}" class="{{ class|length > 0 ? class : 'input__checkbox__input' }}"
|
||||
{% if name|length > 0 %}name="{{ name }}"{% endif %}
|
||||
{% if checked %}checked{% endif %}
|
||||
{% if disabled %}disabled{% endif %}
|
||||
{% if value|length > 0 %}value="{{ value }}"{% endif %}
|
||||
{% for name, value in attributes|default([]) %}
|
||||
{{ name }}{% if value|length > 0 %}="{{ value }}"{% endif %}
|
||||
|
@ -35,11 +36,11 @@
|
|||
{% endspaceless %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro input_checkbox(name, text, checked, class, value, radio, attributes) %}
|
||||
{% macro input_checkbox(name, text, checked, class, value, radio, attributes, disabled) %}
|
||||
{% from _self import input_checkbox_raw %}
|
||||
{% spaceless %}
|
||||
<label class="input__checkbox{% if radio %} input__checkbox--radio{% endif %}{{ class|length > 0 ? ' ' ~ class : '' }}">
|
||||
{{ input_checkbox_raw(name, checked, '', value, radio, attributes) }}
|
||||
<label class="input__checkbox{% if radio %} input__checkbox--radio{% endif %}{% if disabled %} input__checkbox--disabled{% endif %}{{ class|length > 0 ? ' ' ~ class : '' }}">
|
||||
{{ input_checkbox_raw(name, checked, '', value, radio, attributes, disabled) }}
|
||||
<div class="input__checkbox__display">
|
||||
<div class="input__checkbox__display__icon"></div>
|
||||
</div>
|
||||
|
|
|
@ -466,3 +466,49 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro forum_poll(poll, options, results) %}
|
||||
{% from '_layout/input.twig' import input_csrf, input_checkbox %}
|
||||
|
||||
{% if options is iterable and options|length > 0 %}
|
||||
<form method="post" action="{{ url('forum-poll-vote') }}" class="container forum__poll js-forum-poll"
|
||||
data-poll-id="{{ poll.poll_id }}" data-poll-max-votes="{{ poll.poll_max_votes }}">
|
||||
{{ input_csrf('forum_poll') }}
|
||||
|
||||
<div class="forum__poll__options">
|
||||
{% for option in options %}
|
||||
{{ input_checkbox(
|
||||
'polls[' ~ poll.poll_id ~ ']', option.option_text, false,
|
||||
'forum__poll__option', option.option_id, poll.poll_max_votes <= 1,
|
||||
null, poll.poll_expired
|
||||
) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if poll.poll_max_votes > 1 %}
|
||||
<div class="forum__poll__remaining js-forum-poll-remaining" hidden>
|
||||
You have <span class="forum__poll__remaining__num">
|
||||
<span class="js-forum-poll-remaining-count">{{ poll.poll_max_votes }}</span> vote<span class="js-forum-poll-remaining-plural" hidden>s</span>
|
||||
</span> remaining.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if poll.poll_expires is not null %}
|
||||
<div class="forum__poll__expires">
|
||||
{% if poll.poll_expired %}
|
||||
Voting has finished, you can no longer vote.
|
||||
{% else %}
|
||||
Voting on this poll will close <time class="forum__poll__expires__datetime" datetime="{{ poll.poll_expires|date('c') }}" title="{{ poll.poll_expires|date('r') }}">{{ poll.poll_expires|time_diff }}</time>.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="forum__poll__buttons">
|
||||
<button class="input__button forum__poll__button">Vote</button>
|
||||
{% if poll.poll_preview_results %}
|
||||
<a class="input__button forum__poll__button">Results</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
forum_topic_buttons,
|
||||
forum_topic_locked,
|
||||
forum_header,
|
||||
forum_topic_tools
|
||||
forum_topic_tools,
|
||||
forum_poll
|
||||
%}
|
||||
|
||||
{% set title = topic_info.topic_title %}
|
||||
|
@ -55,6 +56,7 @@
|
|||
{% block content %}
|
||||
{{ forum_header(topic_info.topic_title, topic_breadcrumbs, false, canonical_url, topic_actions) }}
|
||||
{{ topic_notice }}
|
||||
{{ forum_poll(topic_info, topic_poll_options) }}
|
||||
{{ topic_tools }}
|
||||
{{ forum_post_listing(topic_posts, current_user.user_id|default(0), topic_perms) }}
|
||||
{{ topic_tools }}
|
||||
|
|
Loading…
Add table
Reference in a new issue