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,297 @@
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 EffectiveCpdAuditReview
7
+ extend ActiveSupport::Concern
8
+
9
+ module Base
10
+ def effective_cpd_audit_review
11
+ include ::EffectiveCpdAuditReview
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ def effective_cpd_audit_review?; true; end
17
+ end
18
+
19
+ included do
20
+ acts_as_email_form
21
+ acts_as_tokened
22
+ acts_as_reportable if respond_to?(:acts_as_reportable)
23
+ log_changes(to: :cpd_audit, except: :wizard_steps) if respond_to?(:log_changes)
24
+
25
+ acts_as_wizard(
26
+ start: 'Start',
27
+ information: 'Information',
28
+ instructions: 'Instructions',
29
+
30
+ # Optional based on cpd_audit_level options
31
+ conflict: 'Conflict of Interest',
32
+
33
+ waiting: 'Waiting on Auditee Submission',
34
+
35
+ files: 'Review Files',
36
+
37
+ statements: 'Review CPD Statements',
38
+ # ... There will be one step per cpd_statement here. "statement1"
39
+
40
+ questionnaire: 'Review Questionnaire Responses',
41
+ # ... There will be one step per cpd_audit_level_sections here
42
+
43
+ recommendation: 'Recommendation',
44
+ submit: 'Submit',
45
+ submitted: 'Submitted'
46
+ )
47
+
48
+ attr_accessor :current_user
49
+ attr_accessor :current_step
50
+
51
+ # App scoped
52
+ belongs_to :cpd_audit, polymorphic: true
53
+ belongs_to :cpd_audit_level, polymorphic: true
54
+
55
+ belongs_to :user, polymorphic: true # Auditor
56
+
57
+ # Effective scoped
58
+ has_many :cpd_audit_review_items, -> { Effective::CpdAuditReviewItem.sorted }, class_name: 'Effective::CpdAuditReviewItem', inverse_of: :cpd_audit_review, dependent: :destroy
59
+ accepts_nested_attributes_for :cpd_audit_review_items, reject_if: :all_blank, allow_destroy: true
60
+
61
+ effective_resource do
62
+ due_date :date, permitted: false
63
+
64
+ # Anonymous Name Mode
65
+ anonymous_name :string, permitted: false # Required when cpd_audit_level.anonymous?
66
+ anonymous_number :integer, permitted: false # A unique value
67
+
68
+ # Auditor response
69
+ conflict_of_interest :boolean
70
+ conflict_of_interest_reason :text
71
+
72
+ # Rolling comments - internal, reviewer/admin only
73
+ comments :text
74
+
75
+ # Comments for the auditee to see
76
+ feedback :text
77
+
78
+ # Recommendation Step
79
+ recommendation :string
80
+
81
+ # Status Dates
82
+ submitted_at :datetime, permitted: false
83
+
84
+ # acts_as_statused
85
+ # I'm not using acts_as_statused yet, but I probably will later
86
+ status :string, permitted: false
87
+ status_steps :text, permitted: false
88
+
89
+ # Acts as tokyyened
90
+ token :string, permitted: false
91
+
92
+ # Wizard Progress
93
+ wizard_steps :text, permitted: false
94
+
95
+ timestamps
96
+ end
97
+
98
+ scope :deep, -> { includes(:cpd_audit, :cpd_audit_level, :user) }
99
+ scope :sorted, -> { order(:id) }
100
+
101
+ scope :available, -> { where(submitted_at: nil) }
102
+ scope :completed, -> { where.not(submitted_at: nil) }
103
+ scope :done, -> { where.not(submitted_at: nil) }
104
+
105
+ # effective_reports
106
+ def reportable_scopes
107
+ { available: nil, done: nil }
108
+ end
109
+
110
+ before_validation(if: -> { new_record? }) do
111
+ self.cpd_audit_level ||= cpd_audit&.cpd_audit_level
112
+ self.due_date ||= deadline_to_review()
113
+ end
114
+
115
+ validate(if: -> { recommendation.present? }) do
116
+ unless cpd_audit_level.recommendations.include?(recommendation)
117
+ self.errors.add(:recommendation, 'must exist in this audit level')
118
+ end
119
+ end
120
+
121
+ with_options(if: -> { cpd_audit_level&.anonymous? }) do
122
+ before_validation { assign_anonymous_name_and_number }
123
+
124
+ validates :anonymous_name, presence: true
125
+ validates :anonymous_number, presence: true
126
+ end
127
+
128
+ after_commit(on: :create) { send_email(:cpd_audit_review_opened) }
129
+ after_commit(on: :destroy) { cpd_audit.try_review! }
130
+
131
+ def dynamic_wizard_statement_steps
132
+ @statement_steps ||= auditee_cpd_statements.each_with_object({}) do |cpd_statement, h|
133
+ h["statement#{cpd_statement.cpd_cycle_id}".to_sym] = "#{cpd_statement.cpd_cycle.to_s} Activities"
134
+ end
135
+ end
136
+
137
+ def dynamic_wizard_questionnaire_steps
138
+ @questionnaire_steps ||= cpd_audit_level.cpd_audit_level_sections.reject(&:skip_review?).each_with_object({}) do |section, h|
139
+ h["section#{section.position+1}".to_sym] = section.title
140
+ end
141
+ end
142
+
143
+ def dynamic_wizard_steps
144
+ dynamic_wizard_statement_steps.merge(dynamic_wizard_questionnaire_steps)
145
+ end
146
+
147
+ def wizard_step_title(step)
148
+ self.class::WIZARD_STEPS[step] || dynamic_wizard_steps.fetch(step)
149
+ end
150
+
151
+ def required_steps
152
+ steps = [:start, :information, :instructions]
153
+
154
+ steps << :conflict if cpd_audit_level.conflict_of_interest?
155
+
156
+ if conflict_of_interest?
157
+ return steps + [:submit, :submitted]
158
+ end
159
+
160
+ steps += [:waiting] unless ready?
161
+
162
+ steps += [:files]
163
+ steps += [:statements] + dynamic_wizard_statement_steps.keys
164
+ steps += [:questionnaire] + dynamic_wizard_questionnaire_steps.keys
165
+ steps += [:recommendation, :submit, :submitted]
166
+
167
+ steps
168
+ end
169
+
170
+ def can_visit_step?(step)
171
+ return (step == :submitted) if completed? # Can only view complete step once submitted
172
+ can_revisit_completed_steps(step)
173
+ end
174
+
175
+ end
176
+
177
+ def to_s
178
+ persisted? ? "#{cpd_audit_level} Audit Review by #{name}" : 'audit review'
179
+ end
180
+
181
+ def name
182
+ anonymous_name.presence || user.to_s
183
+ end
184
+
185
+ def anonymous?
186
+ cpd_audit_level&.anonymous?
187
+ end
188
+
189
+ # The dynamic CPD Audit Level Sections steps
190
+ def cpd_audit_level_section(wizard_step)
191
+ position = (wizard_step.to_s.split('section').last.to_i rescue false)
192
+ cpd_audit_level.cpd_audit_level_sections.find { |section| (section.position + 1) == position }
193
+ end
194
+
195
+ # Find or build
196
+ def cpd_audit_review_item(item)
197
+ unless item.kind_of?(Effective::CpdAuditResponse) || item.kind_of?(Effective::CpdStatementActivity)
198
+ raise("expected a cpd_audit_response or cpd_statement_activity")
199
+ end
200
+
201
+ cpd_audit_review_item = cpd_audit_review_items.find { |cari| cari.item == item }
202
+ cpd_audit_review_item ||= cpd_audit_review_items.build(item: item)
203
+ end
204
+
205
+ # The dynamic CPD Statement review steps
206
+ def auditee_cpd_statements
207
+ cpd_audit.user.cpd_statements.select do |cpd_statement|
208
+ cpd_statement.completed? && (submitted_at.blank? || cpd_statement.submitted_at < submitted_at)
209
+ end
210
+ end
211
+
212
+ def cpd_statement(wizard_step)
213
+ cpd_cycle_id = (wizard_step.to_s.split('statement').last.to_i rescue false)
214
+ auditee_cpd_statements.find { |cpd_statement| cpd_statement.cpd_cycle_id == cpd_cycle_id }
215
+ end
216
+
217
+ # Called by CpdAudit.extension_granted
218
+ def extension_granted!
219
+ self.due_date = deadline_to_review()
220
+ end
221
+
222
+ # Called by CpdAudit.submit!
223
+ def ready!
224
+ send_email(:cpd_audit_review_ready)
225
+ end
226
+
227
+ # Called by review wizard submit step
228
+ def submit!
229
+ update!(submitted_at: Time.zone.now)
230
+ cpd_audit.try_review! # audit might go from completed->reviewed
231
+
232
+ send_email(:cpd_audit_review_submitted)
233
+ end
234
+
235
+ # When ready, the applicant review wizard hides the waiting step
236
+ def ready?
237
+ cpd_audit&.ready_to_review?
238
+ end
239
+
240
+ def draft?
241
+ submitted_at.blank?
242
+ end
243
+
244
+ def in_progress?
245
+ submitted_at.blank?
246
+ end
247
+
248
+ def completed?
249
+ submitted_at.present?
250
+ end
251
+
252
+ def done?
253
+ submitted_at.present?
254
+ end
255
+
256
+ def email_form_defaults(action)
257
+ { from: EffectiveCpd.mailer_sender }
258
+ end
259
+
260
+ def send_email(email)
261
+ EffectiveCpd.send_email(email, self, email_form_params) unless email_form_skip?
262
+ true
263
+ end
264
+
265
+ def deadline_to_conflict_of_interest
266
+ cpd_audit&.deadline_to_conflict_of_interest
267
+ end
268
+
269
+ def deadline_to_review
270
+ return nil unless cpd_audit_level&.days_to_review.present?
271
+
272
+ date = cpd_audit&.deadline_to_submit
273
+ return nil unless date.present?
274
+
275
+ EffectiveResources.advance_date(date, business_days: cpd_audit_level.days_to_review)
276
+ end
277
+
278
+ # The name pattern is A23XXX where XXX is an autoincrement
279
+ def assign_anonymous_name_and_number
280
+ return if anonymous_name.present? || anonymous_number.present?
281
+ return if cpd_audit_level.blank?
282
+
283
+ prefix = cpd_audit_level.anonymous_audit_reviews_prefix
284
+ raise('expected cpd audit level to have an anonymous audit review prefix') unless prefix.present?
285
+
286
+ # Where starts with prefix
287
+ number = cpd_audit.cpd_audit_reviews.map { |ar| ar.anonymous_number }.compact.max if cpd_audit.new_record?
288
+ number ||= (self.class.all.where('anonymous_name LIKE ?', "#{prefix}%").maximum('anonymous_number') || 0)
289
+ number = number + 1 # The next number
290
+
291
+ # Apply prefix and pad number to 3 digits
292
+ name = prefix + number.to_s.rjust(3, '0')
293
+
294
+ assign_attributes(anonymous_number: number, anonymous_name: name)
295
+ end
296
+
297
+ end
@@ -12,15 +12,20 @@ module EffectiveCpdUser
12
12
  end
