From 924705717919d740ec271b4a1a4272c3d8d6d6ec Mon Sep 17 00:00:00 2001 From: Ruben Carlo Benante Date: Sat, 11 Jul 2026 15:34:39 -0300 Subject: [PATCH] per-board study setup (restructure columns, reset, arm RecoReco) via Integrations (v0.3.0) --- Controller/QualKardController.php | 99 +++++++++++++++++++++++++++++++ Helper/QualKardHelper.php | 75 +++++++++++++++++++++++ Plugin.php | 7 ++- Template/project/integration.php | 28 +++++++++ VERSION | 2 +- 5 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 Controller/QualKardController.php create mode 100644 Template/project/integration.php diff --git a/Controller/QualKardController.php b/Controller/QualKardController.php new file mode 100644 index 0000000..8b0572a --- /dev/null +++ b/Controller/QualKardController.php @@ -0,0 +1,99 @@ +authorize(); + $helper = new QualKardHelper($this->container); + + $names = array('Drafts', 'Study', 'Hairy', 'Hard', 'Medium', 'Easy', 'Done'); + $columns = $this->columnModel->getAll($project['id']); // position-ordered rows + $ids = array(); + + for ($i = 0; $i < 7; $i++) { + if (isset($columns[$i])) { + $this->columnModel->update($columns[$i]['id'], $names[$i]); + $ids[$i] = (int) $columns[$i]['id']; + } else { + $ids[$i] = (int) $this->columnModel->create($project['id'], $names[$i]); + } + } + + // Move every card to Drafts BEFORE deleting extras -- remove() is a raw DELETE (tasks cascade). + foreach ($this->taskFinderModel->getAll($project['id']) as $task) { + $this->taskPositionModel->movePosition($project['id'], $task['id'], $ids[0], 1, $task['swimlane_id'], false); + } + + for ($i = 7, $n = count($columns); $i < $n; $i++) { + $this->columnModel->remove($columns[$i]['id']); + } + + $roles = array('draft', 'study', 'hairy', 'hard', 'medium', 'easy', 'done'); + foreach ($roles as $k => $role) { + $this->projectMetadataModel->save($project['id'], array('qualkard_col_'.$role => $ids[$k])); + } + + $helper->resetAll($project['id']); + $this->projectMetadataModel->save($project['id'], array('qualkard_enabled' => 1)); + + $this->flash->success(t('QualKard study board is ready.')); + $this->redirectToIntegrations($project); + } + + /** + * Reset every card on the board: unread, moved to Drafts, armed-inert. Keeps the columns. + */ + public function reset() + { + $project = $this->authorize(); + (new QualKardHelper($this->container))->resetAll($project['id']); + $this->flash->success(t('All cards reset.')); + $this->redirectToIntegrations($project); + } + + /** + * Disable QualKard on this board (non-destructive: leaves columns and cards as they are). + */ + public function disable() + { + $project = $this->authorize(); + $this->projectMetadataModel->save($project['id'], array('qualkard_enabled' => 0)); + $this->flash->success(t('QualKard disabled on this board.')); + $this->redirectToIntegrations($project); + } + + // Project-manager gate + CSRF, shared by every action. + private function authorize() + { + $project = $this->getProject(); + + if (! $this->helper->user->hasProjectAccess('ProjectEditController', 'edit', $project['id'])) { + throw new AccessForbiddenException(); + } + + $this->checkCSRFForm(); + + return $project; + } + + private function redirectToIntegrations(array $project) + { + $this->response->redirect($this->helper->url->to('ProjectViewController', 'integrations', array('project_id' => $project['id']))); + } +} diff --git a/Helper/QualKardHelper.php b/Helper/QualKardHelper.php index a0a1c53..beab78f 100644 --- a/Helper/QualKardHelper.php +++ b/Helper/QualKardHelper.php @@ -3,6 +3,7 @@ namespace Kanboard\Plugin\QualKard\Helper; use Kanboard\Core\Base; +use Kanboard\Model\TaskModel; /** * QualKard helper: the qualcard grading math (ported from Ruben's C program) plus the per-board gates. @@ -118,4 +119,78 @@ class QualKardHelper extends Base { return (int) $this->projectMetadataModel->get($project_id, 'qualkard_col_'.$role, 0); } + + /** + * Write the inert RecoReco config for a card: move-mode to Study, fire from anywhere but Done, + * daily (innocuous -- QualKard sets the due date on each grade), anchor 0 so RecoReco skips it + * until the first grade arms it. QualKard never toggles recoreco_enabled again. + * + * @param int $task_id + * @param int $project_id + */ + public function arm($task_id, $project_id) + { + $this->taskMetadataModel->save($task_id, array( + 'recoreco_enabled' => 1, + 'recoreco_move' => 1, + 'recoreco_target_column' => $this->col($project_id, 'study'), + 'recoreco_trigger_column' => $this->col($project_id, 'done'), + 'recoreco_trigger_invert' => 1, + 'recoreco_frequency' => 'daily', + 'recoreco_anchor' => 0, + )); + } + + /** + * Reset one card: move to Drafts, mark unread, arm inert, clear the due date. + * + * @param int $task_id + * @param int $project_id + */ + public function resetCard($task_id, $project_id) + { + $task = $this->taskFinderModel->getById($task_id); + + if (empty($task)) { + return; + } + + $this->taskPositionModel->movePosition($project_id, $task_id, $this->col($project_id, 'draft'), 1, (int) $task['swimlane_id'], false); + $this->taskMetadataModel->save($task_id, array('qualkard_avg' => '')); // '' = unread + $this->arm($task_id, $project_id); + $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(array('date_due' => 0)); + } + + /** + * Reset every card on a board. + * + * @param int $project_id + */ + public function resetAll($project_id) + { + foreach ($this->taskFinderModel->getAll($project_id) as $task) { + $this->resetCard($task['id'], $project_id); + } + } + + /** + * Grade a card by a score (a grade column's value): update the running average, compute the next + * due date, and arm the anchor. RecoReco moves the card from here. + * + * @param int $task_id + * @param float $score + */ + public function grade($task_id, $score) + { + $raw = $this->taskMetadataModel->get($task_id, 'qualkard_avg', ''); + $old = ($raw === '' || $raw === null) ? null : (float) $raw; + $avg = self::nextAverage($old, $score); + $due = self::dueFrom($avg); + + $this->taskMetadataModel->save($task_id, array( + 'qualkard_avg' => (string) $avg, + 'recoreco_anchor' => $due, + )); + $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(array('date_due' => $due)); + } } diff --git a/Plugin.php b/Plugin.php index 01d8085..44c4d2d 100644 --- a/Plugin.php +++ b/Plugin.php @@ -17,7 +17,10 @@ class Plugin extends Base { public function initialize() { - // Hooks are wired in later versions (setup on Integrations, the grade handler, the badge). + // Per-board setup/reset/disable on Settings -> Integrations. + $this->template->hook->attach('template:project:integrations', 'qualKard:project/integration'); + + // (The grade handler and the badge are wired in later versions.) } public function getHelpers() @@ -44,7 +47,7 @@ class Plugin extends Base public function getPluginVersion() { - return '0.2.0'; + return '0.3.0'; } public function getPluginHomepage() diff --git a/Template/project/integration.php b/Template/project/integration.php new file mode 100644 index 0000000..0ad15dd --- /dev/null +++ b/Template/project/integration.php @@ -0,0 +1,28 @@ +

+
+helper->QualKardHelper->hasRecoReco()): ?> + +

+ +helper->QualKardHelper->isEnabled($project['id'])): ?> + +

+
+ form->csrf() ?> + +
+ + + +

+
+ form->csrf() ?> + +
+
+ form->csrf() ?> + +
+ + +
diff --git a/VERSION b/VERSION index 586edbc..66e2f07 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -QualKard v0.2 +QualKard v0.3