From 58408d900aff55a3877aea546f6bee901c7e8464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Helleboid?= Date: Sun, 3 Nov 2019 11:58:52 +0400 Subject: [PATCH 1/3] README: fix link to Dovico documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b38d3c2..4dde369 100644 --- a/README.md +++ b/README.md @@ -162,4 +162,4 @@ yes You are warmly welcome to contribute to the project! # Dovico API Documentation -* http://apideveloper.dovico.com/ +* https://www.dovico.com/developer/API_doc/ From 1f60f8a796da0d4c2ec8a1e9bfb1107cc566de56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Helleboid?= Date: Sun, 3 Nov 2019 11:59:49 +0400 Subject: [PATCH 2/3] Improve management of list of Assignments - Implement a cache so we don't have to fetch all tasks on every call - Browse Assigments recursively so we handle TaskGroup (and any other forms of tree if it's ever allowed by Dovico) - Improve formatter so we have a nicer display per day --- CHANGELOG.md | 4 + README.md | 5 + lib/dovico.rb | 11 +- lib/dovico/app.rb | 17 +- lib/dovico/model/assignment.rb | 98 +++++++++- lib/dovico/model/assignments.rb | 62 +++++++ lib/dovico/model/client.rb | 4 + lib/dovico/model/employee.rb | 8 +- lib/dovico/model/project.rb | 45 ----- lib/dovico/model/task.rb | 2 - lib/dovico/model/task_group.rb | 4 + lib/dovico/model/time_entry.rb | 6 +- lib/dovico/model/time_entry_formatter.rb | 43 ++--- spec/helper.rb | 14 ++ spec/unit/dovico/model/assignment_spec.rb | 172 +++++++++++++++++- spec/unit/dovico/model/assignments_spec.rb | 151 +++++++++++++++ spec/unit/dovico/model/client_spec.rb | 26 +++ spec/unit/dovico/model/employee_spec.rb | 15 ++ spec/unit/dovico/model/project_spec.rb | 81 +-------- spec/unit/dovico/model/task_group_spec.rb | 26 +++ spec/unit/dovico/model/task_spec.rb | 4 +- .../dovico/model/time_entry_formatter_spec.rb | 53 +++++- spec/unit/dovico/model/time_entry_spec.rb | 11 +- 23 files changed, 688 insertions(+), 174 deletions(-) create mode 100644 lib/dovico/model/assignments.rb create mode 100644 lib/dovico/model/client.rb create mode 100644 lib/dovico/model/task_group.rb create mode 100644 spec/unit/dovico/model/assignments_spec.rb create mode 100644 spec/unit/dovico/model/client_spec.rb create mode 100644 spec/unit/dovico/model/task_group_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 85b6434..f330556 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - Set minimal versions of gems explicitly - Update minimal rubocop gem version (fix CVE-2017-8418) - Implement pagination for Assignments (Projets and Tasks) and Time Entries +- Implement a cache so tasks are saved locally +- Browse Assignments recursively so we handle TaskGroup (and any other forms of tree if it's ever allowed by Dovico) +- Improve formatter so we have a nicer display for each day +- Implement "Client" item # Version 1.4.0 - Fix documentation: Specific months, weeks or days use `special_` prefix. Fixes trainline-eu/dovico#15 diff --git a/README.md b/README.md index 4dde369..3149923 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,11 @@ Project | Task | Description 1600 | 100 | Frodo Project: Go home ~~~ +## Force tasks refresh +`dovico --force-refresh --tasks` + +It will force the application to download a fresh list of assignments from Dovico and save it locally. + ## Fill the timesheet `dovico --fill [date options]` diff --git a/lib/dovico.rb b/lib/dovico.rb index 08a3c46..ba61a83 100644 --- a/lib/dovico.rb +++ b/lib/dovico.rb @@ -1,14 +1,21 @@ require 'active_support' require 'active_support/core_ext/hash' +module Dovico + APP_DIRECTORY = "#{Dir.home}/.dovico/" +end + require 'dovico/version' require 'dovico/api_client' require 'dovico/app' require 'dovico/config_parser' require 'dovico/model/assignment' -require 'dovico/model/time_entry_formatter' -require 'dovico/model/time_entry_generator' +require 'dovico/model/assignments' +require 'dovico/model/client' require 'dovico/model/employee' require 'dovico/model/project' require 'dovico/model/task' +require 'dovico/model/task_group' require 'dovico/model/time_entry' +require 'dovico/model/time_entry_formatter' +require 'dovico/model/time_entry_generator' diff --git a/lib/dovico/app.rb b/lib/dovico/app.rb index ac3404f..9c0909f 100644 --- a/lib/dovico/app.rb +++ b/lib/dovico/app.rb @@ -26,8 +26,9 @@ def initialize def add_script_options config.add_command_line_section('Display informations') do |slop| - slop.on :myself, 'Display info on yourself', argument: false - slop.on :tasks, 'Display info on tasks', argument: false + slop.on :myself, 'Display info on yourself', argument: false + slop.on :tasks, 'Display info on tasks', argument: false + slop.on :force_refresh, 'Force refresh of tasks list', argument: false end config.add_command_line_section('Fill the timesheets') do |slop| slop.on :fill, 'Fill the timesheet', argument: false @@ -100,8 +101,12 @@ def run private + def assignments + @assignments ||= Assignments.new(force_refresh: config[:force_refresh]) + end + def myself - @myself ||= Employee.myself + assignments.myself end def display_myself @@ -112,14 +117,14 @@ def display_myself def display_tasks puts "== List of available projects ==" - puts Project.format_all + puts assignments.format_tree puts "" end def display_time_entries(start_date, end_date) puts "== List of Time Entries between #{start_date} and #{end_date} ==" - formatter = TimeEntryFormatter.new(Project.all) - time_entries = TimeEntry.search(start_date, end_date) + formatter = TimeEntryFormatter.new(assignments) + time_entries = TimeEntry.search(myself.id, start_date, end_date) puts formatter.format_entries(time_entries) puts "" end diff --git a/lib/dovico/model/assignment.rb b/lib/dovico/model/assignment.rb index dfc916e..9b30f78 100644 --- a/lib/dovico/model/assignment.rb +++ b/lib/dovico/model/assignment.rb @@ -6,20 +6,106 @@ class Assignment include ActiveAttr::Model + attribute :assignments + attribute :id attribute :assignement_id + attribute :get_assignments_uri attribute :name attribute :start_date attribute :finish_date + def self.unserialize(assignments_hash) + assignments_hash.map do |assignment_hash| + assignments_array = assignment_hash.delete("assignments") + + assignment = class_for(assignment_hash["assignement_id"].chr).new(assignment_hash) + assignment.assignments = unserialize(assignments_array) + + assignment + end.sort_by(&:id) + end + def self.parse(hash) - self.new( - id: hash["ItemID"], - assignement_id: hash["AssignmentID"], - name: hash["Name"], - start_date: hash["StartDate"], - finish_date: hash["FinishDate"] + assignement_id = hash["AssignmentID"] + + class_for(assignement_id).new( + id: hash["ItemID"], + assignement_id: assignement_id, + get_assignments_uri: hash["GetAssignmentsURI"], + name: hash["Name"], + start_date: hash["StartDate"], + finish_date: hash["FinishDate"], + assignments: [], ) end + + def self.class_for(assignement_id) + case assignement_id.chr + when 'P' + Project + when 'G' + TaskGroup + when 'T' + Task + when 'C' + Client + else + raise "AssignmentID #{assignement_id} unsupported" + end + end + private_class_method :class_for + + def self.fetch_assignments(assignments_path) + assignments_list = ApiClient.get_paginated_list(assignments_path, "Assignments") + + assignments = assignments_list["Assignments"].map do |assignment_hash| + assignment = parse(assignment_hash) + + if assignment.get_assignments_uri.present? && + assignment.get_assignments_uri != "N/A" && + !assignment.is_a?(Task) # Task has EmployeeAssignment as assignements that are not useful to fetch + + assignment.assignments = fetch_assignments(assignment.get_assignments_uri) + end + + assignment + end + + assignments.sort_by(&:id) + end + private_class_method :fetch_assignments + + def self.fetch_all + fetch_assignments(URL_PATH) + end + + def find_object(klass, object_id) + if self.is_a?(klass) && id == object_id + self + elsif assignments.present? + assignments.find do |assignment| + object = assignment.find_object(klass, object_id) + + break object if object.present? + end + end + end + + def to_s(depth = 0) + string = " " * depth*2 + "#{type} ##{id} #{name}" + + if assignments.present? + string += "\n#{assignments.map {|a| a.to_s(depth + 1) }.join("\n")}\n" + end + + string + end + + private + def type + self.class.name.demodulize + end + end end diff --git a/lib/dovico/model/assignments.rb b/lib/dovico/model/assignments.rb new file mode 100644 index 0000000..b8b3364 --- /dev/null +++ b/lib/dovico/model/assignments.rb @@ -0,0 +1,62 @@ +require 'fileutils' + +module Dovico + class Assignments + class CacheError < RuntimeError; end + + attr_reader :assignments, :myself + + CACHE_FILE = "#{APP_DIRECTORY}/assignments.json" + CACHE_VERSION = "2" + + def initialize(force_refresh: false) + if !force_refresh + load_from_cache! + end + + @assignments ||= Assignment.fetch_all + @myself ||= Employee.myself + + save_to_cache! + end + + def format_tree + assignments.map(&:to_s).join("\n") + end + + def find_project_task(project_id, task_id) + project = assignments.find {|assignment| assignment.find_object(Project, project_id) } + task = project.find_object(Task, task_id) + + if project.nil? || task.nil? + raise "Can't find project##{project_id}/task##{task_id}. Try with --force-refresh option" + end + + [project, task] + end + + private + + def load_from_cache! + raise CacheError.new("Cache does not exist") unless File.exists?(CACHE_FILE) + + json = JSON.parse(File.read(CACHE_FILE)) + raise CacheError.new("Cache version mismatch") unless json["version"] == CACHE_VERSION + + @assignments = Assignment.unserialize(json["assignments"]) unless json["assignments"].nil? + @myself = Employee.unserialize(json["myself"]) unless json["myself"].nil? + rescue JSON::ParserError, CacheError => e + end + + def save_to_cache! + cache = { + version: CACHE_VERSION, + assignments: @assignments, + myself: @myself, + } + + FileUtils.mkdir_p(APP_DIRECTORY) + File.write(CACHE_FILE, cache.to_json) + end + end +end diff --git a/lib/dovico/model/client.rb b/lib/dovico/model/client.rb new file mode 100644 index 0000000..5e63efd --- /dev/null +++ b/lib/dovico/model/client.rb @@ -0,0 +1,4 @@ +module Dovico + class Client < Assignment + end +end diff --git a/lib/dovico/model/employee.rb b/lib/dovico/model/employee.rb index f87db86..1e0d4df 100644 --- a/lib/dovico/model/employee.rb +++ b/lib/dovico/model/employee.rb @@ -1,7 +1,5 @@ -require 'active_attr' - module Dovico - class Employee + class Employee < Assignment URL_PATH = 'Employees' include ActiveAttr::Model @@ -24,6 +22,10 @@ def self.myself parse(employees["Employees"].first) end + def self.unserialize(employee_hash) + Employee.new(employee_hash) + end + def to_s %{ - ID: #{id} - First Name: #{first_name} diff --git a/lib/dovico/model/project.rb b/lib/dovico/model/project.rb index f71f5a3..36ce308 100644 --- a/lib/dovico/model/project.rb +++ b/lib/dovico/model/project.rb @@ -1,49 +1,4 @@ -require 'active_attr' - module Dovico class Project < Assignment - - attribute :tasks - - def self.parse(hash) - project = super(hash) - project.tasks ||= [] - project - end - - def self.all - projects_search = ApiClient.get_paginated_list(URL_PATH, "Assignments") - projects = projects_search["Assignments"].map {|project_hash| parse(project_hash) } - - projects.each do |project| - tasks_search = ApiClient.get_paginated_list("#{URL_PATH}/#{project.assignement_id}", "Assignments") - tasks = tasks_search["Assignments"].map {|task_hash| Task.parse(task_hash) } - - project.tasks = tasks.sort_by do |task| - task.id - end - end - - projects - end - - def self.format_all - text = " Project | Task | Description\n" - text += all.map(&:to_s).join("\n") - end - - def to_s - text = '' - - if tasks.count > 0 - text += tasks.map do |task| - sprintf " %7d | %4d | %s: %s", id, task.id, name, task.name - end.join("\n") - else - text += sprintf " %7d | | %s (No tasks linked)", id, name - end - - text - end end end diff --git a/lib/dovico/model/task.rb b/lib/dovico/model/task.rb index 0acfbf0..995996a 100644 --- a/lib/dovico/model/task.rb +++ b/lib/dovico/model/task.rb @@ -1,5 +1,3 @@ -require 'active_attr' - module Dovico class Task < Assignment end diff --git a/lib/dovico/model/task_group.rb b/lib/dovico/model/task_group.rb new file mode 100644 index 0000000..49fed0f --- /dev/null +++ b/lib/dovico/model/task_group.rb @@ -0,0 +1,4 @@ +module Dovico + class TaskGroup < Assignment + end +end diff --git a/lib/dovico/model/time_entry.rb b/lib/dovico/model/time_entry.rb index f3cc242..ae0d0a9 100644 --- a/lib/dovico/model/time_entry.rb +++ b/lib/dovico/model/time_entry.rb @@ -39,9 +39,9 @@ def self.get(id) TimeEntry.parse(entry) end - def self.search(start_date, end_date) + def self.search(employee_id, start_date, end_date) api_response = ApiClient.get_paginated_list( - URL_PATH, + "#{URL_PATH}/Employee/#{employee_id}/", "TimeEntries", params: { daterange: "#{start_date} #{end_date}" @@ -50,7 +50,7 @@ def self.search(start_date, end_date) api_response["TimeEntries"].map do |time_entry| TimeEntry.parse(time_entry) - end + end.sort_by(&:date) end def self.batch_create!(assignments) diff --git a/lib/dovico/model/time_entry_formatter.rb b/lib/dovico/model/time_entry_formatter.rb index 3d7c4a2..40fa59f 100644 --- a/lib/dovico/model/time_entry_formatter.rb +++ b/lib/dovico/model/time_entry_formatter.rb @@ -1,41 +1,42 @@ module Dovico class TimeEntryFormatter - def initialize(projects) - @projects = projects + def initialize(assignments) + @assignments = assignments end def format_entries(time_entries) - text = "" - time_entries.map do |time_entry| - text += "#{}" - time_entry_text(time_entry) - end.join("\n") + time_entries.group_by(&:date).map do |date, day_time_entries| + hours = 0 + + day_time_entries.map do |time_entry| + string = "#{date} #{progress_bar(hours, time_entry.total_hours)} #{time_entry_text(time_entry)}" + hours += time_entry.total_hours.to_f + + string + end + end.flatten.join("\n") end private - attr_accessor :projects + attr_accessor :assignments + + def progress_bar(shift, total_hours) + progress_bar_width = (total_hours.to_f * 2).to_i + sprintf( + "[%- 14s]", " " * shift * 2 + "×" * progress_bar_width + ) + end def time_entry_text(time_entry) - project, task = project_task(time_entry.project_id, time_entry.task_id) + project, task = assignments.find_project_task(time_entry.project_id, time_entry.task_id) - progress_bar_width = (time_entry.total_hours.to_f * 2).to_i - sprintf("%s [%s] %s : [%8s] %2sh %s %s", - time_entry.date, - "×" * progress_bar_width, - " " * [16 - progress_bar_width, 0].max, + sprintf("[%-12s] %2sh %s %s", time_entry.formal_sheet_status, time_entry.total_hours, project.name, task.name, ) end - - def project_task(project_id, task_id) - project = projects.select{ |project| project.id == project_id }.first - task = project.tasks.select{ |task| task.id == task_id }.first - - [project, task] - end end end diff --git a/spec/helper.rb b/spec/helper.rb index e42d690..afd2d3a 100644 --- a/spec/helper.rb +++ b/spec/helper.rb @@ -15,6 +15,8 @@ require "timecop" require "rack/test" require 'webmock/rspec' +require 'fileutils' +require 'tmpdir' require 'pry' @@ -23,3 +25,15 @@ Timecop.safe_mode = true WebMock.disable_net_connect!(allow_localhost: true) + +RSpec.configure do |config| + # Use a temporary directory for cache file + config.before(:example) do + @cache_dir = Dir.mktmpdir("dovico") + stub_const("Dovico::Assignments::CACHE_FILE", "#{@cache_dir}/assignments.json") + end + + config.after(:example) do + FileUtils.rm_rf(@cache_dir) + end +end diff --git a/spec/unit/dovico/model/assignment_spec.rb b/spec/unit/dovico/model/assignment_spec.rb index 6957051..fe64df8 100644 --- a/spec/unit/dovico/model/assignment_spec.rb +++ b/spec/unit/dovico/model/assignment_spec.rb @@ -5,7 +5,7 @@ module Dovico let(:assignment_api_hash) do { "ItemID": "123", - "AssignmentID": "T456", + "AssignmentID": "T123", "Name": "Dovico API Client", "StartDate": "2017-01-01", "FinishDate": "2017-12-31", @@ -18,9 +18,177 @@ module Dovico expect(assignment).to be_an(Dovico::Assignment) expect(assignment.id).to eq('123') - expect(assignment.assignement_id).to eq('T456') + expect(assignment.assignement_id).to eq('T123') expect(assignment.name).to eq('Dovico API Client') end end + + describe ".unserialize" do + let(:assignments_json_hash) do + [ + { + "id" => "123", + "assignement_id" => "P123", + "name" => "Example Project Name", + "start_date" => "2017-01-01", + "finish_date" => "2017-12-31", + "get_assignments_uri" => "https://api.dovico.com/Assignments/P123/?version=5", + "assignments" => [ + { + "id" => "520", + "assignement_id" => "G520", + "name" => "Example TaskGroup Name", + "start_date" => "N/A", + "finish_date" => "N/A", + "assignments" => [], + "get_assignments_uri" => "https://api.dovico.com/Assignments/T987/?version=5" + }, + { + "id" => "888", + "assignement_id" => "C888", + "name" => "Example Client Name", + "start_date" => "N/A", + "finish_date" => "N/A", + "assignments" => [], + "get_assignments_uri" => "https://api.dovico.com/Assignments/C888/?version=5" + }, + { + "id" => "987", + "assignement_id" => "T987", + "name" => "Example Task Name", + "start_date" => "N/A", + "finish_date" => "N/A", + "assignments" => [], + "get_assignments_uri" => "https://api.dovico.com/Assignments/T987/?version=5" + }, + ], + } + ] + end + + it "unserializes a hash to an object" do + assignments = Dovico::Assignment.unserialize(assignments_json_hash) + + expect(assignments.count).to eq(1) + project = assignments.first + expect(project).to be_an(Dovico::Project) + expect(project.id).to eq('123') + expect(project.assignement_id).to eq('P123') + expect(project.name).to eq('Example Project Name') + + expect(project.assignments.count).to eq(3) + + group_task = project.assignments[0] + expect(group_task).to be_an(Dovico::TaskGroup) + expect(group_task.id).to eq('520') + + client = project.assignments[1] + expect(client).to be_an(Dovico::Client) + expect(client.id).to eq('888') + + task = project.assignments[2] + expect(task).to be_an(Dovico::Task) + expect(task.id).to eq('987') + end + + it "raises if the type is unknown" do + expect do + Dovico::Assignment.unserialize([{ "assignement_id" => "X456" }]) + end.to raise_error("AssignmentID X unsupported") + end + end + + describe ".fetch_all" do + let(:project_api_hash) do + { + "ItemID": "123", + "AssignmentID": "P123", + "Name": "Project Dovico API Client", + "StartDate": "2017-01-01", + "FinishDate": "2017-12-31", + "GetAssignmentsURI": "https://dovico.example/Assignments/T456?version=5" + }.stringify_keys + end + let(:projects_api_hash) do + { + "Assignments": [project_api_hash] + }.stringify_keys + end + let(:task_api_hash_1) do + { + "ItemID": "995", + "AssignmentID": "T995", + "Name": "Task write specs, second part", + }.stringify_keys + end + let(:task_api_hash_2) do + { + "ItemID": "789", + "AssignmentID": "T789", + "Name": "Task write specs", + "StartDate": "2016-10-25", + "FinishDate": "2018-05-01", + }.stringify_keys + end + let(:tasks_api_hash) do + { + "Assignments": [task_api_hash_1, task_api_hash_2] + }.stringify_keys + end + + before do + allow(ApiClient).to receive(:get_paginated_list).with(Dovico::Assignment::URL_PATH, "Assignments").and_return(projects_api_hash) + allow(ApiClient).to receive(:get_paginated_list).with("https://dovico.example/Assignments/T456?version=5", "Assignments").and_return(tasks_api_hash) + end + + it "fetches recursively all the assignements" do + projects = Dovico::Assignment.fetch_all + + expect(projects.count).to eq(1) + project = projects.first + expect(project).to be_an(Dovico::Project) + expect(project.id).to eq('123') + expect(project.name).to eq('Project Dovico API Client') + + expect(project.assignments.count).to eq(2) + task = project.assignments.first + expect(task.id).to eq('789') + expect(task.name).to eq('Task write specs') + end + end + + describe "#find_object" do + let(:task) { Dovico::Task.new(id: "789") } + let(:task_group) { Dovico::TaskGroup.new(id: "456", assignments: [ task ]) } + let(:project) { Dovico::Project.new(id: "123", assignments: [ task_group ]) } + + context "when object is self" do + it 'returns itself' do + expect(project.find_object(Project, "123")).to eq(project) + end + end + + context "when object is its assignments" do + it 'searches recursively' do + expect(project.find_object(TaskGroup, "456")).to eq(task_group) + expect(project.find_object(Task, "789")).to eq(task) + end + end + end + + describe "#to_s" do + let(:task) { Dovico::Task.new(id: "789", name: "BigProject") } + let(:task_group) { Dovico::TaskGroup.new(id: "456", name: "MiddleGroup", assignments: [ task ]) } + let(:project) { Dovico::Project.new(id: "123", name: "SmallTask", assignments: [ task_group ]) } + + it 'returns a string representation' do + expect(project.to_s).to eq( + "Project #123 SmallTask\n"+ + " TaskGroup #456 MiddleGroup\n"+ + " Task #789 BigProject\n"+ + "\n" + ) + end + end end end diff --git a/spec/unit/dovico/model/assignments_spec.rb b/spec/unit/dovico/model/assignments_spec.rb new file mode 100644 index 0000000..64bc877 --- /dev/null +++ b/spec/unit/dovico/model/assignments_spec.rb @@ -0,0 +1,151 @@ +require "helper" + +module Dovico + describe Dovico::Assignments do + let(:task_1) { Dovico::Task.new(name: "T789", id: "789") } + let(:task_group) { Dovico::TaskGroup.new(name: "TG555", id: "555", assignments: [task_1]) } + let(:project_1) { Dovico::Project.new(id: "123", name: "P1", assignments: [task_1]) } + let(:project_2) { Dovico::Project.new(id: "456", name: "P2", assignments: [task_group]) } + let(:assignments) { Dovico::Assignments.new } + let(:myself) { Dovico::Employee.new(id: "99") } + let(:assignments_json_hash) do + [ + { + "id" => "123", + "assignement_id" => "P123", + "name" => "Example Project Name", + "start_date" => "2017-01-01", + "finish_date" => "2017-12-31", + "get_assignments_uri" => "https://api.dovico.com/Assignments/P123/?version=5", + "assignments" => [ + { + "id" => "987", + "assignement_id" => "T987", + "name" => "Example Task Name", + "start_date" => "N/A", + "finish_date" => "N/A", + "assignments" => [], + "get_assignments_uri" => "https://api.dovico.com/Assignments/T987/?version=5" + }, + ] + } + ] + end + + before do + allow(Dovico::Assignment).to receive(:fetch_all).and_return([project_1, project_2]) + allow(Dovico::Employee).to receive(:myself).and_return(myself) + end + + describe ".new" do + before do + allow(Dovico::Assignment).to receive(:fetch_all).and_return([project_1, project_2]) + end + + context "when the cache file is valid" do + it 'loads from the cache' do + write_cache_file(Dovico::Assignments::CACHE_VERSION, assignments_json_hash) + + assignments = Dovico::Assignments.new + expect(assignments.assignments.count).to eq(1) + expect(assignments.assignments.first.id).to eq("123") + + expect(Dovico::Assignment).not_to have_received(:fetch_all) + end + end + + context "when force_refresh is true" do + it 'fetches the assignments from the API' do + write_cache_file(Dovico::Assignments::CACHE_VERSION, assignments_json_hash) + + assignments = Dovico::Assignments.new(force_refresh: true) + expect(assignments.assignments.count).to eq(2) + + expect(Dovico::Assignment).to have_received(:fetch_all) + end + end + + context 'when no cache exists' do + it 'fetches the assignments from the API and saves the cache' do + assignments = Dovico::Assignments.new + expect(assignments.assignments.count).to eq(2) + + expect(Dovico::Assignment).to have_received(:fetch_all) + + cache_content = File.read(Dovico::Assignments::CACHE_FILE) + cache_saved = JSON.parse(cache_content) + expect(cache_saved["version"]).to eq(Dovico::Assignments::CACHE_VERSION) + expect(cache_saved["assignments"].count).to eq(2) + expect(cache_saved["assignments"].first["id"]).to eq("123") + end + end + + context 'when the cache file is not JSON valid' do + it 'fetches the assignments from the API' do + File.write(Dovico::Assignments::CACHE_FILE, "err-not-json") + + assignments = Dovico::Assignments.new + expect(assignments.assignments.count).to eq(2) + + expect(Dovico::Assignment).to have_received(:fetch_all) + end + end + + context 'when the cache version does not match' do + it 'fetches the assignments from the API' do + write_cache_file("test-version", assignments_json_hash) + + assignments = Dovico::Assignments.new + expect(assignments.assignments.count).to eq(2) + + expect(Dovico::Assignment).to have_received(:fetch_all) + end + end + end + + describe ".format_tree" do + it 'returns assignments formatted' do + expect(assignments.format_tree).to eq( + "Project #123 P1\n"+ + " Task #789 T789\n"+ + "\n"+ + "Project #456 P2\n"+ + " TaskGroup #555 TG555\n"+ + " Task #789 T789\n\n" +) + end + end + + describe ".find_project_task" do + context 'when a task matches' do + it 'returns project and task id' do + project, task = assignments.find_project_task(project_1.id, task_1.id) + expect(project).to eq(project_1) + expect(task).to eq(task_1) + + project, task = assignments.find_project_task(project_2.id, task_1.id) + expect(project).to eq(project_2) + expect(task).to eq(task_1) + end + + end + + context 'when no tasks match' do + it 'raises an error' do + expect do + assignments.find_project_task(project_1.id, "99999") + end.to raise_error("Can't find project#123/task#99999. Try with --force-refresh option") + end + + end + end + + def write_cache_file(version, assignments) + cache = { + version: version, + assignments: assignments, + } + File.write(Dovico::Assignments::CACHE_FILE, cache.to_json) + end + end +end diff --git a/spec/unit/dovico/model/client_spec.rb b/spec/unit/dovico/model/client_spec.rb new file mode 100644 index 0000000..072c38b --- /dev/null +++ b/spec/unit/dovico/model/client_spec.rb @@ -0,0 +1,26 @@ +require "helper" + +module Dovico + describe Dovico::Client do + let(:client_api_hash) do + { + "ItemID": "123", + "AssignmentID": "C123", + "Name": "Dovico API Client", + "StartDate": "2017-01-01", + "FinishDate": "2017-12-31", + }.stringify_keys + end + + describe ".parse" do + it "parses the API hash" do + client = Dovico::Task.parse(client_api_hash) + + expect(client).to be_an(Dovico::Client) + expect(client.id).to eq('123') + expect(client.assignement_id).to eq('C123') + expect(client.name).to eq('Dovico API Client') + end + end + end +end diff --git a/spec/unit/dovico/model/employee_spec.rb b/spec/unit/dovico/model/employee_spec.rb index 2ab84df..a8c4f88 100644 --- a/spec/unit/dovico/model/employee_spec.rb +++ b/spec/unit/dovico/model/employee_spec.rb @@ -50,5 +50,20 @@ module Dovico expect(subject.to_s).to eq(" - ID: 123\n - First Name: James\n - Last Name: Bond") end end + + describe '.unserialize' do + let(:employee_hash) do + { + id: "007", + first_name: "James", + last_name: "Bond", + } + end + it 'returns new instance of the object' do + employee = Employee.unserialize(employee_hash) + + expect(employee.id).to eq("007") + end + end end end diff --git a/spec/unit/dovico/model/project_spec.rb b/spec/unit/dovico/model/project_spec.rb index b63d64f..fc023ea 100644 --- a/spec/unit/dovico/model/project_spec.rb +++ b/spec/unit/dovico/model/project_spec.rb @@ -2,87 +2,24 @@ module Dovico describe Dovico::Project do - subject do - Dovico::Project.parse(project_api_hash) - end - let(:project_api_hash) do { "ItemID": "123", - "AssignmentID": "T456", - "Name": "Project Dovico API Client", + "AssignmentID": "P123", + "Name": "Dovico API Client", "StartDate": "2017-01-01", "FinishDate": "2017-12-31", }.stringify_keys end - let(:projects_api_hash) do - { - "Assignments": [project_api_hash] - }.stringify_keys - end - let(:task_api_hash_1) do - { - "ItemID": "995", - "AssignmentID": "E456", - "Name": "Task write specs, second part", - }.stringify_keys - end - let(:task_api_hash_2) do - { - "ItemID": "789", - "AssignmentID": "E456", - "Name": "Task write specs", - "StartDate": "2016-10-25", - "FinishDate": "2018-05-01", - }.stringify_keys - end - let(:tasks_api_hash) do - { - "Assignments": [task_api_hash_1, task_api_hash_2] - }.stringify_keys - end - - describe ".all" do - before do - allow(ApiClient).to receive(:get_paginated_list).with(Dovico::Project::URL_PATH, "Assignments").and_return(projects_api_hash) - allow(ApiClient).to receive(:get_paginated_list).with("#{Dovico::Project::URL_PATH}/T456", "Assignments").and_return(tasks_api_hash) - end - - it "lists all the assignements" do - projects = Dovico::Project.all - - expect(projects.count).to eq(1) - project = projects.first - expect(project).to be_an(Dovico::Project) - expect(project.id).to eq('123') - expect(project.name).to eq('Project Dovico API Client') - - expect(project.tasks.count).to eq(2) - task = project.tasks.first - expect(task.id).to eq('789') - expect(task.name).to eq('Task write specs') - end - end - - describe ".format_all" do - before do - allow(ApiClient).to receive(:get_paginated_list).with(Dovico::Project::URL_PATH, "Assignments").and_return(projects_api_hash) - allow(ApiClient).to receive(:get_paginated_list).with("#{Dovico::Project::URL_PATH}/T456", "Assignments").and_return(tasks_api_hash) - end - it 'returns projects with formatted text' do - expected_strings = [ - ' Project | Task | Description', - ' 123 | 789 | Project Dovico API Client: Task write specs', - ' 123 | 995 | Project Dovico API Client: Task write specs, second part', - ] - expect(Dovico::Project.format_all).to eq(expected_strings.join("\n")) - end - end + describe ".parse" do + it "parses the API hash" do + task = Dovico::Project.parse(project_api_hash) - describe '#to_s' do - it 'returns object with formatted text' do - expect(subject.to_s).to eq(" 123 | | Project Dovico API Client (No tasks linked)") + expect(task).to be_an(Dovico::Project) + expect(task.id).to eq('123') + expect(task.assignement_id).to eq('P123') + expect(task.name).to eq('Dovico API Client') end end end diff --git a/spec/unit/dovico/model/task_group_spec.rb b/spec/unit/dovico/model/task_group_spec.rb new file mode 100644 index 0000000..3ad729c --- /dev/null +++ b/spec/unit/dovico/model/task_group_spec.rb @@ -0,0 +1,26 @@ +require "helper" + +module Dovico + describe Dovico::TaskGroup do + let(:task_group_api_hash) do + { + "ItemID": "123", + "AssignmentID": "G123", + "Name": "Dovico API Client", + "StartDate": "2017-01-01", + "FinishDate": "2017-12-31", + }.stringify_keys + end + + describe ".parse" do + it "parses the API hash" do + task = Dovico::TaskGroup.parse(task_group_api_hash) + + expect(task).to be_an(Dovico::TaskGroup) + expect(task.id).to eq('123') + expect(task.assignement_id).to eq('G123') + expect(task.name).to eq('Dovico API Client') + end + end + end +end diff --git a/spec/unit/dovico/model/task_spec.rb b/spec/unit/dovico/model/task_spec.rb index 6f77e76..66b5620 100644 --- a/spec/unit/dovico/model/task_spec.rb +++ b/spec/unit/dovico/model/task_spec.rb @@ -5,7 +5,7 @@ module Dovico let(:task_api_hash) do { "ItemID": "123", - "AssignmentID": "E456", + "AssignmentID": "T123", "Name": "Dovico API Client", "StartDate": "2017-01-01", "FinishDate": "2017-12-31", @@ -18,7 +18,7 @@ module Dovico expect(task).to be_an(Dovico::Task) expect(task.id).to eq('123') - expect(task.assignement_id).to eq('E456') + expect(task.assignement_id).to eq('T123') expect(task.name).to eq('Dovico API Client') end end diff --git a/spec/unit/dovico/model/time_entry_formatter_spec.rb b/spec/unit/dovico/model/time_entry_formatter_spec.rb index 1c40873..010bced 100644 --- a/spec/unit/dovico/model/time_entry_formatter_spec.rb +++ b/spec/unit/dovico/model/time_entry_formatter_spec.rb @@ -2,26 +2,63 @@ module Dovico describe Dovico::TimeEntryFormatter do - let(:task) { Dovico::Task.new(id: "1212", name: "TestTask") } - let(:project) { Dovico::Project.new(id: "9898", name: "TestProject", tasks: [task]) } - let(:time_entry) do + let(:task_1) { Dovico::Task.new(id: "1", name: "TestTask 1") } + let(:task_2) { Dovico::Task.new(id: "2", name: "TestTask 2") } + let(:task_group) { Dovico::TaskGroup.new(assignments: [task_2]) } + let(:project) { Dovico::Project.new(id: "42", name: "TestProject", assignments: [task_group, task_1]) } + let(:myself) { Dovico::Employee.new(id: "99") } + let(:assignments) { Dovico::Assignments.new } + let(:time_entry_1) do Dovico::TimeEntry.new( - project_id: "9898", - task_id: "1212", + project_id: "42", + task_id: "1", start_time: "0900", stop_time: "1200", total_hours: "3", - sheet_status: "U" + sheet_status: "U", + date: "2018-01-01", ) end + let(:time_entry_2) do + Dovico::TimeEntry.new( + project_id: "42", + task_id: "2", + start_time: "1400", + stop_time: "1800", + total_hours: "4", + sheet_status: "U", + date: "2018-01-01", + ) + end + let(:time_entry_3) do + Dovico::TimeEntry.new( + project_id: "42", + task_id: "1", + start_time: "0900", + stop_time: "1200", + total_hours: "3", + sheet_status: "U", + date: "2018-01-02", + ) + end + let(:time_entries) { [time_entry_1, time_entry_2, time_entry_3] } + + before do + allow(Assignment).to receive(:fetch_all).and_return([project]) + allow(Employee).to receive(:myself).and_return(myself) + end subject do - Dovico::TimeEntryFormatter.new([project]) + Dovico::TimeEntryFormatter.new(assignments) end describe "#format_entries" do it 'returns formatted tasks' do - expect(subject.format_entries([time_entry])).to eq(' [××××××] : [under_review] 3h TestProject TestTask') + expect(subject.format_entries(time_entries)).to eq( + "2018-01-01 [×××××× ] [under_review] 3h TestProject TestTask 1\n"+ + "2018-01-01 [ ××××××××] [under_review] 4h TestProject TestTask 2\n"+ + "2018-01-02 [×××××× ] [under_review] 3h TestProject TestTask 1" + ) end end end diff --git a/spec/unit/dovico/model/time_entry_spec.rb b/spec/unit/dovico/model/time_entry_spec.rb index 324ea56..9860afa 100644 --- a/spec/unit/dovico/model/time_entry_spec.rb +++ b/spec/unit/dovico/model/time_entry_spec.rb @@ -41,6 +41,7 @@ module Dovico "Description": "Unit test", }.stringify_keys end + let(:employee_id) { "99" } describe ".parse" do it "parses the API hash" do @@ -99,13 +100,19 @@ module Dovico end it 'calls the API and return an TimeEntry object' do - time_entries = Dovico::TimeEntry.search(Date.parse('2017-01-10'), Date.parse('2017-01-20')) + time_entries = Dovico::TimeEntry.search(employee_id, Date.parse('2017-01-10'), Date.parse('2017-01-20')) expect(time_entries.count).to eq(1) time_entry = time_entries.first - expect(ApiClient).to have_received(:get).with("#{Dovico::TimeEntry::URL_PATH}", { params: { daterange: "2017-01-10 2017-01-20" } }) + expect(ApiClient).to have_received(:get).with("#{Dovico::TimeEntry::URL_PATH}/Employee/#{employee_id}/", + { + params: { + daterange: "2017-01-10 2017-01-20" + } + } + ) expect(time_entry).to be_an(Dovico::TimeEntry) expect(time_entry.id).to eq('456') end From f27e4d252111a42b2e01ef5d992b663005daf8d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Helleboid?= Date: Mon, 25 Nov 2019 10:36:39 +0400 Subject: [PATCH 3/3] Fix missing myself id in clear task --- lib/dovico/app.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/dovico/app.rb b/lib/dovico/app.rb index 9c0909f..8c552af 100644 --- a/lib/dovico/app.rb +++ b/lib/dovico/app.rb @@ -130,7 +130,7 @@ def display_time_entries(start_date, end_date) end def clear_time_entries(start_date, end_date) - time_entries = TimeEntry.search(start_date, end_date) + time_entries = TimeEntry.search(myself.id, start_date, end_date) if highline.agree("• #{time_entries.count} Time Entries to be deleted. Are you sure? (yes/no)") time_entries.each do |time_entry| time_entry.delete!