effective_cpd 0.6.8 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/admin/cpd_audit_levels_controller.rb +7 -1
  3. data/app/controllers/admin/cpd_audit_reviews_controller.rb +7 -1
  4. data/app/controllers/admin/cpd_audits_controller.rb +21 -1
  5. data/app/controllers/admin/cpd_statements_controller.rb +1 -1
  6. data/app/controllers/effective/cpd_audit_reviews_controller.rb +3 -39
  7. data/app/controllers/effective/cpd_audits_controller.rb +3 -41
  8. data/app/datatables/admin/effective_cpd_audit_levels_datatable.rb +1 -1
  9. data/app/datatables/admin/effective_cpd_audit_reviews_datatable.rb +4 -2
  10. data/app/datatables/admin/effective_cpd_audits_datatable.rb +12 -10
  11. data/app/datatables/effective_cpd_available_audit_reviews_datatable.rb +4 -4
  12. data/app/datatables/effective_cpd_available_audits_datatable.rb +1 -1
  13. data/app/datatables/effective_cpd_completed_audit_reviews_datatable.rb +3 -3
  14. data/app/datatables/effective_cpd_completed_audits_datatable.rb +2 -2
  15. data/app/helpers/effective_cpd_audits_helper.rb +0 -32
  16. data/app/mailers/effective/cpd_mailer.rb +37 -24
  17. data/app/models/concerns/effective_cpd_audit.rb +594 -0
  18. data/app/models/concerns/effective_cpd_audit_level.rb +111 -0
  19. data/app/models/concerns/effective_cpd_audit_review.rb +297 -0
  20. data/app/models/concerns/effective_cpd_user.rb +16 -11
  21. data/app/models/effective/cpd_audit.rb +1 -397
  22. data/app/models/effective/cpd_audit_level.rb +1 -83
  23. data/app/models/effective/cpd_audit_level_question.rb +1 -1
  24. data/app/models/effective/cpd_audit_level_section.rb +4 -3
  25. data/app/models/effective/cpd_audit_response.rb +4 -2
  26. data/app/models/effective/cpd_audit_review.rb +1 -224
  27. data/app/models/effective/cpd_audit_review_item.rb +1 -1
  28. data/app/views/admin/cpd_audit_levels/_form_content_audit.html.haml +1 -1
  29. data/app/views/admin/cpd_audit_levels/_form_content_audit_review.html.haml +1 -1
  30. data/app/views/admin/cpd_audit_levels/_form_cpd_audit_level.html.haml +7 -1
  31. data/app/views/admin/cpd_audit_levels/_form_cpd_audit_level_section.html.haml +2 -0
  32. data/app/views/admin/cpd_audit_reviews/_form.html.haml +4 -1
  33. data/app/views/admin/cpd_audits/_form.html.haml +1 -49
  34. data/app/views/admin/cpd_audits/{_form_determination.html.haml → _form_close.html.haml} +1 -1
  35. data/app/views/admin/cpd_audits/_form_complete.html.haml +17 -0
  36. data/app/views/admin/cpd_audits/_form_cpd_audit.html.haml +56 -0
  37. data/app/views/admin/cpd_audits/_form_deadlines.html.haml +8 -9
  38. data/app/views/admin/cpd_audits/_form_exemption.html.haml +1 -1
  39. data/app/views/admin/cpd_audits/_form_extension.html.haml +1 -1
  40. data/app/views/admin/cpd_audits/_form_files.html.haml +6 -0
  41. data/app/views/admin/cpd_audits/_form_missing_info.html.haml +11 -0
  42. data/app/views/admin/cpd_audits/_form_new.html.haml +3 -1
  43. data/app/views/admin/cpd_audits/_form_process.html.haml +18 -0
  44. data/app/views/admin/cpd_audits/_status.html.haml +48 -6
  45. data/app/views/effective/cpd/_dashboard.html.haml +15 -12
  46. data/app/views/effective/cpd_audit_level_questions/_cpd_audit_level_question.html.haml +2 -1
  47. data/app/views/effective/cpd_audit_reviews/_conflict.html.haml +1 -1
  48. data/app/views/effective/cpd_audit_reviews/_cpd_audit_level_section.html.haml +3 -3
  49. data/app/views/effective/cpd_audit_reviews/_cpd_audit_review.html.haml +3 -1
  50. data/app/views/effective/cpd_audit_reviews/_cpd_statement.html.haml +3 -3
  51. data/app/views/effective/cpd_audit_reviews/_feedback.html.haml +9 -0
  52. data/app/views/effective/cpd_audit_reviews/_files.html.haml +12 -0
  53. data/app/views/effective/cpd_audit_reviews/_layout.html.haml +3 -0
  54. data/app/views/effective/cpd_audit_reviews/_recommendation.html.haml +6 -2
  55. data/app/views/effective/cpd_audit_reviews/_summary.html.haml +41 -14
  56. data/app/views/effective/cpd_audit_reviews/conflict.html.haml +5 -1
  57. data/app/views/effective/cpd_audit_reviews/cpd_audit_level_section.html.haml +2 -2
  58. data/app/views/effective/cpd_audit_reviews/feedback.html.haml +16 -0
  59. data/app/views/effective/cpd_audit_reviews/files.html.haml +17 -0
  60. data/app/views/effective/cpd_audit_reviews/recommendation.html.haml +5 -3
  61. data/app/views/effective/cpd_audit_reviews/start.html.haml +10 -3
  62. data/app/views/effective/cpd_audit_reviews/statements.html.haml +4 -5
  63. data/app/views/effective/cpd_audit_reviews/submit.html.haml +1 -1
  64. data/app/views/effective/cpd_audit_reviews/submitted.html.haml +20 -0
  65. data/app/views/effective/cpd_audit_reviews/waiting.html.haml +1 -1
  66. data/app/views/effective/cpd_audits/_conflict.html.haml +1 -1
  67. data/app/views/effective/cpd_audits/_cpd.html.haml +1 -1
  68. data/app/views/effective/cpd_audits/_cpd_audit.html.haml +3 -1
  69. data/app/views/effective/cpd_audits/_cpd_audit_level_section.html.haml +1 -1
  70. data/app/views/effective/cpd_audits/_exemption.html.haml +1 -1
  71. data/app/views/effective/cpd_audits/_extension.html.haml +1 -1
  72. data/app/views/effective/cpd_audits/_files.html.haml +1 -1
  73. data/app/views/effective/cpd_audits/_missing_info.html.haml +19 -0
  74. data/app/views/effective/cpd_audits/_summary.html.haml +30 -12
  75. data/app/views/effective/cpd_audits/_waiting.html.haml +1 -1
  76. data/app/views/effective/cpd_audits/conflict.html.haml +5 -1
  77. data/app/views/effective/cpd_audits/cpd_audit_level_section.html.haml +1 -0
  78. data/app/views/effective/cpd_audits/start.html.haml +10 -4
  79. data/app/views/effective/cpd_audits/submit.html.haml +5 -2
  80. data/app/views/effective/cpd_audits/{complete.html.haml → submitted.haml} +13 -8
  81. data/app/views/effective/cpd_audits/waiting.html.haml +1 -1
  82. data/app/views/effective/cpd_mailer/cpd_audit_missing_info.liquid +15 -0
  83. data/app/views/effective/cpd_mailer/cpd_audit_review_ready.liquid +1 -1
  84. data/app/views/effective/cpd_mailer/cpd_audit_submitted.liquid +3 -1
  85. data/config/effective_cpd.rb +5 -2
  86. data/db/migrate/01_create_effective_cpd.rb.erb +36 -7
  87. data/lib/effective_cpd/engine.rb +4 -0
  88. data/lib/effective_cpd/version.rb +1 -1
  89. data/lib/effective_cpd.rb +13 -1
  90. metadata +19 -5
  91. data/app/views/effective/cpd_audit_reviews/complete.html.haml +0 -20
