forked from OpenVK/openvk
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
25 changed files
with
1,041 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
]); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?php declare(strict_types=1); | ||
namespace openvk\Web\Models\Exceptions; | ||
|
||
final class AlreadyVotedException extends \RuntimeException | ||
{ | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
<?php declare(strict_types=1); | ||
namespace openvk\Web\Models\Exceptions; | ||
|
||
final class InvalidOptionException extends \UnexpectedValueException | ||
{ | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<?php declare(strict_types=1); | ||
namespace openvk\Web\Models\Exceptions; | ||
use Nette\InvalidStateException; | ||
|
||
final class PollLockedException extends InvalidStateException | ||
{ | ||
|
||
} |
Oops, something went wrong.