13
13
  end
14
14
 
15
+ module ClassMethods
16
+ def effective_cpd_user?; true; end
17
+ end
18
+
15
19
  included do
16
20
  # App Scoped
17
- has_many :cpd_statements, -> { order(:cpd_cycle_id) }, inverse_of: :user
21
+ has_many :cpd_statements, -> { order(:cpd_cycle_id) }, inverse_of: :user, dependent: :destroy
22
+ accepts_nested_attributes_for :cpd_statements, allow_destroy: true
18
23
 
19
- # Effective
20
- has_many :cpd_audits, -> { Effective::CpdAudit.sorted }, inverse_of: :user, class_name: 'Effective::CpdAudit'
21
- has_many :cpd_audit_reviews, -> { Effective::CpdAuditReview.sorted }, inverse_of: :user, class_name: 'Effective::CpdAuditReview'
24
+ has_many :cpd_audits, -> { order(:id) }, inverse_of: :user
25
+ has_many :cpd_audit_reviews, -> { order(:id) }, inverse_of: :user
22
26
 
23
- accepts_nested_attributes_for :cpd_statements
27
+ scope :cpd_audit_auditees, -> { without_role(:cpd_audit_reviewer) }
28
+ scope :cpd_audit_reviewers, -> { with_role(:cpd_audit_reviewer) }
24
29
  end
