dovico 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ # Personal token. Can be reset through Dovico setting page
2
+ user_token: "...."
3
+ # Your company token
4
+ client_token: "...."
5
+ assignments:
6
+ default_day:
7
+ - project_id: 1234
8
+ task_id: 100
9
+ hours: 3
10
+ - project_id: 9999
11
+ task_id: 120
12
+ hours: 2
13
+ - project_id: 4321
14
+ task_id: 424
15
+ hours: 2
16
+ # Quotes around day are mandatory
17
+ # On leave: use an empty array
18
+ '2016-01-17': []
19
+ # Specific day: redefine each tasks
20
+ '2017-12-19':
21
+ - project_id: 1234
22
+ task_id: 456
23
+ hours: 6
24
+ - project_id: 4321
25
+ task_id: 424
26
+ hours: 1
@@ -0,0 +1,9 @@
1
+ require 'dovico/version'
2
+ require 'dovico/api_client'
3
+ require 'dovico/app'
4
+ require 'dovico/model/assignment'
5
+ require 'dovico/model/time_entry_generator'
6
+ require 'dovico/model/employee'
7
+ require 'dovico/model/project'
8
+ require 'dovico/model/task'
9
+ require 'dovico/model/time_entry'
@@ -0,0 +1,67 @@
1
+ require 'typhoeus'
2
+ require 'json'
3
+
4
+ module Dovico
5
+ class ApiClient
6
+ API_URL = "https://api.dovico.com/"
7
+ API_VERSION = "5"
8
+
9
+ class << self
10
+ def initialize!(client_token, user_token)
11
+ @client_token = client_token
12
+ @user_token = user_token
13
+ end
14
+
15
+ def get(path, params: {})
16
+ perform!(:get, path, params: params)
17
+ end
18
+
19
+ def post(path, params: {}, body: nil)
20
+ perform!(:post, path, params: params, body: body)
21
+ end
22
+
23
+ def put(path, params: {}, body: nil)
24
+ perform!(:put, path, params: params, body: body)
25
+ end
26
+
27
+ private
28
+
29
+ attr_accessor :client_token, :user_token
30
+
31
+ def authorization_token
32
+ "WRAP access_token=\"client=#{client_token}&user_token=#{user_token}\""
33
+ end
34
+
35
+ def request_headers
36
+ {
37
+ "Accept" => "application/json",
38
+ "Content-Type" => "application/json",
39
+ "Authorization" => authorization_token,
40
+ }
41
+ end
42
+
43
+ def perform!(method, path, params: {}, body: nil)
44
+ request = Typhoeus::Request.new(
45
+ "#{API_URL}#{path}",
46
+ method: method,
47
+ params: params.merge(version: API_VERSION),
48
+ headers: request_headers,
49
+ body: body,
50
+ )
51
+
52
+ response = request.run
53
+
54
+ if response.code != 200
55
+ response = JSON.parse(response.body)
56
+ puts "== Error during HTTP request =="
57
+ puts "Status: #{response["Status"]}"
58
+ puts "Description: #{response["Description"]}"
59
+ puts ""
60
+ raise "Error during HTTP request"
61
+ end
62
+
63
+ JSON.parse(response.body)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,138 @@
1
+ require 'easy_app_helper'
2
+
3
+ module Dovico
4
+ class App
5
+ include EasyAppHelper
6
+
7
+ NAME = 'Dovico Simple Client'
8
+ DESCRIPTION = <<EOL
9
+ Simple client for Dovico TimeSheet web application.
10
+
11
+ Please refer to the README.md page to setup your client.
12
+
13
+ WARNING: --auto and --simulate options are not effective.
14
+ EOL
15
+
16
+ def initialize
17
+ config.config_file_base_name = 'dovico'
18
+ config.describes_application app_name: NAME,
19
+ app_version: Dovico::VERSION,
20
+ app_description: DESCRIPTION
21
+ add_script_options
22
+
23
+ ApiClient.initialize!(config["client_token"], config["user_token"])
24
+ end
25
+
26
+ def add_script_options
27
+ config.add_command_line_section('Display informations') do |slop|
28
+ slop.on :myself, 'Display info on yourself', argument: false
29
+ slop.on :tasks, 'Display info on tasks', argument: false
30
+ end
31
+ config.add_command_line_section('Fill the timesheets') do |slop|
32
+ slop.on :fill, 'Fill the timesheet', argument: false
33
+ end
34
+ config.add_command_line_section('Submit the timesheets') do |slop|
35
+ slop.on :submit, 'Submit timesheets', argument: false
36
+ end
37
+ config.add_command_line_section('Date options (for --fill and --submit)') do |slop|
38
+ slop.on :current_week, 'Current week', argument: false
39
+ slop.on :today, 'Current day', argument: false
40
+ slop.on :day, 'Specific day', argument: true
41
+ slop.on :week, 'Specific "commercial" week. See https://www.epochconverter.com/weeks/', argument: true, as: Integer
42
+ slop.on :year, '[optional] Specifiy year (for --week option), default current year', argument: true, as: Integer
43
+ end
44
+ end
45
+
46
+ def run
47
+ if config[:help] || !(config[:myself] || config[:tasks] || config[:fill] || config[:submit])
48
+ display_help
49
+ exit 0
50
+ end
51
+
52
+ if config[:myself]
53
+ display_myself
54
+ end
55
+
56
+ if config[:tasks]
57
+ display_tasks
58
+ end
59
+
60
+ if config[:fill] || config[:submit]
61
+ start_date, end_date = parse_date_options
62
+
63
+ time_entry_generator = TimeEntryGenerator.new(
64
+ assignments: config["assignments"],
65
+ employee_id: myself.id,
66
+ )
67
+ end
68
+
69
+ if config[:fill]
70
+ time_entries = time_entry_generator.generate(start_date, end_date)
71
+
72
+ saved_time_entries = TimeEntry.batch_create!(time_entries)
73
+ puts "#{saved_time_entries["TimeEntries"].count} Entries created between #{start_date} and #{end_date}"
74
+ end
75
+
76
+ if config[:submit]
77
+ submitted_time_entries = TimeEntry.submit!(myself.id, start_date, end_date)
78
+ puts "#{submitted_time_entries["TimeEntries"].count} Entries submitted between #{start_date} and #{end_date}"
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ def myself
85
+ @myself ||= Employee.myself
86
+ end
87
+
88
+ def display_myself
89
+ puts "Informations about yourself"
90
+ puts " - ID: #{myself.id}"
91
+ puts " - First Name: #{myself.first_name}"
92
+ puts " - Last Name: #{myself.last_name}"
93
+ puts ""
94
+ end
95
+
96
+ def display_tasks
97
+ projects = Project.all
98
+
99
+ puts "== List of available projects =="
100
+ puts " Project | Task | Description"
101
+ projects.each do |project|
102
+ if project.tasks.count > 0
103
+ project.tasks.each do |task|
104
+ puts sprintf ' %7d | %4d | %s: %s', project.id, task.id, project.name, task.name
105
+ end
106
+ else
107
+ puts sprintf " %7d | | %s (No tasks linked)", project.id, task.name
108
+ end
109
+ end
110
+ puts ""
111
+ end
112
+
113
+ def parse_date_options
114
+ if config[:week]
115
+ year = config[:year] || Date.current.year
116
+ start_date = Date.commercial(year, config[:week]).beginning_of_week
117
+ end_date = start_date.advance(days: 4)
118
+ elsif config[:current_week]
119
+ start_date = Date.current.beginning_of_week
120
+ end_date = start_date.advance(days: 4)
121
+ elsif config[:day]
122
+ start_date = end_date = Date.parse(config[:day])
123
+ elsif config[:today]
124
+ start_date = end_date = Date.current
125
+ else
126
+ puts "Error : You must precise one date options"
127
+ display_help
128
+ exit 1
129
+ end
130
+
131
+ [start_date, end_date]
132
+ end
133
+
134
+ def display_help
135
+ puts config.command_line_help
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,26 @@
1
+ require 'active_attr'
2
+
3
+ module Dovico
4
+ class Assignment
5
+ URL_PATH = 'Assignments/'
6
+
7
+ include ActiveAttr::Model
8
+
9
+ attribute :id
10
+ attribute :assignement_id
11
+ attribute :name
12
+ attribute :start_date
13
+ attribute :finish_date
14
+
15
+
16
+ def self.parse(hash)
17
+ self.new(
18
+ id: hash["ItemID"],
19
+ assignement_id: hash["AssignmentID"],
20
+ name: hash["Name"],
21
+ start_date: hash["StartDate"],
22
+ finish_date: hash["FinishDate"]
23
+ )
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ require 'active_attr'
2
+
3
+ module Dovico
4
+ class Employee
5
+ URL_PATH = 'Employees/'
6
+
7
+ include ActiveAttr::Model
8
+
9
+ attribute :id
10
+ attribute :first_name
11
+ attribute :last_name
12
+
13
+ def self.parse(hash)
14
+ Employee.new(
15
+ id: hash["ID"],
16
+ first_name: hash["FirstName"],
17
+ last_name: hash["LastName"],
18
+ )
19
+ end
20
+
21
+ def self.myself
22
+ employees = ApiClient.get("#{URL_PATH}/Me")
23
+
24
+ parse(employees["Employees"].first)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ require 'active_attr'
2
+
3
+ module Dovico
4
+ class Project < Assignment
5
+
6
+ attribute :tasks
7
+
8
+ def self.all
9
+ projects_search = ApiClient.get(URL_PATH)
10
+ projects = projects_search["Assignments"].map {|project_hash| parse(project_hash) }
11
+
12
+ projects.each do |project|
13
+ tasks_search = ApiClient.get("#{URL_PATH}#{project.assignement_id}")
14
+ project.tasks = tasks_search["Assignments"].map {|task_hash| Task.parse(task_hash) }
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ require 'active_attr'
2
+
3
+ module Dovico
4
+ class Task < Assignment
5
+ end
6
+ end
@@ -0,0 +1,83 @@
1
+ require 'active_attr'
2
+
3
+ module Dovico
4
+ class TimeEntry
5
+ URL_PATH = 'TimeEntries/'
6
+
7
+ include ActiveAttr::Model
8
+
9
+ attribute :id
10
+ attribute :start_time
11
+ attribute :stop_time
12
+ attribute :project_id
13
+ attribute :task_id
14
+ attribute :employee_id
15
+ attribute :date
16
+ attribute :total_hours
17
+ attribute :description
18
+
19
+ def self.parse(hash)
20
+ TimeEntry.new(
21
+ id: parse_id(hash['ID']),
22
+ start_time: hash['StartTime'],
23
+ stop_time: hash['StopTime'],
24
+ project_id: hash['Project']['ID'],
25
+ task_id: hash['Task']['ID'],
26
+ employee_id:hash['Employee']['ID'],
27
+ date: hash['Date'],
28
+ total_hours:hash['TotalHours'],
29
+ description:hash['Description']
30
+ )
31
+ end
32
+
33
+ def self.get(id)
34
+ entry = ApiClient.get("#{URL_PATH}/#{id}")["TimeEntries"].first
35
+ TimeEntry.parse(entry)
36
+ end
37
+
38
+ def self.batch_create!(assignments)
39
+ api_assignements = assignments.map(&:to_api)
40
+ ApiClient.post(URL_PATH, body: api_assignements.to_json)
41
+ end
42
+
43
+ def self.submit!(employee_id, start_date, end_date)
44
+ ApiClient.post(
45
+ "#{URL_PATH}/Employee/#{employee_id}/Submit",
46
+ params: {
47
+ daterange: "#{start_date} #{end_date}"
48
+ },
49
+ body: {}.to_json,
50
+ )
51
+ end
52
+
53
+ def create!
54
+ ApiClient.post(URL_PATH, body: [to_api].to_json)
55
+ end
56
+
57
+ def update!
58
+ ApiClient.put(URL_PATH, body: [to_api].to_json)
59
+ end
60
+
61
+ def to_api
62
+ {
63
+ "ID": id,
64
+ "StartTime": start_time,
65
+ "StopTime": stop_time,
66
+ "ProjectID": project_id.to_s,
67
+ "TaskID": task_id.to_s,
68
+ "EmployeeID": employee_id.to_s,
69
+ "Date": date,
70
+ "TotalHours": total_hours.to_s,
71
+ "Description": description,
72
+ }.compact.stringify_keys
73
+ end
74
+
75
+ private
76
+
77
+ def self.parse_id(long_id)
78
+ # T: ID is a GUID, non-approved
79
+ # M: ID is a long, approved
80
+ long_id.sub(/^(T|M)/, "")
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,58 @@
1
+ require 'active_attr'
2
+
3
+ module Dovico
4
+ class TimeEntryGenerator
5
+ DEFAULT_START_HOUR = 9
6
+
7
+ def initialize(assignments:, employee_id:)
8
+ @assignments = assignments
9
+ @employee_id = employee_id
10
+ end
11
+
12
+ def generate(start_date, end_date)
13
+ start_date.upto(end_date).flat_map do |day|
14
+ build_day_time_entries(day)
15
+ end
16
+ end
17
+
18
+ private
19
+ attr_accessor :assignments, :employee_id
20
+
21
+ def build_day_time_entries(day)
22
+ time_entries = []
23
+ start_date = Time.parse(day.to_s).advance(hours: DEFAULT_START_HOUR)
24
+
25
+ day_assignments = day_assignments(day)
26
+ day_assignments.each do |assignment|
27
+ time_entry = build_time_entry(assignment, start_date)
28
+ time_entries << time_entry
29
+
30
+ start_date = start_date.advance(hours: time_entry.total_hours.to_f)
31
+ end
32
+ time_entries
33
+ end
34
+
35
+ def build_time_entry(assignment, start_date)
36
+ project_id = assignment["project_id"]
37
+ task_id = assignment["task_id"]
38
+ hours = assignment["hours"]
39
+ stop_date = start_date.advance(hours: hours)
40
+ start_time = sprintf "%02d%02d", start_date.hour, start_date.min
41
+ stop_time = sprintf "%02d%02d", stop_date.hour, stop_date.min
42
+
43
+ TimeEntry.new(
44
+ employee_id: employee_id,
45
+ project_id: project_id,
46
+ task_id: task_id,
47
+ date: start_date.to_date.to_s,
48
+ total_hours: hours,
49
+ start_time: start_time,
50
+ stop_time: stop_time,
51
+ )
52
+ end
53
+
54
+ def day_assignments(date)
55
+ assignments[date.to_s] || assignments["default_day"]
56
+ end
57
+ end
58
+ end