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 @@ +
= t('QualKard needs the RecoReco plugin (its move engine). Install RecoReco to use QualKard.') ?>
+ +helper->QualKardHelper->isEnabled($project['id'])): ?> + += t('Turn this project into a spaced-repetition study board. This restructures the columns to Drafts | Study | Hairy | Hard | Medium | Easy | Done and moves every card to Drafts.') ?>
+ + + + += t('QualKard is enabled on this board.') ?>
+ + + + +