Compare commits

..

No commits in common. "master" and "0.0.16" have entirely different histories.

14 changed files with 59 additions and 266 deletions

View File

@ -1,25 +1,15 @@
# baikal-storage-plugin # baikal-storage-plugin
## Note
Plugin is still a work in progress
## Install ## Install
``` ```
composer require aerex/baikal-storage-plugin composer require aerex/baikal-storage-plugin
``` ```
## Configuration ## Configuration
You can use the CLI to help you generate a config file or use the example configuration provided in the project. Make sure the file is *writable* by your webserver (e.g Apache, Nginx). Copy sample configuration to your baikal installation. Make sure that the folder is writable
### Use the CLI
Run the command `./vendor/baikalstorage` and follow the instructions
### Manual
Copy the `example-config.yaml` file and rename it to `config.yaml`.
## Usage ## Usage
- Add the plugin to the end of the `Server.php` file located under `Core/Frameworks/Baikal/Core/Server.php`. For example - Add the plugin to `Core/Frameworks/Baikal/Core/Server.php`
``` ```
$this->server->addPlugin(new \Aerex\BaikalStorage\Plugin(<absolute/path/of/config.yaml) $this->server->addPlugin(new \Aerex\BaikalStorage\Plugin(<path-of-config-file>))
``` ```

View File

@ -1,12 +0,0 @@
#!/usr/bin/env php
<?php
require __DIR__.'/../vendor/autoload.php';
use Symfony\Component\Console\Application;
use Aerex\BaikalStorage\Commands\CreateConfigFileCommand;
$application = new Application();
$application->add(new CreateConfigFileCommand());
$application->setName('Baikal Storage Plugin');
$application->run();

View File

@ -15,9 +15,6 @@
"url": "https://git.aerex.me/Aerex/baikal-storage-plugin" "url": "https://git.aerex.me/Aerex/baikal-storage-plugin"
} }
], ],
"bin": [
"bin/baikalstorage"
],
"require": { "require": {
"php": ">=5.5", "php": ">=5.5",
"sabre/dav" : "~4.1.0", "sabre/dav" : "~4.1.0",
@ -29,8 +26,7 @@
"symfony/config": "3.4", "symfony/config": "3.4",
"symfony/process": "^3.4", "symfony/process": "^3.4",
"monolog/monolog": "^2.0", "monolog/monolog": "^2.0",
"symfony/yaml": "~3.0|~4.0", "symfony/yaml": "~3.0|~4.0"
"symfony/console": "^3.4|^4.0|^5.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^8.5.3" "phpunit/phpunit": "^8.5.3"

7
config.yaml Executable file
View File

@ -0,0 +1,7 @@
logger:
file: /home/aerex/baikal-storage-plugin.log
level: 'DEBUG'
taskwarrior:
taskdata: /home/aerex/.task
taskrc: /home/aerex/.taskrc
project_category_prefix: project_

View File

@ -1,6 +0,0 @@
logger:
file: /home/user/baikal-storage-plugin.log
level: 'DEBUG'
taskwarrior:
taskdata: /home/user/.task
taskrc: /home/user/.taskrc

View File

@ -1,158 +0,0 @@
<?php
namespace Aerex\BaikalStorage\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Monolog\Logger as Monolog;
use Aerex\BaikalStorage\Configs\ConfigBuilder;
class CreateConfigFileCommand extends Command {
protected $io;
protected static $defaultName = 'app:create-config';
protected static $CONFIG_FILE_NAME = 'config.yaml';
protected static $LOGGER_FILE_NAME = 'log';
private $configs = [];
private $fullFilePath;
public function addtaskwarriorConfig() {
$configs = [];
$filePath = $this->io->askQuestion(new Question('Where is the location for the taskrc file?'));
$taskrcFilePath = $filePath . '/.taskrc';
if (!file_exists($taskrcFilePath)) {
throw new \RuntimeException("The taskrc file at $taskrcFilePath does not exist");
}
$configs['taskrc'] = $taskrcFilePath;
$filePath = $this->io->askQuestion(new Question('Where is the data location (taskdata)'));
$taskDataFilePath = $filePath;
if (!file_exists($taskDataFilePath)) {
throw new \RuntimeException("The task.data location $taskDataFilePath does not exist");
}
$configs['taskdata'] = $taskDataFilePath;
$displayName = $this->io->askQuestion(new Question('What is the displayname for the default calendar (e.g default)'));
$configs['default_calendar'] = isset($displayName) ? $displayName : 'default';
$this->configs['storages']['taskwarior'] = $configs;
}
protected function configure() {
$this
->setName('create-config')
->setDescription('Create baikal storage plugin configuration file');
}
protected function autocompleteFilePathCallback(string $input): array {
// Strip any characters from the last slash to the end of the string
// to keep only the last directory and generate suggestions for it
$inputPath = preg_replace('%(/|^)[^/]*$%', '$1', $input);
$inputPath = '' === $inputPath ? '.': $inputPath;
$foundFilesAndDirs = scandir($inputPath) ?: [];
// Excluded restricted directories
$restrictedDirs = ['dev', 'bin', 'boot', 'etc', 'input', 'lib',
'lib64', 'mnt', 'proc', 'run', 'sbin', 'sys', 'srv', 'usr', 'var'];
$foundSafeFileAndDirs = array_diff($foundFilesAndDirs, $restrictedDirs);
return array_map(function($dirOrFile) use ($inputPath) {
return $inputPath.$dirOrFile;
}, $foundSafeFileAndDirs);
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->io = new SymfonyStyle($input, $output);
$this->io->title('Baikal Storage Plugin Configuration Creation');
// TODO: move create config file code block to function
$question = new Question('Where to create `config.yaml` configuration file?');
$question->setAutocompleterCallback($this->autocompleteFilePathCallback);
$filePath = $this->io->askQuestion($question);
try {
$this->fullFilePath = $this->verifyAndCreateFile($filePath, CreateConfigFileCommand::$CONFIG_FILE_NAME);
$this->setupGeneralConfigs()
->setupStorages()
->saveConfigs();
} catch (\Exception $e) {
return Command::FAILURE;
}
$this->output->writeln("`config.yaml file created at $filePath");
return Command::SUCCESS;
}
protected function getStorageNames() {
$storageDir = __DIR__ . '/../Storages';
$storageFiles = scandir($storageDir);
$storages = array_values(array_filter($storageFiles, function($storageFile) use ($storageDir) {
return !preg_match('/^IStorage/', $storageFile) && !is_dir("$storageDir/$storageFile");
}));
array_walk($storages, function(&$storage) {
$storage = strtolower(preg_replace('/\.php/', '', $storage));
});
return $storages;
}
protected function verifyAndCreateFile($filePath, $fileName) {
if (empty($filePath)) {
throw new \RuntimeException('Configuration file path cannot be empty');
}
if (!is_dir($filePath)) {
throw new \RuntimeException("File path $filePath does not exist");
}
if ((strrpos($filePath, '/') + 1) === strlen($filePath)) {
$fullFilePath = substr($filePath, 0, -1) . $fileName;
} else {
$fullFilePath = $filePath . '/' . $fileName;
}
if (file_exists($fullFilePath)) {
throw new \RuntimeException("The configuration file at $this->fullFilePath already exists");
}
return $fullFilePath;
}
protected function setupGeneralConfigs() {
$this->io->section('Setting up general configurations');
$this->configs['general'] = [];
if ($this->io->confirm('Enable logging?')) {
$this->configs['general']['logger'] = [];
$logFilePath = $this->io->askQuestion(new Question('Where to create log file?'));
$this->configs['general']['logger']['file'] = $this->verifyAndCreateFile($logFilePath, CreateConfigFileCommand::$LOGGER_FILE_NAME);
$logLevelChoiceQuestion = new ChoiceQuestion('Log level (defaults to ERROR)', array_keys(Monolog::getLevels()), 4);
$logLevelChoiceQuestion->setErrorMessage('Log level %s is invalid');
$logLevel = $this->io->askQuestion($logLevelChoiceQuestion);
$this->configs['general']['logger']['level'] = $logLevel;
}
return $this;
}
protected function setupStorages() {
$this->configs['storages'] = [];
$this->io->section('Setup storage configurations');
$storagesMultiSelectQuestion = new ChoiceQuestion('Select the storages to add', $this->getStorageNames());
$storagesMultiSelectQuestion->setMultiselect(true);
$storagesMultiSelectQuestion->setErrorMessage('Storages %s is not supported');
$storages = $this->io->askQuestion($storagesMultiSelectQuestion);
foreach ($storages as $storage) {
$methodName = "add${storage}Config";
$storageConfigMethod = new \ReflectionMethod(CreateConfigFileCommand::class, $methodName);
$storageConfigMethod->invoke($this);
}
return $this;
}
protected function saveConfigs() {
$configBuilder = new ConfigBuilder($this->fullFilePath);
$configBuilder->saveConfigs($this->configs);
}
}

View File

@ -17,12 +17,12 @@ class TaskwarriorConfig {
->defaultValue('~/.taskrc') ->defaultValue('~/.taskrc')
->info('The enivronment variable overrides the default and the command line specification of the .taskrc file') ->info('The enivronment variable overrides the default and the command line specification of the .taskrc file')
->end() ->end()
->scalarNode('default_calendar') ->scalarNode('project_category_prefix')
->info('The default calendar to send tasks if no task project is set. The value is the calendar\'s displayname') ->defaultValue('project_')
->info('The word after the given prefix for a iCal category will be used to identify a task\'s project')
->end() ->end()
->end(); ->end();
return $node; return $node;
} }
} }

