# Type: Input # Category: Task # Description: Used to put finished tasks into Redmine # Author: Andriy Lesyuk new Orangutan::Context( response => sub { my ($context, $user, $message) = @_; my $regexp; my @result = ( ); # Remove ending $message =~ s/!*\.*$//; # Extract/remove date my $base; my ($day, $month, $year); # On Nth Month, YYYY $regexp = '^(?:On )?(?:[A-z]{3,9}, )?([0-9]{1,2})(?:st|nd|rd|th) ([A-z]{3,9})(?:, ([0-9]{4}))? ?[:\-]? '; if ($message =~ /$regexp/i) { $month = Orangutan::Date::ParseMonth($2); if (defined($month)) { $day = $1; $month++; if (defined($3)) { $year = $3; } else { $year = (localtime(time))[5]; my $time = POSIX::mktime(0, 0, 0, $day, $month - 1, $year); if (defined($time) && ($time > time)) { $year--; } $year += 1900; } $base = POSIX::mktime(0, 0, 0, $day, $month - 1, $year - 1900); if (defined($base)) { $message =~ s/$regexp//i; } } } # On Month N, YYYY $regexp = '^(?:On )?(?:[A-z]{3,9}, )?([A-z]{3,9}) ([0-9]{1,2})(?:, ([0-9]{4}))? ?[:\-]? '; if (!defined($base) && ($message =~ /$regexp/i)) { $month = Orangutan::Date::ParseMonth($1); if (defined($month)) { $day = $2; $month++; if (defined($3)) { $year = $3; } else { $year = (localtime(time))[5]; my $time = POSIX::mktime(0, 0, 0, $day, $month - 1, $year); if (defined($time) && ($time > time)) { $year--; } $year += 1900; } $base = POSIX::mktime(0, 0, 0, $day, $month - 1, $year - 1900); if (defined($base)) { $message =~ s/$regexp//i; } } } # On Weekday $regexp = '(?:On )?([A-z]{3,9}) ?[:\-]? '; if (!defined($base) && ($message =~ /$regexp/i)) { my $wday = Orangutan::Date::ParseWeekDay($1); if (defined($wday)) { my $cwday = (localtime(time))[6]; if ($wday >= $cwday) { ($day, $month, $year) = (localtime(time - (7 - ($wday - $cwday)) * 86400))[3..5]; } else { ($day, $month, $year) = (localtime(time - ($cwday - $wday) * 86400))[3..5]; } $base = POSIX::mktime(0, 0, 0, $day, $month, $year); $message =~ s/$regexp//i; } } # On DD/MM/YYYY $regexp = '^(?:On )?(?:[A-z]{3,9}, )?(?:([0-9]{4})[./-])?([0-9]{1,2})[./-]([0-9]{1,2})(?:[./-]([0-9]{4}))?'; $regexp .= '(?! *(?:till|to|-) *[0-9]{1,2}(?:[:-][0-9]{2})?) ?[:\-]? '; if (!defined($base) && ($message =~ /$regexp/i)) { if (($3 > 12) || defined($1)) { ($month, $day) = ($2, $3); } else { ($day, $month) = ($2, $3); } if (($day > 0) && ($day <= 31) && ($month > 0) && ($month <= 12)) { if (defined($1)) { $year = $1; } elsif (defined($4)) { $year = $4; } else { $year = (localtime(time))[5]; my $time = POSIX::mktime(0, 0, 0, $day, $month - 1, $year); if (defined($time) && ($time > time)) { $year--; } $year += 1900; } $base = POSIX::mktime(0, 0, 0, $day, $month - 1, $year - 1900); if (defined($base)) { $message =~ s/$regexp//i; } } } # Yesterday $regexp = '^Yesterday '; if (!defined($base) && ($message =~ /$regexp/i)) { ($hour, $day, $month, $year) = (localtime(time - 86400))[2..5]; $base = POSIX::mktime(0, 0, 0, $day, $month, $year); if ($hour < 6) { $base -= 86400; } $message =~ s/$regexp//i; } # Default if (!defined($base)) { ($hour, $day, $month, $year) = (localtime(time))[2..5]; $base = POSIX::mktime(0, 0, 0, $day, $month, $year); if ($hour < 6) { $base -= 86400; } } # Extract/remove time $regexp = '^(?:(?:since|from) )?([0-9]{1,2}(?:[:-][0-9]{2})? *(?:(?:a\.?|p\.?)m\.?)?) *'; $regexp .= '(?:till|to|-) *([0-9]{1,2}(?:[:-][0-9]{2})? *(?:(?:a\.?|p\.?)m\.?)?) ?[:\-]? '; if ($message =~ /$regexp/i) { my $start = new Orangutan::Date($1, $base); if ($start) { my $end = new Orangutan::Date($2, $start->Date); if ($end) { unshift(@result, $end); unshift(@result, $start); $message =~ s/$regexp//i; } } } # Extract/remove work time if (scalar @result == 0) { $regexp = '^([0-9]{1,2})(?:([.,:-])([0-9]{1,2}))?(?: hours?)? ?[:\-]? '; if ($message =~ /$regexp/i) { my $duration = $1 * 3600; if ($2) { if (($2 eq '.') || ($2 eq ',')) { if (length($3) > 1) { $duration += (int(($3 * 60) / 100) * 60); } else { $duration += (int(($3 * 10 * 60) / 100) * 60); } } else { $duration += ($3 * 60); } } unshift(@result, $duration); unshift(@result, $base); $message =~ s/$regexp//i; } } if (scalar @result == 0) { $regexp = '^([0-9]{1,2}) hours?(?:,| and)? ([0-9]{1,2}) min(?:ute)?s? ?[:\-]? '; if ($message =~ /$regexp/i) { unshift(@result, $1 * 3600 + $2 * 60); unshift(@result, $base); } } if (scalar @result > 0) { # Extract/remove activity $regexp = ' \(([^)]+)\)$'; if ($message =~ /$regexp/i) { my $activity = $main::query->ActivityExists($1); if (defined($activity) && ($activity ne '')) { unshift(@result, $1); $message =~ s/$regexp//i; } else { unshift(@result, undef); } } else { unshift(@result, undef); } # Extract/remove project $regexp = '(.+) (?:on|for|at) ["\']?(.+?)["\']?$'; if ($message =~ /$regexp/i) { my $project = $main::query->ProjectExists($2); if (defined($project) && (ref $project eq 'HASH')) { unshift(@result, $project); $message =~ s/$regexp/$1/i; } else { unshift(@result, undef); } } else { unshift(@result, undef); } # Extract/remove issue my $trackers = $main::config->Get('redmine', '_trackers_regexp'); if ($trackers) { $regexp = ' (?:(?:on|for|at) )?(?:the )?(?:(?:issue|'.$trackers.') )?(?:#|No\.? ?)?([0-9]+)$'; } else { $regexp = ' (?:(?:on|for|at) )?(?:the )?(?:issue )?(?:#|No\.? ?)?([0-9]+)$'; } if ($message =~ /$regexp/i) { unshift(@result, $1); $message =~ s/$regexp//i; } else { unshift(@result, undef); } # Get task name if ($message =~ /^(?:I(?: was)? )?(.*[a-z]+.*)$/i) { unshift(@result, $1); unshift(@result, 1); } else { @result = ( ); } } return @result; }, handler => sub { my ($context, $user, $item, $name, $issueid, $project, $activity, $start, $end) = @_; $context->UnsetField('break'); $context->UnsetField('eventid'); # Create task my %taskargs = ( ); $taskargs{'name'} = $name; if (defined($issueid)) { $taskargs{'issue'} = $issueid; } elsif (!defined($project) && $user->GetIssue) { $taskargs{'issue'} = $user->GetIssue; } my $issue = undef; if (defined($taskargs{'issue'})) { $issue = $main::query->GetIssue($taskargs{'issue'}); if (defined($issue)) { if ($main::query->Can('edit_time_entries', $user, $issue->{'project_id'})) { if (defined($project)) { if ($project->{'id'} == $issue->{'project_id'}) { $user->SendMessage([ "The project is taken from the issue - so you don't need to specify it.", 'No need to specify the project I determine it using the issue...', 'You may omit the project name next time...', 'The project name is optional...' ]); } elsif (defined($issueid)) { $user->SendMessage([ 'Hm... This issue is not in this project... Ignoring it!', 'Ignoring the project - this issue is in different one.', 'You used invalid project... Anyway ignoring it...' ]); } } elsif ($issue->{'is_closed'}) { $user->SendMessage([ 'I hope you know that the issue is already closed...', 'I guess the issue was not closed at that time...', 'Just note that right now this issue is closed.' ]); } elsif ($issue->{'done_ratio'} == 100) { $user->SendMessage([ 'Oh... I guess at that time the issue was not completed yet...', 'Hm... Right now the issue seems to be completed...' ]); } $taskargs{'project'} = { id => $issue->{'project_id'}, name => $issue->{'project_name'} }; } else { $user->SendMessage([ "I'm not permitted to add tasks if a user is not a member of the project...", "Can't add this task - you must be a member of the project!", 'You are not a member of the project!' ]); return; } } else { $user->SendMessage([ "Can't add the task. Issue with such number does not exist!", 'Hm... I do not know anything about this issue...', 'Invalid issue ID...' ]); return; } } elsif (defined($project)) { if ($main::query->Can('edit_time_entries', $user, $project->{'id'})) { $taskargs{'project'} = { id => $project->{'id'}, name => $project->{'name'} }; } else { $user->SendMessage([ 'Sorry but you are not a member of this project!', "You may not add tasks for this project!", 'You must be a member of this project!' ]); return; } } else { $user->SendMessage([ 'Please always specify at least the project name when creating a task...', 'I need the project name of this task to add it.', 'You should specify the project of the task!' ]); return; } if (defined($activity)) { my $activityid = $main::query->ActivityExists($activity, $taskargs{'project'}->{'id'}); if ($activityid) { $taskargs{'activity'} = { id => $activityid, name => $activity }; } else { my $format = Orangutan::Context::Random( '%s does not seem to be a correct activity for project %s...', 'Activity %s looks to be disabled in project %s...', "I can't find activity %s in project %s..." ); $user->SendMessage(sprintf($format, $activity, $taskargs{'project'}->{'name'})); return; } } elsif ($user->GetActivity) { my $activityid = $main::query->ActivityExists($user->GetActivity, $taskargs{'project'}->{'id'}); if ($activityid) { $taskargs{'activity'} = { id => $activityid, name => $user->GetActivity }; } } if (!defined($taskargs{'activity'})) { my $default_activity = $main::query->GetDefaultActivity($taskargs{'project'}->{'id'}); if (defined($default_activity)) { $taskargs{'activity'} = { id => $default_activity->{'id'}, name => $default_activity->{'name'} }; } else { $user->SendMessage([ 'Please specify the activity in the task description...', 'I need to know the activity of the task...' ]); return; } } if (((ref $start eq 'Orangutan::Date') && ($start->Date > time)) || ((ref $start ne 'Orangutan::Date') && ($start > time))) { $user->SendMessage([ 'Perhaps you made a mistake specifying time - the task seems to be in future.', "Hey! This task is in future - I can't add it!", "I'm not allowed to add future tasks. Sorry!", "I'm not going to add future tasks... Sorry!" ]); return; } if (ref $start eq 'Orangutan::Date') { $taskargs{'start'} = $start; $taskargs{'end'} = $end; } else { $taskargs{'date'} = new Orangutan::Date($start); $taskargs{'duration'} = $end; } my $curtask = $user->GetTask; # Check length if ((defined($taskargs{'end'}) && defined($taskargs{'start'}) && (($taskargs{'end'}->Date - $taskargs{'start'}->Date) < 60)) || (defined($taskargs{'duration'}) && ($taskargs{'duration'} < 60))) { $user->SendMessage([ "Wow! It's unbelievably short task! I won't add it!", "I can't add such a short task! Sorry...", 'Task is too short... I cannot add it!' ]); return; } if (defined($taskargs{'start'}) && defined($taskargs{'end'})) { # Ensure that end time is not earlier than start time if ($taskargs{'end'}->Date < $taskargs{'start'}->Date) { $user->SendMessage([ "I won't add this task - its end time is earlier than its start time!", 'Wow! Task cannot be finished before it started!', 'End time should be later than start time!' ]); return; } # Check start time of current task if (defined($curtask)) { if (($curtask->GetStart->Date >= $taskargs{'start'}->Date) && ($curtask->GetStart->Date < $taskargs{'end'}->Date)) { $user->SendMessage([ "Can't add this task because at its time you started to work on the current one...", 'You need to get rid of the current task somehow to add this one...', "Task's time conflicts with the current task!" ]); return; } } # Get free time periods my @periods = $user->GetFreePeriods($taskargs{'start'}, $taskargs{'end'}); # Check dates if ((scalar @periods == 0) || (scalar @periods > 1) || ($periods[0][0] != $taskargs{'start'}->Date) || ($periods[0][1] != $taskargs{'end'}->Date)) { $user->SendMessage([ 'I cannot create a task with such time - there are conflicts...', 'Task cannot have this time - it conflicts with other tasks...', "Can't add this task - it conflicts with other tasks...", "Task's time conflicts with another task..." ]); if (scalar @periods > 0) { $user->SendMessage([ 'Conflicts can be resolved by any of the following:', 'You can resolve it by (choose any):', 'To resolve conflicts do (one of):' ]); if ($periods[0][0] == $taskargs{'start'}->Date) { my $format = Orangutan::Context::Random( ' o Change end time of the task to %02d:%02d.', ' o Set end time of the task to %02d:%02d.', " o Adjust task's end time to %02d:%02d." ); my $endtime = new Orangutan::Date($periods[0][1]); $user->SendMessage(sprintf($format, $endtime->GetTime)); } if ($periods[$#periods][1] == $taskargs{'end'}->Date) { my $format = Orangutan::Context::Random( ' o Set start time of the task to %02d:%02d.', ' o Change start time to %02d:%02d.' ); my $starttime = new Orangutan::Date($periods[$#periods][0]); $user->SendMessage(sprintf($format, $starttime->GetTime)); } foreach my $period (@periods) { if (($period->[0] != $taskargs{'start'}->Date) && ($period->[1] != $taskargs{'end'}->Date)) { my $format = Orangutan::Context::Random( ' o Change start time to %02d:%02d and end time to %02d:%02d.', ' o Set start time to %02d:%02d and end time to %02d:%02d.' ); my $starttime = new Orangutan::Date($period->[0]); my $endtime = new Orangutan::Date($period->[1]); $user->SendMessage(sprintf($format, $starttime->GetTime, $endtime->GetTime)); } } } return; } } # Notify if description was truncated my $max_length = $main::config->Get('redmine', 'description_max_length'); if (defined($max_length) && ($max_length > 0)) { if (length($name) > $max_length) { $user->SendMessage([ "If I saved this description it would be truncated because it's too long!", 'Redmine does not support such a long description...', 'The description of the task is too long...' ]); return; } } my $notes; if (defined($taskargs{'start'}) && defined($taskargs{'end'})) { # Breaking current task if (defined($curtask) && ($curtask->GetStart->Date < $taskargs{'start'}->Date)) { if ($curtask->GetStart->Elapsed >= 86400) { $user->SendMessage([ 'Adding this task requires breaking your current task. '. 'And I cannot do this (it lasts more than 24 hours)...', 'You need to stop your current task first!' ]); return; } else { my $curcopy = new Orangutan::Task($curtask); $notes = Orangutan::Context::Random( 'Needed to break the current task', 'Breaking the current task' ); # Set activity if it is not set yet if (!defined($curtask->GetActivity('id'))) { my $default_activity = $main::query->GetDefaultActivity($curtask->GetProject('id')); if (defined($default_activity)) { $curtask->SetActivity( id => $default_activity->{'id'}, name => $default_activity->{'name'} ); my $format = Orangutan::Context::Random( 'used activity "%s"...', 'set activity to %s' ); $notes .= ' ('.sprintf($format, $default_activity->{'name'}).')'; } else { $user->SendMessage([ "Can't break the current task - something is wrong with activities...", 'To add this task I would need to break the current one '. "and I can't do this because I don't know its activity..." ]); return; } } $curtask->SetEnd(new Orangutan::Date($taskargs{'start'})); # Submit if ($curtask->GetStart->WorkDayStart->Date != $curtask->GetEnd->WorkDayStart->Date) { my $copy = new Orangutan::Task($curtask); $curtask->SetEnd($curtask->GetStart->WorkDayEnd); $user->SubmitTask; $user->SetTask($copy); $copy->SetStart($copy->GetEnd->WorkDayStart); $user->SubmitTask; } else { $user->SubmitTask; } # Reopen $user->SetTask($curcopy); $curcopy->SetStart(new Orangutan::Date($taskargs{'end'})); $user->FireEvent('task', 'break', $curtask, $curcopy); $notes .= '.'; $context->SetField('break', 1); } } } # Saving task my $task = new Orangutan::Task(%taskargs); if ($task->GetStart && $task->GetEnd && ($task->GetStart->WorkDayStart->Date != $task->GetEnd->WorkDayStart->Date)) { my $copy = new Orangutan::Task($task); $task->SetEnd($task->GetStart->WorkDayEnd); $user->SubmitPreviousTask($task); $copy->SetStart($copy->GetEnd->WorkDayStart); $user->SubmitPreviousTask($copy); $context->SetField('eventid', [ $task, $copy ]); } else { $user->SubmitPreviousTask($task); $context->SetField('eventid', $task); } my $message = Orangutan::Context::Random('Ok.', 'Got it.', 'Cool.'); $message .= ' '.Orangutan::Context::Random('Submitting', 'Posting', 'Writing').': '; my ($day, $mon, $year, $wday) = (localtime($task->GetDate->Date))[3..6]; $message .= '"'.sprintf("%s, %s %d ", $Orangutan::Date::weekdays_short[$wday], $Orangutan::Date::months_short[$mon], $day); if ($task->GetStart && $task->GetEnd) { $message .= sprintf("%02d:%02d - %02d:%02d ", $task->GetStart->GetTime, $task->GetEnd->GetTime); } my $duration = $task->GetDuration; $message .= sprintf("(%d:%02d): ", int($duration / 3600), int(($duration % 3600) / 60)); $message .= $task->GetName.' '; if ($task->GetIssue) { utf8::decode($issue->{'subject'}); $message .= 'on '; $message .= $issue->{'tracker_name'}.' '; $message .= sprintf("#%d ", $issue->{'id'}); $message .= '"'.$issue->{'subject'}.'" '; } $message .= 'for '.$task->GetProject.' ('.$task->GetActivity.')"'; $message .= '.'; if (defined($notes)) { $message .= ' '.$notes; } $user->SendMessage($message); $user->FireEvent('task', 'add', $task); }, undo => sub { my ($context, $user) = @_; my $tasks = $context->GetField('eventid'); if (defined($tasks)) { if ($context->GetField('break')) { $user->SendMessage([ 'In order to add this task I needed to break current one... '. 'So I cannot cancel this. Sorry...', "Oops! I broke your current task... :( Can't undo this...", 'I would undo this if I would not break your current task...' ]); } else { if (ref $tasks eq 'ARRAY') { foreach my $task (@{$tasks}) { if (defined($task)) { $user->RemovePreviousTask($task->GetID); $user->FireEvent('task', 'remove', $task); } } } else { $user->RemovePreviousTask($tasks->GetID); $user->FireEvent('task', 'remove', $tasks); } my $message = Orangutan::Context::Random('Ok.', 'Got it.', 'Okay.', 'Oops.'); $message .= ' Sorry... '; $message .= Orangutan::Context::Random('Removed.', 'Removing.', 'Deleted.', 'Deleting.'); $user->SendMessage($message); } } else { $user->SendMessage([ 'Sorry... I failed to add the task...', 'I did not do anything...' ]); } }, help => { title => 'How can I add a past task?', question => '^(?:How )?(?:(?:can|do) I |to )add (?:a )?(?:past|previous) tasks?\?*!*\.*$', answer => "To add past tasks you must specify start and end times in addition to the description. ". "These are the only requirements. ". "If not specified the default issue, project and activity will be used.\n". "The format of the request is:\n". " - : on ()\n". "Sample requests:\n". " o On 2nd August 10:00 - 13:00: Working on #56 (Work time)\n". " o Yesterday 14:00 - 19:00: Working on #98 (Work time)\n". " o 10:00 - 11:30: Company meeting on #18 (Meeting)\n". " o Monday from 10:00 to 13:00: Working", weight => 670 }, weight => 90, copy => 1 ); # kate: syntax perl