25
30
 
26
31
  # This one will actually be enforced or displayed first
@@ -38,19 +43,19 @@ module EffectiveCpdUser
38
43
  true
39
44
  end
40
45
 
46
+ def cpd_audit_reviewer?
47
+ roles.include?(:cpd_audit_reviewer)
48
+ end
49
+
41
50
  def cpd_statement(cpd_cycle:)
42
- raise('expected an Effective::CpdCycle') unless cpd_cycle.class.respond_to?(:effective_cpd_cpd_cycle?)
51
+ raise('expected an CpdCycle') unless cpd_cycle.class.respond_to?(:effective_cpd_cycle?)
43
52
  cpd_statements.find { |cpd_statement| cpd_statement.cpd_cycle_id == cpd_cycle.id }
44
53
  end
45
54
 
46
55
  # Find or build
47
56
  def build_cpd_statement(cpd_cycle:)
48
- raise('expected an Effective::CpdCycle') unless cpd_cycle.class.respond_to?(:effective_cpd_cpd_cycle?)
57
+ raise('expected an CpdCycle') unless cpd_cycle.class.respond_to?(:effective_cpd_cycle?)
49
58
  cpd_statement(cpd_cycle: cpd_cycle) || cpd_statements.build(cpd_cycle: cpd_cycle)
50
59
  end
51
60
 
52
- module ClassMethods
53
- def effective_cpd_user?; true; end
54
- end
55
-
56
61
  end