Skip to content
Open
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
193 changes: 97 additions & 96 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
@@ -1,96 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>deck</id>
<name>Deck</name>
<summary>Personal planning and team project organization</summary>
<description>Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.


- 📥 Add your tasks to cards and put them in order
- 📄 Write down additional notes in Markdown
- 🔖 Assign labels for even better organization
- 👥 Share with your team, friends or family
- 📎 Attach files and embed them in your Markdown description
- 💬 Discuss with your team using comments
- ⚡ Keep track of changes in the activity stream
- 🚀 Get your project organized

</description>
<version>4.0.0-dev.1</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>
<types>
<dav/>
</types>
<documentation>
<user>https://deck.readthedocs.io/en/latest/User_documentation_en/</user>
<developer>https://deck.readthedocs.io/en/latest/API/</developer>
</documentation>
<category>organization</category>
<category>office</category>
<website>https://github.com/nextcloud/deck</website>
<bugs>https://github.com/nextcloud/deck/issues</bugs>
<repository type="git">https://github.com/nextcloud/deck.git</repository>
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-1.png</screenshot>
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-2.png</screenshot>
<dependencies>
<database min-version="9.4">pgsql</database>
<database>sqlite</database>
<database min-version="8.0">mysql</database>
<nextcloud min-version="35" max-version="35"/>
</dependencies>
<background-jobs>
<job>OCA\Deck\Cron\DeleteCron</job>
<job>OCA\Deck\Cron\ScheduledNotifications</job>
<job>OCA\Deck\Cron\CardDescriptionActivity</job>
<job>OCA\Deck\Cron\SessionsCleanup</job>
</background-jobs>
<repair-steps>
<live-migration>
<step>OCA\Deck\Migration\DeletedCircleCleanup</step>
</live-migration>
<post-migration>
<step>OCA\Deck\Migration\LabelMismatchCleanup</step>
</post-migration>
</repair-steps>
<commands>
<command>OCA\Deck\Command\UserExport</command>
<command>OCA\Deck\Command\BoardImport</command>
<command>OCA\Deck\Command\TransferOwnership</command>
<command>OCA\Deck\Command\CalendarToggle</command>
</commands>
<activity>
<settings>
<setting>OCA\Deck\Activity\SettingChanges</setting>
<setting>OCA\Deck\Activity\SettingDescription</setting>
<setting>OCA\Deck\Activity\SettingComment</setting>
</settings>
<filters>
<filter>OCA\Deck\Activity\Filter</filter>
</filters>
<providers>
<provider>OCA\Deck\Activity\DeckProvider</provider>
</providers>
</activity>
<fulltextsearch>
<provider min-version="16">OCA\Deck\Provider\DeckProvider</provider>
</fulltextsearch>
<navigations>
<navigation>
<name>Deck</name>
<route>deck.page.index</route>
<icon>deck.svg</icon>
<order>10</order>
</navigation>
</navigations>
<sabre>
<calendar-plugins>
<plugin>OCA\Deck\DAV\CalendarPlugin</plugin>
</calendar-plugins>
</sabre>
</info>
<?xml version="1.0" encoding="utf-8"?>
<!--
- SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>deck</id>
<name>Deck</name>
<summary>Personal planning and team project organization</summary>
<description>Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.


- 📥 Add your tasks to cards and put them in order
- 📄 Write down additional notes in Markdown
- 🔖 Assign labels for even better organization
- 👥 Share with your team, friends or family
- 📎 Attach files and embed them in your Markdown description
- 💬 Discuss with your team using comments
- ⚡ Keep track of changes in the activity stream
- 🚀 Get your project organized

