getTask(); $columns = $this->targetColumns($task); $meta = $this->taskMetadataModel->getAll($task['id']); if (empty($values)) { $values = $this->getStoredValues($meta, $task, $columns); } $this->response->html($this->template->render('recoReco:recurrence/edit', array( 'task' => $task, 'values' => $values, 'errors' => $errors, 'columns_list' => $columns, 'frequency_list' => $this->getFrequencyList(), 'has_due_date' => ! empty($task['date_due']), 'is_native' => $task['recurrence_status'] != TaskModel::RECURRING_STATUS_NONE, 'has_target' => ! empty($columns), 'is_clone' => isset($meta['recoreco_clone']) && $meta['recoreco_clone'] == 1, 'source_id' => isset($meta['recoreco_source']) ? (int) $meta['recoreco_source'] : 0, ))); } public function save() { $task = $this->getTask(); $input = $this->request->getValues(); $columns = $this->targetColumns($task); // A clone can never be made recurring (that would recurse). Guard even though the modal // hides the form for clones. $is_clone = (int) $this->taskMetadataModel->get($task['id'], 'recoreco_clone', 0) === 1; // Only a plain card (a due date, not native-recurring, not a clone, a valid target) may enable. $can_recur = ! empty($task['date_due']) && $task['recurrence_status'] == TaskModel::RECURRING_STATUS_NONE && ! $is_clone && ! empty($columns); $enabled = ($can_recur && isset($input['recoreco_enabled']) && $input['recoreco_enabled'] == 1) ? 1 : 0; $frequency = isset($input['recoreco_frequency']) && array_key_exists($input['recoreco_frequency'], $this->getFrequencyList()) ? $input['recoreco_frequency'] : 'monthly_day'; // Keep the lead time under one period (else a copy's fire time crosses the prior occurrence). $daysBefore = isset($input['recoreco_days_before']) && ctype_digit((string) $input['recoreco_days_before']) ? (int) $input['recoreco_days_before'] : 0; $daysBefore = min($daysBefore, $this->maxDaysBefore($frequency)); $values = array( 'recoreco_enabled' => $enabled, 'recoreco_target_column' => $this->resolveTarget(isset($input['recoreco_target_column']) ? $input['recoreco_target_column'] : 0, $columns), 'recoreco_frequency' => $frequency, 'recoreco_last_day' => isset($input['recoreco_last_day']) ? 1 : 0, 'recoreco_days_before' => $daysBefore, 'recoreco_link_copies' => isset($input['recoreco_link_copies']) ? 1 : 0, ); // Capture the anchor (the due date at enable time) -- the drift-free pattern. if ($enabled) { $values['recoreco_anchor'] = (int) $task['date_due']; } $this->taskMetadataModel->save($task['id'], $values); // Reconcile the template's duplicate links with the setting right away. $model = new \Kanboard\Plugin\RecoReco\Model\RecoRecoModel($this->container); $model->syncCloneLinks($task['id'], $values['recoreco_link_copies'] == 1); $this->flash->success(t('Recurring schedule saved.')); return $this->response->redirect($this->helper->url->to('TaskViewController', 'show', array('task_id' => $task['id'])), true); } // Columns the copy can land in: every column except the template's own (the copy must move). private function targetColumns(array $task) { $columns = $this->columnModel->getList($task['project_id']); unset($columns[$task['column_id']]); return $columns; } // A stored/posted target, if still a valid non-current column; otherwise the first one (or 0). private function resolveTarget($candidate, array $columns) { $candidate = (int) $candidate; if (array_key_exists($candidate, $columns)) { return $candidate; } $keys = array_keys($columns); return empty($keys) ? 0 : (int) $keys[0]; } private function getStoredValues(array $meta, array $task, array $columns) { return array( 'recoreco_enabled' => isset($meta['recoreco_enabled']) ? (int) $meta['recoreco_enabled'] : 0, 'recoreco_target_column' => $this->resolveTarget(isset($meta['recoreco_target_column']) ? $meta['recoreco_target_column'] : 0, $columns), 'recoreco_frequency' => isset($meta['recoreco_frequency']) ? $meta['recoreco_frequency'] : 'monthly_day', 'recoreco_last_day' => isset($meta['recoreco_last_day']) ? (int) $meta['recoreco_last_day'] : 0, 'recoreco_days_before' => isset($meta['recoreco_days_before']) ? (int) $meta['recoreco_days_before'] : 0, 'recoreco_link_copies' => isset($meta['recoreco_link_copies']) ? (int) $meta['recoreco_link_copies'] : 0, ); } private function getFrequencyList() { return array( 'yearly' => t('Yearly'), 'monthly_day' => t('Monthly by day'), 'monthly_dow' => t('Monthly by weekday'), 'weekly' => t('Weekly'), 'daily' => t('Daily'), ); } /** * The largest allowed "days before" for a frequency -- kept strictly under one period so a * copy's fire time never crosses the previous occurrence. Daily = 0 (disabled). */ private function maxDaysBefore($frequency) { switch ($frequency) { case 'daily': return 0; case 'weekly': return 6; case 'yearly': return 364; default: // monthly_day, monthly_dow return 27; } } }