effective_cpd 0.6.9 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fc9c5614b8c87479aef3067320ae4f432255f4214f65d5fb6558cb389c557aa
4
- data.tar.gz: 6196e732aaa80aeaf16c8be346d2b28e3fba2251f749bc7038319954cc97d494
3
+ metadata.gz: 5b9ef412f128415399ade394ba5ee59d6799d9f13a600904bccfc6c88aa80fd6
4
+ data.tar.gz: 3c193fe0263c126908e9e2029ea303cfc9842b41d80de7e0bd29761afd97d870
5
5
  SHA512:
6
- metadata.gz: 30b45d45efa81a63adeab41b0da5ce2811e718eb447b77b3419aadddb04b47d01aa9c55d3a71c9615ee56dd19f8a8bfe19b477dd31cc02e2ade811cdbd32038f
7
- data.tar.gz: 7af185b1632ff2928c814efca3e8262a4e1c3b366fdbc511cedfecf93e07cadcea7a8da999d4b8d6a18995bffb75f95af0669f5fa384f5d458e4b0ded953b753
6
+ metadata.gz: b8f785d86dedc5821fb7118a7e8ea2e7c5baedc29abcd470e0c420c8e1352e51aaa4bcd364a0822e75d013778fcfdd21ad20ab5337ee55223f12cda03bf7bcee
7
+ data.tar.gz: 8a817518db9ab7b7a7104f3d6eb4991e9422bce880a4d5817bf9dd6e8917a6fd4070f96fdae059bd978bb6b6f6888faf781e0ee8c84ea959fb39ed9be6d0c129
@@ -5,8 +5,14 @@ module Admin
5
5
 
6
6
  include Effective::CrudController
7
7
 
8
+ resource_scope -> { EffectiveCpd.AuditLevel.deep.all }
9
+ datatable -> { Admin::EffectiveCpdAuditLevelsDatatable.new }
10
+
11
+ private
12
+
8
13
  def permitted_params
9
- params.require(:effective_cpd_audit_level).permit!
14
+ model = (params.key?(:effective_cpd_audit_level) ? :effective_cpd_audit_level : :cpd_audit_level)
15
+ params.require(model).permit!
10
16
  end
11
17
 
12
18
  end
@@ -5,8 +5,14 @@ module Admin
5
5
 
6
6
  include Effective::CrudController
7
7
 
8
+ resource_scope -> { EffectiveCpd.AuditReview.deep.all }
9
+ datatable -> { Admin::EffectiveCpdAuditReviewsDatatable.new }
10
+
11
+ private
12
+
8
13
  def permitted_params
9
- params.require(:effective_cpd_audit_review).permit!
14
+ model = (params.key?(:effective_cpd_audit_review) ? :effective_cpd_audit_review : :cpd_audit_review)
15
+ params.require(model).permit!
10
16
  end
11
17
 
12
18
  end
@@ -5,6 +5,9 @@ module Admin
5
5
 
6
6
  include Effective::CrudController
7
7
 
8
+ resource_scope -> { EffectiveCpd.CpdAudit.deep.all }
9
+ datatable -> { Admin::EffectiveCpdAuditsDatatable.new }
10
+
8
11
  submit :resolve_conflict, 'Resolve Conflict of Interest', success: -> {
9
12
  [
10
13
  "Successfully resolved #{resource}",
@@ -33,8 +36,11 @@ module Admin
33
36
  ].compact.join(' ')
34
37
  }
35
38
 
39
+ private
40
+
36
41
  def permitted_params
37
- params.require(:effective_cpd_audit).permit!
42
+ model = (params.key?(:effective_cpd_audit) ? :effective_cpd_audit : :cpd_audit)
43
+ params.require(model).permit!
38
44
  end
39
45
 
40
46
  end
@@ -14,6 +14,15 @@ module Effective
14
14
  CpdScorer.new(user: resource.user).score!
15
15
  end
16
16
 
17
+ # Enforce one statement per user per cycle. Redirect them to an existing statement for this cycle.
18
+ before_action(only: [:new, :show]) do
19
+ existing = resource_scope.where.not(id: resource).first
20
+
21
+ if existing.present?
22
+ redirect_to effective_cpd.cpd_cycle_cpd_statement_build_path(existing.cpd_cycle, existing, existing.next_step)
23
+ end
24
+ end
25
+
17
26
  # Enforce cycle availability
18
27
  before_action(only: [:show, :update]) do
