dovico 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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