diff --git a/composer.json b/composer.json
index 0b1e408..76623bc 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,8 @@
"php": ">=7.2",
"ext-sqlite3": "*",
"ext-pdo_sqlite": "*",
- "lesstif/php-jira-rest-client": "^1.35"
+ "lesstif/php-jira-rest-client": "^1.35",
+ "morningtrain/toggl-api": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "~6",
diff --git a/composer.lock b/composer.lock
index 280f539..33383f8 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "816c13378857ed822b07c7c66b0219fb",
+ "content-hash": "674ae05c182538ff13c42977a20423a3",
"packages": [
{
"name": "doctrine/annotations",
@@ -1031,6 +1031,54 @@
],
"time": "2017-06-19T01:22:40+00:00"
},
+ {
+ "name": "morningtrain/toggl-api",
+ "version": "v1.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Morning-Train/toggl-api.git",
+ "reference": "231f9bdb7637652e447829bdee694f205b4b106c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Morning-Train/toggl-api/zipball/231f9bdb7637652e447829bdee694f205b4b106c",
+ "reference": "231f9bdb7637652e447829bdee694f205b4b106c",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^6.3"
+ },
+ "require-dev": {
+ "escapestudios/symfony2-coding-standard": "2.*",
+ "squizlabs/php_codesniffer": "2.*"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "MorningTrain\\TogglApi\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GNU General Public License v3.0"
+ ],
+ "authors": [
+ {
+ "name": "Bjarne Bonde",
+ "email": "bb@morningtain.dk",
+ "homepage": "http://morningtrain.dk/",
+ "role": "Developer"
+ }
+ ],
+ "description": "A complete native php wrapper for the Toggl API",
+ "homepage": "http://morningtrain.dk/",
+ "keywords": [
+ "api",
+ "php",
+ "toggl"
+ ],
+ "time": "2019-03-04T07:37:07+00:00"
+ },
{
"name": "netresearch/jsonmapper",
"version": "v1.4.0",
@@ -1769,7 +1817,7 @@
"version": "8.3.1",
"source": {
"type": "git",
- "url": "https://git.drupal.org/project/coder.git",
+ "url": "https://git.drupalcode.org/project/coder.git",
"reference": "29a25627e7148b3119c84f18e087fc3b8c85b959"
},
"require": {
diff --git a/phpcs.xml b/phpcs.xml
index a14c934..b537108 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -17,6 +17,10 @@
Repository.php
+
+ Chunk.php
+ DbRepository.php
+
Schema.php
diff --git a/services.yml b/services.yml
index ac000a5..8eb577d 100644
--- a/services.yml
+++ b/services.yml
@@ -20,16 +20,26 @@ services:
http_client:
class: GuzzleHttp\Client
cache:
- class: Doctrine\Common\Cache\FileSystemCache
+ class: Doctrine\Common\Cache\FilesystemCache
factory: [Larowlan\Tl\CacheFactory, create]
repository:
class: Larowlan\Tl\Repository\DbRepository
arguments: ["@connection"]
connector:
class: Larowlan\Tl\Connector\Manager
- arguments: ['@container', '@cache', '%config%', '%version%']
+ arguments: ['@container', '@cache', '%config%', '%version%', '@reporter']
tags:
- { name: configurable }
+ reporter:
+ class: Larowlan\Tl\Reporter\Manager
+ arguments: ['@container', '@cache', '%config%', '%version%']
+ tags:
+ - { name: configurable }
+ reporter.toggl:
+ class: Larowlan\Tl\Reporter\Toggl
+ arguments: ['%config%', '@cache', '%version%']
+ tags:
+ - { name: reporter }
connector.redmine:
class: Larowlan\Tl\Connector\RedmineConnector
arguments: ['@http_client', '@cache', '%config%', '%version%']
diff --git a/src/Chunk.php b/src/Chunk.php
new file mode 100644
index 0000000..f4efdb4
--- /dev/null
+++ b/src/Chunk.php
@@ -0,0 +1,100 @@
+id = $id;
+ $this->start = $start;
+ $this->end = $end;
+ }
+
+ /**
+ * Gets value of Start.
+ *
+ * @return int
+ * Value of Start.
+ */
+ public function getStart(): int {
+ return $this->start;
+ }
+
+ /**
+ * Gets value of End.
+ *
+ * @return int
+ * Value of End.
+ */
+ public function getEnd(): ?int {
+ return $this->end;
+ }
+
+ /**
+ * Gets value of Id.
+ *
+ * @return int
+ * Value of Id.
+ */
+ public function getId(): int {
+ return $this->id;
+ }
+
+ /**
+ * Sets value of End.
+ *
+ * @param int $end
+ * Value for End.
+ *
+ * @return $this
+ */
+ public function setEnd(int $end): Chunk {
+ $this->end = $end;
+ return $this;
+ }
+
+ /**
+ * Gets duration.
+ *
+ * @return int
+ * Duration.
+ */
+ public function getDuration() : int {
+ return ($this->end ?: time()) - $this->start;
+ }
+
+}
diff --git a/src/Commands/Bitbar.php b/src/Commands/Bitbar.php
index acc8643..8fa40c3 100644
--- a/src/Commands/Bitbar.php
+++ b/src/Commands/Bitbar.php
@@ -49,16 +49,17 @@ protected function configure() {
*/
protected function execute(InputInterface $input, OutputInterface $output) {
if ($open = $this->repository->getActive()) {
- $text = $open->tid . ': ' . Formatter::formatDuration(time() - $open->start) . ' ';
+ $text = $open->getTicketId() . ': ' . Formatter::formatDuration($open->getDuration()) . ' ';
}
else {
$text = 'Inactive ';
}
$total = 0;
+ /** @var \Larowlan\Tl\Slot $data */
foreach ($this->repository->review(Total::ALL) as $data) {
- $total += $data->duration;
+ $total += $data->getDuration(FALSE, TRUE);
}
- $text .= '(' . $total . 'h)';
+ $text .= '(' . $total / 3600 . 'h)';
$output->writeln($text);
}
diff --git a/src/Commands/Combine.php b/src/Commands/Combine.php
index d2021ba..84dd7a9 100644
--- a/src/Commands/Combine.php
+++ b/src/Commands/Combine.php
@@ -54,24 +54,13 @@ protected function configure() {
protected function execute(InputInterface $input, OutputInterface $output) {
$slot1 = $input->getArgument('slot1');
$slot2 = $input->getArgument('slot2');
- list($entry1, $entry2) = $this->validateSlots($slot1, $slot2, $output);
-
- // Create a new combined entry and then remove fields we don't want to keep
- // around in the new entry.
- $combined_entry = clone $entry1;
- unset($combined_entry->id, $combined_entry->category, $combined_entry->comment, $combined_entry->teid, $combined_entry->connector_id);
- $combined_entry->connector_id = ':connector_id';
-
- // Extend the entry date by the amount of time logged in the second entry.
- $combined_entry->end = $entry1->end + ($entry2->end - $entry2->start);
- // Insert the new entry, if all is well delete the two existing ones.
- if ($new_slot = $this->repository->insert((array) $combined_entry, [':connector_id' => $entry1->connector_id])) {
- $this->repository->delete($entry1->id);
- $this->repository->delete($entry2->id);
+ /** @var \Larowlan\Tl\Slot $entry1 */
+ /** @var \Larowlan\Tl\Slot $entry2 */
+ list($entry1, $entry2) = $this->validateSlots($slot1, $slot2, $output);
+ $this->repository->combine($entry1, $entry2);
- $output->writeln(sprintf('Combined %s and %s into new slot %s', $slot1, $slot2, $new_slot));
- }
+ $output->writeln(sprintf('Combined %s and %s into new slot %s', $slot1, $slot2, $slot1));
}
/**
@@ -92,15 +81,15 @@ protected function validateSlots($slot1, $slot2, OutputInterface $output) {
if (!$entry2 = $this->repository->slot($slot2)) {
throw new \InvalidArgumentException(sprintf('Invalid slot id %s', $slot2));
}
- if ($entry1->connector_id !== $entry2->connector_id) {
- throw new \InvalidArgumentException(sprintf('You cannot combine slots from %s backend with slots from %s backend', Manager::formatConnectorId($entry2->connector_id), Manager::formatConnectorId($entry1->connector_id)));
+ if ($entry1->getConnectorId() !== $entry2->getConnectorId()) {
+ throw new \InvalidArgumentException(sprintf('You cannot combine slots from %s backend with slots from %s backend', Manager::formatConnectorId($entry2->getConnectorId()), Manager::formatConnectorId($entry1->getConnectorId())));
}
// Ensure we've not already sent the slots.
- if (!empty($entry1->teid) || !empty($entry1->teid)) {
+ if (!empty($entry1->getRemoteEntryId()) || !empty($entry1->getRemoteEntryId())) {
throw new \InvalidArgumentException('You cannot combine entries that have already been sent.');
}
// Ensure the slots are both against the same job.
- if ($entry1->tid != $entry2->tid) {
+ if ($entry1->getTicketId() != $entry2->getTicketId()) {
throw new \InvalidArgumentException('You cannot combine entries from separate issues.');
}
return [$entry1, $entry2];
@@ -111,12 +100,12 @@ protected function validateSlots($slot1, $slot2, OutputInterface $output) {
*/
protected function stopTicket($slot_id, OutputInterface $output) {
if ($stop = $this->repository->stop($slot_id)) {
- $stopped = $this->connector->ticketDetails($stop->tid, $stop->connector_id);
+ $stopped = $this->connector->ticketDetails($stop->getTicketId(), $stop->getConnectorId());
$output->writeln(sprintf('Closed slot %d against ticket %d: %s, duration %s',
- $stop->id,
- $stop->tid,
+ $stop->getId(),
+ $stop->getTicketId(),
$stopped->getTitle(),
- Formatter::formatDuration($stop->duration)
+ Formatter::formatDuration($stop->getDuration())
));
}
}
diff --git a/src/Commands/Comment.php b/src/Commands/Comment.php
index ec9d57a..176791a 100644
--- a/src/Commands/Comment.php
+++ b/src/Commands/Comment.php
@@ -56,23 +56,24 @@ protected function execute(InputInterface $input, OutputInterface $output) {
$entries = $this->repository->review(Review::ALL, !$input->getOption('recomment'));
$helper = $this->getHelper('question');
$last = FALSE;
+ /** @var \Larowlan\Tl\Slot $entry */
foreach ($entries as $entry) {
- if ($entry->comment && !$input->getOption('recomment')) {
+ if ($entry->getComment() && !$input->getOption('recomment')) {
continue;
}
- $title = $this->connector->ticketDetails($entry->tid, $entry->connector_id);
+ $title = $this->connector->ticketDetails($entry->getTicketId(), $entry->getConnectorId());
$question = new Question(
sprintf('Enter comment for slot %d [%d]: %s [%s h] [%s]',
- $entry->id,
- $entry->tid,
+ $entry->getId(),
+ $entry->getTicketId(),
$title->getTitle(),
- $entry->duration,
+ $entry->getDuration(FALSE, TRUE) / 3600,
$last ?: static::DEFAULT_COMMENT
),
$last ?: static::DEFAULT_COMMENT
);
$comment = $helper->ask($input, $output, $question);
- $this->repository->comment($entry->id, $comment);
+ $this->repository->comment($entry->getId(), $comment);
$last = $comment;
}
if (!$last) {
diff --git a/src/Commands/Continues.php b/src/Commands/Continues.php
index 9f95152..6171f2a 100644
--- a/src/Commands/Continues.php
+++ b/src/Commands/Continues.php
@@ -51,20 +51,21 @@ protected function configure() {
*/
protected function execute(InputInterface $input, OutputInterface $output) {
if ($slot_id = $input->getArgument('slot_id')) {
+ $this->repository->stop();
$slot = $this->repository->slot($slot_id);
}
else {
- $slot = $this->repository->latest();
+ $slot = $this->repository->stop() ?: $this->repository->latest();
}
if ($slot) {
- $details = $this->connector->ticketDetails($slot->tid, $slot->connector_id);
- list($slot_id, $continued) = $this->repository->start($slot->tid, $slot->connector_id, $slot->comment, $slot->id);
+ $details = $this->connector->ticketDetails($slot->getTicketId(), $slot->getConnectorId());
+ $start = $this->repository->start($slot->getTicketId(), $slot->getConnectorId(), $slot->getComment(), $slot->getId());
$output->writeln(sprintf('[%s]> %s entry for %d: %s [slot:%d]',
(new \DateTime())->format('h:i'),
- $continued ? 'Continued' : 'Started new',
- $slot->tid,
+ $start->isContinued() ? 'Continued' : 'Started new',
+ $slot->getTicketId(),
$details->getTitle(),
- $slot_id
+ $slot->getId()
));
return;
diff --git a/src/Commands/Delete.php b/src/Commands/Delete.php
index 2f3cbb7..f7e1bc2 100644
--- a/src/Commands/Delete.php
+++ b/src/Commands/Delete.php
@@ -60,12 +60,12 @@ protected function execute(InputInterface $input, OutputInterface $output) {
$confirm = NULL;
if (($slot = $this->repository->slot($slot_id)) && ($confirm = ($input->getOption('confirm') || $helper->ask($input, $output, $question))) && $this->repository->delete($slot_id)) {
- $deleted = $this->connector->ticketDetails($slot->tid, $slot->connector_id);
+ $deleted = $this->connector->ticketDetails($slot->getTicketId(), $slot->getConnectorId());
$output->writeln(sprintf('Deleted slot %d against ticket %d: %s, duration %s',
- $slot->id,
- $slot->tid,
+ $slot->getId(),
+ $slot->getTicketId(),
$deleted->getTitle(),
- Formatter::formatDuration($slot->end - $slot->start)
+ Formatter::formatDuration($slot->getDuration())
));
return;
}
diff --git a/src/Commands/Edit.php b/src/Commands/Edit.php
index 4c259ff..3ee05d9 100644
--- a/src/Commands/Edit.php
+++ b/src/Commands/Edit.php
@@ -63,13 +63,13 @@ protected function execute(InputInterface $input, OutputInterface $output) {
}
$slot = $this->repository->slot($slot_id);
- if ($slot === FALSE) {
+ if ($slot === NULL) {
$output->writeln('Slot does not exist.');
return 1;
}
- if (isset($slot->teid)) {
+ if ($slot->getRemoteEntryId()) {
$output->writeln('You cannot edit a slot that has been sent to the backend');
return 1;
}
diff --git a/src/Commands/Install.php b/src/Commands/Install.php
index e546205..7a0e323 100644
--- a/src/Commands/Install.php
+++ b/src/Commands/Install.php
@@ -8,6 +8,7 @@
use Larowlan\Tl\Repository\Schema;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
/**
@@ -35,6 +36,8 @@ public function __construct(Connection $connection, $directory, Schema $schema)
protected function configure() {
$this
->setName('install')
+ ->addOption('debug', 'd', InputOption::VALUE_NONE, 'Output the commands to be run')
+ ->addOption('skip-post', 's', InputOption::VALUE_NONE, 'Skip post install commands')
->setDescription('Installs the sqlite schema');
}
@@ -48,9 +51,23 @@ protected function execute(InputInterface $input, OutputInterface $output) {
$comparator = new Comparator();
$difference = $comparator->compare($existing, $schema);
$count = 0;
+ $debug = $input->getOption('debug');
+ $skip = $input->getOption('skip-post');
if ($statements = $difference->toSql($sm->getDatabasePlatform())) {
foreach ($statements as $statement) {
+ $hash = hash('sha256', $statement);
+ $post = $this->postInstall($hash);
+ if ($debug) {
+ $output->writeln($statement);
+ if ($post) {
+ $output->writeln($post);
+ }
+ continue;
+ }
$this->connection->exec($statement);
+ if (!$skip && $post) {
+ $this->connection->exec($post);
+ }
$count++;
}
$output->writeln(sprintf('Executed %d queries', $count));
@@ -60,4 +77,19 @@ protected function execute(InputInterface $input, OutputInterface $output) {
}
}
+ /**
+ * Migration tasks.
+ *
+ * @param string $hash
+ * Statement hash
+ *
+ * @return string
+ */
+ protected function postInstall(string $hash) :?string {
+ if ($hash === '8f5382517658027fcea333e593055c1133fe6f1576b3211391481ba9a8a51e57') {
+ return "INSERT INTO chunks (sid, start, end) SELECT id, start, end FROM slots";
+ }
+ return NULL;
+ }
+
}
diff --git a/src/Commands/MostFrequentlyUsed.php b/src/Commands/MostFrequentlyUsed.php
index dd1390c..78b75e3 100644
--- a/src/Commands/MostFrequentlyUsed.php
+++ b/src/Commands/MostFrequentlyUsed.php
@@ -53,11 +53,12 @@ protected function execute(InputInterface $input, OutputInterface $output) {
$table->setHeaders(['JobId', 'Title']);
$rows = [];
+ /** @var \Larowlan\Tl\Slot $entry */
foreach ($entries as $entry) {
- if (!$details = $this->connector->ticketDetails($entry->tid, $entry->connector_id)) {
+ if (!$details = $this->connector->ticketDetails($entry->getTicketId(), $entry->getConnectorId())) {
continue;
}
- $rows[] = [$entry->tid, $details->getTitle()];
+ $rows[] = [$entry->getTicketId(), $details->getTitle()];
}
$table->setRows($rows);
$table->render();
diff --git a/src/Commands/Open.php b/src/Commands/Open.php
index e6f6144..3bbfb29 100644
--- a/src/Commands/Open.php
+++ b/src/Commands/Open.php
@@ -48,12 +48,12 @@ protected function configure() {
*/
protected function execute(InputInterface $input, OutputInterface $output) {
if ($data = $this->repository->getActive()) {
- $details = $this->connector->ticketDetails($data->tid, $data->connector_id);
+ $details = $this->connector->ticketDetails($data->getTicketId(), $data->getConnectorId());
$output->writeLn(sprintf('%s [%d] - %s [slot: %d]',
$details->getTitle(),
- $data->tid,
- Formatter::formatDuration(time() - $data->start),
- $data->id
+ $data->getTicketId(),
+ Formatter::formatDuration($data->getDuration()),
+ $data->getId()
));
return;
}
diff --git a/src/Commands/Send.php b/src/Commands/Send.php
index 1e46542..16b885f 100644
--- a/src/Commands/Send.php
+++ b/src/Commands/Send.php
@@ -101,19 +101,20 @@ protected function execute(InputInterface $input, OutputInterface $output) {
$progress->setFormat('custom');
$progress->setProgressCharacter("\xF0\x9F\x8D\xBA");
$errors = FALSE;
+ /** @var \Larowlan\Tl\Slot $entry */
foreach ($entries as $entry) {
try {
- if ((float) $entry->duration == 0) {
+ if ((float) $entry->getDuration(FALSE, TRUE) == 0) {
// Nothing to send, but mark sent so it doesn't show up tomorrow.
- $this->repository->store([$entry->tid => 0]);
- $this->progress($progress, sprintf('Marked entry for %d as sent, < 15 minutes', $entry->tid));
+ $this->repository->store([$entry->getTicketId() => 0]);
+ $this->progress($progress, sprintf('Marked entry for %d as sent, < 15 minutes', $entry->getTicketId()));
$progress->advance();
continue;
}
if ($saved = $this->connector->sendEntry($entry)) {
- $entry_ids[$entry->tid] = $saved;
+ $entry_ids[$entry->getTicketId()] = $saved;
// A real entry, give some output.
- $this->progress($progress, sprintf('Stored entry for %d, remote id %d', $entry->tid, $entry_ids[$entry->tid]));
+ $this->progress($progress, sprintf('Stored entry for %d, remote id %d', $entry->getTicketId(), $entry_ids[$entry->getTicketId()]));
$progress->advance();
}
}
diff --git a/src/Commands/Start.php b/src/Commands/Start.php
index 67143b6..8335f51 100644
--- a/src/Commands/Start.php
+++ b/src/Commands/Start.php
@@ -87,16 +87,18 @@ protected function execute(InputInterface $input, OutputInterface $output) {
}
if ($title = $this->connector->ticketDetails($ticket_id, $connector_id)) {
if ($stop = $this->repository->stop()) {
- $stopped = $this->connector->ticketDetails($stop->tid, $stop->connector_id);
+ $stopped = $this->connector->ticketDetails($stop->getTicketId(), $stop->getConnectorId());
$output->writeln(sprintf('Closed slot %d against ticket %d: %s, duration %s',
- $stop->id,
- $stop->tid,
+ $stop->getId(),
+ $stop->getTicketId(),
$stopped->getTitle(),
- Formatter::formatDuration($stop->duration)
+ Formatter::formatDuration($stop->getDuration())
));
}
try {
- list($slot_id, $continued) = $this->repository->start($ticket_id, $connector_id, $input->getArgument('comment'));
+ $started = $this->repository->start($ticket_id, $connector_id, $input->getArgument('comment'));
+ $slot_id = $started->getId();
+ $continued = $started->isContinued();
$output->writeln(sprintf('[%s]> %s entry for %d: %s [slot:%d]',
(new \DateTime())->format('h:i'),
$continued ? 'Continued' : 'Started new',
diff --git a/src/Commands/Status.php b/src/Commands/Status.php
index 21205e8..c00bc15 100644
--- a/src/Commands/Status.php
+++ b/src/Commands/Status.php
@@ -62,15 +62,12 @@ protected function execute(InputInterface $input, OutputInterface $output) {
$table->setHeaders(['Slot', 'JobId', 'Time', 'Title']);
$rows = [];
$total = 0;
+ /** @var \Larowlan\Tl\Slot $record */
foreach ($data as $record) {
- $record->duration = ($record->end ?: time()) - $record->start;
- $total += $record->duration;
- $details = $this->connector->ticketDetails($record->tid, $record->connector_id);
- if (empty($record->end)) {
- $record->tid .= ' *';
- }
- $duration = sprintf('%s>', $details->isBillable() ? 'default' : 'yellow', Formatter::formatDuration($record->duration));
- $rows[] = [$record->id, $record->tid, $duration, $details->getTitle()];
+ $total += $record->getDuration();
+ $details = $this->connector->ticketDetails($record->getTicketId(), $record->getConnectorId());
+ $duration = sprintf('%s>', $details->isBillable() ? 'default' : 'yellow', Formatter::formatDuration($record->getDuration()));
+ $rows[] = [$record->getId(), $record->isOpen() ? sprintf('%s *', $record->getTicketId()) : $record->getTicketId(), $duration, $details->getTitle()];
}
$rows[] = new TableSeparator();
$rows[] = ['', 'Total', '' . Formatter::formatDuration($total) . '', ''];
diff --git a/src/Commands/Stop.php b/src/Commands/Stop.php
index a2575a0..404efd7 100644
--- a/src/Commands/Stop.php
+++ b/src/Commands/Stop.php
@@ -54,17 +54,17 @@ protected function configure() {
*/
protected function execute(InputInterface $input, OutputInterface $output) {
if ($stop = $this->repository->stop()) {
- $stopped = $this->connector->ticketDetails($stop->tid, $stop->connector_id);
+ $stopped = $this->connector->ticketDetails($stop->getTicketId(), $stop->getConnectorId());
$output->writeln(sprintf('[%s]> Closed slot %d against ticket %d: %s, duration %s',
(new \DateTime())->format('h:i'),
- $stop->id,
- $stop->tid,
+ $stop->getId(),
+ $stop->getTicketId(),
$stopped->getTitle(),
- Formatter::formatDuration($stop->duration)
+ Formatter::formatDuration($stop->getDuration())
));
if (($comment = $input->getOption('comment')) || $input->getOption('pause')) {
- if ($this->connector->pause($stop->tid, $comment, $stop->connector_id)) {
- $output->writeln(sprintf('Ticket %s set to paused.', $stop->tid));
+ if ($this->connector->pause($stop->getTicketId(), $comment, $stop->getConnectorId())) {
+ $output->writeln(sprintf('Ticket %s set to paused.', $stop->getTicketId()));
}
else {
$output->writeln('Could not update ticket status');
diff --git a/src/Commands/Tag.php b/src/Commands/Tag.php
index 0637e78..21da249 100644
--- a/src/Commands/Tag.php
+++ b/src/Commands/Tag.php
@@ -84,21 +84,28 @@ protected function tagAll(InputInterface $input, OutputInterface $output) {
$output->writeln('You are offline, please try again later.');
return;
}
+ /** @var \Larowlan\Tl\Slot $entry */
foreach ($entries as $entry) {
- $categories = $grouped_categories[$entry->connector_id];
- if ($entry->category && !$input->getOption('retag')) {
+ $categories = $grouped_categories[$entry->getConnectorId()];
+ if ($entry->getCategory() && !$input->getOption('retag')) {
continue;
}
- $title = $this->connector->ticketDetails($entry->tid, $entry->connector_id);
+ if (count($categories) === 1) {
+ $tag = reset($categories);
+ list(, $tag) = explode(':', $tag);
+ $this->repository->tag($tag, $entry->getId());
+ continue;
+ }
+ $title = $this->connector->ticketDetails($entry->getTicketId(), $entry->getConnectorId());
$default = reset($categories);
$question = new ChoiceQuestion(
sprintf('Enter tag for slot %d [%d]: %s [%s h] [%s] %s',
- $entry->id,
- $entry->tid,
+ $entry->getId(),
+ $entry->getTicketId(),
$title->getTitle(),
- $entry->duration,
+ $entry->getDuration(FALSE, TRUE) / 3600,
$last ?: $default,
- $entry->comment ? '- "' . $entry->comment . '"' : ''
+ $entry->getComment() ? '- "' . $entry->getComment() . '"' : ''
),
$categories,
$last ?: $default
@@ -106,7 +113,7 @@ protected function tagAll(InputInterface $input, OutputInterface $output) {
$tag_id = $helper->ask($input, $output, $question);
$tag = $categories[$tag_id];
list(, $tag) = explode(':', $tag);
- $this->repository->tag($tag, $entry->id);
+ $this->repository->tag($tag, $entry->getId());
$last = $tag_id;
}
if (!$last) {
@@ -125,23 +132,29 @@ protected function tagOne(InputInterface $input, OutputInterface $output, $slot_
if ($entry = $this->repository->slot($slot_id)) {
$helper = $this->getHelper('question');
try {
- $title = $this->connector->ticketDetails($entry->tid, $entry->connector_id);
+ $title = $this->connector->ticketDetails($entry->getTicketId(), $entry->getConnectorId());
$grouped_categories = $this->connector->fetchCategories();
- $categories = $grouped_categories[$entry->connector_id];
+ $categories = $grouped_categories[$entry->getConnectorId()];
}
catch (ConnectException $e) {
$output->writeln('You are offline, please try again later.');
return;
}
+ if (count($categories) === 1) {
+ $tag = reset($categories);
+ list(, $tag) = explode(':', $tag);
+ $this->repository->tag($tag, $entry->getId());
+ return;
+ }
$default = reset($categories);
$question = new ChoiceQuestion(
sprintf('Enter tag for slot %d [%d]: %s [%s h] [%s] %s',
- $entry->id,
- $entry->tid,
+ $entry->getId(),
+ $entry->getTicketId(),
$title->getTitle(),
- Formatter::formatDuration($entry->end - $entry->start),
+ Formatter::formatDuration($entry->getDuration()),
$default,
- $entry->comment ? '- "' . $entry->comment . '"' : ''
+ $entry->getComment() ? '- "' . $entry->getComment() . '"' : ''
),
$categories,
$default
@@ -149,7 +162,7 @@ protected function tagOne(InputInterface $input, OutputInterface $output, $slot_
$tag_id = $helper->ask($input, $output, $question);
$tag = $categories[$tag_id];
list(, $tag) = explode(':', $tag);
- $this->repository->tag($tag, $entry->id);
+ $this->repository->tag($tag, $entry->getId());
}
else {
$output->writeln('No such slot - please check your slot ID');
diff --git a/src/Commands/TagAll.php b/src/Commands/TagAll.php
index 2a0c34a..aa1a50e 100644
--- a/src/Commands/TagAll.php
+++ b/src/Commands/TagAll.php
@@ -57,6 +57,12 @@ protected function execute(InputInterface $input, OutputInterface $output) {
$categories = $this->connector->fetchCategories();
$tags = [];
foreach ($connector_ids as $connector_id) {
+ if (count($categories[$connector_id]) === 1) {
+ $tag = reset($categories);
+ list(, $tag) = explode(':', $tag);
+ $tags[$connector_id] = $tag;
+ continue;
+ }
$question = new ChoiceQuestion(
sprintf('Select tag to use for %s tickets', Manager::formatConnectorId($connector_id)),
$categories[$connector_id]
@@ -67,11 +73,12 @@ protected function execute(InputInterface $input, OutputInterface $output) {
$tags[$connector_id] = $tag;
}
$tagged = FALSE;
+ /** @var \Larowlan\Tl\Slot $entry */
foreach ($entries as $entry) {
- if ($entry->category) {
+ if ($entry->getCategory()) {
continue;
}
- $this->repository->tag($tags[$entry->connector_id], $entry->id);
+ $this->repository->tag($tags[$entry->getConnectorId()], $entry->getId());
$tagged = TRUE;
}
if (!$tagged) {
diff --git a/src/Commands/Visit.php b/src/Commands/Visit.php
index a428057..4d26e89 100644
--- a/src/Commands/Visit.php
+++ b/src/Commands/Visit.php
@@ -52,14 +52,14 @@ protected function configure() {
protected function execute(InputInterface $input, OutputInterface $output) {
if (!($issue_number = $input->getArgument('issue'))) {
if ($data = $this->repository->getActive()) {
- $issue_number = $data->tid;
+ $issue_number = $data->getTicketId();
}
}
if (!$issue_number) {
$output->writeln('No active ticket, please use tl visit {ticket_id} to specifiy a ticket.');
return;
}
- $url = $this->connector->ticketUrl($issue_number, isset($data) ? $data->connector_id : $this->getConnector($input, $output, $issue_number));
+ $url = $this->connector->ticketUrl($issue_number, isset($data) ? $data->getConnectorId() : $this->getConnector($input, $output, $issue_number));
$this->open($url, $output);
}
diff --git a/src/Connector/Connector.php b/src/Connector/Connector.php
index e65e939..0bc8de5 100644
--- a/src/Connector/Connector.php
+++ b/src/Connector/Connector.php
@@ -2,6 +2,8 @@
namespace Larowlan\Tl\Connector;
+use Larowlan\Tl\Slot;
+
/**
* An interface for backend connectors.
*/
@@ -22,11 +24,13 @@ public static function getName();
* The ticket id from the remote system.
* @param string $connectorId
* Connector ID.
+ * @param bool $for_reporting
+ * TRUE if for reporting sake.
*
* @return \Larowlan\Tl\TicketInterface
* Ticket object.
*/
- public function ticketDetails($id, $connectorId);
+ public function ticketDetails($id, $connectorId, $for_reporting = FALSE);
/**
* Fetch the details of time categories from a remote ticketing system.
@@ -39,14 +43,14 @@ public function fetchCategories();
/**
* Send a time entry to the remote ticketing system.
*
- * @param object $entry
+ * @param \Larowlan\Tl\Slot $entry
* A time entry corresponding with a record in the {bot_tl_slot} table.
*
* @return int|null
* The time entry id from the remote system or NULL if the entry was not
* able to be saved.
*/
- public function sendEntry($entry);
+ public function sendEntry(Slot $entry);
/**
* Gets the URL for the given ticket ID.
diff --git a/src/Connector/JiraConnector.php b/src/Connector/JiraConnector.php
index df417f6..33b6481 100644
--- a/src/Connector/JiraConnector.php
+++ b/src/Connector/JiraConnector.php
@@ -12,6 +12,7 @@
use JiraRestApi\JiraException;
use JiraRestApi\Project\ProjectService;
use Larowlan\Tl\Configuration\ConfigurableService;
+use Larowlan\Tl\Slot;
use Larowlan\Tl\Ticket;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Console\Helper\QuestionHelper;
@@ -223,7 +224,7 @@ public static function getName() {
/**
* {@inheritdoc}
*/
- public function ticketDetails($id, $connectorId) {
+ public function ticketDetails($id, $connectorId, $for_reporting = FALSE) {
try {
$issue = $this->issueService->get($id);
}
@@ -273,18 +274,19 @@ public function fetchCategories() {
/**
* {@inheritdoc}
*/
- public function sendEntry($entry) {
- if ((float) $entry->duration == 0) {
+ public function sendEntry(Slot $entry) {
+ if ((float) $entry->getDuration(FALSE, TRUE) == 0) {
// Zero time after rounding.
// Return 0 to ensure doesn't send again.
return 0;
}
$worklog = new Worklog();
- $worklog->setComment($entry->comment)
- ->setStartedDateTime(new \DateTime('@' . $entry->start))
- ->setTimeSpent(sprintf('%sh %sm', floor($entry->duration), ($entry->duration - floor($entry->duration)) * 60));
- $ret = $this->issueService->addWorklog($entry->tid, $worklog);
+ $duration = $entry->getDuration(FALSE, TRUE) / 3600;
+ $worklog->setComment($entry->getComment())
+ ->setStartedDateTime(new \DateTime('@' . $entry->getStart()))
+ ->setTimeSpent(sprintf('%sh %sm', floor($duration), ($duration - floor($duration)) * 60));
+ $ret = $this->issueService->addWorklog($entry->getTicketId(), $worklog);
return $ret->id;
}
diff --git a/src/Connector/Manager.php b/src/Connector/Manager.php
index 2060e65..839bfe3 100644
--- a/src/Connector/Manager.php
+++ b/src/Connector/Manager.php
@@ -4,6 +4,8 @@
use Doctrine\Common\Cache\Cache;
use Larowlan\Tl\Configuration\ConfigurableService;
+use Larowlan\Tl\Reporter\Manager as RepoterManager;
+use Larowlan\Tl\Slot;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\Console\Helper\QuestionHelper;
@@ -42,6 +44,13 @@ class Manager implements ConfigurableService, ConnectorManager {
*/
protected $version;
+ /**
+ * Reporter.
+ *
+ * @var \Larowlan\Tl\Reporter\Manager
+ */
+ private $reporter;
+
/**
* {@inheritdoc}
*/
@@ -52,7 +61,7 @@ public static function getName() {
/**
* {@inheritdoc}
*/
- public function __construct(ContainerBuilder $container, Cache $cache, array $config, $version) {
+ public function __construct(ContainerBuilder $container, Cache $cache, array $config, $version, RepoterManager $reporter) {
if (!empty($config['connector_ids'])) {
foreach ($config['connector_ids'] as $id) {
$this->connectors[$id] = $container->get($id);
@@ -60,6 +69,7 @@ public function __construct(ContainerBuilder $container, Cache $cache, array $co
}
$this->cache = $cache;
$this->version = $version;
+ $this->reporter = $reporter;
}
/**
@@ -113,12 +123,12 @@ public function spotConnector($id, InputInterface $input, OutputInterface $outpu
/**
* {@inheritdoc}
*/
- public function ticketDetails($id, $connectorId) {
- if (($details = $this->cache->fetch($this->version . ':' . $connectorId . ':' . $id))) {
+ public function ticketDetails($id, $connectorId, $for_reporting = FALSE) {
+ if (($details = $this->cache->fetch($this->version . ':' . $connectorId . ':' . $id . ':' . $for_reporting))) {
return $details;
}
- $ticket = $this->connector($connectorId)->ticketDetails($id, $connectorId);
- $this->cache->save($this->version . ':' . $connectorId . ':' . $id, $ticket, static::LIFETIME);
+ $ticket = $this->connector($connectorId)->ticketDetails($id, $connectorId, $for_reporting);
+ $this->cache->save($this->version . ':' . $connectorId . ':' . $id . ':' . $for_reporting, $ticket, static::LIFETIME);
return $ticket;
}
@@ -136,8 +146,18 @@ public function fetchCategories() {
/**
* {@inheritdoc}
*/
- public function sendEntry($entry) {
- return $this->connector($entry->connector_id)->sendEntry($entry);
+ public function sendEntry(Slot $entry) {
+ $connector = $this->connector($entry->getConnectorId());
+ if ($sendEntry = $connector->sendEntry($entry)) {
+ $details = $connector->ticketDetails($entry->getTicketId(), $entry->getConnectorId(), TRUE);
+ $projects = $connector->projectNames();
+ $categories = $connector->fetchCategories();
+ if ($this->reporter->report($entry, $details, $projects, $categories)) {
+ return $sendEntry;
+ }
+ throw new \Exception('Could not complete reporting');
+ }
+ return $sendEntry;
}
/**
diff --git a/src/Connector/RedmineConnector.php b/src/Connector/RedmineConnector.php
index d59a465..04c47ce 100644
--- a/src/Connector/RedmineConnector.php
+++ b/src/Connector/RedmineConnector.php
@@ -7,6 +7,7 @@
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException;
use Larowlan\Tl\Configuration\ConfigurableService;
+use Larowlan\Tl\Slot;
use Larowlan\Tl\Ticket;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\Console\Helper\QuestionHelper;
@@ -90,12 +91,16 @@ public function loadAlias($ticket_id, $connectorId) {
/**
* {@inheritdoc}
*/
- public function ticketDetails($id, $connectorId) {
+ public function ticketDetails($id, $connectorId, $for_reporting = FALSE) {
$url = $this->url . '/issues/' . $id . '.xml';
try {
if ($xml = $this->fetch($url, $this->apiKey)) {
+ $title = $xml->subject . ' (' . $xml->project['name'] . ')';
+ if ($for_reporting) {
+ $title = $xml->subject;
+ }
$entry = new Ticket(
- $xml->subject . ' (' . $xml->project['name'] . ')',
+ $title,
(string) $xml->project['id'],
$this->isBillable((string) $xml->project['id'])
);
@@ -144,21 +149,21 @@ public function fetchCategories() {
/**
* {@inheritdoc}
*/
- public function sendEntry($entry) {
- if ((float) $entry->duration == 0) {
+ public function sendEntry(Slot $entry) {
+ if ((float) $entry->getDuration(FALSE, TRUE) == 0) {
// Zero time after rounding.
// Return 0 to ensure doesn't send again.
return 0;
}
$url = $this->url . '/time_entries.xml';
- $details = $this->ticketDetails($entry->tid, $entry->connector_id);
+ $details = $this->ticketDetails($entry->getTicketId(), $entry->getConnectorId());
$data = [
- 'issue_id' => $entry->tid,
+ 'issue_id' => $entry->getTicketId(),
'project_id' => $details->getProjectId(),
- 'spent_on' => date('Y-m-d', $entry->start),
- 'hours' => $entry->duration,
- 'activity_id' => $entry->category,
- 'comments' => $entry->comment,
+ 'spent_on' => date('Y-m-d', $entry->getStart()),
+ 'hours' => $entry->getDuration(FALSE, TRUE) / 3600,
+ 'activity_id' => $entry->getCategory(),
+ 'comments' => $entry->getComment(),
];
$xml = new \SimpleXMLElement('');
foreach ($data as $key => $value) {
@@ -499,7 +504,7 @@ protected function retrieveProjects($limit, $offset) {
$url = sprintf($this->url . '/projects.xml?limit=%s&status=1&offset=%s', $limit, $offset);
if ($xml = $this->fetch($url, $this->apiKey)) {
foreach ($xml->project as $node) {
- $options[(int) $node->id] = (string) $node->name . '::' . (string) $node->id;
+ $options[(int) $node->id] = (string) $node->name;
}
}
return $options;
diff --git a/src/Reporter/Manager.php b/src/Reporter/Manager.php
new file mode 100644
index 0000000..57b99b4
--- /dev/null
+++ b/src/Reporter/Manager.php
@@ -0,0 +1,135 @@
+reporters[$id] = $container->get($id);
+ }
+ }
+ $this->cache = $cache;
+ $this->version = $version;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getConfiguration(NodeDefinition $root_node, ContainerBuilder $container) {
+ foreach (array_keys($container->findTaggedServiceIds('reporter')) as $id) {
+ $definition = $container->getDefinition($id);
+ $class = $definition->getClass();
+ $class::getConfiguration($root_node, $container);
+ }
+ $root_node->children()
+ ->arrayNode('reporter_ids')
+ ->prototype('scalar')
+ ->end()
+ ->end()
+ ->end();
+ return $root_node;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function askPreBootQuestions(QuestionHelper $helper, InputInterface $input, OutputInterface $output, array $config, ContainerBuilder $container) {
+ $connectorIds = array_keys($container->findTaggedServiceIds('reporter'));
+ $activeIds = [];
+ foreach ($connectorIds as $id) {
+ $definition = $container->getDefinition($id);
+ $class = $definition->getClass();
+ $name = call_user_func([$class, 'getName']);
+ $default = in_array($id, $config['reporter_ids']);
+ $question = new ConfirmationQuestion(sprintf('Do you want to use the %s reporter?[%s/%s]', $name, $default ? 'Y' : 'y', $default ? 'n' : 'N'), $default);
+ if ($helper->ask($input, $output, $question)) {
+ $activeIds[] = $id;
+ $config = $class::askPreBootQuestions($helper, $input, $output, $config, $container) + $config;
+ }
+ }
+ $config['reporter_ids'] = $activeIds;
+ return $config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function askPostBootQuestions(QuestionHelper $helper, InputInterface $input, OutputInterface $output, array $config) {
+ foreach ($this->reporters as $reporter) {
+ $config = $reporter->askPostBootQuestions($helper, $input, $output, $config);
+ }
+ return $config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getDefaults($config, ContainerBuilder $container) {
+ foreach (array_keys($container->findTaggedServiceIds('reporter')) as $id) {
+ $definition = $container->getDefinition($id);
+ $class = $definition->getClass();
+ $class::getDefaults($config, $container);
+ }
+ $config['reporter_ids'] = [];
+ return $config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function report(Slot $entry, TicketInterface $details, array $projects, array $categories) {
+ $return = TRUE;
+ foreach ($this->reporters as $reporter) {
+ $return = $return && $reporter->report($entry, $details, $projects, $categories);
+ }
+ return $return;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getName() {
+ return 'Manager';
+ }
+
+}
diff --git a/src/Reporter/Reporter.php b/src/Reporter/Reporter.php
new file mode 100644
index 0000000..a9e1d24
--- /dev/null
+++ b/src/Reporter/Reporter.php
@@ -0,0 +1,38 @@
+cache = $cache;
+ $this->config = $configuration;
+ $this->version = $version;
+ $this->api = new TogglApi($configuration['toggl_token']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function report(Slot $entry, TicketInterface $details, array $projects, array $categories) {
+ $categories = array_reduce($categories, function (array $carry, $item) {
+ list($name, $id) = explode(':', $item);
+ $carry[$id] = $name;
+ return $carry;
+ }, []);
+ $connector_id = $entry->getConnectorId();
+ list(, $connector_id) = explode('.', $connector_id);
+ $project_id = $this->getProjectId(trim($projects[$details->getProjectId()]), $connector_id);
+ $task_id = $this->getTaskId($entry->getTicketId(), $details->getTitle(), $project_id, $connector_id);
+ $total = $entry->getDuration(FALSE, TRUE);
+ $net = array_sum(array_map(function (Chunk $chunk) {
+ return ($chunk->getEnd() ?: time()) - $chunk->getStart();
+ }, $entry->getChunks()));
+ $difference = $total - $net;
+ foreach ($entry->getChunks() as $chunk) {
+ $duration = ($chunk->getEnd() ?: time()) - $chunk->getStart();
+ $result = $this->api->createTimeEntry([
+ 'description' => $entry->getComment(),
+ 'tid' => $task_id,
+ 'start' => date('c', $chunk->getStart()),
+ 'billable' => $details->isBillable(),
+ 'duration' => $duration + $difference,
+ 'created_with' => 'tl',
+ 'tags' => [
+ $categories[$entry->getCategory()],
+ ],
+ 'duronly' => TRUE,
+ ]);
+ $difference = 0;
+ }
+ return $result->id;
+ }
+
+ /**
+ * Gets the toggl task ID - creating one if needed.
+ *
+ * @param int $tid
+ * Entry task ID.
+ * @param string $name
+ * Task name.
+ * @param int $project_id
+ * Toggl task ID.
+ * @param string $connector_id
+ * Connector ID.
+ *
+ * @return int
+ * Toggl task ID.
+ */
+ protected function getTaskId($tid, $name, $project_id, $connector_id) {
+ $workspace_id = $this->config['toggl_workspace'];
+ $cid = sprintf('%s:%s:toggl-tasks', $this->version, $workspace_id);
+ if (($cache = $this->cache->fetch($cid)) && isset($cache[$project_id][$connector_id][$tid])) {
+ return $cache[$project_id][$connector_id][$tid];
+ }
+ $tasks = $this->api->getProjectTasks($project_id) ?: [];
+ $entries = [];
+ foreach ($tasks as $task) {
+ $matches = [];
+ if (preg_match('/(.*) \((?jira|redmine):(?\d+)\)/', $task->name, $matches)) {
+ $entries[$project_id][$matches['connector']][$matches['id']] = $task->id;
+ }
+ }
+ if (isset($entries[$project_id][$connector_id][$tid])) {
+ $this->cache->save($cid, $entries, self::LIFETIME);
+ return $entries[$project_id][$connector_id][$tid];
+ }
+ // Create a task.
+ $new = $this->api->createTask([
+ 'pid' => $project_id,
+ 'name' => sprintf('%s (%s:%s)', $name, $connector_id, $tid),
+ ]);
+ $entries[$project_id][$connector_id][$tid] = $new->id;
+ $this->cache->save($cid, $entries, self::LIFETIME);
+ return $new->id;
+ }
+
+ /**
+ * Gets the toggl project ID.
+ *
+ * @param string $project_name
+ * Project name.
+ * @param string $connector_id
+ * Connector ID.
+ *
+ * @return int
+ * Project ID.
+ */
+ protected function getProjectId($project_name, $connector_id) {
+ $workspace_id = $this->config['toggl_workspace'];
+ $cid = sprintf('%s:%s:toggl-projects', $this->version, $workspace_id);
+ if (($cache = $this->cache->fetch($cid)) && isset($cache[$connector_id][$project_name])) {
+ return $cache[$connector_id][$project_name];
+ }
+ $projects = $this->api->getWorkspaceProjects($workspace_id) ?: [];
+ $entries = [];
+ foreach ($projects as $project) {
+ $matches = [];
+ if (preg_match('/(?.*) \((?jira|redmine)\)/', $project->name, $matches)) {
+ $entries[$matches['connector']][$matches['name']] = $project->id;
+ }
+ }
+ if (isset($entries[$connector_id][$project_name])) {
+ $this->cache->save($cid, $entries, self::LIFETIME);
+ return $entries[$connector_id][$project_name];
+ }
+ // Create a project.
+ $new = $this->api->createProject([
+ 'wid' => $workspace_id,
+ 'name' => sprintf('%s (%s)', $project_name, $connector_id),
+ ]);
+ $me = $this->api->getMe();
+ $this->api->createProjectUsers([
+ 'pid' => $new->id,
+ 'uid' => implode(',', array_map(function ($item) {
+ return $item->uid;
+ }, array_filter($this->api->getWorkspaceUserRelations($workspace_id), function ($item) use ($me) {
+ return $item->uid !== $me->id;
+ }))),
+ 'manager' => TRUE,
+ ]);
+ $entries[$connector_id][$project_name] = $new->id;
+ $this->cache->save($cid, $entries, self::LIFETIME);
+ return $new->id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getName() {
+ return 'Toggl';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getConfiguration(NodeDefinition $root_node, ContainerBuilder $container) {
+ $root_node->children()
+ ->scalarNode('toggl_token')
+ ->defaultValue('')
+ ->end()
+ ->scalarNode('toggl_workspace')
+ ->defaultValue('')
+ ->end()
+ ->end();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function askPreBootQuestions(QuestionHelper $helper, InputInterface $input, OutputInterface $output, array $config, ContainerBuilder $container) {
+ $default_token = isset($config['toggl_token']) ? $config['toggl_token'] : '';
+ // Reset.
+ $config = [
+ 'toggl_token' => '',
+ ] + $config;
+ $question = new Question(sprintf('Enter your Toggl API token: [%s]', $default_token), $default_token);
+ $question->setValidator(function ($value) {
+ if (trim($value) == '') {
+ throw new \Exception('The token cannot be empty');
+ }
+
+ return $value;
+ });
+ $question->setHidden(TRUE);
+ $config['toggl_token'] = $helper->ask($input, $output, $question) ?: $default_token;
+ return $config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function askPostBootQuestions(QuestionHelper $helper, InputInterface $input, OutputInterface $output, array $config) {
+ $default_workspace = isset($config['toggl_workspace']) ? $config['toggl_workspace'] : '';
+ // Reset.
+ $config = [
+ 'toggl_workspace' => '',
+ ] + $config;
+ try {
+ $workspaces = $this->api->getWorkspaces();
+ $choices = array_map(function ($item) {
+ return $item->name . ':' . $item->id;
+ }, $workspaces);
+ }
+ catch (\Exception $e) {
+ $output->writeln('Could not connect to Toggl, please check your token and that you are online');
+ return $config;
+ }
+ $question = new ChoiceQuestion(sprintf('Enter the ID of your Toggle workspace: [%s]', $default_workspace), array_combine($choices, $choices), $default_workspace);
+ $workspace = $helper->ask($input, $output, $question) ?: $default_workspace;
+ list(, $id) = explode(':', $workspace);
+ $config['toggl_workspace'] = $id;
+ return $config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getDefaults($config, ContainerBuilder $container) {
+ return $config = [
+ 'toggl_workspace' => '',
+ 'toggl_token' => '',
+ ];
+ }
+
+}
diff --git a/src/Repository/DbRepository.php b/src/Repository/DbRepository.php
index c362811..4f05650 100644
--- a/src/Repository/DbRepository.php
+++ b/src/Repository/DbRepository.php
@@ -3,19 +3,13 @@
namespace Larowlan\Tl\Repository;
use Doctrine\DBAL\Connection;
+use Larowlan\Tl\Slot;
/**
* Repository backed by a database.
*/
class DbRepository implements Repository {
- /**
- * Array of user details keyed by irc nick.
- *
- * @var array
- */
- protected $userDetails = [];
-
/**
* The active database connection.
*
@@ -40,28 +34,31 @@ protected function qb() {
/**
* {@inheritdoc}
*/
- public function stop($slot_id = NULL) {
+ public function stop($slot_id = NULL): ?Slot {
if ($open = $this->getActive($slot_id)) {
$end = $this::requestTime();
- $this->qb()->update('slots')
+ $this->qb()->update('chunks')
->set('end', $end)
->where('id = :id')
- ->setParameter('id', $open->id)
+ ->setParameter('tid', $open->getId())
+ ->setParameter('id', $open->lastChunk()->getId())
->execute();
- $open->end = $end;
- $open->duration = $open->end - $open->start;
+ $open->lastChunk()->setEnd($end);
+ $open->getDuration(TRUE);
return $open;
}
- return FALSE;
+ return NULL;
}
/**
* {@inheritdoc}
*/
- public function getActive($slot_id = NULL) {
- $q = $this->qb()->select('*')
+ public function getActive($slot_id = NULL): ?Slot {
+ $q = $this->qb()
+ ->select('s.id', 's.tid', 's.comment', 's.category', 's.connector_id', 's.teid')
->from('slots', 's')
- ->where('s.end IS NULL');
+ ->innerJoin('s', 'chunks', 'c', 'c.sid = s.id')
+ ->where('c.end IS NULL');
if ($slot_id) {
$q = $q->andWhere('s.id = :id')
->setParameter('id', $slot_id);
@@ -69,33 +66,54 @@ public function getActive($slot_id = NULL) {
if ($open = $q
->execute()
->fetch(\PDO::FETCH_OBJ)) {
- return $open;
+ return Slot::fromRecord($open, $this->chunksForSlot($open->id));
}
- return FALSE;
+ return NULL;
+ }
+
+ /**
+ * Gets chunks for a slot.
+ *
+ * @param int $slot_id
+ * Slot ID.
+ *
+ * @return array
+ * Chunk records.
+ */
+ protected function chunksForSlot(int $slot_id): array {
+ return $this->qb()->select('*')
+ ->from('chunks', 'c')
+ ->where('c.sid = :sid')
+ ->setParameter('sid', $slot_id)
+ ->execute()
+ ->fetchAll(\PDO::FETCH_OBJ);
}
/**
* {@inheritdoc}
*/
- public function latest() {
- $q = $this->qb()->select('*')
+ public function latest(): ?Slot {
+ $q = $this->qb()
+ ->select('s.id', 's.tid', 's.comment', 's.category', 's.connector_id', 's.teid')
->from('slots', 's')
- ->orderBy('s.end', 'DESC');
+ ->innerJoin('s', 'chunks', 'c', 'c.sid = s.id')
+ ->orderBy('c.end', 'DESC');
if ($open = $q
->execute()
->fetch(\PDO::FETCH_OBJ)) {
- return $open;
+ return Slot::fromRecord($open, $this->chunksForSlot($open->id));
}
- return FALSE;
+ return NULL;
}
/**
* {@inheritdoc}
*/
- public function start($ticket_id, $connectorId, $comment = '', $force_continue = FALSE) {
- $continue_query = $continue = $this->qb()->select('*')
+ public function start($ticket_id, $connectorId, $comment = '', $force_continue = FALSE): ?Slot {
+ $continue_query = $this->qb()->select('*')
->from('slots', 's')
->where('s.connector_id = :connector_id')
+ ->where('s.teid is NULL')
->where('s.tid = :tid');
if (!$force_continue) {
$continue_query->andWhere('s.comment IS NULL')
@@ -110,14 +128,8 @@ public function start($ticket_id, $connectorId, $comment = '', $force_continue =
->execute()
->fetch(\PDO::FETCH_OBJ);
if ((!$comment || $force_continue) && $continue) {
- $this->qb()->update('slots')
- ->where('id = :id')
- ->setParameter('id', $continue->id)
- ->set('start', $this::requestTime() + $continue->start - $continue->end)
- ->set('end', ':end')
- ->setParameter(':end', NULL)
- ->execute();
- return [$continue->id, TRUE];
+ $this->insertChunk($this::requestTime(), NULL, $continue->id);
+ return Slot::fromRecord($continue, $this->chunksForSlot($continue->id));
}
$record = [
'tid' => $ticket_id,
@@ -130,46 +142,54 @@ public function start($ticket_id, $connectorId, $comment = '', $force_continue =
$params[':comment'] = $comment;
}
- return [$this->insert($record, $params), FALSE];
+ return $this->slot($this->insert($record, $params));
}
/**
* {@inheritdoc}
*/
- public function insert($slot, $params = []) {
+ public function insert($slot, $params = []): int {
+ $start = $slot['start'];
+ $end = $slot['end'] ?? NULL;
+ unset($slot['start'], $slot['end']);
$query = $this->qb()->insert('slots')
->values($slot);
foreach ($params as $key => $value) {
$query->setParameter($key, $value);
}
$query->execute();
- return $this->connection()->lastInsertId();
+ $slot_id = $this->connection()->lastInsertId();
+ $this->insertChunk($start, $end, $slot_id);
+ return $slot_id;
}
/**
* {@inheritdoc}
*/
- public function status($date = NULL) {
+ public function status($date = NULL): array {
if (!$date) {
$stamp = mktime('0', '0');
}
else {
$stamp = strtotime($date);
}
- $return = $this->qb()->select('id', 'tid', 'end', 'start', 'connector_id')
- ->from('slots')
- ->where('start > :start AND start < :end')
+ return array_map(function ($record) {
+ return Slot::fromRecord($record, $this->chunksForSlot($record->id));
+ }, $this->qb()->select('s.id', 's.tid', 's.connector_id', 's.teid', 's.category', 's.comment')
+ ->from('slots', 's')
+ ->innerJoin('s', 'chunks', 'c', 'c.sid = s.id')
+ ->having('c.start > :start AND c.start < :end')
+ ->groupBy('s.id', 's.tid', 's.connector_id', 's.teid', 's.category', 's.comment')
->setParameter(':start', $stamp)
->setParameter(':end', $stamp + (60 * 60 * 24))
->execute()
- ->fetchAll(\PDO::FETCH_OBJ);
- return $return;
+ ->fetchAll(\PDO::FETCH_OBJ));
}
/**
* {@inheritdoc}
*/
- public function review($date = NULL, $check = FALSE) {
+ public function review($date = NULL, $check = FALSE): array {
if (!$date) {
$stamp = mktime('0', '0');
}
@@ -177,11 +197,12 @@ public function review($date = NULL, $check = FALSE) {
$stamp = strtotime($date);
}
$query = $this->qb()
- ->select('end', 'tid', 'category', 'comment', 'id', 'start', 'connector_id')
- ->from('slots');
+ ->select('s.id', 's.tid', 's.connector_id', 's.teid', 's.category', 's.comment', 'c.end', 'c.start', 'c.sid')
+ ->from('slots', 's')
+ ->innerJoin('s', 'chunks', 'c', 'c.sid = s.id');
$where = $this->qb()->expr()->andX(
$this->qb()->expr()->isNull('teid'),
- $this->qb()->expr()->gt('start', ':stamp')
+ $this->qb()->expr()->gt('c.start', ':stamp')
);
$query->setParameter(':stamp', $stamp);
if ($check) {
@@ -191,18 +212,19 @@ public function review($date = NULL, $check = FALSE) {
$this->qb()->expr()->isNull('category')
));
}
- $return = $query->where($where)->execute()->fetchAll(\PDO::FETCH_OBJ);
- foreach ($return as &$row) {
- $row->duration = round((($row->end ?: time()) - $row->start) / 900) * 900 / 3600;
- $row->active = empty($row->end);
- }
- return $return;
+ return array_map(function (array $record) {
+ return Slot::fromRecord($record['record'], $record['chunks']);
+ }, array_reduce($query->where($where)->execute()->fetchAll(\PDO::FETCH_OBJ), function (array $carry, $record) {
+ $carry[$record->sid]['record'] = $record;
+ $carry[$record->sid]['chunks'][] = $record;
+ return $carry;
+ }, []));
}
/**
* {@inheritdoc}
*/
- public function send() {
+ public function send(): array {
return $this->review('19780101');
}
@@ -224,12 +246,37 @@ public function store($entries) {
* {@inheritdoc}
*/
public function edit($slot_id, int $duration) {
- $request_time = $this::requestTime();
- return $this->qb()->update('slots')
- ->set('start', $request_time - $duration)
- ->set('end', $request_time)
+ $slot = $this->slot($slot_id);
+ $chunks = $slot->getChunks();
+ $existing = $slot->getDuration();
+ $difference = $duration - $existing;
+ if ($difference < 0) {
+ $remove = abs($difference);
+ // We're reducing the total.
+ while ($remove) {
+ $chunk = array_pop($chunks);
+ if ($chunk->getDuration() > $remove) {
+ $this->qb()->update('chunks')
+ ->set('end', ($chunk->getEnd() ?: time()) - $remove)
+ ->where('id = :id')
+ ->setParameter(':id', $chunk->getId())->execute();
+ return;
+ }
+ $remove -= $chunk->getDuration();
+ $this->qb()
+ ->delete('chunks')
+ ->where('id = :id')
+ ->setParameter(':id', $chunk->getId())
+ ->execute();
+ }
+ return;
+ }
+ // We're increasing the total.
+ $chunk = $slot->lastChunk();
+ return $this->qb()->update('chunks')
+ ->set('end', ($chunk->getEnd() ?: time()) + $difference)
->where('id = :id')
- ->setParameter(':id', $slot_id)->execute();
+ ->setParameter(':id', $chunk->getId())->execute();
}
/**
@@ -272,39 +319,51 @@ public function comment($slot_id, $comment) {
/**
* {@inheritdoc}
*/
- public function frequent() {
- return $this->qb()
+ public function frequent(): array {
+ return array_map(function ($record) {
+ return Slot::fromRecord($record, $this->chunksForSlot($record->id));
+ }, $this->qb()
->select('tid', 'connector_id')
->from('slots', 's')
->groupBy('tid')
->orderBy('COUNT(*)', 'DESC')
->setMaxResults(10)
->execute()
- ->fetchAll(\PDO::FETCH_OBJ);
+ ->fetchAll(\PDO::FETCH_OBJ));
}
/**
* {@inheritdoc}
*/
- public function slot($slot_id) {
- return $this->qb()->select('*')
+ public function slot($slot_id): ?Slot {
+ $slot = $this->qb()->select('*')
->from('slots')
->where('id = :id')
->setParameter(':id', $slot_id)
->execute()
->fetch(\PDO::FETCH_OBJ);
+ if ($slot) {
+ return Slot::fromRecord($slot, $this->chunksForSlot($slot->id));
+ }
+ return NULL;
}
/**
* {@inheritdoc}
*/
public function delete($slot_id) {
- return $this->qb()->delete('slots')
+ $return = $this->qb()->delete('slots')
->where('id = :id')
// Can't delete sent entries.
->andWhere('teid IS NULL')
->setParameter(':id', $slot_id)
->execute();
+ if ($return) {
+ $this->qb()->delete('chunks')->where('sid = :sid')
+ ->setParameter('sid', $slot_id)
+ ->execute();
+ }
+ return $return;
}
/**
@@ -371,27 +430,66 @@ public function listAliases($filter = '') {
/**
* {@inheritdoc}
*/
- public function totalByTicket($start, $end = NULL) {
+ public function totalByTicket($start, $end = NULL): array {
if (!$end) {
// Some time in the future.
$end = time() + 86400;
}
- $return = $this->qb()->select('tid', 'end', 'start', 'connector_id')
- ->from('slots')
- ->where('start > :start AND start < :end')
+ $return = $this->qb()->select('s.tid', 's.category', 's.comment', 's.connector_id', 's.teid', 'c.start', 'c.end', 'c.sid')
+ ->from('slots', 's')
+ ->innerJoin('s', 'chunks', 'c', 'c.sid = s.id')
+ ->where('c.start > :start AND c.start < :end')
->setParameter(':start', $start)
->setParameter(':end', $end)
->execute()
->fetchAll(\PDO::FETCH_OBJ);
- $totals = [];
+ $ticket_map = [];
foreach ($return as $row) {
- $row->duration = round((($row->end ?: time()) - $row->start) / 900) * 900;
- if (!isset($totals[$row->connector_id][$row->tid])) {
- $totals[$row->connector_id][$row->tid] = 0;
+ $ticket_map[$row->sid] = $row->tid;
+ if (!isset($totals[$row->connector_id][$row->sid])) {
+ $totals[$row->connector_id][$row->sid] = 0;
}
- $totals[$row->connector_id][$row->tid] += $row->duration;
+ $totals[$row->connector_id][$row->sid] += (($row->end ?: time()) - $row->start);
}
- return $totals;
+ $aggregated = [];
+ foreach ($totals as $connector_id => $slots) {
+ $aggregated[$connector_id] = array_reduce(array_keys($slots), function (array $carry, $sid) use ($ticket_map, $slots) {
+ $carry[$ticket_map[$sid]] = ($carry[$ticket_map[$sid]] ?? 0) + round($slots[$sid] / 900) * 900;
+ return $carry;
+ }, []);
+ }
+ return $aggregated;
+ }
+
+ /**
+ * Inserts a chunk.
+ *
+ * @param int $start
+ * Start.
+ * @param int $end
+ * End.
+ * @param int $slot_id
+ * Slot ID.
+ */
+ protected function insertChunk(int $start, ?int $end, int $slot_id) {
+ $chunk = [
+ 'start' => $start,
+ 'end' => $end,
+ 'sid' => $slot_id,
+ ];
+ $this->qb()->insert('chunks')->values(array_filter($chunk))->execute();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function combine(Slot $slot1, Slot $slot2) {
+ $this->qb()->update('chunks')
+ ->where('sid = :sid2')
+ ->set('sid', ':sid1')
+ ->setParameter('sid2', $slot2->getId())
+ ->setParameter('sid1', $slot1->getId())->execute();
+ $this->delete($slot2->getId());
}
}
diff --git a/src/Repository/Repository.php b/src/Repository/Repository.php
index 3a86bad..b35ce60 100644
--- a/src/Repository/Repository.php
+++ b/src/Repository/Repository.php
@@ -2,28 +2,30 @@
namespace Larowlan\Tl\Repository;
+use Larowlan\Tl\Slot;
+
/**
* Interface for storage of slots.
*/
interface Repository {
- public function getActive();
+ public function getActive() : ?Slot;
- public function latest();
+ public function latest() : ?Slot;
- public function stop($slot_id = NULL);
+ public function stop($slot_id = NULL) : ?Slot;
- public function start($ticket_id, $connectorId, $comment = '', $force_continue = FALSE);
+ public function start($ticket_id, $connectorId, $comment = '', $force_continue = FALSE) : ?Slot;
- public function insert($slot, $params = []);
+ public function insert($slot, $params = []) : int;
- public function status($date = NULL);
+ public function status($date = NULL) : array;
public function comment($slot_id, $comment);
- public function review($date = NULL, $check = FALSE);
+ public function review($date = NULL, $check = FALSE) : array;
- public function send();
+ public function send() : array;
public function store($entries);
@@ -31,9 +33,11 @@ public function edit($slot_id, int $duration);
public function tag($tag_id, $slot_id = NULL);
- public function frequent();
+ public function frequent() : array;
+
+ public function slot($slot_id) : ?Slot;
- public function slot($slot_id);
+ public function combine(Slot $slot1, Slot $slot2);
public function delete($slot_id);
@@ -45,6 +49,6 @@ public function loadAlias($alias);
public function listAliases($filter = '');
- public function totalByTicket($start_timestamp, $end_timestamp = NULL);
+ public function totalByTicket($start_timestamp, $end_timestamp = NULL) : array;
}
diff --git a/src/Repository/Schema.php b/src/Repository/Schema.php
index e9a897c..a709864 100644
--- a/src/Repository/Schema.php
+++ b/src/Repository/Schema.php
@@ -26,18 +26,25 @@ public function getSchema() {
$slots->addColumn('id', 'integer')
->setAutoincrement(TRUE);
$slots->addColumn('tid', 'bigint', ['unsigned' => TRUE]);
- $slots->addColumn('start', 'bigint', ['unsigned' => TRUE]);
- $slots->addColumn('end', 'bigint', ['unsigned' => TRUE])->setNotnull(FALSE);
$slots->addColumn('teid', 'bigint', ['unsigned' => TRUE])->setNotnull(FALSE);
$slots->addColumn('comment', 'string', ['length' => 255])->setNotnull(FALSE);
$slots->addColumn('category', 'string', ['length' => 255])->setNotnull(FALSE);
$slots->addColumn('connector_id', 'string', ['length' => 50])->setDefault('connector.redmine')->setNotnull(FALSE);
$slots->setPrimaryKey(['id']);
- $slots->addIndex(['start']);
- $slots->addIndex(['end']);
$slots->addIndex(['tid']);
$slots->addIndex(['teid']);
+ $chunks = $schema->createTable('chunks');
+ $chunks->addColumn('id', 'integer')
+ ->setAutoincrement(TRUE);
+ $chunks->addColumn('sid', 'bigint', ['unsigned' => TRUE]);
+ $chunks->addColumn('start', 'bigint', ['unsigned' => TRUE]);
+ $chunks->addColumn('end', 'bigint', ['unsigned' => TRUE])->setNotnull(FALSE);
+ $chunks->setPrimaryKey(['id']);
+ $chunks->addIndex(['start']);
+ $chunks->addIndex(['end']);
+ $chunks->addIndex(['sid']);
+
$aliases = $schema->createTable('aliases');
$aliases->addColumn('tid', 'bigint', ['unsigned' => TRUE]);
$aliases->addColumn('alias', 'string', ['length' => 255]);
diff --git a/src/Reviewer.php b/src/Reviewer.php
index 29de822..d671c7a 100644
--- a/src/Reviewer.php
+++ b/src/Reviewer.php
@@ -96,36 +96,37 @@ public function getSummary($date = 19780101, $check = FALSE, $exact = FALSE) {
$offline = TRUE;
}
$exact_total = 0;
+ /** @var \Larowlan\Tl\Slot $record */
foreach ($data as $record) {
- $total += $record->duration;
- $details = $this->connector->ticketDetails($record->tid, $record->connector_id);
- $category_id = str_pad($record->category, 3, 0, STR_PAD_LEFT);
+ $total += $record->getDuration(FALSE, TRUE);
+ $details = $this->connector->ticketDetails($record->getTicketId(), $record->getConnectorId());
+ $category_id = str_pad($record->getCategory(), 3, 0, STR_PAD_LEFT);
$category = '';
- if ($record->category) {
+ if ($record->getCategory()) {
if ($offline) {
$category = 'Offline';
}
- elseif (isset($categories[$record->connector_id][$category_id])) {
- $category = $categories[$record->connector_id][$category_id];
+ elseif (isset($categories[$record->getConnectorId()][$category_id])) {
+ $category = $categories[$record->getConnectorId()][$category_id];
}
else {
$category = 'Unknown';
}
}
- $duration = sprintf('%s>', $details->isBillable() ? 'default' : 'yellow', $record->duration);
+ $duration = sprintf('%s>', $details->isBillable() ? 'default' : 'yellow', $record->getDuration(FALSE, TRUE) / 3600);
$row = [
- $record->id,
- sprintf('%s>', $record->active ? 'green' : 'default', $record->tid),
+ $record->getId(),
+ sprintf('%s>', $record->isOpen() ? 'green' : 'default', $record->getTicketId()),
$duration,
];
if ($exact) {
- $row[] = Formatter::formatDuration(($record->end ?: time()) - $record->start);
- $exact_total += (($record->end ?: time()) - $record->start);
+ $row[] = Formatter::formatDuration($record->getDuration());
+ $exact_total += $record->getDuration();
}
$row = array_merge($row, [
substr($details->getTitle(), 0, 25) . '...',
$category,
- $record->comment,
+ $record->getComment(),
]);
$rows[] = $row;
}
@@ -134,7 +135,7 @@ public function getSummary($date = 19780101, $check = FALSE, $exact = FALSE) {
$rows[] = [
'',
'Total',
- '' . $total . ' h',
+ '' . $total / 3600 . ' h',
'' . Formatter::formatDuration($exact_total) . '',
'',
'',
@@ -145,7 +146,7 @@ public function getSummary($date = 19780101, $check = FALSE, $exact = FALSE) {
$rows[] = [
'',
'Total',
- '' . $total . ' h',
+ '' . $total / 3600 . ' h',
'',
'',
'',
diff --git a/src/Slot.php b/src/Slot.php
new file mode 100644
index 0000000..e09e066
--- /dev/null
+++ b/src/Slot.php
@@ -0,0 +1,278 @@
+id;
+ }
+
+ /**
+ * Gets value of Start.
+ *
+ * @return int
+ * Value of Start.
+ */
+ public function getStart(): int {
+ $this->sortChunks();
+ return (int) reset($this->chunks)->getStart();
+ }
+
+ /**
+ * Gets current timestamp.
+ *
+ * @return int
+ * Current timestamp.
+ */
+ protected function now() {
+ return (new \DateTime())->getTimestamp();
+ }
+
+ /**
+ * Gets value of End.
+ *
+ * @return int
+ * Value of End.
+ */
+ public function getEnd(): ?int {
+ $this->sortChunks();
+ return end($this->chunks)->getEnd() ?: $this->now();
+ }
+
+ /**
+ * Check if the slot is open.
+ *
+ * @return bool
+ * TRUE if open
+ */
+ public function isOpen() : bool {
+ $this->sortChunks();
+ return !end($this->chunks)->getEnd();
+ }
+
+ /**
+ * Gets value of RemoteEntryId.
+ *
+ * @return int
+ * Value of RemoteEntryId.
+ */
+ public function getRemoteEntryId(): ?int {
+ return $this->remoteEntryId;
+ }
+
+ /**
+ * Gets value of Comment.
+ *
+ * @return string
+ * Value of Comment.
+ */
+ public function getComment(): ?string {
+ return $this->comment;
+ }
+
+ /**
+ * Gets value of Category.
+ *
+ * @return string
+ * Value of Category.
+ */
+ public function getCategory(): ?string {
+ return $this->category;
+ }
+
+ /**
+ * Gets value of ConnectorId.
+ *
+ * @return string
+ * Value of ConnectorId.
+ */
+ public function getConnectorId(): string {
+ return $this->connectorId;
+ }
+
+ /**
+ * Is the chunk active.
+ *
+ * @return bool
+ * TRUE if it is active.
+ */
+ public function isActive(): bool {
+ return !$this->lastChunk()->getEnd();
+ }
+
+ /**
+ * Gets the last slot.
+ */
+ public function lastChunk() {
+ $this->sortChunks();
+ return end($this->chunks);
+ }
+
+ /**
+ * Gets value of Duration.
+ *
+ * @param bool $reset
+ * TRUE to reset.
+ * @param bool $rounded
+ * TRUE to round.
+ *
+ * @return int
+ * Value of Duration.
+ */
+ public function getDuration($reset = FALSE, $rounded = FALSE): int {
+ if (!$this->duration || $reset) {
+ $this->duration = array_sum(array_map(function (Chunk $chunk) {
+ return ($chunk->getEnd() ?: $this->now()) - $chunk->getStart();
+ }, $this->chunks));
+ }
+ return $rounded ? round($this->duration / 900) * 900 : $this->duration;
+ }
+
+ /**
+ * Gets value of ProjectName.
+ *
+ * @return string
+ * Value of ProjectName.
+ */
+ public function getProjectName(): string {
+ return $this->projectName;
+ }
+
+ /**
+ * Sorts the chunks.
+ */
+ protected function sortChunks(): void {
+ uasort($this->chunks, function (Chunk $a, Chunk $b) {
+ return $a->getStart() <=> $b->getStart();
+ });
+ }
+
+ /**
+ * Is this slot continued.
+ *
+ * @return bool
+ * TRUE if continued.
+ */
+ public function isContinued() : bool {
+ return count($this->chunks) > 1;
+ }
+
+ /**
+ * Creates a slot from DB records.
+ *
+ * @param object $record
+ * DB Record.
+ * @param array $chunk_records
+ * Chunk records.
+ *
+ * @return \Larowlan\Tl\Slot
+ * Slot.
+ */
+ public static function fromRecord($record, array $chunk_records) : Slot {
+ $static = new static();
+ $static->id = $record->id;
+ $static->ticketId = $record->tid;
+ $static->comment = $record->comment;
+ $static->category = $record->category;
+ $static->connectorId = $record->connector_id;
+ $static->remoteEntryId = $record->teid;
+ $static->chunks = array_map(function ($chunk) {
+ return new Chunk($chunk->id, $chunk->start, $chunk->end);
+ }, $chunk_records);
+ return $static;
+ }
+
+ /**
+ * Gets value of TicketId.
+ *
+ * @return int
+ * Value of TicketId.
+ */
+ public function getTicketId(): int {
+ return $this->ticketId;
+ }
+
+ /**
+ * Gets value of Chunks.
+ *
+ * @return \Larowlan\Tl\Chunk[]
+ * Value of Chunks.
+ */
+ public function getChunks(): array {
+ return $this->chunks;
+ }
+
+}
diff --git a/tests/Commands/AddTest.php b/tests/Commands/AddTest.php
index ade4c02..4027155 100644
--- a/tests/Commands/AddTest.php
+++ b/tests/Commands/AddTest.php
@@ -2,6 +2,7 @@
namespace Larowlan\Tl\Tests\Commands;
+use Larowlan\Tl\Slot;
use Larowlan\Tl\Tests\TlTestBase;
use Larowlan\Tl\Ticket;
@@ -19,23 +20,19 @@ class AddTest extends TlTestBase {
* @covers ::execute
*/
public function testAdd() {
- $this->getMockConnector()->expects($this->any())
- ->method('ticketDetails')
- ->with(1234, 'connector.jira')
- ->willReturn(new Ticket('Running tests', 123));
- $this->getMockConnector()->expects($this->any())
- ->method('spotConnector')
- ->willReturn('connector.jira');
+ $this->setupConnector();
+ $now = new \DateTime();
$output = $this->executeCommand('add', [
'issue_number' => 1234,
'duration' => .25,
]);
- $now = new \DateTime();
$this->assertRegExp('/' . $now->format('Y-m-d h:i') . '/', $output->getDisplay());
$this->assertRegExp('/Added entry for 1234: Running tests/', $output->getDisplay());
$this->assertRegExp('/for 15:00 m/', $output->getDisplay());
$slot = $this->assertSlotAdded(1234);
- $this->assertEquals((int) $now->format('U') + .25 * 60 *60, $slot->end);
+ $this->assertEquals(.25 * 60 *60, $slot->getDuration());
+ $this->assertEquals((int) $now->format('U'), $slot->getStart());
+
}
/**
@@ -45,17 +42,18 @@ public function testAdd() {
*/
public function testAddWithComment() {
$this->setupConnector();
+ $now = new \DateTime();
$output = $this->executeCommand('add', [
'issue_number' => 1234,
'duration' => '1h',
'comment' => 'Doing stuff',
]);
- $now = new \DateTime();
$this->assertRegExp('/' . $now->format('Y-m-d h:i') . '/', $output->getDisplay());
$this->assertRegExp('/Added entry for 1234: Running tests/', $output->getDisplay());
$this->assertRegExp('/for 1:00:00/', $output->getDisplay());
$slot = $this->assertSlotAdded(1234, 'Doing stuff');
- $this->assertEquals((int) $now->format('U') + 1 * 60 *60, $slot->end);
+ $this->assertEquals(1 * 60 *60, $slot->getDuration());
+ $this->assertEquals((int) $now->format('U'), $slot->getStart());
}
/**
* Tests add command with start params.
@@ -63,25 +61,20 @@ public function testAddWithComment() {
* @covers ::execute
*/
public function testAddInPast() {
+ $this->setupConnector();
$start = '11 am';
- $this->getMockConnector()->expects($this->any())
- ->method('ticketDetails')
- ->with(1234, 'connector.jira')
- ->willReturn(new Ticket('Running tests', 123));
- $this->getMockConnector()->expects($this->any())
- ->method('spotConnector')
- ->willReturn('connector.jira');
+ $time = new \DateTime($start);
$output = $this->executeCommand('add', [
'issue_number' => 1234,
'duration' => 3.25,
'--start' => $start,
]);
- $time = new \DateTime($start);
$this->assertRegExp('/' . $time->format('Y-m-d h:i') . '/', $output->getDisplay());
$this->assertRegExp('/Added entry for 1234: Running tests/', $output->getDisplay());
$this->assertRegExp('/for 3:15:00./', $output->getDisplay());
$slot = $this->assertSlotAdded(1234);
- $this->assertEquals((int) $time->format('U') + 3.25 * 60 *60, $slot->end);
+ $this->assertEquals(3.25 * 60 *60, $slot->getDuration());
+ $this->assertEquals((int) $time->format('U'), $slot->getStart());
}
/**
@@ -92,17 +85,18 @@ public function testAddInPast() {
* @param string $comment
* (Optional) Comment.
*
- * @return object
+ * @return \Larowlan\Tl\Slot
+ * Slot.
*/
- protected function assertSlotAdded($ticket_id, $comment = NULL) {
+ protected function assertSlotAdded($ticket_id, $comment = NULL) : Slot {
/** @var \Larowlan\Tl\Repository\Repository $repository */
$repository = $this->getRepository();
$slot = $repository->latest();
- $this->assertEquals($ticket_id, $slot->tid);
- $this->assertEquals($comment, $slot->comment);
- $this->assertNotNull($slot->end);
- $this->assertNull($slot->category);
- $this->assertNull($slot->teid);
+ $this->assertEquals($ticket_id, $slot->getTicketId());
+ $this->assertEquals($comment, $slot->getComment());
+ $this->assertNotNull($slot->lastChunk()->getEnd());
+ $this->assertNull($slot->getCategory());
+ $this->assertNull($slot->getRemoteEntryId());
return $slot;
}
diff --git a/tests/Commands/ContinueTest.php b/tests/Commands/ContinueTest.php
index 4b77cbe..09bd01c 100644
--- a/tests/Commands/ContinueTest.php
+++ b/tests/Commands/ContinueTest.php
@@ -23,9 +23,9 @@ protected function setUp() {
$this->getMockConnector()->expects($this->any())
->method('ticketDetails')
->willReturnMap([
- ['1', 'connector.redmine', new Ticket('Do something', 1)],
- ['2', 'connector.redmine', new Ticket('Do something else', 2)],
- ['3', 'connector.redmine', new Ticket('Do something more', 3)],
+ [1, 'connector.redmine', FALSE, new Ticket('Do something', 1)],
+ [2, 'connector.redmine', FALSE, new Ticket('Do something else', 2)],
+ [3, 'connector.redmine', FALSE, new Ticket('Do something more', 3)],
]);
$this->getMockConnector()->expects($this->any())
->method('spotConnector')
@@ -73,8 +73,8 @@ public function testContinueCommand() {
$this->assertTicketIsOpen(3);
$this->getRepository()->stop();
$slot = $this->getRepository()->slot($this->slotId3);
- $this->assertGreaterThanOrEqual(3600 * 9, $slot->end - $slot->start);
- $this->assertLessThanOrEqual((3600 * 9) + 60, $slot->end - $slot->start);
+ $this->assertGreaterThanOrEqual(3600 * 9, $slot->getDuration());
+ $this->assertLessThanOrEqual((3600 * 9) + 60, $slot->getDuration());
}
/**
@@ -87,8 +87,8 @@ public function testContinueCommandWithSlotId() {
$this->assertTicketIsOpen(2);
$this->getRepository()->stop();
$slot = $this->getRepository()->slot($this->slotId2);
- $this->assertGreaterThanOrEqual(3600, $slot->end - $slot->start);
- $this->assertLessThanOrEqual(3600 + 60, $slot->end - $slot->start);
+ $this->assertGreaterThanOrEqual(3600, $slot->getDuration());
+ $this->assertLessThanOrEqual(3600 + 60, $slot->getDuration());
}
}
diff --git a/tests/Commands/EditTest.php b/tests/Commands/EditTest.php
new file mode 100644
index 0000000..7f8ab81
--- /dev/null
+++ b/tests/Commands/EditTest.php
@@ -0,0 +1,114 @@
+getMockConnector()->expects($this->any())
+ ->method('ticketDetails')
+ ->willReturnMap([
+ [1, 'connector.redmine', FALSE, new Ticket('Do something', 1)],
+ [2, 'connector.redmine', FALSE, new Ticket('Do something else', 2)],
+ [3, 'connector.redmine', FALSE, new Ticket('Do something more', 3)],
+ ]);
+ $this->getMockConnector()->expects($this->any())
+ ->method('spotConnector')
+ ->willReturn('connector.redmine');
+ $repository = $this->getRepository();
+ // Five entries for today.
+ $this->start = time();
+ // 7 hrs.
+ $repository->insert([
+ 'tid' => 1,
+ 'start' => $this->start,
+ 'end' => $this->start + 3588 * 7,
+ 'connector_id' => ':connector_id',
+ ], [':connector_id' => 'connector.redmine']);
+ // Add another chunk, 1 hour long.
+ $this->slot = $repository->start(1, 'connector.redmine');
+ $repository->stop($this->slot->getId());
+ }
+
+ /**
+ * Test the basic functionality of the edit command.
+ */
+ public function testEditIncreaseCommand() {
+ $this->setUp();
+ $result = $this->executeCommand('edit', [
+ 'slot_id' => $this->slot->getId(),
+ 'duration' => '8h',
+ ]);
+ $this->assertContains(sprintf('Updated slot %d to 8:00:00', $this->slot->getId()), $result->getDisplay());
+ $slots = $this->getRepository()->review();
+ $total = array_reduce($slots, function (int $carry, Slot $slot) {
+ return $carry + $slot->getDuration();
+ }, 0);
+ $this->assertEquals(8 * 3600, $total);
+ }
+
+ /**
+ * Test the basic functionality of the edit command.
+ */
+ public function testEditDecreaseCommand() {
+ $this->setUp();
+ $result = $this->executeCommand('edit', [
+ 'slot_id' => $this->slot->getId(),
+ 'duration' => '6h',
+ ]);
+ $this->assertContains(sprintf('Updated slot %d to 6:00:00', $this->slot->getId()), $result->getDisplay());
+ $slots = $this->getRepository()->review();
+ $total = array_reduce($slots, function (int $carry, Slot $slot) {
+ return $carry + $slot->getDuration();
+ }, 0);
+ $this->assertEquals(6 * 3600, $total);
+ }
+
+ /**
+ * Test the basic functionality of the edit command when ticket is open
+ */
+ public function testEditWhileOpenCommand() {
+ $this->setUp();
+ $slot = $this->getRepository()->start(2, 'connector.redmine');
+ $slots = $this->getRepository()->review();
+ $this->assertCount(1, end($slots)->getChunks());
+ $result = $this->executeCommand('edit', [
+ 'slot_id' => $slot->getId(),
+ 'duration' => .25,
+ ]);
+ $this->assertContains(sprintf('Updated slot %d to 15:00 m', $slot->getId()), $result->getDisplay());
+ $slots = $this->getRepository()->review();
+ $total = array_reduce($slots, function (int $carry, Slot $theSlot) use ($slot) {
+ return $carry + $theSlot->getId() === $slot->getId() ? $theSlot->getDuration() : 0;
+ }, 0);
+ $this->assertEquals(0.25 * 3600, $total);
+ $this->assertCount(1, end($slots)->getChunks());
+ }
+
+}
diff --git a/tests/Commands/ReviewTest.php b/tests/Commands/ReviewTest.php
index 3b8e12b..22d1609 100644
--- a/tests/Commands/ReviewTest.php
+++ b/tests/Commands/ReviewTest.php
@@ -19,9 +19,9 @@ protected function setUp() {
$this->getMockConnector()->expects($this->any())
->method('ticketDetails')
->willReturnMap([
- ['1', 'connector.redmine', new Ticket('Do something', 1)],
- ['2', 'connector.redmine', new Ticket('Do something else ', 2)],
- ['3', 'connector.redmine', new Ticket('Do something more', 3)],
+ [1, 'connector.redmine', FALSE, new Ticket('Do something', 1)],
+ [2, 'connector.redmine', FALSE, new Ticket('Do something else ', 2)],
+ [3, 'connector.redmine', FALSE, new Ticket('Do something more', 3)],
]);
$this->getMockConnector()->expects($this->any())
->method('spotConnector')
diff --git a/tests/Commands/StartTest.php b/tests/Commands/StartTest.php
index fe1d088..50a09f2 100644
--- a/tests/Commands/StartTest.php
+++ b/tests/Commands/StartTest.php
@@ -57,9 +57,8 @@ public function testStopStart() {
$this->getMockConnector()->expects($this->any())
->method('ticketDetails')
->willReturnMap([
- [1234, 'connector.redmine', new Ticket('Running tests', 123)],
- ["1234", 'connector.redmine', new Ticket('Running tests', 123)],
- [4567, 'connector.redmine', new Ticket('Running more tests', 123)],
+ [1234, 'connector.redmine', FALSE, new Ticket('Running tests', 123)],
+ [4567, 'connector.redmine', FALSE, new Ticket('Running more tests', 123)],
]);
$this->getMockConnector()->expects($this->any())
->method('spotConnector')
@@ -67,14 +66,14 @@ public function testStopStart() {
$output = $this->executeCommand('start', ['issue_number' => 1234]);
$this->assertRegExp('/Started new entry for 1234: Running tests/', $output->getDisplay());
$active = $this->assertTicketIsOpen(1234);
- $slot_id = $active->id;
+ $slot_id = $active->getId();
$output = $this->executeCommand('start', ['issue_number' => 4567]);
$this->assertRegExp('/Closed slot [0-9]+ against ticket 1234/', $output->getDisplay());
$this->assertRegExp('/Started new entry for 4567: Running more tests/', $output->getDisplay());
$this->assertTicketIsOpen('4567');
$closed = $this->getRepository()->slot($slot_id);
- $this->assertNotNull($closed->end);
- $this->assertEquals('1234', $closed->tid);
+ $this->assertFalse($closed->isOpen());
+ $this->assertEquals('1234', $closed->getTicketId());
}
/**
diff --git a/tests/TlTestBase.php b/tests/TlTestBase.php
index ea6b225..109f33e 100644
--- a/tests/TlTestBase.php
+++ b/tests/TlTestBase.php
@@ -4,6 +4,7 @@
use Larowlan\Tl\Application;
use Larowlan\Tl\Connector\ConnectorManager;
+use Larowlan\Tl\Slot;
use Larowlan\Tl\Tests\Commands\AliasTest;
use Larowlan\Tl\Ticket;
use PHPUnit\Framework\TestCase;
@@ -67,7 +68,7 @@ protected function setUp() {
if ($this->installSchema) {
$install = $container->get('app.command.install');
$install->setApplication($this->application);
- $input = new ArrayInput(['command' => 'install']);
+ $input = new ArrayInput(['command' => 'install', '--skip-post' => TRUE]);
$output = $this->createMock(OutputInterface::class);
$install->run($input, $output);
}
@@ -129,17 +130,17 @@ protected function executeCommand($name, array $input = []) {
}
/**
- * @return mixed
+ * @return \Larowlan\Tl\Slot
*/
- protected function assertTicketIsOpen($ticket_id, $comment = NULL) {
+ protected function assertTicketIsOpen($ticket_id, $comment = NULL) : Slot {
/** @var \Larowlan\Tl\Repository\Repository $repository */
$repository = $this->getRepository();
$active = $repository->getActive();
- $this->assertEquals($ticket_id, $active->tid);
- $this->assertEquals($comment, $active->comment);
- $this->assertNull($active->end);
- $this->assertNull($active->category);
- $this->assertNull($active->teid);
+ $this->assertEquals($ticket_id, $active->getTicketId());
+ $this->assertEquals($comment, $active->getComment());
+ $this->assertNull($active->lastChunk()->getEnd());
+ $this->assertNull($active->getCategory());
+ $this->assertNull($active->getRemoteEntryId());
return $active;
}