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