Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b866c03
add iso date and timezone name
dartcafe Dec 21, 2025
f61e29c
change cloning and permission checks
dartcafe Dec 29, 2025
8f76a3d
add timezone name to poll configuration
dartcafe Jan 4, 2026
98a4941
apply timezone to poll and chose
dartcafe Jan 10, 2026
536963b
remove timezone from infoline
dartcafe Jan 11, 2026
4ba03ba
Add timezone info to PollInformation popover
dartcafe Jan 11, 2026
c392fe7
add dates with flexible timezones
dartcafe Jan 18, 2026
561f991
build(deps): Bump dessant/lock-threads from 5 to 6
dependabot[bot] Dec 19, 2025
1a9454a
build(deps): Bump vue from 3.5.25 to 3.5.26
dependabot[bot] Dec 19, 2025
0fb38cc
build(deps-dev): Bump baseline-browser-mapping from 2.9.10 to 2.9.11
dependabot[bot] Dec 22, 2025
318560b
fix(l10n): Update translations from Transifex
nextcloud-bot Dec 23, 2025
f177b97
build(deps): Bump qs from 6.14.0 to 6.14.1
dependabot[bot] Jan 1, 2026
f1336b8
build(deps-dev): Bump vite from 7.3.0 to 7.3.1
dependabot[bot] Jan 8, 2026
85c5e01
build(deps-dev): Bump baseline-browser-mapping from 2.9.11 to 2.9.12
dependabot[bot] Jan 8, 2026
6b0c2d0
fix(l10n): Update translations from Transifex
nextcloud-bot Jan 10, 2026
2379b1f
build(deps): Bump @nextcloud/vue from 9.3.1 to 9.3.2
dependabot[bot] Jan 9, 2026
fe296fc
build(deps-dev): Bump baseline-browser-mapping from 2.9.12 to 2.9.13
dependabot[bot] Jan 9, 2026
6943d2f
fix(l10n): Update translations from Transifex
nextcloud-bot Jan 11, 2026
fe20316
build(deps-dev): Bump baseline-browser-mapping from 2.9.13 to 2.9.14
dependabot[bot] Jan 12, 2026
c2f1429
build(deps): Bump @nextcloud/vue from 9.3.2 to 9.3.3
dependabot[bot] Jan 12, 2026
34418f2
build(deps-dev): Bump @types/lodash from 4.17.21 to 4.17.23
dependabot[bot] Jan 12, 2026
b0f641b
fix(l10n): Update translations from Transifex
nextcloud-bot Jan 14, 2026
c14820c
fix(l10n): Update translations from Transifex
nextcloud-bot Jan 16, 2026
d6bd827
fix(l10n): Update translations from Transifex
nextcloud-bot Jan 17, 2026
6e522c0
build(deps-dev): Bump eslint-plugin-prettier from 5.5.4 to 5.5.5
dependabot[bot] Jan 15, 2026
3e78792
build(deps): Bump actions/setup-node from 6.1.0 to 6.2.0
dependabot[bot] Jan 16, 2026
915f494
build(deps-dev): Bump prettier from 3.7.4 to 3.8.0
dependabot[bot] Jan 17, 2026
971f639
build(deps-dev): Bump vite-plugin-node-polyfills from 0.24.0 to 0.25.0
dependabot[bot] Jan 17, 2026
ed7f7cd
bring back missing user menu items in table view
dartcafe Jan 18, 2026
8663bad
support Nextcloud 33
dartcafe Jan 18, 2026
aa01ff8
changelog
dartcafe Jan 18, 2026
52743f6
8.6.3
dartcafe Jan 18, 2026
06327a0
fixes
dartcafe Jan 18, 2026
5dbc7a3
suppress UnusedClass
dartcafe Jan 18, 2026
b96a986
changelog
dartcafe Jan 18, 2026
d75923b
Merge branch 'main' into enh/new-date-handling
dartcafe Jan 18, 2026
fe8596d
changing dialog
dartcafe Feb 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
# Changelog
All notable changes to this project will be documented in this file.

## [unreleased] - tbd
### Added
- Timezone support for polls and options
- Set default timezone for any poll
- Choose timezone to display, if the poll's original timezone differs from the user's one
- Choose timezone, when adding options in case of different timezones between poll and user

### Changed
- Use ISO formatted timestamps and durations

