Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
52 changes: 37 additions & 15 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,33 +1,55 @@
# SPDX-FileCopyrightText: 2016 Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
/assets/

# build dirs
/assets
/css/*.map
/js/*
/css/*
/js
/css
/build

# Mac nonsense
.DS_Store

# Win nonsense
Thumbs.db

# local folders and files
.local

# caches
.sass-cache/
.php_cs.cache
.php-cs-fixer.cache
.psalm.cache
tests/.phpunit.result.cache

# logs
npm-debug.log
yarn-error.log

# IDEs
.project/
.idea/
.vscode/
build/
nbproject/
*.iml
*.ntvs*
*.suo
*.sln
*.njsproj

# modules
node_modules/
tests/.phpunit.result.cache
npm-debug.log

# Composer vendors
vendor
vendor-bin/**/vendor/
vendor-bin/**/*.lock
update-workflows.sh
yarn-error.log
Thumbs.db

# local command files
*.sh
*.cmd
*.env
*.iml
*.ntvs*
*.njsproj
*.sln
*.suo

# misc
tests/Api
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ All notable changes to this project will be documented in this file.
- Optimized janitor cron
- Optimized rebuild command
- Optimized poll loading by migrating subqueries to join (#3692)
- Accelerated loading performance of polls
- Separated pollGroups from polls (Store, Service, Mapper, ...)
- Catch CronJob runs and report as error, but avoid crash at higher thread levels
- Changed poll loading triggers (mainly navigation affected)
- Added some status to the watchWorker
- removed performance setting in favor of lazy loading participants
- removed performance user setting in favor of lazy loading participants
- reduce noise by avoiding toasts for obvious changes

### Fixes
- Fixed broken endpoint for manually calling autoReminderCron
- fix avatar foreground color

## [8.0.6] - 2025-07-03
### Changes (8.0.6)
Expand Down
2 changes: 1 addition & 1 deletion lib/Command/Share/Add.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$emails = $input->getOption('email');

try {
$poll = $this->pollMapper->find($pollId);
$poll = $this->pollMapper->get($pollId);
} catch (DoesNotExistException $e) {
$output->writeln('<error>Poll not found.</error>');
return 1;
Expand Down
2 changes: 1 addition & 1 deletion lib/Command/Share/Remove.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$emails = $input->getOption('email');

try {
$poll = $this->pollMapper->find($pollId);
$poll = $this->pollMapper->get($pollId);
} catch (DoesNotExistException $e) {
$output->writeln('<error>Poll not found.</error>');
return 1;
Expand Down
68 changes: 60 additions & 8 deletions lib/Controller/PollController.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,66 @@ public function get(int $pollId): JSONResponse {
#[NoAdminRequired]
#[FrontpageRoute(verb: 'GET', url: '/poll/{pollId}')]
public function getFull(int $pollId): JSONResponse {
return $this->response(fn () => [
'poll' => $this->pollService->get($pollId),
'options' => $this->optionService->list($pollId),
'votes' => $this->voteService->list($pollId),
'comments' => $this->commentService->list($pollId),
'shares' => $this->shareService->list($pollId),
'subscribed' => $this->subscriptionService->get($pollId),
]);
return $this->response(fn () => $this->getFullPoll($pollId), Http::STATUS_OK);

// return $this->response(fn () => [
// 'poll' => $this->pollService->get($pollId),
// 'options' => $this->optionService->list($pollId),
// 'votes' => $this->voteService->list($pollId),
// 'comments' => $this->commentService->list($pollId),
// 'shares' => $this->shareService->list($pollId),
// 'subscribed' => $this->subscriptionService->get($pollId),
// ]);
}

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

$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,
'votes' => $votes,
'comments' => $comments,
'shares' => $shares,
'subscribed' => $subscribed,
];
}

/**
Expand Down
19 changes: 15 additions & 4 deletions lib/Db/Poll.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@
* @method void setVotingVariant(string $value)
*
* Magic functions for joined columns
* @method int getMinDate()
* @method int getMaxDate()
* @method int getShareToken()
* @method int getOptionsCount()
* @method int getProposalsCount()
Expand Down Expand Up @@ -168,8 +166,8 @@ class Poll extends EntityWithUser implements JsonSerializable {

// joined columns
protected ?int $isCurrentUserLocked = 0;
protected int $maxDate = 0;
protected int $minDate = 0;
protected ?int $maxDate = 0;
protected ?int $minDate = 0;
protected string $userRole = self::ROLE_NONE;
protected string $shareToken = '';
protected ?string $groupShares = '';
Expand Down Expand Up @@ -517,6 +515,19 @@ private function getDescriptionSafe(): string {
return htmlspecialchars($this->getDescription());
}

private function getMaxDate(): int {
if ($this->maxDate === null) {
return 0;
}
return $this->maxDate;
}

private function getMinDate(): int {
if ($this->minDate === null) {
return time();
}
return $this->minDate;
}

private function setMiscSettingsArray(array $value): void {
$this->setMiscSettings(json_encode($value));
Expand Down
43 changes: 30 additions & 13 deletions lib/Db/PollMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,27 @@ public function __construct(
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException if more than one result
* @return Poll
*/
public function get(int $id, bool $getDeleted = false): Poll {
public function get(int $id, bool $getDeleted = false, bool $withRoles = false): Poll {
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
$qb->select(self::TABLE . '.*')
->from($this->getTableName(), self::TABLE)
->where($qb->expr()->eq(self::TABLE . '.id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->groupBy(self::TABLE . '.id');

if (!$getDeleted) {
$qb->andWhere($qb->expr()->eq('deleted', $qb->expr()->literal(0, IQueryBuilder::PARAM_INT)));
$qb->andWhere($qb->expr()->eq(self::TABLE . '.deleted', $qb->expr()->literal(0, IQueryBuilder::PARAM_INT)));
}

if ($withRoles) {
$pollGroupsAlias = 'poll_groups';
$currentUserId = $this->userSession->getCurrentUserId();
// $this->joinOptions($qb, self::TABLE);
$this->joinUserRole($qb, self::TABLE, $currentUserId);
$this->joinGroupShares($qb, self::TABLE);
$this->joinPollGroups($qb, self::TABLE, $pollGroupsAlias);
$this->joinPollGroupShares($qb, $pollGroupsAlias, $currentUserId, $pollGroupsAlias);
// $this->joinVotesCount($qb, self::TABLE, $currentUserId);
// $this->joinParticipantsCount($qb, self::TABLE);
}
return $this->findEntity($qb);
}
Expand Down Expand Up @@ -184,19 +198,19 @@ public function deleteByUserId(string $userId): void {
* Build the enhanced query with joined tables
*/
protected function buildQuery(): IQueryBuilder {
$currentUserId = $this->userSession->getCurrentUserId();
$qb = $this->db->getQueryBuilder();

$qb->select(self::TABLE . '.*')
->from($this->getTableName(), self::TABLE)
->groupBy(self::TABLE . '.id');

$currentUserId = $this->userSession->getCurrentUserId();
$pollGroupsAlias = 'poll_groups';
$this->joinOptions($qb, self::TABLE);
$this->joinUserRole($qb, self::TABLE, $currentUserId);
$this->joinGroupShares($qb, self::TABLE);
$this->joinPollGroups($qb, self::TABLE, $pollGroupsAlias);
$this->joinUserSharesfromPollGroups($qb, $pollGroupsAlias, $currentUserId, $pollGroupsAlias);
$this->joinPollGroupShares($qb, $pollGroupsAlias, $currentUserId, $pollGroupsAlias);
$this->joinVotesCount($qb, self::TABLE, $currentUserId);
$this->joinParticipantsCount($qb, self::TABLE);
return $qb;
Expand Down Expand Up @@ -299,7 +313,7 @@ protected function joinPollGroups(
* Supported share types are User and Admin
* Groups, Teams will not work atm.
*/
protected function joinUserSharesfromPollGroups(
protected function joinPollGroupShares(
IQueryBuilder $qb,
string $fromAlias,
string $currentUserId,
Expand Down Expand Up @@ -338,13 +352,16 @@ protected function joinOptions(
string $fromAlias,
string $joinAlias = 'options',
): void {
// add highest option date
$qb->addSelect($qb->createFunction('MAX(' . $joinAlias . '.timestamp) AS max_date'));

// add lowest option date
$qb->addSelect($qb->createFunction('MIN(' . $joinAlias . '.timestamp) AS min_date'));

$zero = $qb->expr()->literal(0, IQueryBuilder::PARAM_INT);
$saveMin = $qb->createNamedParameter(time(), IQueryBuilder::PARAM_INT);
// add number of options with an owner (results in number of proposals)
$qb->addSelect($qb->createFunction('COUNT(DISTINCT(CASE WHEN ' . $joinAlias . '.owner != \'\' THEN 1 END)) AS proposals_count'));

$qb->addSelect($qb->createFunction('coalesce(MAX(' . $joinAlias . '.timestamp), ' . $zero . ') AS max_date'))
->addSelect($qb->createFunction('coalesce(MIN(' . $joinAlias . '.timestamp), ' . $saveMin . ') AS min_date'))
->addSelect($qb->createFunction('COUNT(DISTINCT(CASE WHEN ' . $joinAlias . '.owner != \'\' THEN 1 END)) AS proposals_count'));
// count number of options by counting unique ids
$qb->selectAlias($qb->func()->count($joinAlias . '.id'), 'optionsCount');

$qb->leftJoin(
Expand Down
2 changes: 1 addition & 1 deletion lib/Helper/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public static function getPoll(int $pollId, bool $getDeleted = false): Poll {
}

public static function queryPoll(int $pollId): Poll {
return Server::get(PollMapper::class)->find($pollId);
return Server::get(PollMapper::class)->get($pollId);
}

public static function findShare(int $pollId, string $userId): Share {
Expand Down
8 changes: 4 additions & 4 deletions lib/Service/CommentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,11 @@ public function __construct(
*/
public function list(int $pollId): array {
try {
$this->pollMapper->find($pollId)->request(Poll::PERMISSION_COMMENT_ADD);
$this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_COMMENT_ADD);
} catch (Exception $e) {
return [];
}
$this->pollMapper->find($pollId)->request(Poll::PERMISSION_COMMENT_ADD);
$this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_COMMENT_ADD);

$comments = $this->commentMapper->findByPoll($pollId);
// treat comments from the same user within 5 minutes as grouped comments
Expand All @@ -66,7 +66,7 @@ public function list(int $pollId): array {
* Add comment
*/
public function add(string $message, int $pollId, ?bool $confidential = false): Comment {
$poll = $this->pollMapper->find($pollId);
$poll = $this->pollMapper->get($pollId, withRoles: true);
$poll->request(Poll::PERMISSION_COMMENT_ADD);

if ($poll->getForceConfidentialComments()) {
Expand Down Expand Up @@ -104,7 +104,7 @@ public function delete(int $commentId, bool $restore = false): Comment {
$this->comment = $this->commentMapper->find($commentId);

if (!$this->comment->getCurrentUserIsEntityUser()) {
$this->pollMapper->find($this->comment->getPollId())->request(Poll::PERMISSION_COMMENT_DELETE);
$this->pollMapper->get($this->comment->getPollId(), withRoles: true)->request(Poll::PERMISSION_COMMENT_DELETE);
}

$this->comment->setDeleted($restore ? 0 : time());
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/MailService.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ public function sendAutoReminder(): void {
* Send a confirmation mail for the poll to all participants
*/
public function sendConfirmations(int $pollId): SentResult {
$this->pollMapper->find($pollId)->request(Poll::PERMISSION_POLL_EDIT);
$this->pollMapper->get($pollId, withRoles: true)->request(Poll::PERMISSION_POLL_EDIT);
$sentResult = new SentResult();

$participants = $this->userMapper->getParticipants($pollId);
Expand Down
8 changes: 4 additions & 4 deletions lib/Service/OptionService.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public function delete(int $optionId, bool $restore = false): Option {
$option = $this->optionMapper->find($optionId);

if (!$option->getCurrentUserIsEntityUser()) {
$this->pollMapper->find($option->getPollId())->request(Poll::PERMISSION_OPTION_DELETE);
$this->pollMapper->get($option->getPollId(), withRoles: true)->request(Poll::PERMISSION_OPTION_DELETE);
}

$option->setDeleted($restore ? 0 : time());
Expand Down Expand Up @@ -314,8 +314,8 @@ public function shift(int $pollId, int $step, string $unit): array {
* Copy options from $fromPoll to $toPoll
*/
public function clone(int $fromPollId, int $toPollId): void {
$this->pollMapper->find($fromPollId)->request(Poll::PERMISSION_POLL_VIEW);
$this->pollMapper->find($toPollId)->request(Poll::PERMISSION_OPTION_ADD);
$this->pollMapper->get($fromPollId, withRoles: true)->request(Poll::PERMISSION_POLL_VIEW);
$this->pollMapper->get($toPollId, withRoles: true)->request(Poll::PERMISSION_OPTION_ADD);

foreach ($this->optionMapper->findByPoll($fromPollId) as $origin) {
$option = new Option();
Expand Down Expand Up @@ -426,7 +426,7 @@ private function moveModifier(int $moveFrom, int $moveTo, int $currentPosition):
*/
private function getPoll(int $pollId, string $permission = Poll::PERMISSION_POLL_VIEW): void {
if ($this->poll->getId() !== $pollId) {
$this->poll = $this->pollMapper->find($pollId);
$this->poll = $this->pollMapper->get($pollId, withRoles: true);
}
$this->poll->request($permission);
}
Expand Down
Loading