effective_cpd 0.6.9 → 1.0.0

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 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