effective_cpd 0.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/MIT-LICENSE +20 -0
- data/README.md +117 -0
- data/Rakefile +18 -0
- data/app/assets/config/effective_cpd_manifest.js +2 -0
- data/app/assets/javascripts/effective_cpd.js +2 -0
- data/app/assets/javascripts/effective_cpd/activities.js +49 -0
- data/app/assets/javascripts/effective_cpd/activities_new.js +21 -0
- data/app/assets/stylesheets/effective_cpd.scss +1 -0
- data/app/assets/stylesheets/effective_cpd/_activities.scss +19 -0
- data/app/controllers/admin/cpd_activities_controller.rb +13 -0
- data/app/controllers/admin/cpd_categories_controller.rb +13 -0
- data/app/controllers/admin/cpd_cycles_controller.rb +13 -0
- data/app/controllers/admin/cpd_rules_controller.rb +13 -0
- data/app/controllers/admin/cpd_statements_controller.rb +13 -0
- data/app/controllers/effective/cpd_cycles_controller.rb +19 -0
- data/app/controllers/effective/cpd_statement_activities_controller.rb +27 -0
- data/app/controllers/effective/cpd_statements_controller.rb +71 -0
- data/app/datatables/admin/effective_cpd_activities_datatable.rb +31 -0
- data/app/datatables/admin/effective_cpd_categories_datatable.rb +24 -0
- data/app/datatables/admin/effective_cpd_cycles_datatable.rb +22 -0
- data/app/datatables/admin/effective_cpd_rules_datatable.rb +43 -0
- data/app/datatables/admin/effective_cpd_statements_datatable.rb +30 -0
- data/app/datatables/effective_cpd_datatable.rb +30 -0
- data/app/datatables/effective_cpd_statements_datatable.rb +23 -0
- data/app/helpers/effective_cpd_helper.rb +37 -0
- data/app/mailers/effective/cpd_mailer.rb +7 -0
- data/app/models/effective/cpd_activity.rb +41 -0
- data/app/models/effective/cpd_category.rb +35 -0
- data/app/models/effective/cpd_cycle.rb +113 -0
- data/app/models/effective/cpd_rule.rb +108 -0
- data/app/models/effective/cpd_scorer.rb +158 -0
- data/app/models/effective/cpd_statement.rb +95 -0
- data/app/models/effective/cpd_statement_activity.rb +78 -0
- data/app/views/admin/cpd_activities/_form.html.haml +20 -0
- data/app/views/admin/cpd_categories/_form.html.haml +21 -0
- data/app/views/admin/cpd_categories/_form_cpd_category.html.haml +5 -0
- data/app/views/admin/cpd_cycles/_form.html.haml +17 -0
- data/app/views/admin/cpd_cycles/_form_content.html.haml +32 -0
- data/app/views/admin/cpd_cycles/_form_cpd_cycle.html.haml +21 -0
- data/app/views/admin/cpd_cycles/_form_cpd_rules.html.haml +56 -0
- data/app/views/admin/cpd_statements/_form.html.haml +6 -0
- data/app/views/effective/cpd_statement_activities/_form.html.haml +70 -0
- data/app/views/effective/cpd_statements/_activities.html.haml +64 -0
- data/app/views/effective/cpd_statements/_activities_new.html.haml +39 -0
- data/app/views/effective/cpd_statements/_agreements.html.haml +6 -0
- data/app/views/effective/cpd_statements/_cpd_statement.html.haml +5 -0
- data/app/views/effective/cpd_statements/_layout.html.haml +37 -0
- data/app/views/effective/cpd_statements/_summary.html.haml +36 -0
- data/app/views/effective/cpd_statements/activities.html.haml +25 -0
- data/app/views/effective/cpd_statements/agreements.html.haml +14 -0
- data/app/views/effective/cpd_statements/complete.html.haml +13 -0
- data/app/views/effective/cpd_statements/start.html.haml +13 -0
- data/app/views/effective/cpd_statements/submit.html.haml +20 -0
- data/app/views/layouts/effective_cpd_mailer_layout.html.haml +7 -0
- data/config/effective_cpd.rb +29 -0
- data/config/routes.rb +28 -0
- data/db/migrate/01_create_effective_cpd.rb.erb +98 -0
- data/db/seeds.rb +472 -0
- data/lib/effective_cpd.rb +18 -0
- data/lib/effective_cpd/engine.rb +11 -0
- data/lib/effective_cpd/version.rb +3 -0
- data/lib/generators/effective_cpd/install_generator.rb +46 -0
- data/lib/generators/templates/effective_cpd_mailer_preview.rb +4 -0
- data/lib/tasks/effective_cpd_tasks.rake +6 -0
- metadata +233 -0
@@ -0,0 +1,108 @@
|
|
1
|
+
module Effective
|
2
|
+
class CpdRule < ActiveRecord::Base
|
3
|
+
belongs_to :cpd_cycle
|
4
|
+
belongs_to :ruleable, polymorphic: true # Activity or Category
|
5
|
+
|
6
|
+
log_changes(to: :cpd_cycle) if respond_to?(:log_changes)
|
7
|
+
|
8
|
+
# Only permit the words amount, amount2 and any charater 0-9 + - / * ( )
|
9
|
+
INVALID_FORMULA_CHARS = /[^0-9\+\-\/\*\(\)]/
|
10
|
+
|
11
|
+
effective_resource do
|
12
|
+
# A plaintext description of the formula
|
13
|
+
# For a Category: A maximum of 35 PDHs/year may be claimed in the Contributions to Knowledge category
|
14
|
+
# For a Activity: 15 hours of work equals 1 credit
|
15
|
+
credit_description :text
|
16
|
+
|
17
|
+
# The maximum credits per cycle a statement. Nil for no limit
|
18
|
+
max_credits_per_cycle :integer
|
19
|
+
|
20
|
+
# (amount / 15) or (30) or (amount * 2) or (amount + (amount2 * 10))
|
21
|
+
formula :string
|
22
|
+
|
23
|
+
# Maximum number of cycles can carry forward
|
24
|
+
max_cycles_can_carry_forward :integer
|
25
|
+
|
26
|
+
# Cannot be entered in this cycle
|
27
|
+
unavailable :boolean
|
28
|
+
|
29
|
+
timestamps
|
30
|
+
end
|
31
|
+
|
32
|
+
scope :deep, -> { includes(:cpd_cycle, :ruleable) }
|
33
|
+
scope :categories, -> { where(ruleable_type: 'Effective::CpdCategory') }
|
34
|
+
scope :activities, -> { where(ruleable_type: 'Effective::CpdActivity') }
|
35
|
+
scope :unavailable, -> { where(unavailable: true) }
|
36
|
+
|
37
|
+
#validates :cpd_cycle_id, uniqueness: { scope: [:ruleable_id, :ruleable_type] }
|
38
|
+
validates :credit_description, presence: true
|
39
|
+
validates :max_credits_per_cycle, numericality: { greater_than: 0, allow_nil: true }
|
40
|
+
validates :max_cycles_can_carry_forward, numericality: { greater_than: 0, allow_nil: true }
|
41
|
+
|
42
|
+
validates :formula, presence: true, if: -> { activity? }
|
43
|
+
validates :formula, absence: true, if: -> { category? }
|
44
|
+
|
45
|
+
validate(if: -> { formula.present? }) do
|
46
|
+
if formula.gsub('amount2', '').gsub('amount', '').gsub(' ', '').match(INVALID_FORMULA_CHARS).present?
|
47
|
+
self.errors.add(:formula, "may only contain amount, amount2 and 0-9 + - / * ( ) characters")
|
48
|
+
else
|
49
|
+
begin
|
50
|
+
eval_equation(amount: 0, amount2: 0)
|
51
|
+
rescue Exception => e
|
52
|
+
self.errors.add(:formula, e.message)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# The formula is determined by the cpd_activity's amount_label and amount2_label presence
|
58
|
+
validate(if: -> { formula.present? && activity? }) do
|
59
|
+
amount = formula.gsub('amount2', '').include?('amount')
|
60
|
+
amount2 = formula.include?('amount2')
|
61
|
+
|
62
|
+
cpd_activity = ruleable
|
63
|
+
|
64
|
+
if cpd_activity.amount_label.present? && cpd_activity.amount2_label.present?
|
65
|
+
self.errors.add(:formula, 'must include "amount"') unless amount.present?
|
66
|
+
self.errors.add(:formula, 'must include "amount2"') unless amount2.present?
|
67
|
+
elsif cpd_activity.amount_label.present?
|
68
|
+
self.errors.add(:formula, 'must include "amount"') unless amount.present?
|
69
|
+
self.errors.add(:formula, 'must not include "amount2"') if amount2.present?
|
70
|
+
elsif cpd_activity.amount2_label.present?
|
71
|
+
self.errors.add(:formula, 'must include "amount2"') unless amount2.present?
|
72
|
+
self.errors.add(:formula, 'must not include "amount"') if amount.present?
|
73
|
+
else
|
74
|
+
self.errors.add(:formula, 'must not include "amount"') if amount.present?
|
75
|
+
self.errors.add(:formula, 'must not include "amount2"') if amount2.present?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_s
|
80
|
+
formula.presence || 'category'
|
81
|
+
end
|
82
|
+
|
83
|
+
def activity?
|
84
|
+
ruleable.kind_of?(CpdActivity)
|
85
|
+
end
|
86
|
+
|
87
|
+
def category?
|
88
|
+
ruleable.kind_of?(CpdCategory)
|
89
|
+
end
|
90
|
+
|
91
|
+
def score(cpd_statement_activity:)
|
92
|
+
raise('cpd_cycles must match') unless cpd_statement_activity.cpd_statement.cpd_cycle_id == cpd_cycle_id
|
93
|
+
raise('cpd_activities must match') unless cpd_statement_activity.cpd_activity_id == ruleable_id
|
94
|
+
|
95
|
+
return cpd_statement_activity.carry_over if cpd_statement_activity.is_carry_over?
|
96
|
+
|
97
|
+
eval_equation(amount: cpd_statement_activity.amount, amount2: cpd_statement_activity.amount2)
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def eval_equation(amount: nil, amount2: nil)
|
103
|
+
equation = formula.gsub('amount2', amount2.to_s).gsub('amount', amount.to_s)
|
104
|
+
eval(equation).to_i
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
module Effective
|
2
|
+
class CpdScorer
|
3
|
+
include EffectiveCpdHelper
|
4
|
+
|
5
|
+
def initialize(user:, from: nil)
|
6
|
+
@cycles = CpdCycle.deep.sorted.all
|
7
|
+
@statements = CpdStatement.deep.where(user: user).sorted.all
|
8
|
+
|
9
|
+
if from.present?
|
10
|
+
raise('expected from to be a CpdStatement') unless from.kind_of?(CpdStatement)
|
11
|
+
@statements = @statements.where('cpd_cycle_id >= ?', from.cpd_cycle_id)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
def score!
|
17
|
+
@statements.each_with_index do |statement, index|
|
18
|
+
prev_statement = @statements[index-1] if index > 0
|
19
|
+
|
20
|
+
Array(prev_statement&.cpd_statement_activities).each do |activity|
|
21
|
+
if activity.marked_for_destruction? # Cascade this down the line
|
22
|
+
statement.cpd_statement_activities.each { |a| a.mark_for_destruction if a.original == activity }
|
23
|
+
end
|
24
|
+
|
25
|
+
if can_carry_forward?(activity, statement.cpd_cycle)
|
26
|
+
save_carry_forward_activity(activity, statement)
|
27
|
+
else
|
28
|
+
delete_carry_forward_activity(activity, statement)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# An activity was deleted from a previous statement
|
33
|
+
statement.cpd_statement_activities.each do |activity|
|
34
|
+
activity.mark_for_destruction if activity.original_id.present? && activity.original.blank?
|
35
|
+
end
|
36
|
+
|
37
|
+
score_statement(statement)
|
38
|
+
end
|
39
|
+
|
40
|
+
save!
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def save!
|
46
|
+
CpdStatement.transaction do
|
47
|
+
@statements.each { |statement| statement.save! }; true
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def score_statement(statement)
|
52
|
+
cycle = statement.cpd_cycle
|
53
|
+
|
54
|
+
# Reset the current carry_forwards and messages
|
55
|
+
statement.cpd_statement_activities.each do |activity|
|
56
|
+
activity.carry_forward = 0
|
57
|
+
activity.reduced_messages.clear
|
58
|
+
end
|
59
|
+
|
60
|
+
# This scores and enforces CycleActivity.max_credits_per_cycle
|
61
|
+
statement.cpd_statement_activities.group_by(&:cpd_activity).each do |cpd_activity, activities|
|
62
|
+
rule = cycle.rule_for(cpd_activity)
|
63
|
+
max_credits_per_cycle = rule.max_credits_per_cycle || 9999999
|
64
|
+
|
65
|
+
activities.each do |activity|
|
66
|
+
next if activity.marked_for_destruction?
|
67
|
+
|
68
|
+
activity.score = rule.score(cpd_statement_activity: activity)
|
69
|
+
activity.max_score = activity.score # Hack for Category maximums below
|
70
|
+
|
71
|
+
max_credits_per_cycle -= activity.score # Counting down...
|
72
|
+
|
73
|
+
if max_credits_per_cycle < 0
|
74
|
+
activity.carry_forward = [0 - max_credits_per_cycle, activity.score].min
|
75
|
+
activity.reduced_messages["activity_#{cpd_activity.id}"] = "You have reached the maximum of #{rule.max_credits_per_cycle}/#{cpd_cycle_label} for this type of activity"
|
76
|
+
activity.score = [activity.score + max_credits_per_cycle, 0].max
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# This enforced CycleCategory.max_credits_per_cycle
|
82
|
+
statement.cpd_statement_activities.group_by(&:cpd_category).each do |cpd_category, activities|
|
83
|
+
rule = cycle.rule_for(cpd_category)
|
84
|
+
max_credits_per_cycle = rule.max_credits_per_cycle
|
85
|
+
|
86
|
+
next if max_credits_per_cycle == nil
|
87
|
+
|
88
|
+
activities.each do |activity|
|
89
|
+
next if activity.marked_for_destruction?
|
90
|
+
|
91
|
+
max_credits_per_cycle -= activity.score # We're already scored. Counting down...
|
92
|
+
|
93
|
+
if max_credits_per_cycle < 0
|
94
|
+
activity.score = [activity.score + max_credits_per_cycle, 0].max
|
95
|
+
activity.carry_forward = activity.max_score - activity.score
|
96
|
+
activity.reduced_messages["category_#{cpd_category.id}"] = "You have reached the maximum of #{rule.max_credits_per_cycle}/#{cpd_cycle_label} for activities in the #{cpd_category} category"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# This enforces the max_cycles_can_carry_forward logic
|
102
|
+
# If an Activity cannot be carried forward another cycle, its carry_forward should be 0
|
103
|
+
next_cycle = @cycles[@cycles.index(cycle) + 1]
|
104
|
+
|
105
|
+
statement.cpd_statement_activities.each do |activity|
|
106
|
+
next if (activity.carry_forward == 0 || activity.marked_for_destruction?)
|
107
|
+
|
108
|
+
unless can_carry_forward?(activity, next_cycle)
|
109
|
+
activity.carry_forward = 0
|
110
|
+
activity.reduced_messages['max_cycles_can_carry_forward'] = "This activity cannot be carried forward any further"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Finally set the score from the sum of activitiy scores
|
115
|
+
statement.score = statement.cpd_statement_activities.map { |activity| activity.marked_for_destruction? ? 0 : activity.score }.sum
|
116
|
+
end
|
117
|
+
|
118
|
+
def can_carry_forward?(activity, to_cycle = nil) # This is a StatementActivity being passed
|
119
|
+
return false if (activity.carry_forward == 0 || activity.marked_for_destruction?)
|
120
|
+
|
121
|
+
from_cycle = @cycles.find { |cycle| cycle.id == (activity.original || activity).cpd_statement.cpd_cycle_id }
|
122
|
+
max_cycles_can_carry_forward = from_cycle.rule_for(activity.cpd_activity).max_cycles_can_carry_forward
|
123
|
+
|
124
|
+
return true if max_cycles_can_carry_forward.blank?
|
125
|
+
|
126
|
+
cycles_carried = (@cycles.index(to_cycle) || @cycles.size) - @cycles.index(from_cycle)
|
127
|
+
cycles_carried <= max_cycles_can_carry_forward
|
128
|
+
end
|
129
|
+
|
130
|
+
def save_carry_forward_activity(existing, statement)
|
131
|
+
activity = statement.cpd_statement_activities.find { |a| a.original == (existing.original || existing) }
|
132
|
+
activity ||= statement.cpd_statement_activities.build()
|
133
|
+
|
134
|
+
activity.assign_attributes(
|
135
|
+
cpd_category: existing.cpd_category,
|
136
|
+
cpd_activity: existing.cpd_activity,
|
137
|
+
amount: existing.amount,
|
138
|
+
amount2: existing.amount2,
|
139
|
+
description: existing.description,
|
140
|
+
)
|
141
|
+
|
142
|
+
existing.files.each { |file| activity.files.attach(file.blob) }
|
143
|
+
|
144
|
+
activity.assign_attributes(
|
145
|
+
carry_over: existing.carry_forward,
|
146
|
+
original: existing.original || existing
|
147
|
+
)
|
148
|
+
|
149
|
+
activity
|
150
|
+
end
|
151
|
+
|
152
|
+
def delete_carry_forward_activity(existing, statement)
|
153
|
+
activity = statement.cpd_statement_activities.find { |a| a.original == (existing.original || existing) }
|
154
|
+
activity.mark_for_destruction if activity
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
module Effective
|
2
|
+
class CpdStatement < ActiveRecord::Base
|
3
|
+
attr_accessor :current_user
|
4
|
+
attr_accessor :current_step
|
5
|
+
|
6
|
+
belongs_to :cpd_cycle
|
7
|
+
belongs_to :user, polymorphic: true
|
8
|
+
|
9
|
+
has_many :cpd_statement_activities, -> { order(:id) }, inverse_of: :cpd_statement, dependent: :destroy
|
10
|
+
accepts_nested_attributes_for :cpd_statement_activities
|
11
|
+
|
12
|
+
has_many_attached :files
|
13
|
+
log_changes if respond_to?(:log_changes)
|
14
|
+
|
15
|
+
acts_as_tokened
|
16
|
+
|
17
|
+
acts_as_wizard(
|
18
|
+
start: 'Start',
|
19
|
+
activities: 'Enter Activities',
|
20
|
+
agreements: 'Sign Agreements',
|
21
|
+
submit: 'Confirm & Submit',
|
22
|
+
complete: 'Complete'
|
23
|
+
)
|
24
|
+
|
25
|
+
effective_resource do
|
26
|
+
score :integer
|
27
|
+
|
28
|
+
confirm_read :boolean
|
29
|
+
confirm_factual :boolean
|
30
|
+
confirm_readonly :boolean
|
31
|
+
|
32
|
+
completed_at :datetime, permitted: false
|
33
|
+
|
34
|
+
# Acts as tokened
|
35
|
+
token :string, permitted: false
|
36
|
+
|
37
|
+
# Acts as Wizard
|
38
|
+
wizard_steps :text, permitted: false
|
39
|
+
timestamps
|
40
|
+
end
|
41
|
+
|
42
|
+
scope :deep, -> { includes(:cpd_cycle, :user, cpd_statement_activities: [:files_attachments, :cpd_category, :original, cpd_activity: [:rich_text_body]]) }
|
43
|
+
scope :sorted, -> { order(:cpd_cycle_id) }
|
44
|
+
|
45
|
+
scope :draft, -> { where(completed_at: nil) }
|
46
|
+
scope :completed, -> { where.not(completed_at: nil) }
|
47
|
+
|
48
|
+
before_validation(if: -> { new_record? }) do
|
49
|
+
self.user ||= current_user
|
50
|
+
self.score ||= 0
|
51
|
+
end
|
52
|
+
|
53
|
+
validate(if: -> { completed? && cpd_cycle.required_score.present? }) do
|
54
|
+
min = cpd_cycle.required_score
|
55
|
+
self.errors.add(:score, "must be #{min} or greater to submit statement") if score < min
|
56
|
+
end
|
57
|
+
|
58
|
+
with_options(if: -> { current_step == :agreements }) do
|
59
|
+
validates :confirm_read, acceptance: true
|
60
|
+
validates :confirm_factual, acceptance: true
|
61
|
+
end
|
62
|
+
|
63
|
+
with_options(if: -> { current_step == :submit}) do
|
64
|
+
validates :confirm_readonly, acceptance: true
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_s
|
68
|
+
(cpd_cycle || 'statement').to_s
|
69
|
+
end
|
70
|
+
|
71
|
+
# This is the review step where they click Submit Ballot
|
72
|
+
def submit!
|
73
|
+
wizard_steps[:complete] ||= Time.zone.now
|
74
|
+
self.completed_at ||= Time.zone.now
|
75
|
+
|
76
|
+
save!
|
77
|
+
end
|
78
|
+
|
79
|
+
def completed?
|
80
|
+
completed_at.present?
|
81
|
+
end
|
82
|
+
|
83
|
+
def carry_forward
|
84
|
+
cpd_statement_activities.sum { |activity| activity.carry_forward.to_i }
|
85
|
+
end
|
86
|
+
|
87
|
+
# {category_id => 20, category_id => 15}
|
88
|
+
def score_per_category
|
89
|
+
@score_per_category ||= Hash.new(0).tap do |scores|
|
90
|
+
cpd_statement_activities.each { |activity| scores[activity.cpd_category_id] += activity.score.to_i }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Effective
|
2
|
+
class CpdStatementActivity < ActiveRecord::Base
|
3
|
+
attr_accessor :max_score
|
4
|
+
|
5
|
+
belongs_to :cpd_statement
|
6
|
+
belongs_to :cpd_category
|
7
|
+
belongs_to :cpd_activity
|
8
|
+
|
9
|
+
belongs_to :original, class_name: 'CpdStatementActivity', optional: true # If this is a Carryover, the original_statement_activity will be set.
|
10
|
+
#has_many :carried, class_name: 'CpdStatementActivity', foreign_key: 'original_id', dependent: :delete_all
|
11
|
+
|
12
|
+
has_many_attached :files
|
13
|
+
|
14
|
+
if respond_to?(:log_changes)
|
15
|
+
log_changes(to: :cpd_statement)
|
16
|
+
end
|
17
|
+
|
18
|
+
effective_resource do
|
19
|
+
amount :integer
|
20
|
+
amount2 :integer
|
21
|
+
|
22
|
+
description :text
|
23
|
+
|
24
|
+
carry_over :integer # carry_over_from_last_cycle
|
25
|
+
score :integer
|
26
|
+
carry_forward :integer # carry_forward_to_next_cycle
|
27
|
+
|
28
|
+
reduced_messages :text
|
29
|
+
|
30
|
+
timestamps
|
31
|
+
end
|
32
|
+
|
33
|
+
serialize :reduced_messages, Hash
|
34
|
+
|
35
|
+
scope :deep, -> { includes(:cpd_statement, :cpd_category, :cpd_activity, :original) }
|
36
|
+
scope :sorted, -> { order(:id) }
|
37
|
+
|
38
|
+
validates :original, presence: true, if: -> { carry_over.to_i > 0 }
|
39
|
+
validates :description, presence: true
|
40
|
+
|
41
|
+
validate(if: -> { cpd_statement.present? }) do
|
42
|
+
self.errors.add(:base, "statement has already been completed") if cpd_statement.completed?
|
43
|
+
self.errors.add(:base, "cycle is unavailable") unless cpd_statement.cpd_cycle.available?
|
44
|
+
end
|
45
|
+
|
46
|
+
def destroy
|
47
|
+
return false if cpd_statement.completed?
|
48
|
+
return false unless cpd_statement.cpd_cycle.available?
|
49
|
+
super
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
(cpd_activity || 'activity').to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
def reduced_messages
|
57
|
+
self[:reduced_messages] ||= {}
|
58
|
+
end
|
59
|
+
|
60
|
+
def is_carry_over?
|
61
|
+
original.present?
|
62
|
+
end
|
63
|
+
|
64
|
+
def original_cycle
|
65
|
+
(original || self).cpd_statement.cpd_cycle
|
66
|
+
end
|
67
|
+
|
68
|
+
# Will display as read-only on form
|
69
|
+
def locked?
|
70
|
+
is_carry_over? || cpd_statement&.completed?
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_debug
|
74
|
+
"id=#{id}, score=#{score}, carry_foward=#{carry_forward}, carry_from_last=#{carry_over}, original=#{original&.id}"
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
= effective_form_with(model: [:admin, cpd_activity], engine: true) do |f|
|
2
|
+
- if inline_datatable?
|
3
|
+
= f.hidden_field :cpd_category_id
|
4
|
+
- else
|
5
|
+
= f.select :cpd_category_id, Effective::CpdCategory.sorted.all, label: 'Category'
|
6
|
+
|
7
|
+
.row
|
8
|
+
.col
|
9
|
+
= f.text_field :title, label: 'Title'
|
10
|
+
= f.rich_text_area :body, label: "Body"
|
11
|
+
.col
|
12
|
+
= f.text_field :amount_label,
|
13
|
+
hint: 'hours of work, papers written, courses attended'
|
14
|
+
|
15
|
+
= f.text_field :amount2_label,
|
16
|
+
hint: 'continuing education units'
|
17
|
+
|
18
|
+
= f.check_box :requires_upload_file
|
19
|
+
|
20
|
+
= effective_submit(f)
|