19
28
  cycle = resource.cpd_cycle
@@ -28,7 +28,7 @@ module Admin
28
28
  end
29
29
 
30
30
  collection do
31
- Effective::CpdAuditLevel.all.deep
31
+ EffectiveCpd.CpdAuditLevel.all.deep
32
32
  end
33
33
  end
34
34
  end
@@ -30,7 +30,7 @@ module Admin
30
30
  end
31
31
 
32
32
  collection do
33
- Effective::CpdAuditReview.all.deep
33
+ EffectiveCpd.CpdAuditReview.all.deep
34
34
  end
35
35
  end
36
36
  end
@@ -94,7 +94,7 @@ module Admin
94
94
  end
95
95
 
96
96
  collection do
97
- Effective::CpdAudit.all.deep
97
+ EffectiveCpd.CpdAudit.all.deep
98
98
  end
99
99
  end
100
100
  end
@@ -0,0 +1,425 @@
1
+ # frozen_string_literal: true
2
+
3
+ # EffectiveCpdAudit
4
+ #
5
+ # Mark your owner model with effective_cpd_audit to get all the includes
6
+
7
+ module EffectiveCpdAudit
8
+ extend ActiveSupport::Concern
9
+
10
+ module Base
11
+ def effective_cpd_audit
12
+ include ::EffectiveCpdAudit
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def effective_cpd_audit?; true; end
18
+ end
19
+
20
+ included do
21
+ acts_as_email_form
22
+ acts_as_tokened
23
+ acts_as_reportable if respond_to?(:acts_as_reportable)
24
+ log_changes(except: [:wizard_steps, :cpd_audit_reviews]) if respond_to?(:log_changes)
25
+
26
+ acts_as_statused(
27
+ :opened, # Just Opened
28
+ :started, # First screen clicked
29
+ :conflicted, # Auditee has declared a conflict of interest
30
+ :conflicted_resolved, # The conflict of interest has been resolved
31
+ :exemption_requested, # Auditee has requested an exemption
32
+ :exemption_granted, # Exemption granted -> Audit is cancelled. Exit state.
33
+ :exemption_denied, # Exemption denied
34
+ :extension_requested, # Audittee has requested an extension
35
+ :extension_granted, # Extension granted
36
+ :extension_denied, # Extension denied
37
+ :submitted, # Audittee has completed questionnaire submitted. Audittee is done.
38
+ :reviewed, # All audit reviews completed. Ready for a determination.
39
+ :closed # Determination made by admin and/or audit committee. Exit state. All done.
40
+ )
41
+
42
+ acts_as_wizard(
43
+ start: 'Start',
44
+ information: 'Information',
45
+ instructions: 'Instructions',
46
+
47
+ # These 4 steps are determined by audit_level settings
48
+ conflict: 'Conflict of Interest',
49
+ exemption: 'Request Exemption',
50
+ extension: 'Request Extension',
51
+ waiting: 'Waiting',
52
+ cpd: 'CPD',
53
+
54
+ questionnaire: 'Questionnaire',
55
+ # ... There will be one step per cpd_audit_level_sections here
56
+ files: 'Upload Resume',
57
+
58
+ submit: 'Confirm & Submit',
59
+ complete: 'Complete'
60
+ )
61
+
62
+ attr_accessor :current_user
63
+ attr_accessor :current_step
64
+ attr_accessor :admin_process_request
65
+
66
+ # App scoped
67
+ belongs_to :cpd_audit_level, polymorphic: true
68
+ belongs_to :user, polymorphic: true # The user being audited
69
+
70
+ has_many :cpd_audit_reviews, -> { order(:id) }, inverse_of: :cpd_audit, dependent: :destroy
71
+ accepts_nested_attributes_for :cpd_audit_reviews, allow_destroy: true
72
+
73
+ # Effective Scoped
74
+ has_many :cpd_audit_responses, -> { Effective::CpdAuditResponse.sorted }, class_name: 'Effective::CpdAuditResponse', inverse_of: :cpd_audit, dependent: :destroy
75
+ accepts_nested_attributes_for :cpd_audit_responses
76
+
77
+ has_many_attached :files
78
+
79
+ ADMIN_PROCESS_REQUEST_OPTIONS = ['Granted', 'Denied']
80
+ COMPLETED_STATES = [:exemption_granted, :closed]
81
+ WAITING_ON_ADMIN_STATES = [:conflicted, :exemption_requested, :extension_requested, :reviewed]
82
+ WAITING_ON_REVIEWERS_STATES = [:submitted]
83
+ WAITING_ON_AUDITEE_STATES = [:opened, :started, :conflicted_resolved, :exemption_denied, :extension_granted, :extension_denied]
84
+
85
+ effective_resource do
86
+ due_date :date # Computed due date based on notification and extension date
87
+
88
+ selection :string
89
+ region :string
90
+ notes :text
91
+
92
+ # Anonymous Name
93
+ anonymous_name :string # Required when cpd_audit_level.anonymous?
94
+
95
+ # Important dates
96
+ notification_date :date # Can be set on CpdAudits#new, but basically created_at
97
+ extension_date :date # set by admin if extension if granted
98
+
99
+ # Final determination
100
+ determination :string
101
+
102
+ # Override Deadlines
103
+ ignore_deadlines :boolean
104
+
105
+ # Auditee response
106
+ conflict_of_interest :boolean
107
+ conflict_of_interest_reason :text
108
+
109
+ exemption_request :boolean
110
+ exemption_request_reason :text
111
+
112
+ extension_request :boolean
113
+ extension_request_date :date
114
+ extension_request_reason :text
115
+
116
+ # acts_as_statused
117
+ status :string
118
+ status_steps :text
119
+
120
+ # Status dates
121
+ started_at :datetime
122
+ submitted_at :datetime
123
+ reviewed_at :datetime
124
+ closed_at :datetime
125
+
126
+ # Acts as tokened
127
+ token :string
128
+
129
+ # Acts as Wizard
130
+ wizard_steps :text
131
+
132
+ timestamps
133
+ end
134
+
135
+ scope :deep, -> { includes(:cpd_audit_level, user: [:cpd_statements], cpd_audit_reviews: [:cpd_audit_level, :user, :cpd_audit_review_items]) }
136
+ scope :sorted, -> { order(:id) }
137
+
138
+ scope :draft, -> { where(submitted_at: nil) }
139
+ scope :available, -> { where.not(status: COMPLETED_STATES) }
140
+ scope :completed, -> { where(status: COMPLETED_STATES) }
141
+
142
+ scope :waiting_on_admin, -> { where(status: WAITING_ON_ADMIN_STATES) }
143
+ scope :waiting_on_auditee, -> { where(status: WAITING_ON_AUDITEE_STATES) }
144
+ scope :waiting_on_reviewers, -> { where(status: WAITING_ON_REVIEWERS_STATES) }
145
+
146
+ # effective_reports
147
+ def reportable_scopes
148
+ { draft: nil, available: nil, completed: nil, waiting_on_admin: nil, waiting_on_auditee: nil, waiting_on_reviewers: nil }
149
+ end
150
+
151
+ before_validation(if: -> { new_record? }) do
152
+ self.notification_date ||= Time.zone.now
153
+ self.due_date ||= deadline_to_submit()
154
+ end
155
+
156
+ validates :anonymous_name, presence: true, if: -> { cpd_audit_level&.anonymous? }
157
+
158
+ validates :notification_date, presence: true
159
+ validates :determination, presence: true, if: -> { closed? }
160
+
161
+ validates :conflict_of_interest_reason, presence: true, if: -> { conflict_of_interest? }
162
+ validates :exemption_request_reason, presence: true, if: -> { exemption_request? }
163
+ validates :extension_request_date, presence: true, if: -> { extension_request? }
164
+ validates :extension_request_reason, presence: true, if: -> { extension_request? }
165
+
166
+ validate(if: -> { current_step == :conflict && conflict_of_interest? && !ignore_deadlines? }) do
167
+ deadline = deadline_to_conflict_of_interest()
168
+ self.errors.add(:base, 'deadline to declare conflict of interest has already passed') if deadline && deadline < Time.zone.now
169
+ end
170
+
171
+ validate(if: -> { current_step == :exemption && exemption_request? && !ignore_deadlines? }) do
172
+ deadline = deadline_to_exemption()
173
+ self.errors.add(:base, 'deadline to request exemption has already passed') if deadline && deadline < Time.zone.now
174
+ end
175
+
176
+ validate(if: -> { current_step == :extension && extension_request? && !ignore_deadlines? }) do
177
+ deadline = deadline_to_extension()
178
+ self.errors.add(:base, 'deadline to request extension has already passed') if deadline && deadline < Time.zone.now
179
+ end
180
+
181
+ validate(if: -> { determination.present? }) do
182
+ unless cpd_audit_level.determinations.include?(determination)
183
+ self.errors.add(:determination, 'must exist in this audit level')
184
+ end
185
+ end
186
+
187
+ # If we're submitted. Check if we can go into reviewed?
188
+ before_save(if: -> { submitted? }) { review! }
189
+
190
+ after_commit(on: :create) do
191
+ send_email(:cpd_audit_opened)
192
+ end
193
+ end
194
+
195
+ def to_s
196
+ persisted? ? "#{cpd_audit_level} Audit of #{user}" : 'audit'
197
+ end
198
+
199
+ def dynamic_wizard_steps
200
+ cpd_audit_level.cpd_audit_level_sections.each_with_object({}) do |section, h|
201
+ h["section#{section.position+1}".to_sym] = section.title
202
+ end
203
+ end
204
+
205
+ def can_visit_step?(step)
206
+ return (step == :complete) if was_submitted? # Can only view complete step once submitted
207
+ can_revisit_completed_steps(step)
208
+ end
209
+
210
+ def required_steps
211
+ steps = [:start, :information, :instructions]
212
+
213
+ steps << :conflict if cpd_audit_level.conflict_of_interest?
214
+
215
+ steps << :exemption if cpd_audit_level.can_request_exemption?
216
+
217
+ unless exemption_requested?
218
+ steps << :extension if cpd_audit_level.can_request_extension?
219
+ end
220
+
221
+ if exemption_requested? || extension_requested?
222
+ steps += [:waiting]
223
+ end
224
+
225
+ steps += [:cpd, :questionnaire] + dynamic_wizard_steps.keys + [:files, :submit, :complete]
226
+
227
+ steps
228
+ end
229
+
230
+ def wizard_step_title(step)
231
+ WIZARD_STEPS[step] || dynamic_wizard_steps.fetch(step)
232
+ end
233
+
234
+ def deadline_date
235
+ (extension_date || notification_date)
236
+ end
237
+
238
+ def completed?
239
+ COMPLETED_STATES.include?(status.to_sym)
240
+ end
241
+
242
+ def in_progress?
243
+ COMPLETED_STATES.include?(status.to_sym) == false
244
+ end
245
+
246
+ def cpd_audit_level_section(wizard_step)
247
+ position = (wizard_step.to_s.split('section').last.to_i rescue false)
248
+ cpd_audit_level.cpd_audit_level_sections.find { |section| (section.position + 1) == position }
249
+ end
250
+
251
+ # Find or build
252
+ def cpd_audit_response(cpd_audit_level_question)
253
+ cpd_audit_response = cpd_audit_responses.find { |r| r.cpd_audit_level_question_id == cpd_audit_level_question.id }
254
+ cpd_audit_response ||= cpd_audit_responses.build(cpd_audit: self, cpd_audit_level_question: cpd_audit_level_question)
255
+ end
256
+
257
+ # Auditee wizard action
258
+ def start!
259
+ started!
260
+ end
261
+
262
+ # Admin action
263
+ def resolve_conflict!
264
+ wizard_steps[:conflict] = nil # Have them complete the conflict step again.
265
+
266
+ assign_attributes(conflict_of_interest: false, conflict_of_interest_reason: nil)
267
+ conflicted_resolved!
268
+ submitted!
269
+
270
+ send_email(:cpd_audit_conflict_resolved)
271
+ true
272
+ end
273
+
274
+ # Auditee wizard action
275
+ def exemption!
276
+ return started! unless exemption_request?
277
+
278
+ update!(status: :exemption_requested)
279
+ send_email(:cpd_audit_exemption_request)
280
+ end
281
+
282
+ # Admin action
283
+ def process_exemption!
284
+ case admin_process_request
285
+ when 'Granted' then grant_exemption!
286
+ when 'Denied' then deny_exemption!
287
+ else
288
+ self.errors.add(:admin_process_request, "can't be blank"); save!
289
+ end
290
+ end
291
+
292
+ def grant_exemption!
293
+ wizard_steps[:submit] ||= Time.zone.now
294
+ submitted! && exemption_granted!
295
+ send_email(:cpd_audit_exemption_granted)
296
+ end
297
+
298
+ def deny_exemption!
299
+ assign_attributes(exemption_request: false)
300
+ exemption_denied!
301
+ send_email(:cpd_audit_exemption_denied)
302
+ end
303
+
304
+ # Auditee wizard action
305
+ def extension!
306
+ return started! unless extension_request?
307
+
308
+ update!(status: :extension_requested)
309
+ send_email(:cpd_audit_extension_request)
310
+ end
311
+
312
+ # Admin action
313
+ def process_extension!
314
+ case admin_process_request
315
+ when 'Granted' then grant_extension!
316
+ when 'Denied' then deny_extension!
317
+ else
318
+ self.errors.add(:admin_process_request, "can't be blank"); save!
319
+ end
320
+ end
321
+
322
+ def grant_extension!
323
+ self.extension_date = extension_request_date
324
+ self.due_date = deadline_to_submit()
325
+
326
+ cpd_audit_reviews.each { |cpd_audit_review| cpd_audit_review.extension_granted! }
327
+ extension_granted!
328
+ send_email(:cpd_audit_extension_granted)
329
+ end
330
+
331
+ def deny_extension!
332
+ assign_attributes(extension_request: false)
333
+ extension_denied!
334
+ send_email(:cpd_audit_extension_denied)
335
+ end
336
+
337
+ # Require CPD step
338
+ def user_cpd_required?
339
+ return false unless user.cpd_audit_cpd_required?
340
+ required_cpd_cycle.present?
341
+ end
342
+
343
+ def user_cpd_completed?
344
+ return true if required_cpd_cycle.blank?
345
+ user.cpd_statements.any? { |s| s.completed? && s.cpd_cycle_id == required_cpd_cycle.id }
346
+ end
347
+
348
+ def required_cpd_cycle
349
+ @required_cpd_cycle ||= begin
350
+ last_year = ((notification_date || created_at || Time.zone.now) - 1.year).all_year
351
+ EffectiveCpd.CpdCycle.available.where(start_at: last_year).first
352
+ end
353
+ end
354
+
355
+ # Auditee wizard action
356
+ def submit!
357
+ if conflict_of_interest?
358
+ conflicted!
359
+ send_email(:cpd_audit_conflicted)
360
+ else
361
+ submitted!
362
+
363
+ cpd_audit_reviews.each { |cpd_audit_review| cpd_audit_review.ready! }
364
+ send_email(:cpd_audit_submitted)
365
+ end
366
+
367
+ true
368
+ end
369
+
370
+ # Called in a before_save. Intended for applicant_review to call in its submit! method
371
+ def review!
372
+ return false unless submitted?
373
+ return false unless cpd_audit_reviews.present? && cpd_audit_reviews.all?(&:completed?)
374
+
375
+ reviewed!
376
+ send_email(:cpd_audit_reviewed)
377
+ end
378
+
379
+ # Admin action
380
+ def close!
381
+ closed!
382
+ send_email(:cpd_audit_closed)
383
+ end
384
+
385
+ def email_form_defaults(action)
386
+ { from: EffectiveCpd.mailer_sender }
387
+ end
388
+
389
+ def send_email(email)
390
+ EffectiveCpd.send_email(email, self, email_form_params) unless email_form_skip?
391
+ true
392
+ end
393
+
394
+ def deadline_to_conflict_of_interest
395
+ return nil unless cpd_audit_level&.conflict_of_interest?
396
+ return nil unless cpd_audit_level.days_to_declare_conflict.present?
397
+
398
+ date = (notification_date || created_at || Time.zone.now)
399
+ EffectiveResources.advance_date(date, business_days: cpd_audit_level.days_to_declare_conflict)
400
+ end
401
+
402
+ def deadline_to_exemption
403
+ return nil unless cpd_audit_level&.can_request_exemption?
404
+ return nil unless cpd_audit_level.days_to_request_exemption.present?
405
+
406
+ date = (notification_date || created_at || Time.zone.now)
407
+ EffectiveResources.advance_date(date, business_days: cpd_audit_level.days_to_request_exemption)
408
+ end
409
+
410
+ def deadline_to_extension
411
+ return nil unless cpd_audit_level&.can_request_extension?
412
+ return nil unless cpd_audit_level.days_to_request_extension.present?
413
+
414
+ date = (notification_date || created_at || Time.zone.now)
415
+ EffectiveResources.advance_date(date, business_days: cpd_audit_level.days_to_request_extension)
416
+ end
417
+
418
+ def deadline_to_submit
419
+ return nil unless cpd_audit_level&.days_to_submit.present?
420
+
421
+ date = (extension_date || notification_date || created_at || Time.zone.now)
422
+ EffectiveResources.advance_date(date, business_days: cpd_audit_level.days_to_submit)
423
+ end
424
+
425
+ end
@@ -0,0 +1,108 @@
1
+ # EffectiveCpdUser
2
+ #
3
+ # Mark your user model with effective_cpd_user to get a few helpers
4
+ # And user specific point required scores
5
+
6
+ module EffectiveCpdAuditLevel
7
+ extend ActiveSupport::Concern
8
+
9
+ module Base
10
+ def effective_cpd_audit_level
11
+ include ::EffectiveCpdAuditLevel
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ def effective_cpd_audit_level?; true; end
17
+ end
18
+
19
+ included do
20
+ has_many_rich_texts
21
+
22
+ # For each cpd audit and cpd audit review wizard step
23
+ # rich_text_all_steps_audit_content
24
+ # rich_text_step_audit_content
25
+
26
+ # rich_text_all_steps_audit_review_content
27
+ # rich_text_step_audit_review_content
28
+
29
+ # App scoped
30
+ has_many :cpd_audits
31
+
32
+ has_many :cpd_audit_reviews, -> { EffectiveCpd.CpdAuditReview.sorted }, inverse_of: :cpd_audit_level, dependent: :destroy
33
+ accepts_nested_attributes_for :cpd_audit_reviews, allow_destroy: true
34
+
35
+ # Effective Scoped
36
+ has_many :cpd_audit_level_sections, -> { Effective::CpdAuditLevelSection.sorted }, class_name: 'Effective::CpdAuditLevelSection', inverse_of: :cpd_audit_level, dependent: :destroy
37
+ accepts_nested_attributes_for :cpd_audit_level_sections, allow_destroy: true
38
+
39
+ has_many :cpd_audit_level_questions, -> { Effective::CpdAuditLevelQuestion.sorted }, class_name: 'Effective::CpdAuditLevelQuestion', through: :cpd_audit_level_sections
40
+
41
+ if respond_to?(:log_changes)
42
+ log_changes(except: [:cpd_audits, :cpd_audit_reviews, :cpd_audit_level_sections, :cpd_audit_level_questions])
43
+ end
44
+
45
+ effective_resource do
46
+ title :string
47
+
48
+ anonymous :boolean
49
+
50
+ determinations :text # Final determination by auditor
51
+ recommendations :text # Recommendations by audit reviewers
52
+
53
+ days_to_submit :integer # For auditee to submit statement
54
+ days_to_review :integer # For auditor/audit_review to be completed
55
+
56
+ conflict_of_interest :boolean # Feature flags
57
+ can_request_exemption :boolean
58
+ can_request_extension :boolean
59
+
60
+ days_to_declare_conflict :integer
61
+ days_to_request_exemption :integer
62
+ days_to_request_extension :integer
63
+
64
+ timestamps
65
+ end
66
+
67
+ serialize :determinations, Array
68
+ serialize :recommendations, Array
69
+
70
+ scope :deep, -> { all }
71
+ scope :sorted, -> { order(:title) }
72
+
73
+ validates :title, presence: true
74
+ validates :determinations, presence: true
75
+ validates :recommendations, presence: true
76
+
77
+ validates :days_to_submit, numericality: { greater_than: 0, allow_nil: true }
78
+ validates :days_to_review, numericality: { greater_than: 0, allow_nil: true }
79
+
80
+ validates :days_to_declare_conflict, presence: true, if: -> { conflict_of_interest? }
81
+ validates :days_to_declare_conflict, numericality: { greater_than: 0, allow_nil: true }
82
+
83
+ validates :days_to_request_exemption, presence: true, if: -> { can_request_exemption? }
84
+ validates :days_to_request_exemption, numericality: { greater_than: 0, allow_nil: true }
85
+
86
+ validates :days_to_request_extension, presence: true, if: -> { can_request_extension? }
87
+ validates :days_to_request_extension, numericality: { greater_than: 0, allow_nil: true }
88
+
89
+ before_destroy do
90
+ if (count = cpd_audits.length) > 0
91
+ raise("#{count} audits belong to this audit level")
92
+ end
93
+ end
94
+ end
95
+
96
+ def to_s
97
+ title.presence || 'audit level'
98
+ end
99
+
100
+ def determinations
101
+ Array(self[:determinations]) - [nil, '']
102
+ end
103
+
104
+ def recommendations
105
+ Array(self[:recommendations]) - [nil, '']
106
+ end
107
+
108
+ end