mirror of
https://github.com/openvk/openvk
synced 2025-02-02 21:15:42 +03:00
Alexander Minkin
6ec54a379d
* feat(lint): add php-cs-fixer for linting Removing previous CODE_STYLE as it was not enforced anyway and using PER-CS 2.0. This is not the reformatting commit. * style: format code according to PER-CS 2.0 with php-cs-fixer * ci(actions): add lint action Resolves #1132.
354 lines
9.8 KiB
PHP
354 lines
9.8 KiB
PHP
<?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, Posts};
|
|
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 = [];
|
|
|
|
public function getTitle(): string
|
|
{
|
|
return $this->getRecord()->title;
|
|
}
|
|
|
|
public 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);
|
|
}
|
|
|
|
public function getOwner(): User
|
|
{
|
|
return (new Users())->get($this->getRecord()->owner);
|
|
}
|
|
|
|
public function getOptions(): array
|
|
{
|
|
$options = $this->getRecord()->related("poll_options.poll");
|
|
$res = [];
|
|
foreach ($options as $opt) {
|
|
$res[$opt->id] = $opt->name;
|
|
}
|
|
|
|
return $res;
|
|
}
|
|
|
|
public 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;
|
|
}
|
|
|
|
public function getVoters(int $optionId, int $page = 1, ?int $perPage = null): array
|
|
{
|
|
$res = [];
|
|
$ctx = DatabaseConnection::i()->getContext();
|
|
$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;
|
|
}
|
|
|
|
public 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();
|
|
}
|
|
|
|
public 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 : min(100, floor(($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;
|
|
}
|
|
|
|
public function isAnonymous(): bool
|
|
{
|
|
return (bool) $this->getRecord()->is_anonymous;
|
|
}
|
|
|
|
public function isMultipleChoice(): bool
|
|
{
|
|
return (bool) $this->getRecord()->allows_multiple;
|
|
}
|
|
|
|
public function isRevotable(): bool
|
|
{
|
|
return (bool) $this->getRecord()->can_revote;
|
|
}
|
|
|
|
public function endsAt(): ?DateTime
|
|
{
|
|
if (!$this->getRecord()->until) {
|
|
return null;
|
|
}
|
|
|
|
return new DateTime($this->getRecord()->until);
|
|
}
|
|
|
|
public function hasEnded(): bool
|
|
{
|
|
if ($this->getRecord()->ended) {
|
|
return true;
|
|
}
|
|
|
|
if (!is_null($this->getRecord()->until)) {
|
|
return time() >= $this->getRecord()->until;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function hasVoted(User $user): bool
|
|
{
|
|
return !is_null($this->getUserVote($user));
|
|
}
|
|
|
|
public function canVote(User $user): bool
|
|
{
|
|
return !$this->hasEnded() && !$this->hasVoted($user) && !is_null($this->getAttachedPost()) && $this->getAttachedPost()->getSuggestionType() == 0;
|
|
}
|
|
|
|
public 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,
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function revokeVote(User $user): void
|
|
{
|
|
if (!$this->isRevotable()) {
|
|
throw new PollLockedException();
|
|
}
|
|
|
|
$this->getRecord()->related("poll_votes.poll")
|
|
->where("user", $user->getId())->delete();
|
|
}
|
|
|
|
public function setOwner(User $owner): void
|
|
{
|
|
$this->stateChanges("owner", $owner->getId());
|
|
}
|
|
|
|
public function setEndDate(int $timestamp): void
|
|
{
|
|
if (!is_null($this->getRecord())) {
|
|
throw new PollLockedException();
|
|
}
|
|
|
|
$this->stateChanges("until", $timestamp);
|
|
}
|
|
|
|
public function setEnded(): void
|
|
{
|
|
$this->stateChanges("ended", 1);
|
|
}
|
|
|
|
public 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;
|
|
}
|
|
|
|
public function setRevotability(bool $canReVote): void
|
|
{
|
|
if (!is_null($this->getRecord())) {
|
|
throw new PollLockedException();
|
|
}
|
|
|
|
$this->stateChanges("can_revote", $canReVote);
|
|
}
|
|
|
|
public function setAnonymity(bool $anonymous): void
|
|
{
|
|
$this->stateChanges("is_anonymous", $anonymous);
|
|
}
|
|
|
|
public function setMultipleChoice(bool $mc): void
|
|
{
|
|
$this->stateChanges("allows_multiple", $mc);
|
|
}
|
|
|
|
public 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") == "no");
|
|
if (ctype_digit((string) ($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);
|
|
}
|
|
|
|
public static function import(User $owner, string $xml): Poll
|
|
{
|
|
$poll = new Poll();
|
|
$poll->importXML($owner, $xml);
|
|
$poll->save();
|
|
|
|
return $poll;
|
|
}
|
|
|
|
public function canBeViewedBy(?User $user = null): bool
|
|
{
|
|
# waiting for #935 :(
|
|
/*if(!is_null($this->getAttachedPost())) {
|
|
return $this->getAttachedPost()->canBeViewedBy($user);
|
|
} else {*/
|
|
return true;
|
|
#}
|
|
|
|
}
|
|
|
|
public function save(?bool $log = false): void
|
|
{
|
|
if (empty($this->choicesToPersist)) {
|
|
throw new InvalidStateException();
|
|
}
|
|
|
|
parent::save($log);
|
|
foreach ($this->choicesToPersist as $option) {
|
|
DatabaseConnection::i()->getContext()->table("poll_options")->insert([
|
|
"poll" => $this->getId(),
|
|
"name" => $option,
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function getAttachedPost()
|
|
{
|
|
$post = DatabaseConnection::i()->getContext()->table("attachments")
|
|
->where(
|
|
["attachable_type" => static::class,
|
|
"attachable_id" => $this->getId()]
|
|
)->fetch();
|
|
|
|
if (!is_null($post->target_id)) {
|
|
return (new Posts())->get($post->target_id);
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}
|