## [8.6.3] - 2026-01-18
### Fixed
- Fixed missing user menu items, when table view is active
Expand Down
40 changes: 4 additions & 36 deletions lib/Controller/PollController.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,49 +106,17 @@ public function get(int $pollId): JSONResponse {
#[OpenAPI(OpenAPI::SCOPE_IGNORE)]
#[FrontpageRoute(verb: 'GET', url: '/poll/{pollId}')]
public function getFull(int $pollId): JSONResponse {
return $this->response(fn () => $this->getFullPoll($pollId, true), Http::STATUS_OK);
return $this->response(fn () => $this->getFullPoll($pollId), Http::STATUS_OK);
}

private function getFullPoll(int $pollId, bool $withTimings = false): array {
$timerMicro['start'] = microtime(true);

private function getFullPoll(int $pollId): array {
$poll = $this->pollService->get($pollId);
$timerMicro['poll'] = microtime(true);

$options = $this->optionService->list($pollId);
$timerMicro['options'] = microtime(true);

$votes = $this->voteService->list($pollId);
$timerMicro['votes'] = microtime(true);

$comments = $this->commentService->list($pollId);
$timerMicro['comments'] = microtime(true);

$shares = $this->shareService->list($pollId);
$timerMicro['shares'] = microtime(true);

$subscribed = $this->subscriptionService->get($pollId);
$timerMicro['subscribed'] = microtime(true);

$diffMicro['total'] = microtime(true) - $timerMicro['start'];
$diffMicro['poll'] = $timerMicro['poll'] - $timerMicro['start'];
$diffMicro['options'] = $timerMicro['options'] - $timerMicro['poll'];
$diffMicro['votes'] = $timerMicro['votes'] - $timerMicro['options'];
$diffMicro['comments'] = $timerMicro['comments'] - $timerMicro['votes'];
$diffMicro['shares'] = $timerMicro['shares'] - $timerMicro['comments'];
$diffMicro['subscribed'] = $timerMicro['subscribed'] - $timerMicro['shares'];

if ($withTimings) {
return [
'poll' => $poll,
'options' => $options,
'votes' => $votes,
'comments' => $comments,
'shares' => $shares,
'subscribed' => $subscribed,
'diffMicro' => $diffMicro,
];
}
return [
'poll' => $poll,
'options' => $options,
Expand All @@ -170,10 +138,10 @@ private function getFullPoll(int $pollId, bool $withTimings = false): array {
#[NoAdminRequired]
#[OpenAPI(OpenAPI::SCOPE_IGNORE)]
#[FrontpageRoute(verb: 'POST', url: '/poll/add')]
public function add(string $type, string $title, string $votingVariant = Poll::VARIANT_SIMPLE): JSONResponse {
public function add(string $type, string $title, ?string $timezoneName = null, string $votingVariant = Poll::VARIANT_SIMPLE): JSONResponse {
return $this->response(
fn () => [
'poll' => $this->pollService->add($type, $title, $votingVariant)
'poll' => $this->pollService->add($type, $title, $timezoneName, $votingVariant)
],
Http::STATUS_CREATED
);
Expand Down
7 changes: 7 additions & 0 deletions lib/Controller/PublicController.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use OCA\Polls\AppConstants;
use OCA\Polls\Attributes\ShareTokenRequired;
use OCA\Polls\Exceptions\InsufficientAttributesException;
use OCA\Polls\Model\SentResult;
use OCA\Polls\Model\Sequence;
use OCA\Polls\Model\Settings\AppSettings;
Expand Down Expand Up @@ -224,6 +225,12 @@ public function addOption(
?array $sequence = null,
): JSONResponse {
$pollId = $this->userSession->getShare()->getPollId();

/** @psalm-suppress RiskyTruthyFalsyComparison */
if (!$pollId) {
throw new InsufficientAttributesException('PollId of share is missing or invalid');
}

return $this->response(fn () => array_merge(
$this->optionService->addWithSequenceAndAutoVote(
$pollId,
Expand Down
21 changes: 21 additions & 0 deletions lib/Db/Option.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
* @method void setTimestamp(int $value)
* @method int getDeleted()
* @method void setDeleted(int $value)
* @method int getIsoTimestamp()
* @method void setIsoTimestamp(string $value)
* @method int getIsoDuration()
* @method void setIsoDuration(string $value)
*
* Joined Attributes
* @method string getUserVoteAnswer()
Expand All @@ -64,7 +68,9 @@ class Option extends EntityWithUser implements JsonSerializable {
protected string $pollOptionText = '';
protected string $pollOptionHash = '';
protected int $timestamp = 0;
protected string $isoTimestamp = '';
protected int $duration = 0;
protected string $isoDuration = '';
protected int $order = 0;
protected int $confirmed = 0;
protected string $owner = '';
Expand Down Expand Up @@ -102,6 +108,13 @@ public function __construct() {
$this->addType('showResults', 'integer');
}

public function __clone() {
$this->setPollId(0);
$this->setDeleted(0);
$this->setConfirmed(0);
$this->setOwner('');
$this->updateHash();
}
/**
* @return array
*
Expand All @@ -113,6 +126,8 @@ public function jsonSerialize(): array {
'pollId' => $this->getPollId(),
'text' => $this->getPollOptionText(),
'timestamp' => $this->getTimestamp(),
'isoTimestamp' => $this->getIsoTimestamp(),
'isoDuration' => $this->getIsoDuration(),
'deleted' => $this->getDeleted(),
'order' => $this->getOrder(),
'confirmed' => $this->getConfirmed(),
Expand Down Expand Up @@ -148,6 +163,8 @@ public function setFromSimpleOption(SimpleOption $option): void {
$option->getDuration(),
$option->getText(),
$option->getOrder(),
$option->getIsoTimestamp(),
$option->getIsoDuration(),
);
$this->setDeleted(0);
$this->syncOption();
Expand All @@ -168,11 +185,15 @@ public function setOption(
int $duration = 0,
string $pollOptionText = '',
int $order = 0,
string $isoTimestamp = '',
string $isoDuration = '',
): void {

if ($timestamp) {
$this->setTimestamp($timestamp);
$this->setDuration($duration);
$this->setIsoTimestamp($isoTimestamp);
$this->setIsoDuration($isoDuration);
} elseif ($pollOptionText) {
$this->setPollOptionText($pollOptionText);
if ($order > 0) {
Expand Down
50 changes: 34 additions & 16 deletions lib/Db/Poll.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
use OCA\Polls\Helper\Container;
use OCA\Polls\Model\Settings\AppSettings;
use OCA\Polls\Model\Settings\SystemSettings;
use OCA\Polls\Model\User\User;
use OCA\Polls\Model\UserBase;
use OCA\Polls\UserSession;
use OCP\IURLGenerator;
Expand Down Expand Up @@ -65,6 +64,7 @@
* @method void setMiscSettings(string $value)
* @method string getVotingVariant()
* @method void setVotingVariant(string $value)
* @method void setTimezoneName(?string $value)
*
* Magic functions for joined columns
* @method string getShareToken()
Expand Down Expand Up @@ -162,6 +162,7 @@ class Poll extends EntityWithUser implements JsonSerializable {
protected int $lastInteraction = 0;
protected ?string $miscSettings = '';
protected string $votingVariant = '';
protected ?string $timezoneName = null;

// joined columns
protected ?int $isCurrentUserLocked = 0;
Expand Down Expand Up @@ -209,6 +210,16 @@ public function __construct() {
$this->userSession = Container::queryClass(UserSession::class);
}

public function __clone() {
$this->setId(0);
$this->setTitle('Clone of ' . $this->getTitle());
$this->setDeleted(0);
$this->setCreated(time());
$this->setAccess(self::ACCESS_PRIVATE);
// deanonymize cloned polls by default, to avoid locked anonymous polls
$this->setAnonymous(0);
}

/**
* @return array
*
Expand Down Expand Up @@ -237,7 +248,7 @@ public function getStatusArray(): array {
'created' => $this->getCreated(),
'isAnonymous' => boolval($this->getAnonymous()),
'isArchived' => boolval($this->getDeleted()),
'isExpired' => $this->getExpired(),
'isExpired' => $this->getIsExpired(),
'isRealAnonymous' => $this->getAnonymous() < 0,
'relevantThreshold' => $this->getRelevantThreshold(),
'deletionDate' => $this->getDeletionDate(),
Expand Down Expand Up @@ -268,6 +279,7 @@ public function getConfigurationArray(): array {
'showResults' => $this->getShowResults(),
'title' => $this->getTitle(),
'useNo' => boolval($this->getUseNo()),
'timezoneName' => $this->getTimezoneName(),
];
}

Expand Down Expand Up @@ -336,10 +348,11 @@ public function deserializeArray(array $pollConfiguration): self {
$this->setUseNo($pollConfiguration['useNo'] ?? $this->getUseNo());
$this->setOptionLimit($pollConfiguration['maxVotesPerOption'] ?? $this->getOptionLimit());
$this->setVoteLimit($pollConfiguration['maxVotesPerUser'] ?? $this->getVoteLimit());
$this->setTimezoneName($pollConfiguration['timezoneName'] ?? $this->getTimezoneName());
return $this;
}

public function getExpired(): bool {
public function getIsExpired(): bool {
$compareTime = time();
$expiry = $this->getExpire();

Expand All @@ -353,6 +366,16 @@ public function getPollOwnerId() {
return $this->getOwner();
}

/**
* @psalm-return non-empty-string|null
*/
public function getTimezoneName(): ?string {
if ($this->timezoneName === '') {
return null;
}
return $this->timezoneName;
}

public function getUserRole(): string {
if ($this->getCurrentUserIsEntityUser()) {
return self::ROLE_OWNER;
Expand Down Expand Up @@ -473,6 +496,7 @@ private function getGroupShares(): array {
* @psalm-return list<int>
*/
public function getPollGroups(): array {
/** @psalm-suppress RiskyTruthyFalsyComparison */
if (!$this->pollGroups) {
return [];
}
Expand All @@ -487,6 +511,7 @@ public function getPollGroups(): array {
* @psalm-return list<string>
*/
public function getPollGroupUserShares(): array {
/** @psalm-suppress RiskyTruthyFalsyComparison */
if (!$this->pollGroupUserShares) {
return [];
}
Expand All @@ -503,7 +528,7 @@ private function getAccess(): string {
return $this->access;
}

private function getProposalsExpired(): bool {
private function getProposalPeriodIsExpired(): bool {
return (
$this->getProposalsExpire() > 0
&& $this->getProposalsExpire() < time()
Expand Down Expand Up @@ -798,7 +823,7 @@ private function getAllowAddOptions(): bool {
}

// Request for option proposals is expired, deny
if ($this->getProposalsExpired()) {
if ($this->getProposalPeriodIsExpired()) {
return false;
}

Expand Down Expand Up @@ -826,21 +851,14 @@ private function getAllowDeleteOption(): bool {
* Is current user allowed to confirm options
*/
private function getAllowConfirmOption(): bool {
return $this->getAllowEditPoll() && $this->getExpired();
return $this->getAllowEditPoll() && $this->getIsExpired();
}

/**
* Is current user allowed to confirm options
*/
private function getAllowReorderOptions(): bool {
return $this->getAllowEditPoll() && !$this->getExpired() && $this->getType() === Poll::TYPE_TEXT;
}

/**
* Compare $userId with current user's id
*/
public function matchUser(string $userId): bool {
return (bool)$this->userSession->getCurrentUser()->getId() && $this->userSession->getCurrentUser()->getId() === $userId;
return $this->getAllowEditPoll() && !$this->getIsExpired() && $this->getType() === Poll::TYPE_TEXT;
}

/**
Expand Down Expand Up @@ -931,7 +949,7 @@ private function getAllowVote(): bool {
}

// deny votes, if poll is expired
return !$this->getExpired();
return !$this->getIsExpired();
}

/**
Expand Down Expand Up @@ -961,7 +979,7 @@ private function getAllowShowResults(): bool {
}

// show results, when poll is closed
if ($this->getShowResults() === Poll::SHOW_RESULTS_CLOSED && $this->getExpired()) {
if ($this->getShowResults() === Poll::SHOW_RESULTS_CLOSED && $this->getIsExpired()) {
return true;
}
// return poll settings
Expand Down
2 changes: 1 addition & 1 deletion lib/Exceptions/InsufficientAttributesException.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ class InsufficientAttributesException extends Exception {
public function __construct(
string $e = 'Attribut constraints not met',
) {
parent::__construct($e, Http::STATUS_CONFLICT);
parent::__construct($e, Http::STATUS_BAD_REQUEST);
}
}
Loading