5 Commits
v1.0 ... v1.1.4

4 changed files with 63 additions and 14 deletions

View File

@@ -17,6 +17,11 @@ class QualKardHelper extends Base
{ {
const SCORE = array('hairy' => 0.0, 'hard' => 1.3, 'medium' => 3.0, 'easy' => 5.0); const SCORE = array('hairy' => 0.0, 'hard' => 1.3, 'medium' => 3.0, 'easy' => 5.0);
// Stored qualkard_avg value for an unread card. A literal, non-empty marker: it survives PHP's
// empty() -- which drops the numeric string "0" on save -- and stays distinct from a real average
// of 0 (a card graded hairy from new). Graded averages are stored with decimals for the same reason.
const UNREAD = 'new';
/** /**
* Running average. New card (unread, $old === null) seeds at N - 23%; otherwise the qualcard mean. * Running average. New card (unread, $old === null) seeds at N - 23%; otherwise the qualcard mean.
*/ */
@@ -81,6 +86,18 @@ class QualKardHelper extends Base
return array('label' => $pct.'%', 'class' => $class); return array('label' => $pct.'%', 'class' => $class);
} }
/**
* A stored qualkard_avg -> float, or null when the card is unread. The unread marker is the
* literal self::UNREAD; '' and null are also treated as unread for any card written before it.
*
* @param string|null $raw
* @return float|null
*/
private static function parseAvg($raw)
{
return ($raw === '' || $raw === null || $raw === self::UNREAD) ? null : (float) $raw;
}
/** /**
* Is QualKard enabled for this board? * Is QualKard enabled for this board?
* *
@@ -151,6 +168,13 @@ class QualKardHelper extends Base
'recoreco_frequency' => 'daily', 'recoreco_frequency' => 'daily',
'recoreco_anchor' => 0, 'recoreco_anchor' => 0,
)); ));
// QualKard adopts the card as a fresh flashcard, so wipe any stale RecoReco copy lineage. A
// card that was a RecoReco duplicate carries recoreco_clone/recoreco_source; left in place it
// makes an armed QualKard card look like a copy (yellow icon, "cannot be made recurring"
// modal) while the cron still moves it. Removing them makes it a clean move-mode template.
$this->taskMetadataModel->remove($task_id, 'recoreco_clone');
$this->taskMetadataModel->remove($task_id, 'recoreco_source');
} }
/** /**
@@ -168,7 +192,7 @@ class QualKardHelper extends Base
} }
$this->taskPositionModel->movePosition($project_id, $task_id, $this->col($project_id, 'draft'), 1, (int) $task['swimlane_id'], false); $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->taskMetadataModel->save($task_id, array('qualkard_avg' => self::UNREAD)); // brand-new card
$this->arm($task_id, $project_id); $this->arm($task_id, $project_id);
$this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(array('date_due' => 0)); $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(array('date_due' => 0));
} }
@@ -185,6 +209,25 @@ class QualKardHelper extends Base
} }
} }
/**
* Surgically un-study ONE card that the user dropped back into Drafts: wipe its mastery to
* unread, clear the due date, and make it inert to RecoReco. Unlike resetCard() this does not
* move or re-arm the card (it is already in Drafts and already armed) -- a lighter, per-card
* counterpart to the board-wide Reset.
*
* The anchor is REMOVED, not saved as 0: Kanboard's metadata save drops the string "0" (empty()),
* which would leave the old positive anchor in place and let RecoReco pull the card back out of
* Drafts on the next cron. A removed key reads back as the default 0, so RecoReco skips it.
*
* @param int $task_id
*/
public function park($task_id)
{
$this->taskMetadataModel->save($task_id, array('qualkard_avg' => self::UNREAD)); // mastery -> new
$this->taskMetadataModel->remove($task_id, 'recoreco_anchor'); // -> inert
$this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(array('date_due' => 0));
}
/** /**
* Grade a card by a score (a grade column's value): update the running average, compute the next * 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. * due date, and arm the anchor. RecoReco moves the card from here.
@@ -194,13 +237,14 @@ class QualKardHelper extends Base
*/ */
public function grade($task_id, $score) public function grade($task_id, $score)
{ {
$raw = $this->taskMetadataModel->get($task_id, 'qualkard_avg', ''); $raw = $this->taskMetadataModel->get($task_id, 'qualkard_avg', self::UNREAD);
$old = ($raw === '' || $raw === null) ? null : (float) $raw; $old = self::parseAvg($raw);
$avg = self::nextAverage($old, $score); $avg = self::nextAverage($old, $score);
$due = self::dueFrom($avg); $due = self::dueFrom($avg);
// Store with decimals so an exact 0 is "0.0000", never the empty()-dropped "0".
$this->taskMetadataModel->save($task_id, array( $this->taskMetadataModel->save($task_id, array(
'qualkard_avg' => (string) $avg, 'qualkard_avg' => sprintf('%.4f', $avg),
'recoreco_anchor' => $due, 'recoreco_anchor' => $due,
)); ));
$this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(array('date_due' => $due)); $this->db->table(TaskModel::TABLE)->eq('id', $task_id)->update(array('date_due' => $due));
@@ -214,9 +258,8 @@ class QualKardHelper extends Base
*/ */
public function cardBadge($task_id) public function cardBadge($task_id)
{ {
$raw = $this->taskMetadataModel->get($task_id, 'qualkard_avg', ''); $raw = $this->taskMetadataModel->get($task_id, 'qualkard_avg', self::UNREAD);
$avg = ($raw === '' || $raw === null) ? null : (float) $raw;
return self::badge($avg); return self::badge(self::parseAvg($raw));
} }
} }

