openproject-service_packs 1.0.1
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +19 -0
- data/README.md +4 -0
- data/app/assets/javascripts/assigns.js +20 -0
- data/app/assets/javascripts/service_packs.js +17 -0
- data/app/assets/stylesheets/assigns.css +9 -0
- data/app/assets/stylesheets/service_packs.css +132 -0
- data/app/controllers/assigns_controller.rb +157 -0
- data/app/controllers/service_packs_controller.rb +219 -0
- data/app/helpers/service_pack_presenter.rb +39 -0
- data/app/helpers/service_pack_report.rb +65 -0
- data/app/helpers/service_packs_notification.rb +13 -0
- data/app/helpers/sp_assignment_manager.rb +25 -0
- data/app/mailers/application_mailer.rb +5 -0
- data/app/mailers/service_packs_mailer.rb +43 -0
- data/app/models/application_record.rb +3 -0
- data/app/models/assign.rb +13 -0
- data/app/models/mapping_rate.rb +14 -0
- data/app/models/service_pack.rb +180 -0
- data/app/models/service_pack_entry.rb +6 -0
- data/app/views/assigns/already_assigned.html.erb +14 -0
- data/app/views/assigns/not_assigned_yet.html.erb +19 -0
- data/app/views/assigns/unassignable.html.erb +4 -0
- data/app/views/layouts/mailer.html.erb +13 -0
- data/app/views/layouts/mailer.text.erb +1 -0
- data/app/views/service_packs/_active_assignments.html.erb +14 -0
- data/app/views/service_packs/_form.html.erb +80 -0
- data/app/views/service_packs/_rates_input.html.erb +15 -0
- data/app/views/service_packs/edit.html.erb +75 -0
- data/app/views/service_packs/index.html.erb +33 -0
- data/app/views/service_packs/new.html.erb +6 -0
- data/app/views/service_packs/show.html.erb +48 -0
- data/app/views/service_packs_mailer/expired_email.html.erb +1 -0
- data/app/views/service_packs_mailer/expired_email.text.erb +1 -0
- data/app/views/service_packs_mailer/notify_under_threshold1.html.erb +2 -0
- data/app/views/service_packs_mailer/notify_under_threshold1.text.erb +1 -0
- data/app/views/service_packs_mailer/notify_under_threshold2.html.erb +2 -0
- data/app/views/service_packs_mailer/notify_under_threshold2.text.erb +1 -0
- data/app/views/service_packs_mailer/used_up_email.html.erb +14 -0
- data/app/views/service_packs_mailer/used_up_email.text.erb +1 -0
- data/app/workers/expired_sp_worker.rb +11 -0
- data/app/workers/used_up_service_pack_job.rb +13 -0
- data/config/application.rb +4 -0
- data/config/routes.rb +13 -0
- data/config/schedule.example.rb +27 -0
- data/config/schedule.rb +28 -0
- data/config/sidekiq.yml +4 -0
- data/db/migrate/20190108031704_create_assigns.rb +11 -0
- data/db/migrate/20190108031712_create_service_packs.rb +14 -0
- data/db/migrate/20190108111243_create_mapping_rates.rb +11 -0
- data/db/migrate/20190113111300_rename_assign.rb +6 -0
- data/db/migrate/20190116085528_create_service_pack_entries.rb +9 -0
- data/db/migrate/20190121072000_final_assigns_table.rb +5 -0
- data/db/migrate/20190123150023_add_service_pack_ref_to_service_pack_entries.rb +5 -0
- data/db/migrate/20190123205130_feature_a_done.rb +16 -0
- data/db/migrate/20190301070654_change_units_of_service_pack_entries.rb +5 -0
- data/db/migrate/20190301071911_change_total_and_remained_units_of_service_pack.rb +6 -0
- data/lib/open_project/service_packs.rb +5 -0
- data/lib/open_project/service_packs/engine.rb +50 -0
- data/lib/open_project/service_packs/patches.rb +4 -0
- data/lib/open_project/service_packs/patches/enumeration_patch.rb +37 -0
- data/lib/open_project/service_packs/patches/project_patch.rb +14 -0
- data/lib/open_project/service_packs/patches/time_entry_activity_patch.rb +16 -0
- data/lib/open_project/service_packs/patches/time_entry_patch.rb +89 -0
- data/lib/open_project/service_packs/version.rb +5 -0
- data/lib/openproject-service_packs.rb +1 -0
- metadata +121 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 527fadb4ab0d898c0b42fab884ceb22b11244060c7599fed1d212fcd67c095d4
|
|
4
|
+
data.tar.gz: d19e0f5302fe898a109df7c6537e970ab2d6f431eeeca4d3cec1f0f6fd929e00
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 403086129a426fd15ad56843e4a9f74adde12177b7da7149599831cfe0abb5474ccfa2885560701979b739dbf069ca0f75971ccfd4aa4249a4417c048fa1cc67
|
|
7
|
+
data.tar.gz: 1358d495404e2159a6577012ebebcec5845ac2e5dab1b3de889798382ef56e4b118ea61ff32d2d27d649ba047070b2e895a343990902749bbac94f3af7816790
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!---- copyright
|
|
2
|
+
OpenProject Plugins Plugin
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2013 - 2014 the OpenProject Foundation (OPF)
|
|
5
|
+
|
|
6
|
+
This program is free software; you can redistribute it and/or
|
|
7
|
+
modify it under the terms of the GNU General Public License version 3.
|
|
8
|
+
|
|
9
|
+
You should have received a copy of the GNU General Public License
|
|
10
|
+
along with this program; if not, write to the Free Software
|
|
11
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
12
|
+
|
|
13
|
+
See doc/COPYRIGHT.md for more details.
|
|
14
|
+
|
|
15
|
+
++-->
|
|
16
|
+
|
|
17
|
+
# Changelog
|
|
18
|
+
|
|
19
|
+
* `#<ticket number>` Create plugin
|
data/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//use <% javascript_include_tag %>
|
|
2
|
+
function loadServicePack() {
|
|
3
|
+
if (this.selectedIndex == 0) {
|
|
4
|
+
document.querySelector("#sp-content").innerHTML = "Please select a Service Pack";
|
|
5
|
+
document.querySelector("#sp-assign-button").disabled = true;
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
var comp = this.options[this.selectedIndex];
|
|
9
|
+
var str = "Activation Date: " + comp.dataset.start + "<br/>";
|
|
10
|
+
str += "Expiration Date: " + comp.dataset.end + "<br/>";
|
|
11
|
+
str += "Capacity: " + comp.dataset.cap + "<br/>";
|
|
12
|
+
str += "Remained: " + comp.dataset.rem + "<br/>";
|
|
13
|
+
// click for more
|
|
14
|
+
document.querySelector("#sp-content").innerHTML = str;
|
|
15
|
+
document.querySelector("#sp-assign-button").disabled = false;
|
|
16
|
+
}
|
|
17
|
+
document.addEventListener("DOMContentLoaded", function(event) {
|
|
18
|
+
document.querySelector("#select-sp").addEventListener("change", loadServicePack);
|
|
19
|
+
document.querySelector("#sp-assign-button").disabled = true;
|
|
20
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
document.addEventListener("DOMContentLoaded", function(event) {
|
|
2
|
+
document.getElementById("view-stat").addEventListener("click", function(){
|
|
3
|
+
var xhr = new XMLHttpRequest();
|
|
4
|
+
// replace the link with statistics link
|
|
5
|
+
xhr.open('GET', 'http://localhost:3000/service_packs/1.json');
|
|
6
|
+
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
7
|
+
xhr.onload = function() {
|
|
8
|
+
if (xhr.status === 200) {
|
|
9
|
+
console.log(JSON.parse(xhr.responseText))
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
alert('Request failed. Returned status of ' + xhr.status);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
xhr.send();
|
|
16
|
+
});
|
|
17
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
label, input {
|
|
2
|
+
margin-bottom: 0 !important;
|
|
3
|
+
max-width: 500px !important;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
input.rates {
|
|
7
|
+
max-width: 465px !important;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
table {
|
|
11
|
+
font-family: arial, sans-serif;
|
|
12
|
+
border-collapse: collapse;
|
|
13
|
+
width: 100%;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
td, th {
|
|
17
|
+
border: 1px solid #dddddd;
|
|
18
|
+
text-align: left;
|
|
19
|
+
padding: 8px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.destroy {
|
|
23
|
+
background-color: #fc0000 !important;
|
|
24
|
+
color: #ffeeee !important;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
a.blocked {
|
|
28
|
+
opacity: 0.3;
|
|
29
|
+
margin-right: 8px;
|
|
30
|
+
cursor: default !important;
|
|
31
|
+
margin-left: -5px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#warnbox {
|
|
35
|
+
font-size: 0.75em;
|
|
36
|
+
text-align: center;
|
|
37
|
+
position: relative;
|
|
38
|
+
bottom: 7px;
|
|
39
|
+
right: 2px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
hr {
|
|
43
|
+
margin-top: 12px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.button.-highlight.-edit {
|
|
47
|
+
margin-bottom: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.button.-alt-highlight.-new {
|
|
51
|
+
margin-bottom: 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.button-container {
|
|
55
|
+
margin-bottom: 10px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#rates-input {
|
|
59
|
+
padding-left: 30px;
|
|
60
|
+
padding-bottom: 20px;
|
|
61
|
+
overflow-x: auto;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#table-rates-input {
|
|
65
|
+
width: 40%;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
table#table-rates-input td {
|
|
69
|
+
border: 0px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
table#table-rates-input th {
|
|
73
|
+
border: 0px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#error-explanation {
|
|
77
|
+
background-color: rgba(255, 255, 0, 0.41);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#error-explanation h2 {
|
|
81
|
+
color: #fc0000;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
#detailed-sp {
|
|
86
|
+
position: relative;
|
|
87
|
+
right: 15px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#block-left {
|
|
91
|
+
float: left;
|
|
92
|
+
width: 47%;
|
|
93
|
+
height: auto;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#block-right {
|
|
97
|
+
float: right;
|
|
98
|
+
width: 47%;
|
|
99
|
+
height: auto;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@media screen and (max-width: 860px) {
|
|
103
|
+
#block-left, #block-right {
|
|
104
|
+
width: 95%;
|
|
105
|
+
float: none;
|
|
106
|
+
height: auto;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.sp {
|
|
111
|
+
border: 1.5px solid;
|
|
112
|
+
border-color: #EAEAEA;
|
|
113
|
+
min-height: 200px;
|
|
114
|
+
margin: 0 20px 20px 20px;
|
|
115
|
+
padding: 20px 0 20px 20px;
|
|
116
|
+
box-sizing: border-box;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.sp.description {
|
|
120
|
+
float: left;
|
|
121
|
+
width: 100%;
|
|
122
|
+
line-height: 1.8em;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.sp.rates, .sp.assigned {
|
|
126
|
+
width: 100%;
|
|
127
|
+
height: auto;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.clearfix {
|
|
131
|
+
clear: both;
|
|
132
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
class AssignsController < ApplicationController
|
|
2
|
+
#layout 'admin'
|
|
3
|
+
before_action :find_project_by_project_id
|
|
4
|
+
include SPAssignmentManager
|
|
5
|
+
|
|
6
|
+
def assign
|
|
7
|
+
# binding.pry
|
|
8
|
+
return head 403 unless @can_assign = User.current.allowed_to?(:assign_service_packs, @project)
|
|
9
|
+
|
|
10
|
+
if assigned?(@project)
|
|
11
|
+
flash.now[:alert] = "You must unassign first!"
|
|
12
|
+
render_400 and return
|
|
13
|
+
end
|
|
14
|
+
@service_pack = ServicePack.find_by(id: params[:assign][:service_pack_id])
|
|
15
|
+
if @service_pack.nil?
|
|
16
|
+
flash.now[:alert] = "Service Pack not found"
|
|
17
|
+
render_404 and return
|
|
18
|
+
end
|
|
19
|
+
if @service_pack.available?
|
|
20
|
+
# binding.pry
|
|
21
|
+
assign_to(@service_pack, @project)
|
|
22
|
+
flash.now[:notice] = "Service Pack '#{@service_pack.name}' successfully assigned to project '#{@project.name}'"
|
|
23
|
+
render 'already_assigned' and return
|
|
24
|
+
else
|
|
25
|
+
# already assigned for another project
|
|
26
|
+
# constraint need
|
|
27
|
+
flash.now[:alert] = "Service Pack '#{@service_pack.name}' has been already assigned"
|
|
28
|
+
render_400 and return
|
|
29
|
+
end
|
|
30
|
+
flash.now[:alert] = 'Service Pack cannot be assigned'
|
|
31
|
+
redirect_to action: :show
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def unassign
|
|
35
|
+
return head 403 unless @can_unassign = User.current.allowed_to?(:unassign_service_packs, @project)
|
|
36
|
+
|
|
37
|
+
if unassigned?(@project)
|
|
38
|
+
flash[:alert] = 'No Service Pack is assigned to this project'
|
|
39
|
+
render_404 and return
|
|
40
|
+
end
|
|
41
|
+
_unassign(@project)
|
|
42
|
+
flash[:notice] = 'Unassigned a Service Pack from this project'
|
|
43
|
+
redirect_to action: :show and return
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def show
|
|
47
|
+
# This will lock even admins out if the module is not activated.
|
|
48
|
+
return head 403 unless
|
|
49
|
+
User.current.allowed_to?(:see_assigned_service_packs, @project) ||
|
|
50
|
+
(@can_assign = User.current.allowed_to?(:assign_service_packs, @project)) ||
|
|
51
|
+
(@can_unassign = User.current.allowed_to?(:unassign_service_packs, @project))
|
|
52
|
+
# binding.pry
|
|
53
|
+
if @assignment = @project.assigns.find_by(assigned: true)
|
|
54
|
+
if @assignment.service_pack.unavailable?
|
|
55
|
+
@assignment.terminate
|
|
56
|
+
@assignment = nil # signifying no assignments are in effect
|
|
57
|
+
# as the single one is terminated.
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
# binding.pry
|
|
61
|
+
if @assignment.nil?
|
|
62
|
+
if @can_assign ||= User.current.allowed_to?(:assign_service_packs, @project)
|
|
63
|
+
@assignables = ServicePack.availables
|
|
64
|
+
if @assignables.exists?
|
|
65
|
+
@assignment = Assign.new
|
|
66
|
+
render -'not_assigned_yet' and return
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
render -'unassignable'
|
|
70
|
+
# binding.pry
|
|
71
|
+
else
|
|
72
|
+
@service_pack = @assignment.service_pack
|
|
73
|
+
render -'already_assigned'
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def select_to_transfer
|
|
78
|
+
return head 403 unless @can_assign = User.current.allowed_to?(:assign_service_packs, @project)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def report
|
|
82
|
+
return head 403 unless User.current.allowed_to?(:see_assigned_service_packs, @project)
|
|
83
|
+
if assignment = assigned?(@project)
|
|
84
|
+
render csv: ServicePackReport.new(assignment.service_pack).call(@project), filename: "ServicePackReport_#{@project.name.gsub(/\s+/, -'_')}.csv"
|
|
85
|
+
else
|
|
86
|
+
render_404
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# =======================================================
|
|
91
|
+
# :Docs
|
|
92
|
+
# * Limit: Serving JSON only. Intended to restrict to :show.
|
|
93
|
+
# * Purpose:
|
|
94
|
+
# Return a table with consumed units by a Project grouped by service
|
|
95
|
+
# pack, then activities and sorted from large to small.
|
|
96
|
+
# * Expected Inputs:
|
|
97
|
+
# [project_id]: Sharing the same route with the resourceful default.
|
|
98
|
+
# Put in the link. Mandatory.
|
|
99
|
+
# [start_period]: Beginning of the counting period. As a date. Optional.
|
|
100
|
+
# [end_period]: Ending of the counting period. As a date. Optional.
|
|
101
|
+
# start_period MUST NOT be later than end_period.
|
|
102
|
+
# Both or none of [start_period, end_period] can be present.
|
|
103
|
+
# * Expected Outputs
|
|
104
|
+
# Top class: None
|
|
105
|
+
# Content: Array of object having [name, act_name, consumed]
|
|
106
|
+
# - Name: Name of Service Pack
|
|
107
|
+
# - act_name: Name of activity
|
|
108
|
+
# - consumed: How many units are consumed (in given period)
|
|
109
|
+
# Status: 200
|
|
110
|
+
# * When raising error
|
|
111
|
+
# HTTP 400: Malformed request.
|
|
112
|
+
# =======================================================
|
|
113
|
+
|
|
114
|
+
def statistics
|
|
115
|
+
return head 403 unless
|
|
116
|
+
User.current.allowed_to?(:see_assigned_service_packs, @project) ||
|
|
117
|
+
User.current.allowed_to?(:assign_service_packs, @project) ||
|
|
118
|
+
User.current.allowed_to?(:unassign_service_packs, @project)
|
|
119
|
+
|
|
120
|
+
start_day = params[:start_period]&.to_date # ruby >= 2.3.0
|
|
121
|
+
end_day = params[:end_period]&.to_date
|
|
122
|
+
if start_day.nil? ^ end_day.nil?
|
|
123
|
+
render json: { error: 'GET OUT!'}, status: 400 and return
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Notice: Change max(t4.name) to ANY_VALUE(t4.name) on production builds.
|
|
127
|
+
# MySQL specific >= 5.7.5
|
|
128
|
+
# https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html
|
|
129
|
+
|
|
130
|
+
get_parent_id = <<-SQL
|
|
131
|
+
SELECT id, name, COALESCE(parent_id, id) AS pid
|
|
132
|
+
FROM #{TimeEntryActivity.table_name}
|
|
133
|
+
WHERE type = 'TimeEntryActivity'
|
|
134
|
+
SQL
|
|
135
|
+
body_query = <<-SQL
|
|
136
|
+
SELECT t1.service_pack_id AS spid, max(t4.name) AS name, t3.pid AS pid,
|
|
137
|
+
max(t3.name) AS act_name, sum(t1.units) AS consumed
|
|
138
|
+
FROM #{ServicePackEntry.table_name} t1
|
|
139
|
+
INNER JOIN #{TimeEntry.table_name} t2
|
|
140
|
+
ON t1.time_entry_id = t2.id
|
|
141
|
+
INNER JOIN (#{get_parent_id}) t3
|
|
142
|
+
ON t2.activity_id = t3.id
|
|
143
|
+
INNER JOIN #{ServicePack.table_name} t4
|
|
144
|
+
ON t1.service_pack_id = t4.id
|
|
145
|
+
SQL
|
|
146
|
+
group_clause = <<-SQL
|
|
147
|
+
GROUP BY t1.service_pack_id, t3.pid
|
|
148
|
+
ORDER BY consumed DESC
|
|
149
|
+
SQL
|
|
150
|
+
where_clause = "WHERE t2.project_id = ?"
|
|
151
|
+
where_clause << (start_day.nil? ? '' : ' AND t1.created_at BETWEEN ? AND ?')
|
|
152
|
+
query = body_query + where_clause + group_clause
|
|
153
|
+
par = start_day.nil? ? [query, params[@project.id]] : [query, params[@project.id], start_day, end_day]
|
|
154
|
+
sql = ActiveRecord::Base.send(:sanitize_sql_array, par)
|
|
155
|
+
render json: ActiveRecord::Base.connection.exec_query(sql).to_hash, status: 200
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
class ServicePacksController < ApplicationController
|
|
2
|
+
# only allow admin
|
|
3
|
+
before_action :require_admin
|
|
4
|
+
|
|
5
|
+
# Specifying Layouts for Controllers, looking at OPENPROJECT_ROOT/app/views/layouts/admin
|
|
6
|
+
layout 'admin'
|
|
7
|
+
|
|
8
|
+
def index
|
|
9
|
+
@service_packs = ServicePack.all
|
|
10
|
+
# for demo
|
|
11
|
+
#ServicePacksMailer.notify_under_threshold1(User.first,@service_packs.first).deliver_now
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def new
|
|
15
|
+
@service_pack = ServicePack.new
|
|
16
|
+
# TimeEntryActivity.shared.count.times {@service_pack.mapping_rates.build}
|
|
17
|
+
@sh = TimeEntryActivity.shared
|
|
18
|
+
@c = TimeEntryActivity.shared.count
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def show
|
|
22
|
+
@service_pack = ServicePack.find(params[:id])
|
|
23
|
+
# controller chooses not to get the thresholds.
|
|
24
|
+
# assume the service pack exists.
|
|
25
|
+
# TODO: make a separate action JSON only.
|
|
26
|
+
respond_to do |format|
|
|
27
|
+
format.json {
|
|
28
|
+
# the function already converted this to json
|
|
29
|
+
render plain: ServicePackPresenter.new(@service_pack).json_export(:rate), status: 200
|
|
30
|
+
}
|
|
31
|
+
format.html {
|
|
32
|
+
# http://www.chrisrolle.com/en/blog/benchmark-preload-vs-eager_load
|
|
33
|
+
@rates = @service_pack.mapping_rates.preload(:activity)
|
|
34
|
+
@assignments = @service_pack.assignments.preload(:project)
|
|
35
|
+
}
|
|
36
|
+
format.csv {
|
|
37
|
+
render csv: ServicePackReport.new(@service_pack).call, filename: "service_pack_#{@service_pack.name}.csv"
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The string with the minus sign in front is a shorthand for <string>.freeze
|
|
43
|
+
# reducing server processing time (and testing time) by 30%!
|
|
44
|
+
# Freezing a string literal will stop it from being created anew over and over.
|
|
45
|
+
# All literal strings will be frozen in Ruby 3 by default, which is a good idea.
|
|
46
|
+
|
|
47
|
+
def create
|
|
48
|
+
mapping_rate_attribute = params[:service_pack][:mapping_rates_attributes]
|
|
49
|
+
# binding.pry
|
|
50
|
+
activity_id = []
|
|
51
|
+
mapping_rate_attribute.each {|_index, hash_value| activity_id.push(hash_value[:activity_id])}
|
|
52
|
+
|
|
53
|
+
if activity_id.uniq.length == activity_id.length
|
|
54
|
+
@service_pack = ServicePack.new(service_pack_params)
|
|
55
|
+
# render plain: 'not duplicated'
|
|
56
|
+
if @service_pack.save
|
|
57
|
+
flash[:notice] = -'Service Pack creation successful.'
|
|
58
|
+
redirect_to action: :show, id: @service_pack.id and return
|
|
59
|
+
else
|
|
60
|
+
flash.now[:error] = -'Service Pack creation failed.'
|
|
61
|
+
end
|
|
62
|
+
else
|
|
63
|
+
# render plain: 'duplicated'
|
|
64
|
+
flash.now[:error] = -'Only one rate can be defined to one activity.'
|
|
65
|
+
end
|
|
66
|
+
# the only successful path has returned 10 lines ago.
|
|
67
|
+
@sh = TimeEntryActivity.shared
|
|
68
|
+
@c = TimeEntryActivity.shared.count
|
|
69
|
+
render 'new'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def edit
|
|
73
|
+
@sp = ServicePack.find_by(id: params[:id])
|
|
74
|
+
if @sp.nil?
|
|
75
|
+
flash[:error] = -"Service Pack not found"
|
|
76
|
+
redirect_to action: :index and return
|
|
77
|
+
end
|
|
78
|
+
# @activity = @sp.time_entry_activities.build
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def update
|
|
82
|
+
@sp = ServicePack.find_by(id: params[:id])
|
|
83
|
+
if @sp.nil?
|
|
84
|
+
flash[:error] = -"Service Pack not found"
|
|
85
|
+
redirect_to action: :index and return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
mapping_rate_attribute = params[:service_pack][:mapping_rates_attributes]
|
|
89
|
+
activity_id = []
|
|
90
|
+
mapping_rate_attribute.each {|_index, hash_value| activity_id.push(hash_value[:activity_id])}
|
|
91
|
+
|
|
92
|
+
if activity_id.uniq.length == activity_id.length
|
|
93
|
+
# No duplication
|
|
94
|
+
add_units
|
|
95
|
+
@sp.assign_attributes(service_pack_edit_params)
|
|
96
|
+
# binding.pry
|
|
97
|
+
if @sp.save
|
|
98
|
+
flash[:notice] = -'Service Pack update successful.'
|
|
99
|
+
redirect_to @sp
|
|
100
|
+
else
|
|
101
|
+
flash.now[:error] = -'Service Pack update failed.'
|
|
102
|
+
render -'edit'
|
|
103
|
+
end
|
|
104
|
+
else
|
|
105
|
+
# Duplication
|
|
106
|
+
flash.now[:error] = -'Only one rate can be defined to one activity.'
|
|
107
|
+
render -'edit'
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def destroy
|
|
112
|
+
@sp = ServicePack.find_by(id: params[:id])
|
|
113
|
+
if @sp.nil?
|
|
114
|
+
flash[:error] = -"Service Pack not found"
|
|
115
|
+
redirect_to action: :index and return
|
|
116
|
+
end
|
|
117
|
+
if @sp.assigned?
|
|
118
|
+
flash.now[:error] = "Please unassign this SP from all projects before proceeding!"
|
|
119
|
+
redirect_to @sp
|
|
120
|
+
end
|
|
121
|
+
@sp.destroy!
|
|
122
|
+
|
|
123
|
+
redirect_to service_packs_path
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# for breadcrumb code
|
|
127
|
+
def show_local_breadcrumb
|
|
128
|
+
true
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def default_breadcrumb
|
|
132
|
+
action_name == 'index' ? -'Service Packs' : ActionController::Base.helpers.link_to(-'Service Packs', service_packs_path)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# =======================================================
|
|
136
|
+
# :Docs
|
|
137
|
+
# * Limit: Serving JSON only. Must be Admin to access.
|
|
138
|
+
# * Purpose:
|
|
139
|
+
# Return a table with consumed units for a Service Pack grouped by activities and sorted
|
|
140
|
+
# from large to small.
|
|
141
|
+
# * Expected Inputs:
|
|
142
|
+
# [service_pack_id]: Sharing the same route with the resourceful default.
|
|
143
|
+
# Put in the link. Mandatory.
|
|
144
|
+
# [start_period]: Beginning of the counting period. As a date. Optional.
|
|
145
|
+
# [end_period]: Ending of the counting period. As a date. Optional.
|
|
146
|
+
# start_period MUST NOT be later than end_period.
|
|
147
|
+
# Both or none of [start_period, end_period] can be present.
|
|
148
|
+
# * Expected Outputs
|
|
149
|
+
# Top class: None
|
|
150
|
+
# Content: Array of object having [name, consumed]
|
|
151
|
+
# - consumed: How many units are consumed (in given period)
|
|
152
|
+
# - act_name: Name of activity
|
|
153
|
+
# Status: 200
|
|
154
|
+
# * When raising error
|
|
155
|
+
# HTTP 404: SP not found
|
|
156
|
+
# HTTP 400: Malformed request.
|
|
157
|
+
# =======================================================
|
|
158
|
+
|
|
159
|
+
def statistics
|
|
160
|
+
start_day = params[:start_period]&.to_date # ruby >= 2.3.0
|
|
161
|
+
end_day = params[:end_period]&.to_date
|
|
162
|
+
if start_day.nil? ^ end_day.nil?
|
|
163
|
+
render json: {error: 'GET OUT!'}, status: 400 and return
|
|
164
|
+
end
|
|
165
|
+
if !ServicePack.find_by(id: params[:service_pack_id])
|
|
166
|
+
render json: {error: 'NOT FOUND'}, status: 404 and return
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Notice: Change max(t3.name) to ANY_VALUE(t3.name) on production builds.
|
|
170
|
+
# MySQL specific >= 5.7.5
|
|
171
|
+
# https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html
|
|
172
|
+
|
|
173
|
+
get_parent_id = <<-SQL
|
|
174
|
+
SELECT id, name, COALESCE(parent_id, id) AS pid
|
|
175
|
+
FROM #{TimeEntryActivity.table_name}
|
|
176
|
+
WHERE type = 'TimeEntryActivity'
|
|
177
|
+
SQL
|
|
178
|
+
body_query = <<-SQL
|
|
179
|
+
SELECT t3.pid AS act_id, max(t3.name) AS act_name, sum(t1.units) AS consumed
|
|
180
|
+
FROM #{ServicePackEntry.table_name} t1
|
|
181
|
+
INNER JOIN #{TimeEntry.table_name} t2
|
|
182
|
+
ON t1.time_entry_id = t2.id
|
|
183
|
+
INNER JOIN (#{get_parent_id}) t3
|
|
184
|
+
ON t2.activity_id = t3.id
|
|
185
|
+
SQL
|
|
186
|
+
group_clause = <<-SQL
|
|
187
|
+
GROUP BY t3.pid
|
|
188
|
+
ORDER BY consumed DESC
|
|
189
|
+
SQL
|
|
190
|
+
where_clause = "WHERE t1.service_pack_id = ?"
|
|
191
|
+
where_clause << (start_day.nil? ? '' : ' AND t1.created_at BETWEEN ? AND ?')
|
|
192
|
+
query = body_query + where_clause + group_clause
|
|
193
|
+
# binding.pry
|
|
194
|
+
par = start_day.nil? ? [query, params[:service_pack_id]] : [query, params[:service_pack_id], start_day, end_day]
|
|
195
|
+
sql = ActiveRecord::Base.send(:sanitize_sql_array, par)
|
|
196
|
+
render json: ActiveRecord::Base.connection.exec_query(sql).to_hash, status: 200
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private
|
|
200
|
+
|
|
201
|
+
def service_pack_params
|
|
202
|
+
params.require(:service_pack).permit(:name, :total_units, :started_date, :expired_date, :threshold1, :threshold2,
|
|
203
|
+
mapping_rates_attributes: [:id, :activity_id, :service_pack_id, :units_per_hour, :_destroy])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def service_pack_edit_params
|
|
207
|
+
params.require(:service_pack).permit(:total_units, :threshold1, :threshold2,
|
|
208
|
+
mapping_rates_attributes: [:id, :activity_id, :service_pack_id, :units_per_hour, :_destroy])
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def add_units
|
|
212
|
+
return unless params[:service_pack][:total_units]
|
|
213
|
+
if (t = params[:service_pack][:total_units].to_f) <= 0.0
|
|
214
|
+
@sp.errors.add(:total_units, 'is invalid') and return
|
|
215
|
+
end
|
|
216
|
+
@sp.grant(t - @sp.total_units) unless t == @sp.total_units
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
end
|