</description>
<version>4.0.0-dev.2</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>
<types>
<dav/>
</types>
<documentation>
<user>https://deck.readthedocs.io/en/latest/User_documentation_en/</user>
<developer>https://deck.readthedocs.io/en/latest/API/</developer>
</documentation>
<category>organization</category>
<category>office</category>
<website>https://github.com/nextcloud/deck</website>
<bugs>https://github.com/nextcloud/deck/issues</bugs>
<repository type="git">https://github.com/nextcloud/deck.git</repository>
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-1.png</screenshot>
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-2.png</screenshot>
<dependencies>
<database min-version="9.4">pgsql</database>
<database>sqlite</database>
<database min-version="8.0">mysql</database>
<nextcloud min-version="35" max-version="35"/>
</dependencies>
<background-jobs>
<job>OCA\Deck\Cron\DeleteCron</job>
<job>OCA\Deck\Cron\ScheduledNotifications</job>
<job>OCA\Deck\Cron\CardDescriptionActivity</job>
<job>OCA\Deck\Cron\SessionsCleanup</job>
</background-jobs>
<repair-steps>
<live-migration>
<step>OCA\Deck\Migration\DeletedCircleCleanup</step>
</live-migration>
<post-migration>
<step>OCA\Deck\Migration\LabelMismatchCleanup</step>
<step>OCA\Deck\Migration\AclTimestampBackfill</step>
</post-migration>
</repair-steps>
<commands>
<command>OCA\Deck\Command\UserExport</command>
<command>OCA\Deck\Command\BoardImport</command>
<command>OCA\Deck\Command\TransferOwnership</command>
<command>OCA\Deck\Command\CalendarToggle</command>
</commands>
<activity>
<settings>
<setting>OCA\Deck\Activity\SettingChanges</setting>
<setting>OCA\Deck\Activity\SettingDescription</setting>
<setting>OCA\Deck\Activity\SettingComment</setting>
</settings>
<filters>
<filter>OCA\Deck\Activity\Filter</filter>
</filters>
<providers>
<provider>OCA\Deck\Activity\DeckProvider</provider>
</providers>
</activity>
<fulltextsearch>
<provider min-version="16">OCA\Deck\Provider\DeckProvider</provider>
</fulltextsearch>
<navigations>
<navigation>
<name>Deck</name>
<route>deck.page.index</route>
<icon>deck.svg</icon>
<order>10</order>
</navigation>
</navigations>
<sabre>
<calendar-plugins>
<plugin>OCA\Deck\DAV\CalendarPlugin</plugin>
</calendar-plugins>
</sabre>
</info>
8 changes: 8 additions & 0 deletions lib/Db/Acl.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
* @method void setOwner(int $owner)
* @method void setToken(string $token)
* @method string getToken()
* @method int getCreatedAt()
* @method void setCreatedAt(int $createdAt)
* @method int getLastModifiedAt()
* @method void setLastModifiedAt(int $lastModifiedAt)
*
*/
class Acl extends RelationalEntity {
Expand All @@ -42,6 +46,8 @@ class Acl extends RelationalEntity {
protected $permissionManage = false;
protected $owner = false;
protected $token = null;
protected $createdAt = 0;
protected $lastModifiedAt = 0;

public function __construct() {
$this->addType('id', 'integer');
Expand All @@ -52,6 +58,8 @@ public function __construct() {
$this->addType('type', 'integer');
$this->addType('owner', 'boolean');
$this->addType('token', 'string');
$this->addType('createdAt', 'integer');
$this->addType('lastModifiedAt', 'integer');
$this->addRelation('owner');
$this->addResolvable('participant');
}
Expand Down
21 changes: 18 additions & 3 deletions lib/Db/AclMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace OCA\Deck\Db;

use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
Expand All @@ -20,7 +21,7 @@ public function __construct(IDBConnection $db) {

public function findByAccessToken(string $accessToken) {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage', 'token')
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage', 'token', 'created_at', 'last_modified_at')
->from('deck_board_acl')
->where($qb->expr()->eq('token', $qb->createNamedParameter($accessToken, IQueryBuilder::PARAM_STR)))
->setMaxResults(1);
Expand All @@ -34,7 +35,7 @@ public function findByAccessToken(string $accessToken) {
*/
public function findAll(int $boardId, ?int $limit = null, ?int $offset = null) {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage', 'token')
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage', 'token', 'created_at', 'last_modified_at')
->from('deck_board_acl')
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
->setMaxResults($limit)
Expand All @@ -45,7 +46,7 @@ public function findAll(int $boardId, ?int $limit = null, ?int $offset = null) {

public function findIn(array $boardIds, ?int $limit = null, ?int $offset = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage')
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage', 'created_at', 'last_modified_at')
->from('deck_board_acl')
->where($qb->expr()->in('board_id', $qb->createParameter('boardIds')))
->setMaxResults($limit)
Expand Down Expand Up @@ -127,4 +128,18 @@ public function findByType(int $type): array {
->where($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
return $this->findEntities($qb);
}

public function insert(Entity $entity): Entity {
/** @var Acl $entity */
$now = time();
$entity->setCreatedAt($now);
$entity->setLastModifiedAt($now);
return parent::insert($entity);
}

public function update(Entity $entity): Entity {
/** @var Acl $entity */
$entity->setLastModifiedAt(time());
return parent::update($entity);
}
}
68 changes: 68 additions & 0 deletions lib/Migration/AclTimestampBackfill.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);

namespace OCA\Deck\Migration;

use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\IRepairStep;

class AclTimestampBackfill implements IRepairStep {
public function __construct(
private IDBConnection $db,
) {
}

public function getName(): string {
return 'Backfill board last_modified into deck_board_acl created_at / last_modified_at';
}

public function run(IOutput $output): void {
$selectQb = $this->db->getQueryBuilder();
$selectQb->select('a.id AS acl_id', 'b.last_modified AS board_last_modified')
->from('deck_board_acl', 'a')
->join('a', 'deck_boards', 'b', $selectQb->expr()->eq('b.id', 'a.board_id'))
->where($selectQb->expr()->eq('a.created_at', $selectQb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));

$result = $selectQb->executeQuery();
$rows = $result->fetchAll();
$result->closeCursor();

if ($rows === []) {
$output->info('AclTimestampBackfill: no rows to update');
return;
}

// Group ACL IDs by target timestamp to issue one UPDATE per group
// rather than one UPDATE per row (avoids N+1 queries).
$now = time();
$groups = [];
foreach ($rows as $row) {
$timestamp = ((int)$row['board_last_modified'] > 0) ? (int)$row['board_last_modified'] : $now;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk if we should really backfill that way, does the app you're doing all that for not have an option to have an unknown value?
The values we're writing in here 99% of the time are not the real created/last_modified of the acl, so an unknown value would be better imo, or at least just the "now" value so it is clear that the value was written when the update was done.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use any we would want, the share review app would else have to assume one and either that is a fully hard-coded and fixed date, no matter the share, or needs to be computed each time the shares are loaded for review. So to me, compute-once made the most sense and going with a creation/modification date of the board itself seemed most feasible. However we could also go for $now if you think that would be better. But in any care I suggest to pre-compute and migrate to a date and not have a fallback logic because we lacked dates in the past.

WDYT @grnd-alt @blizzz ? "No date" feels wrong to me, but that might be me, than we can also just leave it at 0 and take that as "unknown".

Both has its advantages, one is unknown: cause it hasn't been tracked but mid-term I find this confusing to users when there are shares with known and unknown dates.

Both works for me. I can find a way to explain the unknown in the review app of course.

$groups[$timestamp][] = (int)$row['acl_id'];
}

$updated = 0;
foreach ($groups as $timestamp => $ids) {
// Chunk at 1000 for Oracle compatibility (same limit used by chunkQuery).
foreach (array_chunk($ids, 1000) as $chunk) {
$updateQb = $this->db->getQueryBuilder();
$updateQb->update('deck_board_acl')
->set('created_at', $updateQb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))
->set('last_modified_at', $updateQb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))
->where($updateQb->expr()->in('id', $updateQb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)));
$updateQb->executeStatement();
$updated += count($chunk);
}
}

$output->info('AclTimestampBackfill: updated ' . $updated . ' row(s)');
}
}
41 changes: 41 additions & 0 deletions lib/Migration/Version11002Date20260611000000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

declare(strict_types=1);

namespace OCA\Deck\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version11002Date20260611000000 extends SimpleMigrationStep {
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$table = $schema->getTable('deck_board_acl');

if (!$table->hasColumn('created_at')) {
$table->addColumn('created_at', 'integer', [
'notnull' => true,
'default' => 0,
'unsigned' => true,
]);
}

if (!$table->hasColumn('last_modified_at')) {
$table->addColumn('last_modified_at', 'integer', [
'notnull' => true,
'default' => 0,
'unsigned' => true,
]);
}

return $schema;
}
}
Loading
Loading