View File

@@ -47,6 +47,11 @@ class Plugin extends Base
return; return;
} }
} }
// Dropped back into Drafts -> surgically un-study this one card (new badge, no due, inert).
if ($dst === $helper->col($task['project_id'], 'draft')) {
$helper->park($task['id']);
}
}); });
// Mastery badge under the card title on the board, plus its stylesheet. // Mastery badge under the card title on the board, plus its stylesheet.
@@ -81,7 +86,7 @@ class Plugin extends Base
public function getPluginVersion() public function getPluginVersion()
{ {
return '1.0.0'; return '1.1.4';
} }
public function getPluginHomepage() public function getPluginHomepage()

View File

@@ -112,15 +112,16 @@ harmlessly in place -- your columns and cards are untouched.
AGPL-3.0. See LICENSE. AGPL-3.0. See LICENSE.
## More Kanboard plugins by Ruben (drbeco) ## More Kanboard plugins by Dr. Beco
All free and AGPL-3.0, at [code.beco.cc](https://code.beco.cc/beco): All free and AGPL-3.0, at [code.beco.cc](https://code.beco.cc/beco):
- **[RecoReco](https://code.beco.cc/beco/RecoReco)** -- calendar-scheduled recurring cards (yearly, - **[RecoReco](https://code.beco.cc/beco/RecoReco)** -- calendar-scheduled recurring cards (yearly,
monthly, weekly, daily), driven by the card due date. QualKard rides RecoReco's move engine to monthly, weekly, daily), driven by the card due date.
bring due flashcards back to the Study column. - **[FinanceBuddy](https://code.beco.cc/beco/FinanceBuddy)** -- attach a money value (debit/credit
- **[FinanceBuddy](https://code.beco.cc/beco/FinanceBuddy)** -- attach a money value to each card, and installments) to cards, shown on the card and totalled per column.
show it on the card, and total it per column, so a board can double as a simple finance board. - **[WorkspaceOrg](https://code.beco.cc/beco/WorkspaceOrg)** -- group your projects into personal
per-user workspaces, shown on a "My workspaces" dashboard page with a badge on each project.
- **[OrganonTweaks](https://code.beco.cc/beco/OrganonTweaks)** -- an umbrella of small - **[OrganonTweaks](https://code.beco.cc/beco/OrganonTweaks)** -- an umbrella of small
quality-of-life board tweaks: remove an empty column, always show the comment icon, emphasize due quality-of-life board tweaks: remove an empty column, always show the comment icon, emphasize due
dates, extra search keywords and shared board filters, and more. dates, extra search keywords and shared board filters, and more.

View File

@@ -1 +1 @@
QualKard v1.0 QualKard v1.1.4