View File

@ -33,10 +33,9 @@ class Plugin extends ServerPlugin {
*/ */
protected $storageManager; protected $storageManager;
/**
* @var $rawconfigs
*/
protected $rawConfigs; protected $rawConfigs;
/** /**
* Creates the Storage plugin * Creates the Storage plugin
@ -50,16 +49,6 @@ class Plugin extends ServerPlugin {
$this->initializeStorages($this->rawConfigs); $this->initializeStorages($this->rawConfigs);
} }
private function getDisplayName($path) {
// Remove filepath (e.g Remove xxxx.ics from calendars/collection_name/xxxx.ics)
$urlParts = explode('/', $path);
$calendarUrl = implode('/', array_slice($urlParts, 0, sizeof($urlParts)-1));
// Get displayname from collection
$properties = $this->server->getProperties($calendarUrl, ['{DAV:}displayname']);
return $properties['{DAV:}displayname'] ?? '';
}
public function buildConfigurations($configFile) { public function buildConfigurations($configFile) {
$this->config = new ConfigBuilder($configFile); $this->config = new ConfigBuilder($configFile);
$this->config->add(new TaskwarriorConfig()); $this->config->add(new TaskwarriorConfig());
@ -72,8 +61,7 @@ class Plugin extends ServerPlugin {
*/ */
public function initializeStorages($configs) { public function initializeStorages($configs) {
$taskwarrior = new Taskwarrior(new Console(['rc.verbose=nothing', $taskwarrior = new Taskwarrior(new Console(['rc.verbose=nothing', 'rc.hooks=off', 'rc.confirmation=no']), $configs, new Logger($configs, 'Taskwarrior'));
'rc.hooks=off', 'rc.confirmation=no']), $configs, new Logger($configs, 'Taskwarrior'));
$this->storageManager->addStorage(Taskwarrior::NAME, $taskwarrior); $this->storageManager->addStorage(Taskwarrior::NAME, $taskwarrior);
} }
@ -126,6 +114,8 @@ class Plugin extends ServerPlugin {
return; return;
} }
} }
/** /**
* This method handles the PUT method. * This method handles the PUT method.
@ -137,11 +127,8 @@ class Plugin extends ServerPlugin {
function httpPut(RequestInterface $request){ function httpPut(RequestInterface $request){
$body = $request->getBodyAsString(); $body = $request->getBodyAsString();
$vCal = \Sabre\VObject\Reader::read($body); $vCal = \Sabre\VObject\Reader::read($body);
$displayname = $this->getDisplayName($request->getPath());
try { try {
if (!$this->storageManager->fromStorageSource($vCal)) { $this->storageManager->import($vCal);
$this->storageManager->import($vCal, $displayname);
}
} catch(BadRequest $e){ } catch(BadRequest $e){
throw new BadRequest($e->getMessage(), null, $e); throw new BadRequest($e->getMessage(), null, $e);
} catch(\Exception $e){ } catch(\Exception $e){
@ -200,10 +187,7 @@ class Plugin extends ServerPlugin {
$paths = explode('/', $path); $paths = explode('/', $path);
if (sizeof($paths) > 1) { if (sizeof($paths) > 1) {
$uid = str_replace('.ics', '', $paths[sizeof($paths)-1]); $uid = str_replace('.ics', '', $paths[sizeof($paths)-1]);
// Attempt to delete if we are removing an ics file $this->storageManager->remove($uid);
if ($uid != '') {
$this->storageManager->remove($uid);
}
} }
} catch(BadRequest $e){ } catch(BadRequest $e){
throw new BadRequest($e->getMessage(), null, $e); throw new BadRequest($e->getMessage(), null, $e);
@ -300,11 +284,10 @@ class Plugin extends ServerPlugin {
return [ return [
'name' => $this->getPluginName(), 'name' => $this->getPluginName(),
'description' => 'The plugin provides synchronization between remote storages and iCal todo events', 'description' => 'The plugin provides synchronization between taskwarrior tasks and iCAL events',
'link' => 'https://git.aerex.me/Aerex/baikal-storage-plugin', 'link' => null,
'config' => true 'config' => true
]; ];
} }
} }

View File

@ -22,18 +22,6 @@ class StorageManager {
$this->configs = $configs; $this->configs = $configs;
} }
public function fromStorageSource(Calendar $calendar) {
if (!isset($this->configs)) {
throw new \Exception('StorageManager was not initialize or configs are not defined');
}
foreach ($this->configs['storages'] as $storage => $value) {
if (stristr($calendar->PRODID, $storage)) {
return true;
}
}
return false;
}
public function getStorages() { public function getStorages() {
return $this->storages; return $this->storages;
} }
@ -46,7 +34,7 @@ class StorageManager {
$this->storages[$name] = $storage; $this->storages[$name] = $storage;
} }
public function import(Calendar $calendar, string $displayname) { public function import(Calendar $calendar) {
if (!isset($this->configs)) { if (!isset($this->configs)) {
throw new \Exception('StorageManger was not initialize or configs are not defined'); throw new \Exception('StorageManger was not initialize or configs are not defined');
} }
@ -55,7 +43,7 @@ class StorageManager {
if (!isset($storage)){ if (!isset($storage)){
throw new \Exception(); throw new \Exception();
} }
$storage->save($calendar, $displayname); $storage->save($calendar);
} }
} }
@ -72,4 +60,3 @@ class StorageManager {
} }
} }
} }

View File

@ -5,10 +5,9 @@ namespace Aerex\BaikalStorage\Storages;
use Sabre\VObject\Component\VCalendar as Calendar; use Sabre\VObject\Component\VCalendar as Calendar;
interface IStorage { interface IStorage {
public function save(Calendar $c, string $displayname); public function save(Calendar $c);
public function remove($uid); public function remove($uid);
public function refresh(); public function refresh();
public function getConfigBrowser(); public function getConfigBrowser();
public function updateConfigs($postData); public function updateConfigs($postData);
} }

View File

@ -18,6 +18,11 @@ class Taskwarrior implements IStorage {
} }
public function getConfigBrowser() { public function getConfigBrowser() {
$html = '<tr>';
$html .= '<th>taskrc</th>';
$html .= '<td>The enivronment variable overrides the default and the command line specification of the .taskrc file</td>';
$html .= '<td><input name="tw_taskrc" type="text" value="' . $this->configs['taskrc'] . '"></td>';
$html .= '</tr>';
$html = '<tr>'; $html = '<tr>';
$html .= '<th>taskrc</th>'; $html .= '<th>taskrc</th>';
$html .= '<td>The enivronment variable overrides the default and the command line specification of the .taskrc file</td>'; $html .= '<td>The enivronment variable overrides the default and the command line specification of the .taskrc file</td>';
@ -29,9 +34,9 @@ class Taskwarrior implements IStorage {
$html .= '<td><input name="tw_taskdata" type="text" value="' . $this->configs['taskdata'] . '"></td>'; $html .= '<td><input name="tw_taskdata" type="text" value="' . $this->configs['taskdata'] . '"></td>';
$html .= '</tr>'; $html .= '</tr>';
$html .= '<tr>'; $html .= '<tr>';
$html .= '<th>default_calendar</th>'; $html .= '<th>project_category_prefix</th>';
$html .= '<td>The default calendar to send tasks if no task project is set. The value is the calendar\'s displayname</td>'; $html .= "<td>The word after the given prefix for a iCal category will be used to identify a task's project</td>";
$html .= '<td><input name="tw_default_calendar" type="text" value="' . $this->configs['default_calendar'] . '"></td>'; $html .= '<td><input name="tw_project_category_prefix" placeholder ="project_" name="tw_project_category_prefix" type="text" value="' . $this->configs['project_category_prefix'] . '"></td>';
$html .= '</tr>'; $html .= '</tr>';
return $html; return $html;
} }
@ -44,8 +49,9 @@ class Taskwarrior implements IStorage {
if (isset($postData['tw_taskdata'])){ if (isset($postData['tw_taskdata'])){
$this->configs['taskdata'] = $postData['tw_taskdata']; $this->configs['taskdata'] = $postData['tw_taskdata'];
} }
if (isset($postData['tw_default_calendar'])){
$this->configs['default_calendar'] = $postData['tw_default_calendar']; if (isset($postData['tw_project_category_prefix'])){
$this->configs['project_category_prefix'] = $postData['tw_project_category_prefix'];
} }
return $this->configs; return $this->configs;
@ -53,8 +59,7 @@ class Taskwarrior implements IStorage {
} }
public function refresh() { public function refresh() {
$this->logger->info('Syncing taskwarrior tasks...'); $output = $this->console->execute('task', ['sync'], null,
$this->console->execute('task', ['sync'], null,
['TASKRC' => $this->configs['taskrc'],'TASKDATA' => $this->configs['taskdata']]); ['TASKRC' => $this->configs['taskrc'],'TASKDATA' => $this->configs['taskdata']]);
$this->tasks = json_decode($this->console->execute('task', ['export'], null, $this->tasks = json_decode($this->console->execute('task', ['export'], null,
['TASKRC' => $this->configs['taskrc'], 'TASKDATA' => $this->configs['taskdata']]), true); ['TASKRC' => $this->configs['taskrc'], 'TASKDATA' => $this->configs['taskdata']]), true);
@ -63,9 +68,10 @@ class Taskwarrior implements IStorage {
$this->tasks[$task['uid']] = $task; $this->tasks[$task['uid']] = $task;
} }
} }
$this->logger->info($output);
} }
public function vObjectToTask($vtodo, string $displayname) { public function vObjectToTask($vtodo) {
if (isset($this->tasks[(string)$vtodo->UID])) { if (isset($this->tasks[(string)$vtodo->UID])) {
$task = $this->tasks[(string)$vtodo->UID]; $task = $this->tasks[(string)$vtodo->UID];
} else { } else {
@ -123,10 +129,8 @@ class Taskwarrior implements IStorage {
} }
} }
if (isset($vtodo->DUE)) { if (isset($vtodo->DUE)){
$task['due'] = $vtodo->DUE->getDateTime()->format(\DateTime::ISO8601); $task['due'] = $vtodo->DUE->getDateTime()->format(\DateTime::ISO8601);
} else if (isset($task['due'])) {
$task['due'] = '';
} }
if (isset($vtodo->RRULE)) { if (isset($vtodo->RRULE)) {
@ -160,35 +164,40 @@ class Taskwarrior implements IStorage {
} }
if (isset($vtodo->CATEGORIES)) { if (isset($vtodo->CATEGORIES)) {
$task['tags'] = $vtodo->CATEGORIES->getJsonValue(); $task['tags'] = [];
foreach ($vtodo->CATEGORIES as $category) {
if (isset($this->configs['project_category_prefix'])) {
$projTagSuffixRegExp = sprintf('/^%s/', $this->configs['project_category_prefix']);
if (preg_match($projTagSuffixRegExp, $category)) {
$task['project'] = preg_replace($projTagSuffixRegExp, '', $category);
continue;
}
}
$task['tags'] = $category;
}
} }
if (isset($vtodo->GEO)){ if (isset($vtodo->GEO)){
$task['geo'] = $vtodo->GEO->getRawMimeDirValue(); $task['geo'] = $vtodo->GEO->getRawMimeDirValue();
} }
if ($this->configs['default_calendar'] != $displayname) {
$task['project'] = $displayname;
}
return $task; return $task;
} }
public function save(Calendar $c, string $displayname) { public function save(Calendar $c) {
try { try {
if (!isset($c->VTODO)){ if (!isset($c->VTODO)){
throw new \Exception('Calendar event does not contain VTODO'); throw new \Exception('Calendar event does not contain VTODO');
} }
$this->logger->info(json_encode($c->jsonSerialize())); $this->logger->info(json_encode($c->jsonSerialize()));
$this->refresh(); $this->refresh();
$task = $this->vObjectToTask($c->VTODO, $displayname); $task = $this->vObjectToTask($c->VTODO);
$this->logger->info(json_encode($task)); $this->logger->info(json_encode($task));
$this->logger->info( $this->logger->info(
sprintf('Executing TASKRC = %s TASKDATA = %s task import %s', $this->configs['taskrc'], $this->configs['taskdata'], json_encode($task)) sprintf('Executing TASKRC = %s TASKDATA = %s task import %s', $this->configs['taskrc'], $this->configs['taskdata'], json_encode($task))
); );
$output = $this->console->execute('task', ['import'], $task, $output = $this->console->execute('task', ['import'], $task,
['TASKRC' => $this->configs['taskrc'],'TASKDATA' => $this->configs['taskdata']]); ['TASKRC' => $this->configs['taskrc'],'TASKDATA' => $this->configs['taskdata']]);
$this->refresh();
$this->logger->info($output); $this->logger->info($output);
} catch (\Exception $e) { } catch (\Exception $e) {
$this->logger->error($e->getTraceAsString()); $this->logger->error($e->getTraceAsString());
@ -200,10 +209,6 @@ class Taskwarrior implements IStorage {
try { try {
$this->logger->info(sprintf('Deleting iCal %s from taskwarrior', $uid)); $this->logger->info(sprintf('Deleting iCal %s from taskwarrior', $uid));
$this->refresh(); $this->refresh();
if (!array_key_exists((string)$uid, $this->tasks)) {
$this->logger->warn(sprintf('Could not find task %s to be remove. Skipping', (string)$uid));
return;
}
$task = $this->tasks[(string)$uid]; $task = $this->tasks[(string)$uid];
if (isset($task) && $task['status'] !== 'deleted') { if (isset($task) && $task['status'] !== 'deleted') {
$uuid = $task['uuid']; $uuid = $task['uuid'];
@ -213,7 +218,6 @@ class Taskwarrior implements IStorage {
$output = $this->console->execute('task', ['delete', (string)$uuid], null, $output = $this->console->execute('task', ['delete', (string)$uuid], null,
['TASKRC' => $this->configs['taskrc'],'TASKDATA' => $this->configs['taskdata']]); ['TASKRC' => $this->configs['taskrc'],'TASKDATA' => $this->configs['taskdata']]);
$this->logger->info($output); $this->logger->info($output);
$this->refresh();
} else if (isset($task) && $task['status'] === 'deleted') { } else if (isset($task) && $task['status'] === 'deleted') {
$this->logger->warn(sprintf('Task %s has already been deleted', $task['uuid'])); $this->logger->warn(sprintf('Task %s has already been deleted', $task['uuid']));
} else { } else {

View File

@ -8,8 +8,8 @@
beStrictAboutTestsThatDoNotTestAnything="true"> beStrictAboutTestsThatDoNotTestAnything="true">
<testsuite name="Baikal Storage Plugin Tests"> <testsuite name="Baikal Storage Plug Tests">
<directory>./tests/</directory> <directory>./lib/tests/</directory>
</testsuite> </testsuite>
<filter> <filter>

View File

@ -40,6 +40,8 @@ class ConfigTest extends TestCase {
$this->assertEquals($taskwarriorConfigs['taskrc'], '/home/aerex/.taskrc'); $this->assertEquals($taskwarriorConfigs['taskrc'], '/home/aerex/.taskrc');
$this->assertArrayHasKey('taskdata', $taskwarriorConfigs, 'taskwarrior config is missing taskdata property'); $this->assertArrayHasKey('taskdata', $taskwarriorConfigs, 'taskwarrior config is missing taskdata property');
$this->assertEquals($taskwarriorConfigs['taskdata'], '/home/aerex/.task'); $this->assertEquals($taskwarriorConfigs['taskdata'], '/home/aerex/.task');
$this->assertArrayHasKey('project_category_prefix', $taskwarriorConfigs, 'taskwarrior config is missing project_category_prefix property');
$this->assertEquals($taskwarriorConfigs['project_category_prefix'], 'project_');
} }
} }

View File

@ -6,3 +6,4 @@ storages:
taskwarrior: taskwarrior:
taskdata: /home/aerex/.task taskdata: /home/aerex/.task
taskrc: /home/aerex/.taskrc taskrc: /home/aerex/.taskrc
project_category_prefix: project_