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