WIP poll support.

This commit is contained in:
flash 2019-04-18 01:59:33 +02:00
parent aea03db8e4
commit f3a2285509
16 changed files with 399 additions and 8 deletions

View file

@ -4,6 +4,7 @@
@import "confirm";
@import "header";
@import "post";
@import "poll";
@import "status";
@import "topic";
@import "topics";

View 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;
}
}

View file

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

View file

@ -96,6 +96,12 @@ html {
}
}
// Just in case.
[hidden] {
display: none !important;
visibility: hidden !important;
}
// Misc
@import "animations";
@import "link";

View 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;
}
}

View file

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

View 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`");
}

View file

@ -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
View 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);
}

View file

@ -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
View 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();
}

View file

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

View file

@ -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>']],

View file

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

View file

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

View file

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