mirror of
https://github.com/openvk/openvk
synced 2024-12-23 00:51:03 +03:00
Add polls
This commit is contained in:
parent
8c314adf6c
commit
4ebeaa3f2f
23 changed files with 977 additions and 59 deletions
70
ServiceAPI/Polls.php
Normal file
70
ServiceAPI/Polls.php
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace openvk\ServiceAPI;
|
||||||
|
use Chandler\MVC\Routing\Router;
|
||||||
|
use openvk\Web\Models\Entities\User;
|
||||||
|
use openvk\Web\Models\Exceptions\{AlreadyVotedException, InvalidOptionException, PollLockedException};
|
||||||
|
use openvk\Web\Models\Repositories\Polls as PollRepo;
|
||||||
|
use UnexpectedValueException;
|
||||||
|
|
||||||
|
class Polls implements Handler
|
||||||
|
{
|
||||||
|
protected $user;
|
||||||
|
protected $polls;
|
||||||
|
|
||||||
|
function __construct(?User $user)
|
||||||
|
{
|
||||||
|
$this->user = $user;
|
||||||
|
$this->polls = new PollRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getPollHtml(int $poll): string
|
||||||
|
{
|
||||||
|
return Router::i()->execute("/poll$poll", "SAPI");
|
||||||
|
}
|
||||||
|
|
||||||
|
function vote(int $pollId, string $options, callable $resolve, callable $reject): void
|
||||||
|
{
|
||||||
|
$poll = $this->polls->get($pollId);
|
||||||
|
if(!$poll) {
|
||||||
|
$reject("Poll not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$options = explode(",", $options);
|
||||||
|
$poll->vote($this->user, $options);
|
||||||
|
} catch(AlreadyVotedException $ex) {
|
||||||
|
$reject("Poll state changed: user has already voted.");
|
||||||
|
return;
|
||||||
|
} catch(PollLockedException $ex) {
|
||||||
|
$reject("Poll state changed: poll has ended.");
|
||||||
|
return;
|
||||||
|
} catch(InvalidOptionException $ex) {
|
||||||
|
$reject("Foreign options passed.");
|
||||||
|
return;
|
||||||
|
} catch(UnexpectedValueException $ex) {
|
||||||
|
$reject("Too much options passed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolve(["html" => $this->getPollHtml($pollId)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unvote(int $pollId, callable $resolve, callable $reject): void
|
||||||
|
{
|
||||||
|
$poll = $this->polls->get($pollId);
|
||||||
|
if(!$poll) {
|
||||||
|
$reject("Poll not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$poll->revokeVote($this->user);
|
||||||
|
} catch(PollLockedException $ex) {
|
||||||
|
$reject("Votes can't be revoked from this poll.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resolve(["html" => $this->getPollHtml($pollId)]);
|
||||||
|
}
|
||||||
|
}
|
295
Web/Models/Entities/Poll.php
Normal file
295
Web/Models/Entities/Poll.php
Normal file
|
@ -0,0 +1,295 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace openvk\Web\Models\Entities;
|
||||||
|
use openvk\Web\Models\Exceptions\TooMuchOptionsException;
|
||||||
|
use openvk\Web\Util\DateTime;
|
||||||
|
use \UnexpectedValueException;
|
||||||
|
use Nette\InvalidStateException;
|
||||||
|
use openvk\Web\Models\Repositories\Users;
|
||||||
|
use Chandler\Database\DatabaseConnection;
|
||||||
|
use openvk\Web\Models\Exceptions\PollLockedException;
|
||||||
|
use openvk\Web\Models\Exceptions\AlreadyVotedException;
|
||||||
|
use openvk\Web\Models\Exceptions\InvalidOptionException;
|
||||||
|
|
||||||
|
class Poll extends Attachable
|
||||||
|
{
|
||||||
|
protected $tableName = "polls";
|
||||||
|
|
||||||
|
private $choicesToPersist = [];
|
||||||
|
|
||||||
|
function getTitle(): string
|
||||||
|
{
|
||||||
|
return $this->getRecord()->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMetaDescription(): string
|
||||||
|
{
|
||||||
|
$props = [];
|
||||||
|
$props[] = tr($this->isAnonymous() ? "poll_anon" : "poll_public");
|
||||||
|
if($this->isMultipleChoice()) $props[] = tr("poll_multi");
|
||||||
|
if(!$this->isRevotable()) $props[] = tr("poll_lock");
|
||||||
|
if(!is_null($this->endsAt())) $props[] = tr("poll_until", $this->endsAt());
|
||||||
|
|
||||||
|
return implode(" • ", $props);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOwner(): User
|
||||||
|
{
|
||||||
|
return (new Users)->get($this->getRecord()->owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptions(): array
|
||||||
|
{
|
||||||
|
$options = $this->getRecord()->related("poll_options.poll");
|
||||||
|
$res = [];
|
||||||
|
foreach($options as $opt)
|
||||||
|
$res[$opt->id] = $opt->name;
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserVote(User $user): ?array
|
||||||
|
{
|
||||||
|
$ctx = DatabaseConnection::i()->getContext();
|
||||||
|
$votedOpts = $ctx->table("poll_votes")
|
||||||
|
->where(["user" => $user->getId(), "poll" => $this->getId()]);
|
||||||
|
|
||||||
|
if($votedOpts->count() == 0)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
$res = [];
|
||||||
|
foreach($votedOpts as $votedOpt) {
|
||||||
|
$option = $ctx->table("poll_options")->get($votedOpt->option);
|
||||||
|
$res[] = [$option->id, $option->name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVoters(int $optionId, int $page = 1, ?int $perPage = NULL): array
|
||||||
|
{
|
||||||
|
$res = [];
|
||||||
|
$ctx = DatabaseConnection::i()->getContext();
|
||||||
|
$perPage = $perPage ?? OPENVK_DEFAULT_PER_PAGE;
|
||||||
|
$voters = $ctx->table("poll_votes")->where(["poll" => $this->getId(), "option" => $optionId]);
|
||||||
|
foreach($voters->page($page, $perPage) as $vote)
|
||||||
|
$res[] = (new Users)->get($vote->user);
|
||||||
|
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVoterCount(?int $optionId = NULL): int
|
||||||
|
{
|
||||||
|
$votes = DatabaseConnection::i()->getContext()->table("poll_votes");
|
||||||
|
if(!$optionId)
|
||||||
|
return $votes->select("COUNT(DISTINCT user) AS c")->where("poll", $this->getId())->fetch()->c;
|
||||||
|
|
||||||
|
return $votes->where(["poll" => $this->getId(), "option" => $optionId])->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getResults(?User $user = NULL): object
|
||||||
|
{
|
||||||
|
$ctx = DatabaseConnection::i()->getContext();
|
||||||
|
$voted = NULL;
|
||||||
|
if(!is_null($user))
|
||||||
|
$voted = $this->getUserVote($user);
|
||||||
|
|
||||||
|
$result = (object) [];
|
||||||
|
$result->totalVotes = $this->getVoterCount();
|
||||||
|
|
||||||
|
$unsOptions = [];
|
||||||
|
foreach($this->getOptions() as $id => $title) {
|
||||||
|
$option = (object) [];
|
||||||
|
$option->id = $id;
|
||||||
|
$option->name = $title;
|
||||||
|
|
||||||
|
$option->votes = $this->getVoterCount($id);
|
||||||
|
$option->pct = $result->totalVotes == 0 ? 0 : (($option->votes / $result->totalVotes) * 100);
|
||||||
|
$option->voters = $this->getVoters($id, 1, 10);
|
||||||
|
if(!$user || !$voted)
|
||||||
|
$option->voted = NULL;
|
||||||
|
else
|
||||||
|
$option->voted = in_array([$id, $title], $voted);
|
||||||
|
|
||||||
|
$unsOptions[$id] = $option;
|
||||||
|
}
|
||||||
|
|
||||||
|
$optionsC = sizeof($unsOptions);
|
||||||
|
$sOptions = $unsOptions;
|
||||||
|
usort($sOptions, function($a, $b) { return $a->votes <=> $b->votes; });
|
||||||
|
for($i = 0; $i < $optionsC; $i++)
|
||||||
|
$unsOptions[$id]->rate = $optionsC - $i - 1;
|
||||||
|
|
||||||
|
$result->options = array_values($unsOptions);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAnonymous(): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->getRecord()->is_anonymous;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMultipleChoice(): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->getRecord()->allows_multiple;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRevotable(): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->getRecord()->can_revote;
|
||||||
|
}
|
||||||
|
|
||||||
|
function endsAt(): ?DateTime
|
||||||
|
{
|
||||||
|
if(!$this->getRecord()->until)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
return new DateTime($this->getRecord()->until);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasEnded(): bool
|
||||||
|
{
|
||||||
|
if($this->getRecord()->ended)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if(!is_null($this->getRecord()->until))
|
||||||
|
return time() >= $this->getRecord()->until;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasVoted(User $user): bool
|
||||||
|
{
|
||||||
|
return !is_null($this->getUserVote($user));
|
||||||
|
}
|
||||||
|
|
||||||
|
function canVote(User $user): bool
|
||||||
|
{
|
||||||
|
return !$this->hasEnded() && !$this->hasVoted($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vote(User $user, array $optionIds): void
|
||||||
|
{
|
||||||
|
if($this->hasEnded())
|
||||||
|
throw new PollLockedException;
|
||||||
|
|
||||||
|
if($this->hasVoted($user))
|
||||||
|
throw new AlreadyVotedException;
|
||||||
|
|
||||||
|
$optionIds = array_map(function($x) { return (int) $x; }, array_unique($optionIds));
|
||||||
|
$validOpts = array_keys($this->getOptions());
|
||||||
|
if(empty($optionIds) || (sizeof($optionIds) > 1 && !$this->isMultipleChoice()))
|
||||||
|
throw new UnexpectedValueException;
|
||||||
|
|
||||||
|
if(sizeof(array_diff($optionIds, $validOpts)) > 0)
|
||||||
|
throw new InvalidOptionException;
|
||||||
|
|
||||||
|
foreach($optionIds as $opt) {
|
||||||
|
DatabaseConnection::i()->getContext()->table("poll_votes")->insert([
|
||||||
|
"user" => $user->getId(),
|
||||||
|
"poll" => $this->getId(),
|
||||||
|
"option" => $opt,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function revokeVote(User $user): void
|
||||||
|
{
|
||||||
|
if(!$this->isRevotable())
|
||||||
|
throw new PollLockedException;
|
||||||
|
|
||||||
|
$this->getRecord()->related("poll_votes.poll")
|
||||||
|
->where("user", $user->getId())->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOwner(User $owner): void
|
||||||
|
{
|
||||||
|
$this->stateChanges("owner", $owner->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEndDate(int $timestamp): void
|
||||||
|
{
|
||||||
|
if(!is_null($this->getRecord()))
|
||||||
|
throw new PollLockedException;
|
||||||
|
|
||||||
|
$this->stateChanges("until", $timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setEnded(): void
|
||||||
|
{
|
||||||
|
$this->stateChanges("ended", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOptions(array $options): void
|
||||||
|
{
|
||||||
|
if(!is_null($this->getRecord()))
|
||||||
|
throw new PollLockedException;
|
||||||
|
|
||||||
|
if(sizeof($options) > ovkGetQuirk("polls.max-opts"))
|
||||||
|
throw new TooMuchOptionsException;
|
||||||
|
|
||||||
|
$this->choicesToPersist = $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRevotability(bool $canReVote): void
|
||||||
|
{
|
||||||
|
if(!is_null($this->getRecord()))
|
||||||
|
throw new PollLockedException;
|
||||||
|
|
||||||
|
$this->stateChanges("can_revote", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAnonymity(bool $anonymous): void
|
||||||
|
{
|
||||||
|
$this->stateChanges("is_anonymous", $anonymous);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMultipleChoice(bool $mc): void
|
||||||
|
{
|
||||||
|
$this->stateChanges("allows_multiple", $mc);
|
||||||
|
}
|
||||||
|
|
||||||
|
function importXML(User $owner, string $xml): void
|
||||||
|
{
|
||||||
|
$xml = simplexml_load_string($xml);
|
||||||
|
$this->setOwner($owner);
|
||||||
|
$this->setTitle($xml["title"] ?? "Untitled");
|
||||||
|
$this->setMultipleChoice(($xml["multiple"] ?? "no") == "yes");
|
||||||
|
$this->setAnonymity(($xml["anonymous"] ?? "no") == "yes");
|
||||||
|
$this->setRevotability(($xml["locked"] ?? "no") == "yes");
|
||||||
|
if(ctype_digit($xml["duration"] ?? ""))
|
||||||
|
$this->setEndDate(time() + ((86400 * (int) $xml["duration"])));
|
||||||
|
|
||||||
|
$options = [];
|
||||||
|
foreach($xml->options->option as $opt)
|
||||||
|
$options[] = (string) $opt;
|
||||||
|
|
||||||
|
if(empty($options))
|
||||||
|
throw new UnexpectedValueException;
|
||||||
|
|
||||||
|
$this->setOptions($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
static function import(User $owner, string $xml): Poll
|
||||||
|
{
|
||||||
|
$poll = new Poll;
|
||||||
|
$poll->importXML($owner, $xml);
|
||||||
|
$poll->save();
|
||||||
|
|
||||||
|
return $poll;
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(): void
|
||||||
|
{
|
||||||
|
if(empty($this->choicesToPersist))
|
||||||
|
throw new InvalidStateException;
|
||||||
|
|
||||||
|
parent::save();
|
||||||
|
foreach($this->choicesToPersist as $option) {
|
||||||
|
DatabaseConnection::i()->getContext()->table("poll_options")->insert([
|
||||||
|
"poll" => $this->getId(),
|
||||||
|
"name" => $option,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
Web/Models/Exceptions/AlreadyVotedException.php
Normal file
7
Web/Models/Exceptions/AlreadyVotedException.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace openvk\Web\Models\Exceptions;
|
||||||
|
|
||||||
|
final class AlreadyVotedException extends \RuntimeException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
7
Web/Models/Exceptions/InvalidOptionException.php
Normal file
7
Web/Models/Exceptions/InvalidOptionException.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace openvk\Web\Models\Exceptions;
|
||||||
|
|
||||||
|
final class InvalidOptionException extends \UnexpectedValueException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
8
Web/Models/Exceptions/PollLockedException.php
Normal file
8
Web/Models/Exceptions/PollLockedException.php
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace openvk\Web\Models\Exceptions;
|
||||||
|
use Nette\InvalidStateException;
|
||||||
|
|
||||||
|
final class PollLockedException extends InvalidStateException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
7
Web/Models/Exceptions/TooMuchOptionsException.php
Normal file
7
Web/Models/Exceptions/TooMuchOptionsException.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace openvk\Web\Models\Exceptions;
|
||||||
|
|
||||||
|
final class TooMuchOptionsException extends \UnexpectedValueException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
23
Web/Models/Repositories/Polls.php
Normal file
23
Web/Models/Repositories/Polls.php
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace openvk\Web\Models\Repositories;
|
||||||
|
use Chandler\Database\DatabaseConnection;
|
||||||
|
use openvk\Web\Models\Entities\Poll;
|
||||||
|
|
||||||
|
class Polls
|
||||||
|
{
|
||||||
|
private $polls;
|
||||||
|
|
||||||
|
function __construct()
|
||||||
|
{
|
||||||
|
$this->polls = DatabaseConnection::i()->getContext()->table("polls");
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(int $id): ?Poll
|
||||||
|
{
|
||||||
|
$poll = $this->polls->get($id);
|
||||||
|
if(!$poll)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
return new Poll($poll);
|
||||||
|
}
|
||||||
|
}
|
70
Web/Presenters/PollPresenter.php
Normal file
70
Web/Presenters/PollPresenter.php
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
namespace openvk\Web\Presenters;
|
||||||
|
use openvk\Web\Models\Entities\Poll;
|
||||||
|
use openvk\Web\Models\Repositories\Polls;
|
||||||
|
|
||||||
|
final class PollPresenter extends OpenVKPresenter
|
||||||
|
{
|
||||||
|
private $polls;
|
||||||
|
|
||||||
|
function __construct(Polls $polls)
|
||||||
|
{
|
||||||
|
$this->polls = $polls;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderView(int $id): void
|
||||||
|
{
|
||||||
|
$poll = $this->polls->get($id);
|
||||||
|
if(!$poll)
|
||||||
|
$this->notFound();
|
||||||
|
|
||||||
|
$this->template->id = $poll->getId();
|
||||||
|
$this->template->title = $poll->getTitle();
|
||||||
|
$this->template->isAnon = $poll->isAnonymous();
|
||||||
|
$this->template->multiple = $poll->isMultipleChoice();
|
||||||
|
$this->template->unlocked = $poll->isRevotable();
|
||||||
|
$this->template->until = $poll->endsAt();
|
||||||
|
$this->template->votes = $poll->getVoterCount();
|
||||||
|
$this->template->meta = $poll->getMetaDescription();
|
||||||
|
if(is_null($this->user) || $poll->canVote($this->user->identity)) {
|
||||||
|
$this->template->options = $poll->getOptions();
|
||||||
|
|
||||||
|
$this->template->_template = "Poll/Poll.xml";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->template->voted = $poll->hasVoted($this->user->identity);
|
||||||
|
$this->template->ended = $poll->hasEnded();
|
||||||
|
$this->template->results = $poll->getResults($this->user->identity);
|
||||||
|
|
||||||
|
$this->template->_template = "Poll/PollResults.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderVoters(int $pollId): void
|
||||||
|
{
|
||||||
|
$poll = $this->polls->get($pollId);
|
||||||
|
if(!$poll)
|
||||||
|
$this->notFound();
|
||||||
|
|
||||||
|
if($poll->isAnonymous())
|
||||||
|
$this->flashFail("err", tr("forbidden"), tr("poll_err_anonymous"));
|
||||||
|
|
||||||
|
$options = $poll->getOptions();
|
||||||
|
$option = (int) base_convert($this->queryParam("option"), 32, 10);
|
||||||
|
if(!in_array($option, array_keys($options)))
|
||||||
|
$this->notFound();
|
||||||
|
|
||||||
|
$page = (int) ($this->queryParam("p") ?? 1);
|
||||||
|
$voters = $poll->getVoters($option, $page);
|
||||||
|
|
||||||
|
$this->template->pollId = $pollId;
|
||||||
|
$this->template->options = $options;
|
||||||
|
$this->template->option = [$option, $options[$option]];
|
||||||
|
$this->template->tabs = $options;
|
||||||
|
$this->template->iterator = $voters;
|
||||||
|
$this->template->count = $poll->getVoterCount($option);
|
||||||
|
$this->template->page = $page;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
namespace openvk\Web\Presenters;
|
namespace openvk\Web\Presenters;
|
||||||
use openvk\Web\Models\Entities\{Post, Photo, Video, Club, User};
|
use openvk\Web\Models\Exceptions\TooMuchOptionsException;
|
||||||
|
use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User};
|
||||||
use openvk\Web\Models\Entities\Notifications\{RepostNotification, WallPostNotification};
|
use openvk\Web\Models\Entities\Notifications\{RepostNotification, WallPostNotification};
|
||||||
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums};
|
use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums};
|
||||||
use Chandler\Database\DatabaseConnection;
|
use Chandler\Database\DatabaseConnection;
|
||||||
|
@ -259,16 +260,26 @@ final class WallPresenter extends OpenVKPresenter
|
||||||
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon);
|
$photo = Photo::fastMake($this->user->id, $this->postParam("text"), $_FILES["_pic_attachment"], $album, $anon);
|
||||||
}
|
}
|
||||||
|
|
||||||
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK) {
|
if($_FILES["_vid_attachment"]["error"] === UPLOAD_ERR_OK)
|
||||||
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"], $anon);
|
$video = Video::fastMake($this->user->id, $this->postParam("text"), $_FILES["_vid_attachment"], $anon);
|
||||||
}
|
|
||||||
} catch(\DomainException $ex) {
|
} catch(\DomainException $ex) {
|
||||||
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted"));
|
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted"));
|
||||||
} catch(ISE $ex) {
|
} catch(ISE $ex) {
|
||||||
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large"));
|
$this->flashFail("err", tr("failed_to_publish_post"), tr("media_file_corrupted_or_too_large"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(empty($this->postParam("text")) && !$photo && !$video)
|
try {
|
||||||
|
$poll = NULL;
|
||||||
|
$xml = $this->postParam("poll");
|
||||||
|
if (!is_null($xml) && $xml != "none")
|
||||||
|
$poll = Poll::import($this->user->identity, $xml);
|
||||||
|
} catch(TooMuchOptionsException $e) {
|
||||||
|
$this->flashFail("err", tr("failed_to_publish_post"), tr("poll_err_to_much_options"));
|
||||||
|
} catch(\UnexpectedValueException $e) {
|
||||||
|
$this->flashFail("err", tr("failed_to_publish_post"), "Poll format invalid");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(empty($this->postParam("text")) && !$photo && !$video && !$poll)
|
||||||
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
|
$this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -291,6 +302,9 @@ final class WallPresenter extends OpenVKPresenter
|
||||||
if(!is_null($video))
|
if(!is_null($video))
|
||||||
$post->attach($video);
|
$post->attach($video);
|
||||||
|
|
||||||
|
if(!is_null($poll))
|
||||||
|
$post->attach($poll);
|
||||||
|
|
||||||
if($wall > 0 && $wall !== $this->user->identity->getId())
|
if($wall > 0 && $wall !== $this->user->identity->getId())
|
||||||
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();
|
(new WallPostNotification($wallOwner, $post, $this->user->identity))->emit();
|
||||||
|
|
||||||
|
|
|
@ -29,58 +29,7 @@
|
||||||
{script "js/timezone.js"}
|
{script "js/timezone.js"}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{ifset $thisUser}
|
{include "_includeCSS.xml"}
|
||||||
{if $thisUser->getNsfwTolerance() < 2}
|
|
||||||
{css "css/nsfw-posts.css"}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{if $theme !== NULL}
|
|
||||||
{if $theme->inheritDefault()}
|
|
||||||
{css "css/style.css"}
|
|
||||||
{css "css/dialog.css"}
|
|
||||||
{css "css/notifications.css"}
|
|
||||||
|
|
||||||
{if $isXmas}
|
|
||||||
{css "css/xmas.css"}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/stylesheet/styles.css" />
|
|
||||||
|
|
||||||
{if $isXmas}
|
|
||||||
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/resource/xmas.css" />
|
|
||||||
{/if}
|
|
||||||
{else}
|
|
||||||
{css "css/style.css"}
|
|
||||||
{css "css/dialog.css"}
|
|
||||||
{css "css/notifications.css"}
|
|
||||||
|
|
||||||
{if $isXmas}
|
|
||||||
{css "css/xmas.css"}
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{if $thisUser->getStyleAvatar() == 1}
|
|
||||||
{css "css/avatar.1.css"}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{if $thisUser->getStyleAvatar() == 2}
|
|
||||||
{css "css/avatar.2.css"}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{if $thisUser->hasMicroblogEnabled() == 1}
|
|
||||||
{css "css/microblog.css"}
|
|
||||||
{/if}
|
|
||||||
{else}
|
|
||||||
{css "css/style.css"}
|
|
||||||
{css "css/dialog.css"}
|
|
||||||
{css "css/nsfw-posts.css"}
|
|
||||||
{css "css/notifications.css"}
|
|
||||||
|
|
||||||
{if $isXmas}
|
|
||||||
{css "css/xmas.css"}
|
|
||||||
{/if}
|
|
||||||
{/ifset}
|
|
||||||
|
|
||||||
{ifset headIncludes}
|
{ifset headIncludes}
|
||||||
{include headIncludes}
|
{include headIncludes}
|
||||||
|
@ -334,6 +283,7 @@
|
||||||
{script "js/al_wall.js"}
|
{script "js/al_wall.js"}
|
||||||
{script "js/al_api.js"}
|
{script "js/al_api.js"}
|
||||||
{script "js/al_mentions.js"}
|
{script "js/al_mentions.js"}
|
||||||
|
{script "js/al_polls.js"}
|
||||||
|
|
||||||
{ifset $thisUser}
|
{ifset $thisUser}
|
||||||
{script "js/al_notifs.js"}
|
{script "js/al_notifs.js"}
|
||||||
|
|
44
Web/Presenters/templates/Poll/Poll.xml
Normal file
44
Web/Presenters/templates/Poll/Poll.xml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{if !isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'}
|
||||||
|
<link rel="shortcut icon" href="/assets/packages/static/openvk/img/icon.ico" />
|
||||||
|
<meta n:ifset="$csrfToken" name="csrf" value="{$csrfToken}" />
|
||||||
|
<script src="/language/{getLanguage()}.js" crossorigin="anonymous"></script>
|
||||||
|
{script "js/node_modules/jquery/dist/jquery.min.js"}
|
||||||
|
{script "js/node_modules/umbrellajs/umbrella.min.js"}
|
||||||
|
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
|
||||||
|
{script "js/messagebox.js"}
|
||||||
|
{script "js/l10n.js"}
|
||||||
|
{script "js/al_api.js"}
|
||||||
|
{script "js/al_polls.js"}
|
||||||
|
{include "../_includeCSS.xml"}
|
||||||
|
|
||||||
|
<style>body { margin: 8px; }</style>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="poll">
|
||||||
|
<h4>{$title}</h4>
|
||||||
|
<form onsubmit="pollFormSubmit(event, this)" data-multi="{$multiple ? '1' : '0'}" data-pid="{$id}">
|
||||||
|
<div class="poll-options">
|
||||||
|
<div n:foreach="$options as $oid => $option" class="poll-option">
|
||||||
|
<label>
|
||||||
|
{if $multiple}
|
||||||
|
<input n:attr="disabled: is_null($thisUser)" type="checkbox" name="option{$oid}" onclick="pollCheckBoxPressed(this)" />
|
||||||
|
{else}
|
||||||
|
<input n:attr="disabled: is_null($thisUser)" type="radio" value="{$oid}" name="vote" onclick="pollRadioPressed(this)" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{$option}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{if $multiple}
|
||||||
|
<br/>
|
||||||
|
<input type="submit" class="button" value="{_cast_vote}" disabled="disabled" />
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="poll-meta">
|
||||||
|
{tr("poll_voter_count", $votes)|noescape}<br/>
|
||||||
|
<span class="nobold">{$meta}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
44
Web/Presenters/templates/Poll/PollResults.xml
Normal file
44
Web/Presenters/templates/Poll/PollResults.xml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{if !isset($parentModule) || substr($parentModule, 0, 21) === 'libchandler:absolute.'}
|
||||||
|
<link rel="shortcut icon" href="/assets/packages/static/openvk/img/icon.ico" />
|
||||||
|
<meta n:ifset="$csrfToken" name="csrf" value="{$csrfToken}" />
|
||||||
|
<script src="/language/{getLanguage()}.js" crossorigin="anonymous"></script>
|
||||||
|
{script "js/node_modules/jquery/dist/jquery.min.js"}
|
||||||
|
{script "js/node_modules/umbrellajs/umbrella.min.js"}
|
||||||
|
{script "js/node_modules/msgpack-lite/dist/msgpack.min.js"}
|
||||||
|
{script "js/messagebox.js"}
|
||||||
|
{script "js/l10n.js"}
|
||||||
|
{script "js/al_api.js"}
|
||||||
|
{script "js/al_polls.js"}
|
||||||
|
{include "../_includeCSS.xml"}
|
||||||
|
|
||||||
|
<style>body { margin: 8px; } .poll { border: 1px solid #e3e3e3; }</style>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="poll" data-id="{$id}">
|
||||||
|
<a n:if="$unlocked && $voted" href="javascript:pollRetractVote({$id})" class="poll-retract-vote">{_retract_vote}</a>
|
||||||
|
<h4>{$title}</h4>
|
||||||
|
<div class="poll-results">
|
||||||
|
<div n:foreach="$results->options as $option" class="poll-result">
|
||||||
|
{if $isAnon}
|
||||||
|
<a href="javascript:false">{$option->name}</a>
|
||||||
|
{else}
|
||||||
|
<a href="/poll{$id}/voters?option={base_convert($option->id, 10, 32)}">{$option->name}</a>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="poll-result-barspace">
|
||||||
|
<div class="poll-result-bar">
|
||||||
|
<span class="poll-result-count">{$option->votes}</span>
|
||||||
|
<div class="poll-result-bar-sub" style="width: {$option->pct}%"> </div>
|
||||||
|
</div>
|
||||||
|
<div class="poll-result-pct">
|
||||||
|
<strong>{$option->pct}%</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="poll-meta">
|
||||||
|
{tr("poll_voter_count", $votes)|noescape}<br/>
|
||||||
|
<span class="nobold">{$meta}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
40
Web/Presenters/templates/Poll/Voters.xml
Normal file
40
Web/Presenters/templates/Poll/Voters.xml
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{extends "../@listView.xml"}
|
||||||
|
|
||||||
|
{block title}
|
||||||
|
{_poll_voters_list}
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block header}
|
||||||
|
{_poll_voters_list} »
|
||||||
|
{$option[1]}
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block tabs}
|
||||||
|
<div n:foreach="$options as $optionId => $optionName" class="tab" id="{$optionId == $option[0] ? 'activetabs' : 'ki'}">
|
||||||
|
<a id="{$optionId == $option[0] ? 'act_tab_a' : ''}" href="/poll{$pollId}/voters?option={$optionId}">{$optionName}</a>
|
||||||
|
</div>
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{* BEGIN ELEMENTS DESCRIPTION *}
|
||||||
|
|
||||||
|
{block link|strip|stripHtml}
|
||||||
|
{$x->getURL()}
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block preview}
|
||||||
|
<img src="{$x->getAvatarUrl('miniscule')}" width="75" alt="Фотография пользователя" />
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block name}
|
||||||
|
{$x->getCanonicalName()}
|
||||||
|
<img n:if="$x->isVerified()"
|
||||||
|
class="name-checkmark"
|
||||||
|
src="/assets/packages/static/openvk/img/checkmark.png"
|
||||||
|
/>
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block description}
|
||||||
|
{/block}
|
||||||
|
|
||||||
|
{block actions}
|
||||||
|
{/block}
|
52
Web/Presenters/templates/_includeCSS.xml
Normal file
52
Web/Presenters/templates/_includeCSS.xml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
{ifset $thisUser}
|
||||||
|
{if $thisUser->getNsfwTolerance() < 2}
|
||||||
|
{css "css/nsfw-posts.css"}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{if $theme !== NULL}
|
||||||
|
{if $theme->inheritDefault()}
|
||||||
|
{css "css/style.css"}
|
||||||
|
{css "css/dialog.css"}
|
||||||
|
{css "css/notifications.css"}
|
||||||
|
|
||||||
|
{if $isXmas}
|
||||||
|
{css "css/xmas.css"}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/stylesheet/styles.css" />
|
||||||
|
|
||||||
|
{if $isXmas}
|
||||||
|
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/resource/xmas.css" />
|
||||||
|
{/if}
|
||||||
|
{else}
|
||||||
|
{css "css/style.css"}
|
||||||
|
{css "css/dialog.css"}
|
||||||
|
{css "css/notifications.css"}
|
||||||
|
|
||||||
|
{if $isXmas}
|
||||||
|
{css "css/xmas.css"}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{if $thisUser->getStyleAvatar() == 1}
|
||||||
|
{css "css/avatar.1.css"}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{if $thisUser->getStyleAvatar() == 2}
|
||||||
|
{css "css/avatar.2.css"}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{if $thisUser->hasMicroblogEnabled() == 1}
|
||||||
|
{css "css/microblog.css"}
|
||||||
|
{/if}
|
||||||
|
{else}
|
||||||
|
{css "css/style.css"}
|
||||||
|
{css "css/dialog.css"}
|
||||||
|
{css "css/nsfw-posts.css"}
|
||||||
|
{css "css/notifications.css"}
|
||||||
|
|
||||||
|
{if $isXmas}
|
||||||
|
{css "css/xmas.css"}
|
||||||
|
{/if}
|
||||||
|
{/ifset}
|
|
@ -11,6 +11,8 @@
|
||||||
{/if}
|
{/if}
|
||||||
{elseif $attachment instanceof \openvk\Web\Models\Entities\Video}
|
{elseif $attachment instanceof \openvk\Web\Models\Entities\Video}
|
||||||
<video class="media" src="{$attachment->getURL()}" controls="controls"></video>
|
<video class="media" src="{$attachment->getURL()}" controls="controls"></video>
|
||||||
|
{elseif $attachment instanceof \openvk\Web\Models\Entities\Poll}
|
||||||
|
{presenter "openvk!Poll->view", $attachment->getId()}
|
||||||
{elseif $attachment instanceof \openvk\Web\Models\Entities\Post}
|
{elseif $attachment instanceof \openvk\Web\Models\Entities\Post}
|
||||||
{php $GLOBALS["_nesAttGloCou"] = (isset($GLOBALS["_nesAttGloCou"]) ? $GLOBALS["_nesAttGloCou"] : 0) + 1}
|
{php $GLOBALS["_nesAttGloCou"] = (isset($GLOBALS["_nesAttGloCou"]) ? $GLOBALS["_nesAttGloCou"] : 0) + 1}
|
||||||
{if $GLOBALS["_nesAttGloCou"] > 2}
|
{if $GLOBALS["_nesAttGloCou"] > 2}
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
<div class="post-upload">
|
<div class="post-upload">
|
||||||
{_attachment}: <span>(unknown)</span>
|
{_attachment}: <span>(unknown)</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="post-has-poll">
|
||||||
|
{_poll}
|
||||||
|
</div>
|
||||||
<div n:if="$postOpts ?? true" class="post-opts">
|
<div n:if="$postOpts ?? true" class="post-opts">
|
||||||
{var $anonEnabled = OPENVK_ROOT_CONF['openvk']['preferences']['wall']['anonymousPosting']['enable']}
|
{var $anonEnabled = OPENVK_ROOT_CONF['openvk']['preferences']['wall']['anonymousPosting']['enable']}
|
||||||
|
|
||||||
|
@ -50,6 +53,7 @@
|
||||||
</div>
|
</div>
|
||||||
<input type="file" class="postFileSel" id="postFilePic" name="_pic_attachment" accept="image/*" style="display:none;" />
|
<input type="file" class="postFileSel" id="postFilePic" name="_pic_attachment" accept="image/*" style="display:none;" />
|
||||||
<input type="file" class="postFileSel" id="postFileVid" name="_vid_attachment" accept="video/*" style="display:none;" />
|
<input type="file" class="postFileSel" id="postFileVid" name="_vid_attachment" accept="video/*" style="display:none;" />
|
||||||
|
<input type="hidden" name="poll" value="none" />
|
||||||
<input type="hidden" name="type" value="1" />
|
<input type="hidden" name="type" value="1" />
|
||||||
<input type="hidden" name="hash" value="{$csrfToken}" />
|
<input type="hidden" name="hash" value="{$csrfToken}" />
|
||||||
<br/>
|
<br/>
|
||||||
|
@ -75,6 +79,10 @@
|
||||||
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/draw-brush.png" />
|
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/draw-brush.png" />
|
||||||
{_graffiti}
|
{_graffiti}
|
||||||
</a>
|
</a>
|
||||||
|
<a n:if="$polls ?? false" href="javascript:initPoll({$textAreaId})">
|
||||||
|
<img src="/assets/packages/static/openvk/img/oxygen-icons/16x16/actions/office-chart-bar-stacked.png" />
|
||||||
|
{_poll}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div n:if="$canPost" class="content_subtitle">
|
<div n:if="$canPost" class="content_subtitle">
|
||||||
{include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true}
|
{include "../components/textArea.xml", route => "/wall$owner/makePost", graffiti => true, polls => true}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
|
@ -23,9 +23,11 @@ services:
|
||||||
- openvk\Web\Presenters\AppsPresenter
|
- openvk\Web\Presenters\AppsPresenter
|
||||||
- openvk\Web\Presenters\ThemepacksPresenter
|
- openvk\Web\Presenters\ThemepacksPresenter
|
||||||
- openvk\Web\Presenters\VKAPIPresenter
|
- openvk\Web\Presenters\VKAPIPresenter
|
||||||
|
- openvk\Web\Presenters\PollPresenter
|
||||||
- openvk\Web\Presenters\BannedLinkPresenter
|
- openvk\Web\Presenters\BannedLinkPresenter
|
||||||
- openvk\Web\Models\Repositories\Users
|
- openvk\Web\Models\Repositories\Users
|
||||||
- openvk\Web\Models\Repositories\Posts
|
- openvk\Web\Models\Repositories\Posts
|
||||||
|
- openvk\Web\Models\Repositories\Polls
|
||||||
- openvk\Web\Models\Repositories\Photos
|
- openvk\Web\Models\Repositories\Photos
|
||||||
- openvk\Web\Models\Repositories\Albums
|
- openvk\Web\Models\Repositories\Albums
|
||||||
- openvk\Web\Models\Repositories\Clubs
|
- openvk\Web\Models\Repositories\Clubs
|
||||||
|
|
|
@ -273,6 +273,10 @@ routes:
|
||||||
handler: "Apps->edit"
|
handler: "Apps->edit"
|
||||||
- url: "/apps/uninstall"
|
- url: "/apps/uninstall"
|
||||||
handler: "Apps->unInstall"
|
handler: "Apps->unInstall"
|
||||||
|
- url: "/poll{num}"
|
||||||
|
handler: "Poll->view"
|
||||||
|
- url: "/poll{num}/voters"
|
||||||
|
handler: "Poll->voters"
|
||||||
- url: "/admin"
|
- url: "/admin"
|
||||||
handler: "Admin->index"
|
handler: "Admin->index"
|
||||||
- url: "/admin/users"
|
- url: "/admin/users"
|
||||||
|
|
|
@ -448,6 +448,15 @@ table {
|
||||||
color: #e8e8e8;
|
color: #e8e8e8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button[disabled] {
|
||||||
|
filter: opacity(0.5);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[disabled]:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.button-loading {
|
.button-loading {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-image: url('/assets/packages/static/openvk/img/loading_mini.gif');
|
background-image: url('/assets/packages/static/openvk/img/loading_mini.gif');
|
||||||
|
@ -801,6 +810,8 @@ table.User {
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
margin-left: -10px;
|
margin-left: -10px;
|
||||||
width: 607px;
|
width: 607px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs.stupid-fix {
|
.tabs.stupid-fix {
|
||||||
|
@ -1338,14 +1349,14 @@ body.scrolled .toTop:hover {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-upload {
|
.post-upload, .post-has-poll {
|
||||||
margin-top: 11px;
|
margin-top: 11px;
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
color: #3c3c3c;
|
color: #3c3c3c;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-upload::before {
|
.post-upload::before, .post-has-poll::before {
|
||||||
content: " ";
|
content: " ";
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
|
@ -2051,6 +2062,81 @@ table td[width="120"] {
|
||||||
max-height: 250px;
|
max-height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.poll {
|
||||||
|
padding: 8px;
|
||||||
|
transition: .1s filter ease-in;
|
||||||
|
border: 1px solid #e3e3e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll.loading {
|
||||||
|
filter: opacity(0.5);
|
||||||
|
cursor: progress;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll.loading * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-embed {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll h4 {
|
||||||
|
padding-bottom: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-meta .nobold {
|
||||||
|
font-style: oblique;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-result-barspace {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-result {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-result a {
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-result-bar {
|
||||||
|
position: relative;
|
||||||
|
margin: 5px 0;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
height: 13pt;
|
||||||
|
flex: 14;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.poll-result-count {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-right: -50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #7d96af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-result-bar-sub {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #d9e1ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.poll-result-pct {
|
||||||
|
flex: 2;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.poll-retract-vote {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
.tippy-box[data-theme~="vk"] {
|
.tippy-box[data-theme~="vk"] {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
|
151
Web/static/js/al_polls.js
Normal file
151
Web/static/js/al_polls.js
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
async function pollRetractVote(id) {
|
||||||
|
let poll = $(`.poll[data-id=${id}]`);
|
||||||
|
|
||||||
|
poll.addClass("loading");
|
||||||
|
try {
|
||||||
|
let html = (await API.Polls.unvote(poll.data("id"))).html;
|
||||||
|
poll.prop("outerHTML", html);
|
||||||
|
} catch(e) {
|
||||||
|
MessageBox(tr("error"), "Sorry: " + e.message, ["OK"], [Function.noop]);
|
||||||
|
} finally {
|
||||||
|
poll.removeClass("loading");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollFormSubmit(e, form) {
|
||||||
|
e.preventDefault();
|
||||||
|
form = $(form);
|
||||||
|
|
||||||
|
let options;
|
||||||
|
let isMultiple = form.data("multi");
|
||||||
|
let pollId = form.data("pid");
|
||||||
|
|
||||||
|
let formData = form.serializeArray();
|
||||||
|
if(!isMultiple) {
|
||||||
|
options = [Number(formData[0].value)];
|
||||||
|
} else {
|
||||||
|
options = [];
|
||||||
|
formData.forEach(function(record) {
|
||||||
|
if(!record.name.startsWith("option") || record.value !== "on")
|
||||||
|
return;
|
||||||
|
|
||||||
|
options.push(Number(record.name.substr(6)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let poll = form.parent();
|
||||||
|
poll.addClass("loading");
|
||||||
|
try {
|
||||||
|
let html = (await API.Polls.vote(pollId, options.join(","))).html;
|
||||||
|
poll.prop("outerHTML", html);
|
||||||
|
} catch(e) {
|
||||||
|
MessageBox(tr("error"), "Sorry: " + e.message, ["OK"], [Function.noop]);
|
||||||
|
} finally {
|
||||||
|
poll.removeClass("loading");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollCheckBoxPressed(cb) {
|
||||||
|
cb = $(cb);
|
||||||
|
let form = cb.parent().parent().parent().parent();
|
||||||
|
let checked = $("input:checked", form);
|
||||||
|
if(checked.length >= 1)
|
||||||
|
$("input[type=submit]", form).removeAttr("disabled");
|
||||||
|
else
|
||||||
|
$("input[type=submit]", form).attr("disabled", "disabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollRadioPressed(radio) {
|
||||||
|
let form = $(radio).parent().parent().parent().parent();
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initPoll(id) {
|
||||||
|
let form = $(`#wall-post-input${id}`).parent();
|
||||||
|
|
||||||
|
let mBody = `
|
||||||
|
<div id="poll_editor${id}">
|
||||||
|
<input type="text" name="title" placeholder="${tr("poll_title")}" />
|
||||||
|
<div class="poll-options" style="margin-top: 10px;"></div>
|
||||||
|
<input type="text" name="newOption" placeholder="${tr("poll_add_option")}" style="margin: 5px 0;" />
|
||||||
|
<hr/>
|
||||||
|
<label><input type="checkbox" name="anon" /> ${tr("poll_anonymous")}</label><br/>
|
||||||
|
<label><input type="checkbox" name="multi" /> ${tr("poll_multiple")}</label><br/>
|
||||||
|
<label><input type="checkbox" name="locked" /> ${tr("poll_locked")}</label><br/>
|
||||||
|
<label style="display: none;">
|
||||||
|
<input type="checkbox" name="expires" />
|
||||||
|
${tr("poll_edit_expires")}
|
||||||
|
<select name="expires_in" style="width: unset;">
|
||||||
|
${[...Array(32).keys()].reduce((p, c) => (!p ? '' : p) + ("<option value='" + c + "'>" + c + " " + tr("poll_edit_expires_days") + "</option>\n"))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div class="nobold" style="margin: 10px 5px 0">${tr("poll_editor_tips")}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
MessageBox(tr("create_poll"), mBody, [tr("attach"), tr("cancel")], [
|
||||||
|
function() {
|
||||||
|
let dialog = $(this.$dialog().nodes[0]);
|
||||||
|
$("input", dialog).unbind();
|
||||||
|
|
||||||
|
let title = $("input[name=title]", dialog).val();
|
||||||
|
let anon = $("input[name=anon]", dialog).prop("checked") ? "yes" : "no";
|
||||||
|
let multi = $("input[name=multi]", dialog).prop("checked") ? "yes" : "no";
|
||||||
|
let lock = $("input[name=locked]", dialog).prop("checked") ? "yes" : "no";
|
||||||
|
let expires = "infinite";
|
||||||
|
if($("input[name=expires]", dialog).prop("checked"))
|
||||||
|
expires = $("select[name=expires_in]", dialog).val();
|
||||||
|
|
||||||
|
let options = "";
|
||||||
|
$(".poll-option", dialog).each(function() {
|
||||||
|
if($(this).val().length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
options += `<option>${$(this).val()}</option>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
let xml = `
|
||||||
|
<Poll title="${title}" anonymous="${anon}" multiple="${multi}" locked="${lock}" duration="${expires}">
|
||||||
|
<options>${options}</options>
|
||||||
|
</Poll>
|
||||||
|
`;
|
||||||
|
$("input[name=poll]", form).val(xml);
|
||||||
|
$(".post-has-poll", form).show();
|
||||||
|
console.log(xml);
|
||||||
|
},
|
||||||
|
function() {
|
||||||
|
$("input", $(this.$dialog().nodes[0])).unbind();
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
let editor = $(`#poll_editor${id}`);
|
||||||
|
$("input[name=newOption]", editor).bind("focus", function() {
|
||||||
|
let newOption = $('<input type="text" class="poll-option" style="margin: 5px 0;" />');
|
||||||
|
newOption.appendTo($(".poll-options", editor));
|
||||||
|
newOption.focus();
|
||||||
|
newOption.bind("keydown", function(e) {
|
||||||
|
if(e.key === "Enter" && $(this).next().length === 0) {
|
||||||
|
$("input[name=newOption]", editor).focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($(this).val().length > 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if(e.key !== "Backspace")
|
||||||
|
return;
|
||||||
|
|
||||||
|
if($(this).siblings().length === 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if($(this).prev().length === 0)
|
||||||
|
$(this).next().focus();
|
||||||
|
else
|
||||||
|
$(this).prev().focus();
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
$(this).unbind("keydown");
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -920,6 +920,38 @@
|
||||||
"messages_error_1" = "Сообщение не доставлено";
|
"messages_error_1" = "Сообщение не доставлено";
|
||||||
"messages_error_1_description" = "При отправке этого сообщения произошла ошибка общего характера...";
|
"messages_error_1_description" = "При отправке этого сообщения произошла ошибка общего характера...";
|
||||||
|
|
||||||
|
/* Polls */
|
||||||
|
"poll" = "Опрос";
|
||||||
|
"create_poll" = "Новый опрос";
|
||||||
|
"poll_title" = "Тема опроса";
|
||||||
|
"poll_add_option" = "Добавить вариант ответа";
|
||||||
|
"poll_anonymous" = "Анонимный опрос";
|
||||||
|
"poll_multiple" = "Множественный выбор";
|
||||||
|
"poll_locked" = "Запретить отменять свой голос";
|
||||||
|
"poll_edit_expires" = "Голосование истекает через: ";
|
||||||
|
"poll_edit_expires_days" = "дней";
|
||||||
|
"poll_editor_tips" = "Нажатие Backspace в пустом варианте приводит к его удалению. Tab/Enter в последнем добавляет новый.";
|
||||||
|
"poll_embed" = "Получить код";
|
||||||
|
|
||||||
|
"poll_voter_count_zero" = "Будьте <b>первым</b>, кто проголосует!";
|
||||||
|
"poll_voter_count_one" = "В опросе проголосовал <b>один</b> человек.";
|
||||||
|
"poll_voter_count_few" = "В опросе проголосовало <b>$1</b> человека.";
|
||||||
|
"poll_voter_count_many" = "В опросе проголосовало <b>$1</b> человек.";
|
||||||
|
"poll_voter_count_other" = "В опросе проголосовало <b>$1</b> человек.";
|
||||||
|
|
||||||
|
"poll_voters_list" = "Список проголосовавших";
|
||||||
|
|
||||||
|
"poll_anon" = "Анонимное голосование";
|
||||||
|
"poll_public" = "Публичное голосование";
|
||||||
|
"poll_multi" = "много вариантов";
|
||||||
|
"poll_lock" = "нельзя переголосовать";
|
||||||
|
"poll_until" = "до $1";
|
||||||
|
|
||||||
|
"poll_err_to_much_options" = "Слишком много вариантов в опросе.";
|
||||||
|
"poll_err_anonymous" = "Невозможно просмотреть список проголосовавших в анонимном голосовании.";
|
||||||
|
"cast_vote" = "Проголосовать";
|
||||||
|
"retract_vote" = "Отменить голос";
|
||||||
|
|
||||||
/* Discussions */
|
/* Discussions */
|
||||||
|
|
||||||
"discussions" = "Обсуждения";
|
"discussions" = "Обсуждения";
|
||||||
|
|
|
@ -44,3 +44,5 @@ comments.allow-graffiti: 0
|
||||||
# + Set this option to -1 if you want to disable the limit
|
# + Set this option to -1 if you want to disable the limit
|
||||||
# + Set this option to any non-negative number to be this limit
|
# + Set this option to any non-negative number to be this limit
|
||||||
wall.repost-liking-recursion-limit: 10
|
wall.repost-liking-recursion-limit: 10
|
||||||
|
|
||||||
|
polls.max-opts: 10
|
Loading…
Reference in a new issue