@@ -0,0 +1,594 @@
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
+
19
+ def admin_process_request_options
20
+ ['Granted', 'Denied']
21
+ end
22
+
23
+ def done_states
24
+ [:exemption_granted, :closed]
25
+ end
26
+
27
+ def waiting_on_admin_states
28
+ [:conflicted, :exemption_requested, :extension_requested, :submitted, :reviewed]
29
+ end
30
+
31
+ def waiting_on_reviewers_states
32
+ [:completed]
33
+ end
34
+
35
+ def waiting_on_auditee_states
36
+ [:opened, :started, :conflicted_resolved, :exemption_denied, :extension_granted, :extension_denied]
37
+ end
38
+
39
+ end
40
+
41
+ included do
42
+ acts_as_email_form
43
+ acts_as_tokened
44
+ acts_as_reportable if respond_to?(:acts_as_reportable)
45
+ log_changes(except: [:wizard_steps, :cpd_audit_reviews]) if respond_to?(:log_changes)
46
+
47
+ acts_as_statused(
48
+ :opened, # Just Opened
49
+ :started, # First screen clicked
50
+ :conflicted, # Auditee has declared a conflict of interest
51
+ :conflicted_resolved, # The conflict of interest has been resolved
52
+ :exemption_requested, # Auditee has requested an exemption
53
+ :exemption_granted, # Exemption granted -> Audit is cancelled. Exit state.
54
+ :exemption_denied, # Exemption denied
55
+ :extension_requested, # Audittee has requested an extension
56
+ :extension_granted, # Extension granted
57
+ :extension_denied, # Extension denied
58
+ :submitted, # Audittee has completed questionnaire submitted. Audittee is done.
59
+ :missing_info, # Admin has indicated information is missing. The audittee can edit applicant and add info
60
+ :completed, # Admin has received all deliverables. The audit is complete and ready for review.
61
+ :reviewed, # All audit reviews completed. Ready for a determination.
62
+ :closed # Determination made by admin and/or audit committee. Exit state. All done.
63
+ )
64
+
65
+ acts_as_wizard(
66
+ start: 'Start',
67
+ information: 'Information',
68
+ instructions: 'Instructions',
69
+
70
+ # These 4 steps are determined by audit_level settings
71
+ conflict: 'Conflict of Interest',
72
+ exemption: 'Request Exemption',
73
+ extension: 'Request Extension',
74
+ waiting: 'Waiting',
75
+ cpd: 'CPD',
76
+
77
+ questionnaire: 'Questionnaire',
78
+ # ... There will be one step per cpd_audit_level_sections here
79
+ files: 'Upload Resume',
80
+
81
+ submit: 'Submit',
82
+ submitted: 'Submitted'
83
+ )
84
+
85
+ attr_accessor :current_user
86
+ attr_accessor :current_step
87
+ attr_accessor :admin_process_request
88
+
89
+ # App scoped
90
+ belongs_to :cpd_audit_level, polymorphic: true
91
+ belongs_to :user, polymorphic: true # The user being audited
92
+
93
+ has_many :cpd_audit_reviews, -> { order(:id) }, inverse_of: :cpd_audit, dependent: :destroy
94
+ accepts_nested_attributes_for :cpd_audit_reviews, allow_destroy: true
95
+
96
+ # Effective Scoped
97
+ has_many :cpd_audit_responses, -> { Effective::CpdAuditResponse.sorted }, class_name: 'Effective::CpdAuditResponse', inverse_of: :cpd_audit, dependent: :destroy
98
+ accepts_nested_attributes_for :cpd_audit_responses
99
+
100
+ has_many_attached :files
101
+
102
+ effective_resource do
103
+ due_date :date, permitted: false # Computed due date based on notification and extension date
104
+
105
+ # Additional Info. Not really used.
106
+ notes :text, permitted: false
107
+
108
+ # Anonymous Name Mode
109
+ anonymous_name :string, permitted: false # Required when cpd_audit_level.anonymous?
110
+ anonymous_number :integer, permitted: false # A unique value
111
+
112
+ # Missing Info
113
+ missing_info_at :datetime
114
+ missing_info_reason :text
115
+
116
+ # Important dates
117
+ notification_date :date, permitted: false # Can be set on CpdAudits#new, but basically created_at
118
+ extension_date :date, permitted: false # set by admin if extension if granted
119
+
120
+ # Final determination
121
+ determination :string, permitted: false
122
+
123
+ # Override Deadlines
124
+ ignore_deadlines :boolean, permitted: false
125
+
126
+ # Auditee response
127
+ conflict_of_interest :boolean
128
+ conflict_of_interest_reason :text
129
+
130
+ exemption_request :boolean
131
+ exemption_request_reason :text
132
+
133
+ extension_request :boolean
134
+ extension_request_date :date
135
+ extension_request_reason :text
136
+
137
+ # acts_as_statused
138
+ status :string, permitted: false
139
+ status_steps :text
140
+
141
+ # Status dates
142
+ started_at :datetime, permitted: false
143
+ submitted_at :datetime, permitted: false
144
+ completed_at :datetime, permitted: false
145
+ reviewed_at :datetime, permitted: false
146
+ closed_at :datetime, permitted: false
147
+
148
+ # Acts as tokyyened
149
+ token :string, permitted: false
150
+
151
+ # Acts as Wizard
152
+ wizard_steps :text, permitted: false
153
+
154
+ timestamps
155
+ end
156
+
157
+ scope :deep, -> { includes(:cpd_audit_level, user: [:cpd_statements], cpd_audit_reviews: [:cpd_audit_level, :user, :cpd_audit_review_items]) }
158
+ scope :sorted, -> { order(:id) }
159
+
160
+ scope :draft, -> { where(submitted_at: nil) }
161
+
162
+ scope :available, -> { where.not(status: done_states) }
163
+ scope :in_progress, -> { where.not(status: done_states) }
164
+
165
+ scope :done, -> { where(status: done_states) }
166
+
167
+ scope :waiting_on_admin, -> { where(status: waiting_on_admin_states) }
168
+ scope :waiting_on_auditee, -> { where(status: waiting_on_auditee_states) }
169
+ scope :waiting_on_reviewers, -> { where(status: waiting_on_reviewers_states) }
170
+
171
+ # effective_reports
172
+ def reportable_scopes
173
+ { draft: nil, available: nil, done: nil, waiting_on_admin: nil, waiting_on_auditee: nil, waiting_on_reviewers: nil }
174
+ end
175
+
176
+ before_validation(if: -> { new_record? }) do
177
+ self.notification_date ||= Time.zone.now
178
+ self.due_date ||= deadline_to_submit()
179
+ end
180
+
181
+ with_options(if: -> { cpd_audit_level&.anonymous? }) do
182
+ before_validation { assign_anonymous_name_and_number }
183
+
184
+ validates :anonymous_name, presence: true
185
+ validates :anonymous_number, presence: true
186
+ end
187
+
188
+ validates :notification_date, presence: true
189
+ validates :determination, presence: true, if: -> { closed? }
190
+
191
+ validates :conflict_of_interest_reason, presence: true, if: -> { conflict_of_interest? }
192
+ validates :exemption_request_reason, presence: true, if: -> { exemption_request? }
193
+ validates :extension_request_date, presence: true, if: -> { extension_request? }
194
+ validates :extension_request_reason, presence: true, if: -> { extension_request? }
195
+
196
+ # Admin Missing Info
197
+ validates :missing_info_reason, presence: true, if: -> { missing_info? }
198
+
199
+ validate(if: -> { current_step == :conflict && conflict_of_interest? && !ignore_deadlines? }) do
200
+ deadline = deadline_to_conflict_of_interest()
201
+ self.errors.add(:base, 'deadline to declare conflict of interest has already passed') if deadline && deadline < Time.zone.now
202
+ end
203
+
204
+ validate(if: -> { current_step == :exemption && exemption_request? && !ignore_deadlines? }) do
205
+ deadline = deadline_to_exemption()
206
+ self.errors.add(:base, 'deadline to request exemption has already passed') if deadline && deadline < Time.zone.now
207
+ end
208
+
209
+ validate(if: -> { current_step == :extension && extension_request? && !ignore_deadlines? }) do
210
+ deadline = deadline_to_extension()
211
+ self.errors.add(:base, 'deadline to request extension has already passed') if deadline && deadline < Time.zone.now
212
+ end
213
+
214
+ validate(if: -> { determination.present? }) do
215
+ unless cpd_audit_level.determinations.include?(determination)
216
+ self.errors.add(:determination, 'must exist in this audit level')
217
+ end
218
+ end
219
+
220
+ # If we're submitted. Check if we can go into reviewed?
221
+ before_save(if: -> { submitted? }) { try_complete! }
222
+ before_save(if: -> { completed? }) { try_review! }
223
+
224
+ after_commit(on: :create) do
225
+ send_email(:cpd_audit_opened)
226
+ end
227
+
228
+ def dynamic_wizard_steps
229
+ cpd_audit_level.cpd_audit_level_sections.each_with_object({}) do |section, h|
230
+ h["section#{section.position+1}".to_sym] = section.title
231
+ end
232
+ end
233
+
234
+ def wizard_step_title(step)
235
+ self.class::WIZARD_STEPS[step] || dynamic_wizard_steps.fetch(step)
236
+ end
237
+
238
+ def required_steps
239
+ steps = [:start, :information, :instructions]
240
+
241
+ steps << :conflict if cpd_audit_level.conflict_of_interest?
242
+
243
+ steps << :exemption if cpd_audit_level.can_request_exemption?
244
+
245
+ unless exemption_requested?
246
+ steps << :extension if cpd_audit_level.can_request_extension?
247
+ end
248
+
249
+ if exemption_requested? || extension_requested?
250
+ steps += [:waiting]
251
+ end
252
+
253
+ if user_cpd_required?
254
+ steps += [:cpd]
255
+ end
256
+
257
+ steps += [:questionnaire] + dynamic_wizard_steps.keys + [:files, :submit, :submitted]
258
+
259
+ steps
260
+ end
261
+
262
+ def can_visit_step?(step)
263
+ return [:start].exclude?(step) if missing_info? # Can revisit whole audit if missing info
264
+ return (step == :submitted) if was_submitted? # Can only view complete step once submitted
265
+
266
+ can_revisit_completed_steps(step)
267
+ end
268
+ end
269
+
270
+ def to_s
271
+ persisted? ? "#{cpd_audit_level} Audit of #{name}" : 'audit'
272
+ end
273
+
274
+ def name
275
+ anonymous_name.presence || user.to_s
276
+ end
277
+
278
+ def anonymous?
279
+ cpd_audit_level&.anonymous?
280
+ end
281
+
282
+ def deadline_date
283
+ (extension_date || notification_date)
284
+ end
285
+
286
+ def draft?
287
+ !was_submitted?
288
+ end
289
+
290
+ def in_progress?
291
+ self.class.done_states.include?(status.to_sym) == false
292
+ end
293
+
294
+ def done?
295
+ self.class.done_states.include?(status.to_sym)
296
+ end
297
+
298
+ def ready_to_review?
299
+ was_completed?
300
+ end
301
+
302
+ def status_label
303
+ (status_was || status).to_s.gsub('_', ' ')
304
+ end
305
+
306
+ def summary
307
+ case status_was
308
+ when 'opened'
309
+ "The audit has been opened. The auditee and audit reviewers have been notified. Waiting for the auditee to submit their audit questionnaire."
310
+ when 'started'
311
+ "The auditee has begun their audit questionnaire. Waiting for the auditee to submit their audit questionnaire."
312
+ when 'conflicted'
313
+ "The auditee has declared a conflict of interest. A new reviewer will be assigned."
314
+ when 'conflicted_resolved'
315
+ "The auditee had declared a conflict of interest. This has been resolved. Waiting for the auditee to submit their audit questionnaire."
316
+ when 'exemption_requested'
317
+ "The auditee has requested an exemption. Waiting for request to be granted or denied."
318
+ when 'exemption_granted'
319
+ "The exemption request has been granted. This audit may now be closed."
320
+ when 'exemption_denied'
321
+ "The exemption request has been denied. The audit will continue. Waiting for the auditee to submit their audit questionnaire."
322
+ when 'extension_requested'
323
+ "The auditee has requested an extension. Waiting for request to be granted or denied."
324
+ when 'extension_granted'
325
+ "The extension request has been granted. There is a new deadline. Waiting for the auditee to submit their audit questionnaire."
326
+ when 'extension_denied'
327
+ "The extension request has been denied. The deadline remains. Waiting for the auditee to submit their audit questionnaire."
328
+ when 'submitted'
329
+ summary = "Auditee has submitted their audit wizard."
330
+ tasks = "The following tasks remain before it can be completed:"
331
+ approval = "Waiting on complete, review and determination."
332
+ items = completed_requirements.map { |item, done| "<li>#{item}: #{done ? 'Complete' : 'Incomplete'}</li>" }.join
333
+ completed_requirements.present? ? "<p>#{summary} #{tasks}</p><ul>#{items}</ul>" : "#{summary} #{approval}"
334
+ when 'completed'
335
+ "All required materials have been provided. This audit will transition to 'reviewed' after all reviewers have finished."
336
+ when 'missing_info'
337
+ "Missing the following information: <ul><li>#{missing_info_reason}</li></ul>"
338
+ when 'reviewed'
339
+ "The audit has been reviewed and is ready for the final determination to be made."
340
+ when 'closed'
341
+ "This audit has been closed with a final determination #{determination}. All done."
342
+ else
343
+ raise("unexpected status #{status}")
344
+ end.html_safe
345
+ end
346
+
347
+ def cpd_audit_level_section(wizard_step)
348
+ position = (wizard_step.to_s.split('section').last.to_i rescue false)
349
+ cpd_audit_level.cpd_audit_level_sections.find { |section| (section.position + 1) == position }
350
+ end
351
+
352
+ # Find or build
353
+ def cpd_audit_response(cpd_audit_level_question)
354
+ cpd_audit_response = cpd_audit_responses.find { |r| r.cpd_audit_level_question_id == cpd_audit_level_question.id }
355
+ cpd_audit_response ||= cpd_audit_responses.build(cpd_audit: self, cpd_audit_level_question: cpd_audit_level_question)
356
+ end
357
+
358
+ # Auditee wizard action
359
+ def start!
360
+ started!
361
+ end
362
+
363
+ # Admin action
364
+ def resolve_conflict!
365
+ wizard_steps[:conflict] = nil # Have them complete the conflict step again.
366
+
367
+ assign_attributes(conflict_of_interest: false, conflict_of_interest_reason: nil)
368
+ conflicted_resolved!
369
+ submitted!
370
+
371
+ send_email(:cpd_audit_conflict_resolved)
372
+ true
373
+ end
374
+
375
+ # Auditee wizard action
376
+ def exemption!
377
+ return started! unless exemption_request?
378
+
379
+ update!(status: :exemption_requested)
380
+ send_email(:cpd_audit_exemption_request)
381
+ end
382
+
383
+ # Admin action
384
+ def process_exemption!
385
+ case admin_process_request
386
+ when 'Granted' then grant_exemption!
387
+ when 'Denied' then deny_exemption!
388
+ else
389
+ self.errors.add(:admin_process_request, "can't be blank"); save!
390
+ end
391
+ end
392
+
393
+ def grant_exemption!
394
+ wizard_steps[:submit] ||= Time.zone.now
395
+ submitted! && exemption_granted!
396
+ send_email(:cpd_audit_exemption_granted)
397
+ end
398
+
399
+ def deny_exemption!
400
+ assign_attributes(exemption_request: false)
401
+ exemption_denied!
402
+ send_email(:cpd_audit_exemption_denied)
403
+ end
404
+
405
+ # Auditee wizard action
406
+ def extension!
407
+ return started! unless extension_request?
408
+
409
+ update!(status: :extension_requested)
410
+ send_email(:cpd_audit_extension_request)
411
+ end
412
+
413
+ # Admin action
414
+ def process_extension!
415
+ case admin_process_request
416
+ when 'Granted' then grant_extension!
417
+ when 'Denied' then deny_extension!
418
+ else
419
+ self.errors.add(:admin_process_request, "can't be blank"); save!
420
+ end
421
+ end
422
+
423
+ def grant_extension!
424
+ self.extension_date = extension_request_date
425
+ self.due_date = deadline_to_submit()
426
+
427
+ cpd_audit_reviews.each { |cpd_audit_review| cpd_audit_review.extension_granted! }
428
+ extension_granted!
429
+ send_email(:cpd_audit_extension_granted)
430
+ end
431
+
432
+ def deny_extension!
433
+ assign_attributes(extension_request: false)
434
+ extension_denied!
435
+ send_email(:cpd_audit_extension_denied)
436
+ end
437
+
438
+ # Require CPD step
439
+ def user_cpd_required?
440
+ return false unless user.cpd_audit_cpd_required?
441
+ required_cpd_cycle.present?
442
+ end
443
+
444
+ def user_cpd_completed?
445
+ return true if required_cpd_cycle.blank?
446
+ user.cpd_statements.any? { |s| s.completed? && s.cpd_cycle_id == required_cpd_cycle.id }
447
+ end
448
+
449
+ def required_cpd_cycle
450
+ @required_cpd_cycle ||= begin
451
+ last_year = ((notification_date || created_at || Time.zone.now) - 1.year).all_year
452
+ Effective::CpdCycle.available.where(start_at: last_year).first
453
+ end
454
+ end
455
+
456
+ # Auditee wizard action
457
+ def submit!
458
+ # Complete the very last step too
459
+ wizard_steps[:submitted] = Time.zone.now
460
+
461
+ if conflict_of_interest?
462
+ conflicted!
463
+ send_email(:cpd_audit_conflicted)
464
+ else
465
+ submitted!
466
+ send_email(:cpd_audit_submitted)
467
+ end
468
+
469
+ true
470
+ end
471
+
472
+ # When an audit is submitted, these must be done to go to completed.
473
+ # An Admin can override this and just set them to completed.
474
+ def completed_requirements
475
+ {}
476
+ end
477
+
478
+ # called by a before_save when submitted
479
+ def try_complete!
480
+ false # Nothing to do. Admin completes audits.
481
+ end
482
+
483
+ def complete!
484
+ raise('audit must have been submitted to complete!') unless was_submitted?
485
+
486
+ assign_attributes(missing_info_reason: nil)
487
+ completed!
488
+
489
+ # Each of these sends a cpd_audit_review_ready email
490
+ cpd_audit_reviews.each { |cpd_audit_review| cpd_audit_review.ready! }
491
+
492
+ true
493
+ end
494
+
495
+ def missing!
496
+ raise('audit must have been submitted to missing!') unless was_submitted?
497
+
498
+ missing_info!
499
+ send_email(:cpd_audit_missing_info)
500
+ end
501
+
502
+ def resubmit!
503
+ raise('audit must have been submitted and missing info to resubmit!') unless was_submitted? && was_missing_info?
504
+ raise('already submitted') if submitted?
505
+
506
+ assign_attributes(skip_to_step: :submitted, submitted_at: Time.zone.now)
507
+
508
+ submitted!
509
+ send_email(:cpd_audit_submitted)
510
+ end
511
+
512
+ def try_review!
513
+ return false unless was_submitted?
514
+ return false unless completed?
515
+ return false unless cpd_audit_reviews.present? && cpd_audit_reviews.all?(&:completed?)
516
+
517
+ review!
518
+ end
519
+
520
+ # Called in a before_save. Intended for applicant_review to call in its submit! method
521
+ def review!
522
+ raise('already reviewed') if was_reviewed?
523
+ raise('audit must have been submitted to review!') unless was_submitted?
524
+ raise('audit must have been completed to review!') unless was_completed?
525
+
526
+ reviewed!
527
+ send_email(:cpd_audit_reviewed)
528
+ end
529
+
530
+ # Admin action
531
+ def close!
532
+ closed!
533
+ send_email(:cpd_audit_closed)
534
+ end
535
+
536
+ def email_form_defaults(action)
537
+ { from: EffectiveCpd.mailer_sender }
538
+ end
539
+
540
+ def send_email(email)
541
+ EffectiveCpd.send_email(email, self, email_form_params) unless email_form_skip?
542
+ true
543
+ end
544
+
545
+ def deadline_to_conflict_of_interest
546
+ return nil unless cpd_audit_level&.conflict_of_interest?
547
+ return nil unless cpd_audit_level.days_to_declare_conflict.present?
548
+
549
+ date = (notification_date || created_at || Time.zone.now)
550
+ EffectiveResources.advance_date(date, business_days: cpd_audit_level.days_to_declare_conflict)
551
+ end
552
+
553
+ def deadline_to_exemption
554
+ return nil unless cpd_audit_level&.can_request_exemption?
555
+ return nil unless cpd_audit_level.days_to_request_exemption.present?
556
+
557
+ date = (notification_date || created_at || Time.zone.now)
558
+ EffectiveResources.advance_date(date, business_days: cpd_audit_level.days_to_request_exemption)
559
+ end
560
+
561
+ def deadline_to_extension
562
+ return nil unless cpd_audit_level&.can_request_extension?
563
+ return nil unless cpd_audit_level.days_to_request_extension.present?
564
+
565
+ date = (notification_date || created_at || Time.zone.now)
566
+ EffectiveResources.advance_date(date, business_days: cpd_audit_level.days_to_request_extension)
567
+ end
568
+
569
+ def deadline_to_submit
570
+ return nil unless cpd_audit_level&.days_to_submit.present?
571
+
572
+ date = (extension_date || notification_date || created_at || Time.zone.now)
573
+ EffectiveResources.advance_date(date, business_days: cpd_audit_level.days_to_submit)
574
+ end
575
+
576
+ # The name pattern is A23XXX where XXX is an autoincrement
577
+ # The name pattern is A23XXX where XXX is an autoincrement
578
+ def assign_anonymous_name_and_number
579
+ return if anonymous_name.present? || anonymous_number.present?
580
+ return if cpd_audit_level.blank?
581
+
582
+ prefix = cpd_audit_level.anonymous_audits_prefix
583
+ raise('expected cpd audit level to have an anonymous prefix') unless prefix.present?
584
+
585
+ # Where starts with prefix
586
+ number = (self.class.all.where('anonymous_name LIKE ?', "#{prefix}%").maximum('anonymous_number') || 0) + 1 # The next number
587
+
588
+ # Apply prefix and pad number to 3 digits
589
+ name = prefix + number.to_s.rjust(3, '0')
590
+
591
+ assign_attributes(anonymous_number: number, anonymous_name: name)
592
+ end
593
+
594
+ end
@@ -0,0 +1,111 @@
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, -> { order(:id) }, 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
+ anonymous_audits_prefix :string
50
+ anonymous_audit_reviews_prefix :string
51
+
52
+ determinations :text # Final determination by auditor
53
+ recommendations :text # Recommendations by audit reviewers
54
+
55
+ days_to_submit :integer # For auditee to submit statement
56
+ days_to_review :integer # For auditor/audit_review to be completed
57
+
58
+ conflict_of_interest :boolean # Feature flags
59
+ can_request_exemption :boolean
60
+ can_request_extension :boolean
61
+
62
+ days_to_declare_conflict :integer
63
+ days_to_request_exemption :integer
64
+ days_to_request_extension :integer
65
+
66
+ timestamps
67
+ end
68
+
69
+ serialize :determinations, Array
70
+ serialize :recommendations, Array
71
+
72
+ scope :deep, -> { all }
73
+ scope :sorted, -> { order(:title) }
74
+
75
+ validates :title, presence: true
76
+ validates :determinations, presence: true
77
+ validates :recommendations, presence: true
78
+
79
+ validates :days_to_submit, numericality: { greater_than: 0, allow_nil: true }
80
+ validates :days_to_review, numericality: { greater_than: 0, allow_nil: true }
81
+
82
+ validates :days_to_declare_conflict, presence: true, if: -> { conflict_of_interest? }
83
+ validates :days_to_declare_conflict, numericality: { greater_than: 0, allow_nil: true }
84
+
85
+ validates :days_to_request_exemption, presence: true, if: -> { can_request_exemption? }
86
+ validates :days_to_request_exemption, numericality: { greater_than: 0, allow_nil: true }
87
+
88
+ validates :days_to_request_extension, presence: true, if: -> { can_request_extension? }
89
+ validates :days_to_request_extension, numericality: { greater_than: 0, allow_nil: true }
90
+
91
+ validates :anonymous_audits_prefix, presence: true, if: -> { anonymous? }
92
+ validates :anonymous_audit_reviews_prefix, presence: true, if: -> { anonymous? }
93
+
94
+ before_destroy do
95
+ raise("can't destroy audit level with existing audits") if cpd_audits.length > 0
96
+ end
97
+ end
98
+
99
+ def to_s
100
+ title.presence || 'audit level'
101
+ end
102
+
103
+ def determinations
104
+ Array(self[:determinations]) - [nil, '']
105
+ end
106
+
107
+ def recommendations
108
+ Array(self[:recommendations]) - [nil, '']
109
+ end
110
+
111
+ end