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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class ServicePackPresenter
|
|
2
|
+
attr_reader :service_pack
|
|
3
|
+
def initialize(service_pack)
|
|
4
|
+
# we don't take NIL as an option
|
|
5
|
+
if service_pack&.id
|
|
6
|
+
@service_pack = service_pack
|
|
7
|
+
else
|
|
8
|
+
raise 'This is NIL cannot print'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
def json_full_header
|
|
12
|
+
# not recommended in production
|
|
13
|
+
@service_pack.to_json
|
|
14
|
+
end
|
|
15
|
+
def hash_lite_header
|
|
16
|
+
@service_pack.as_json(except: [:id, :threshold1, :threshold2, :updated_at, :created_at])
|
|
17
|
+
end
|
|
18
|
+
def json_lite_header
|
|
19
|
+
hash_lite_header.to_json
|
|
20
|
+
end
|
|
21
|
+
def hash_rate_only
|
|
22
|
+
# ActiveRecord join returns an array!
|
|
23
|
+
q = <<-SQL
|
|
24
|
+
SELECT name, units_per_hour AS upt
|
|
25
|
+
FROM mapping_rates t1
|
|
26
|
+
INNER JOIN #{TimeEntryActivity.table_name} t2
|
|
27
|
+
ON t1.activity_id = t2.id
|
|
28
|
+
WHERE t1.service_pack_id = #{@service_pack.id}
|
|
29
|
+
SQL
|
|
30
|
+
ActiveRecord::Base.connection.exec_query(q).to_hash
|
|
31
|
+
end
|
|
32
|
+
def json_rate_only
|
|
33
|
+
hash_rate_only.to_json
|
|
34
|
+
end
|
|
35
|
+
def json_export(sym=:header)
|
|
36
|
+
err = { :error => 422, :name => "Unsupported format"}
|
|
37
|
+
sym == :header ? json_lite_header : (sym == :rate ? json_rate_only : err.to_json)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
class ServicePackReport
|
|
2
|
+
attr_reader :service_pack
|
|
3
|
+
|
|
4
|
+
def initialize(service_pack)
|
|
5
|
+
if service_pack&.id
|
|
6
|
+
@service_pack = service_pack
|
|
7
|
+
else
|
|
8
|
+
raise -'This service pack is NIL, cannot report'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def query(project = nil)
|
|
13
|
+
sql = <<-SQL
|
|
14
|
+
SELECT t2.spent_on, concat(t4.firstname, ' ', t4.lastname) AS user_name, t3.name AS activity_name,
|
|
15
|
+
t5.id AS work_package_id, t7.name AS type_name, t5.subject AS subject, t2.comments AS comment,
|
|
16
|
+
t1.units AS units, t2.hours AS hours, #{project.nil? ? 't6.name' : "'#{project.name}'"} AS project_name
|
|
17
|
+
FROM service_pack_entries t1
|
|
18
|
+
INNER JOIN #{TimeEntry.table_name} t2
|
|
19
|
+
ON t1.time_entry_id = t2.id
|
|
20
|
+
INNER JOIN #{TimeEntryActivity.table_name} t3
|
|
21
|
+
ON t2.activity_id = t3.id
|
|
22
|
+
INNER JOIN users t4
|
|
23
|
+
ON t2.user_id = t4.id
|
|
24
|
+
#{project.nil? ? 'INNER JOIN projects t6 ON t2.project_id = t6.id' : ''}
|
|
25
|
+
LEFT JOIN #{WorkPackage.table_name} t5
|
|
26
|
+
ON t2.work_package_id = t5.id
|
|
27
|
+
LEFT JOIN types t7
|
|
28
|
+
ON t5.type_id = t7.id
|
|
29
|
+
WHERE service_pack_id = #{@service_pack.id}
|
|
30
|
+
#{project.nil? ? '' : "AND t2.project_id = #{project.id}"}
|
|
31
|
+
ORDER BY spent_on DESC
|
|
32
|
+
SQL
|
|
33
|
+
@entries = ActiveRecord::Base.connection.exec_query(sql).to_hash
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def csv_extractor
|
|
37
|
+
raise -'Query not run yet' unless @entries
|
|
38
|
+
decimal_separator = I18n.t(:general_csv_decimal_separator)
|
|
39
|
+
export = CSV.generate(col_sep: ';') { |csv|
|
|
40
|
+
headers = [-'Date', -'User', -'Activity', -'Project', -'Work Package', -'Hours', -'Type', -'Subject', -'Units', -'Comments']
|
|
41
|
+
# headers += custom_fields.map(&:name) # not supported
|
|
42
|
+
csv << headers
|
|
43
|
+
@entries.each do |entry|
|
|
44
|
+
fields = [entry[-'spent_on'],
|
|
45
|
+
entry[-'user_name'],
|
|
46
|
+
entry[-'activity_name'],
|
|
47
|
+
entry[-'project_name'],
|
|
48
|
+
entry[-'work_package_id'],
|
|
49
|
+
entry[-'hours'].round(2).to_s.gsub(-'.', decimal_separator),
|
|
50
|
+
entry[-'type_name'],
|
|
51
|
+
entry[-'subject'],
|
|
52
|
+
entry[-'units'].round(0),
|
|
53
|
+
entry[-'comment']
|
|
54
|
+
]
|
|
55
|
+
# fields += custom_fields.map { |f| show_value(entry.custom_value_for(f)) }
|
|
56
|
+
csv << fields
|
|
57
|
+
end
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def call(project=nil)
|
|
62
|
+
self.query(project)
|
|
63
|
+
self.csv_extractor
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module ServicePacksNotification
|
|
2
|
+
|
|
3
|
+
def self.notify_under_threshold1
|
|
4
|
+
# https://blog.arkency.com/2013/12/rails4-preloading/
|
|
5
|
+
service_packs = ServicePack.where('remained_units <= total_units / 100.0 * threshold1').preload(:consuming_projects)
|
|
6
|
+
ServicePack.find_each do |sp|
|
|
7
|
+
sp.consuming_projects.find_each do |project|
|
|
8
|
+
users = User.allowed(:see_assigned_service_packs, project)
|
|
9
|
+
users.each do |user| ServicePacksMailer.notify_under_threshold1(user, sp).deliver_later end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module SPAssignmentManager
|
|
2
|
+
# Implementation is subject to change.
|
|
3
|
+
def assign_to(service_pack, project)
|
|
4
|
+
# binding.pry
|
|
5
|
+
ActiveRecord::Base.transaction do
|
|
6
|
+
# one query only
|
|
7
|
+
project.assigns.update_all(assigned: false)
|
|
8
|
+
@assignment = service_pack.assigns.find_by(project_id: project.id) || project.assigns.new
|
|
9
|
+
@assignment.assigned = true
|
|
10
|
+
@assignment.assign_date = Date.today
|
|
11
|
+
@assignment.unassign_date = service_pack.expired_date
|
|
12
|
+
@assignment.service_pack_id = service_pack.id if @assignment.new_record?
|
|
13
|
+
@assignment.save!
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
def _unassign(project)
|
|
17
|
+
project.assigns.find_by(assigned: true)&.terminate # ruby >= 2.3.0 "safe navigation operator"
|
|
18
|
+
end
|
|
19
|
+
def unassigned?(project)
|
|
20
|
+
!assigned?(project)
|
|
21
|
+
end
|
|
22
|
+
def assigned?(project)
|
|
23
|
+
project.assigns.find_by(assigned: true)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
class ServicePacksMailer < ApplicationMailer
|
|
2
|
+
def expired_email(user, service_pack)
|
|
3
|
+
@user = user
|
|
4
|
+
@sp = service_pack
|
|
5
|
+
# binding.pry
|
|
6
|
+
|
|
7
|
+
mail to: @user.mail, subject: "The service pack #{@sp.name} has expired" do |format|
|
|
8
|
+
format.text
|
|
9
|
+
format.html
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def notify_under_threshold1(user, service_pack)
|
|
14
|
+
@user = user
|
|
15
|
+
@sp = service_pack
|
|
16
|
+
# binding.pry
|
|
17
|
+
mail to: @user.mail, subject: "The service pack #{@sp.name} is running out" do |format|
|
|
18
|
+
format.text
|
|
19
|
+
format.html
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def notify_under_threshold2(user, service_pack)
|
|
24
|
+
@user = user
|
|
25
|
+
@sp = service_pack
|
|
26
|
+
# binding.pry
|
|
27
|
+
mail to: @user.mail, subject: "The service pack #{@sp.name} is running out" do |format|
|
|
28
|
+
format.text
|
|
29
|
+
format.html
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def used_up_email(user, service_pack)
|
|
34
|
+
@user = user
|
|
35
|
+
@sp = service_pack
|
|
36
|
+
|
|
37
|
+
mail to: @user.mail, subject: "The service pack #{@sp.name} ran out of units" do |format|
|
|
38
|
+
format.text
|
|
39
|
+
format.html
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class Assign < ApplicationRecord
|
|
2
|
+
belongs_to :service_pack
|
|
3
|
+
belongs_to :project
|
|
4
|
+
scope :active, ->{where("assigned = ? and unassign_date > ?", true, Date.today)}
|
|
5
|
+
def terminate
|
|
6
|
+
self.assigned = false
|
|
7
|
+
self.unassign_date = Date.today
|
|
8
|
+
self.save!
|
|
9
|
+
end
|
|
10
|
+
def overdue?
|
|
11
|
+
service_pack.unavailable?
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class MappingRate < ApplicationRecord
|
|
2
|
+
# DO NOT ADD :inverse_of TO THIS CLASS - it breaks the SP#edit and #new
|
|
3
|
+
belongs_to :activity, class_name: 'TimeEntryActivity', foreign_key: 'activity_id'
|
|
4
|
+
belongs_to :service_pack
|
|
5
|
+
# validates_uniqueness_of :activity, scope: :service_pack
|
|
6
|
+
|
|
7
|
+
validates_numericality_of :units_per_hour, greater_than_or_equal_to: 0
|
|
8
|
+
validate :only_define_rates_on_shared_activity
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
def only_define_rates_on_shared_activity
|
|
12
|
+
errors.add(:activity, 'invalid') if !activity.shared?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# freeze_literal_string: true
|
|
2
|
+
class ServicePack < ApplicationRecord
|
|
3
|
+
before_create :default_remained_units
|
|
4
|
+
after_save :revoke_all_assignments, if: :expired? # should be time-based only.
|
|
5
|
+
after_save :knock_out, if: :used_up?, on: :consumption
|
|
6
|
+
|
|
7
|
+
has_many :assigns, dependent: :destroy
|
|
8
|
+
has_many :active_assignments, -> {where('assigned = ? and unassign_date >= ?', true, Date.today)}, class_name: 'Assign'
|
|
9
|
+
has_many :projects, through: :assigns
|
|
10
|
+
has_many :consuming_projects, through: :active_assignments, source: :project
|
|
11
|
+
has_many :mapping_rates, inverse_of: :service_pack, dependent: :delete_all
|
|
12
|
+
has_many :time_entry_activities, through: :mapping_rates, source: :activity
|
|
13
|
+
has_many :service_pack_entries, inverse_of: :service_pack, dependent: :delete_all
|
|
14
|
+
# :source is the name of association on the "going out" side of the joining table
|
|
15
|
+
# (the "going in" side is taken by this association)
|
|
16
|
+
# example: User has many :pets, Dog is a :pets and has many :breeds. Breeds have ...
|
|
17
|
+
# Rails will look for :dog_breeds by default! (e.g. User.pets.dog_breeds)
|
|
18
|
+
# sauce: https://stackoverflow.com/a/4632472
|
|
19
|
+
|
|
20
|
+
accepts_nested_attributes_for :mapping_rates, allow_destroy: true, reject_if: ->(attributes) {attributes['units_per_hour'].blank?}
|
|
21
|
+
|
|
22
|
+
validates_presence_of :name, :threshold1, :threshold2, :expired_date, :started_date, :total_units
|
|
23
|
+
|
|
24
|
+
validates_uniqueness_of :name, on: :create # SP name never changes
|
|
25
|
+
# https://rubular.com/r/CCtRDRq9jDuMmb
|
|
26
|
+
|
|
27
|
+
validates_format_of :name, with: /\A[^_`~^*\\+=\{\}\|\\;"'<>.\/]+\Z/, message: 'has invalid character(s)'
|
|
28
|
+
|
|
29
|
+
validates_numericality_of :total_units, greater_than: 0
|
|
30
|
+
validates_numericality_of :threshold1, :threshold2, only_integer: true, greater_than: 0
|
|
31
|
+
|
|
32
|
+
validate :threshold2_is_greater_than_threshold1
|
|
33
|
+
validate :end_after_start
|
|
34
|
+
validate :must_not_expire_in_the_past
|
|
35
|
+
validate :threshold1_is_greater_than_total_units
|
|
36
|
+
validate :threshold2_is_greater_than_total_units
|
|
37
|
+
|
|
38
|
+
scope :assignments, -> {joins(:assigns).where(assigned: true)}
|
|
39
|
+
scope :availables, -> {where('remained_units > 0 and expired_date >= ?', Date.today)}
|
|
40
|
+
scope :notifiable, ->(thresno) {where("remained_units <= threshold#{thresno}")}
|
|
41
|
+
|
|
42
|
+
def default_remained_units
|
|
43
|
+
self.remained_units = total_units
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def revoke_all_assignments
|
|
47
|
+
assignments.where(assigned: true).update_all(assigned: false, unassign_date: Date.today)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
### CHECKERS ###
|
|
51
|
+
def expired?
|
|
52
|
+
true if Time.now > expired_date
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def used_up?
|
|
56
|
+
true if remained_units <= 0
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def unavailable? # available SP might not be assignable
|
|
60
|
+
used_up? && expired?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def available?
|
|
64
|
+
!unavailable?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def is_notify?
|
|
68
|
+
# so what is with the two thresholds!?
|
|
69
|
+
dates_to_notify = (expired_date - Date.today).to_i
|
|
70
|
+
dates_to_notify.between?(1, 2)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def assigned?
|
|
74
|
+
assigns.where(assigned: true).exists?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# def total_unit_updatable?(new_value, old_value = total_units)
|
|
78
|
+
# # old_value=total_units
|
|
79
|
+
# if new_value > old_value
|
|
80
|
+
# true
|
|
81
|
+
# elsif new_value == old_value
|
|
82
|
+
# true
|
|
83
|
+
# elsif new_value < old_value
|
|
84
|
+
# unit_subtract_number = old_value - new_value
|
|
85
|
+
# !(remained_units < unit_subtract_number)
|
|
86
|
+
# end
|
|
87
|
+
# end
|
|
88
|
+
|
|
89
|
+
### END CHECKERS ###
|
|
90
|
+
|
|
91
|
+
# FOR TESTING ONLY
|
|
92
|
+
def expired_notification # send to the first user in the first record in the DB
|
|
93
|
+
if expired?
|
|
94
|
+
user = User.first
|
|
95
|
+
ServicePacksMailer.expired_email(user, self).deliver_later
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def cron_send_specific
|
|
100
|
+
# modify the User param
|
|
101
|
+
ServicePacksMailer.expired_email(User.last, ServicePack.first).deliver_later
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# def self.cron_send_default
|
|
105
|
+
# # modify the User param
|
|
106
|
+
# ServicePack.find_each do |sp|
|
|
107
|
+
# ExpiredSpMailer.expired_email(User.last, sp).deliver_now
|
|
108
|
+
# end
|
|
109
|
+
# end
|
|
110
|
+
# END TESTING ONLY
|
|
111
|
+
|
|
112
|
+
def assignments
|
|
113
|
+
assigns.where(assigned: true)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def grant(units)
|
|
117
|
+
self.total_units += units
|
|
118
|
+
self.remained_units += units
|
|
119
|
+
self
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
### START CRON JOBS ###
|
|
123
|
+
# modify User param first
|
|
124
|
+
# deliver_later doesn't work
|
|
125
|
+
|
|
126
|
+
def self.check_expired_sp
|
|
127
|
+
ServicePack.find_each do |sp|
|
|
128
|
+
ServicePacksMailer.expired_email(User.last, sp).deliver_now if sp.expired?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# will be replaced
|
|
133
|
+
# # notify immediately at entries
|
|
134
|
+
# def self.check_used_up
|
|
135
|
+
# ServicePack.find_each do |sp|
|
|
136
|
+
# ServicePacksMailer.used_up_email(User.last, sp).deliver_now if sp.used_up?
|
|
137
|
+
# end
|
|
138
|
+
# end
|
|
139
|
+
|
|
140
|
+
def self.check_threshold1
|
|
141
|
+
ServicePack.notifiable(1).find_each do |sp|
|
|
142
|
+
ServicePacksMailer.notify_under_threshold1(User.last, sp).deliver_now
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.check_threshold2
|
|
147
|
+
ServicePack.notifiable(2).find_each do |sp|
|
|
148
|
+
ServicePacksMailer.notify_under_threshold2(User.last, sp).deliver_now
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
### END CRON JOBS ###
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def threshold1_is_greater_than_total_units
|
|
157
|
+
@errors.add(:threshold1, 'must be smaller than total units') if threshold1 >= total_units
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def threshold2_is_greater_than_total_units
|
|
161
|
+
@errors.add(:threshold2, 'must be smaller than total units') if threshold2 >= total_units
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def threshold2_is_greater_than_threshold1
|
|
165
|
+
@errors.add(:threshold2, 'must be less than threshold 1') if threshold2 >= threshold1
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def end_after_start
|
|
169
|
+
@errors.add(:expired_date, 'must be after start date') if expired_date < started_date
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def must_not_expire_in_the_past
|
|
173
|
+
@errors.add(:expired_date, 'must not be in the past') if expired_date < Date.today
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def knock_out
|
|
177
|
+
revoke_all_assignments
|
|
178
|
+
Delayed::Job.enqueue UsedUpServicePackJob.new(self)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<%= stylesheet_link_tag 'assigns', plugin: :openproject_service_packs %>
|
|
2
|
+
|
|
3
|
+
<div id="sp-display">
|
|
4
|
+
<h2>Service Pack <%= @service_pack.name %> already assigned to <%= @project.name %></h2><hr/>
|
|
5
|
+
<strong>Assignment Date</strong>: <%= @assignment.assign_date %>
|
|
6
|
+
<strong>Valid Until</strong>: <%= @assignment.unassign_date %>
|
|
7
|
+
<strong>Remained:</strong> <%= @service_pack.remained_units %>/<%= @service_pack.total_units %>
|
|
8
|
+
</div>
|
|
9
|
+
<br/>
|
|
10
|
+
<% if @can_unassign ||= User.current.allowed_to?(:unassign_service_packs, @project) %>
|
|
11
|
+
<%= link_to -'Unassign', assigns_unassign_path(@project), method: :post, class: -'button norm', data: { confirm: -'Are you sure?' }%>
|
|
12
|
+
<% end %>
|
|
13
|
+
<hr/>
|
|
14
|
+
<p class='other-formats'>Report available in: <%= link_to -'CSV', {action: :report, format: :csv} %></p>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<%= javascript_include_tag 'assigns.js', plugin: :openproject_service_packs %>
|
|
2
|
+
|
|
3
|
+
<div id="sp-assign">
|
|
4
|
+
<h2>No Service Pack is assigned to this project.</h2>
|
|
5
|
+
<br/>
|
|
6
|
+
|
|
7
|
+
<%= form_with model: @assignment, url: assigns_assign_path(@project), local: true, method: :post do |f| %>
|
|
8
|
+
Choose a Service Pack to apply:
|
|
9
|
+
<%= f.select :service_pack_id, {},{},{id: "select-sp"} do %>
|
|
10
|
+
<option selected>--Pick one Service Pack--</option>
|
|
11
|
+
<% @assignables.each do |assignable| %>
|
|
12
|
+
<%= content_tag(:option, assignable.name, value: assignable.id, data: {start: assignable.started_date.to_s, end: assignable.expired_date.to_s, cap: assignable.total_units, rem: assignable.remained_units}) %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% end %>
|
|
15
|
+
<br/>
|
|
16
|
+
<%= f.submit -'Assign a Service Pack', class: -'button -alt-highlight', id: 'sp-assign-button' %>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
<div id="sp-content"></div>
|