effective_cpd 0.0.1 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (182) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +122 -10
  3. data/app/assets/javascripts/effective_cpd/activities.js +6 -0
  4. data/app/controllers/admin/cpd_audit_level_questions_controller.rb +13 -0
  5. data/app/controllers/admin/cpd_audit_levels_controller.rb +13 -0
  6. data/app/controllers/admin/cpd_audit_reviews_controller.rb +13 -0
  7. data/app/controllers/admin/cpd_audits_controller.rb +41 -0
  8. data/app/controllers/admin/cpd_special_rules_controller.rb +13 -0
  9. data/app/controllers/effective/cpd_audit_reviews_controller.rb +60 -0
  10. data/app/controllers/effective/cpd_audits_controller.rb +55 -0
  11. data/app/controllers/effective/cpd_cycles_controller.rb +1 -0
  12. data/app/controllers/effective/cpd_statements_controller.rb +1 -3
  13. data/app/datatables/admin/effective_cpd_audit_level_questions_datatable.rb +30 -0
  14. data/app/datatables/admin/effective_cpd_audit_levels_datatable.rb +34 -0
  15. data/app/datatables/admin/effective_cpd_audit_reviews_datatable.rb +29 -0
  16. data/app/datatables/admin/effective_cpd_audits_datatable.rb +67 -0
  17. data/app/datatables/admin/effective_cpd_special_rules_datatable.rb +23 -0
  18. data/app/datatables/admin/effective_cpd_statements_datatable.rb +3 -3
  19. data/app/datatables/effective_cpd_available_audit_reviews_datatable.rb +37 -0
  20. data/app/datatables/effective_cpd_available_audits_datatable.rb +30 -0
  21. data/app/datatables/{effective_cpd_datatable.rb → effective_cpd_available_cycles_datatable.rb} +3 -4
  22. data/app/datatables/effective_cpd_completed_audit_reviews_datatable.rb +32 -0
  23. data/app/datatables/effective_cpd_completed_audits_datatable.rb +24 -0
  24. data/app/datatables/effective_cpd_completed_statements_datatable.rb +28 -0
  25. data/app/helpers/effective_cpd_audits_helper.rb +48 -0
  26. data/app/helpers/effective_cpd_helper.rb +10 -0
  27. data/app/mailers/effective/cpd_mailer.rb +155 -3
  28. data/app/models/concerns/effective_cpd_user.rb +29 -0
  29. data/app/models/effective/cpd_activity.rb +16 -3
  30. data/app/models/effective/cpd_audit.rb +380 -0
  31. data/app/models/effective/cpd_audit_level.rb +87 -0
  32. data/app/models/effective/cpd_audit_level_question.rb +91 -0
  33. data/app/models/effective/cpd_audit_level_question_option.rb +34 -0
  34. data/app/models/effective/cpd_audit_level_section.rb +50 -0
  35. data/app/models/effective/cpd_audit_response.rb +86 -0
  36. data/app/models/effective/cpd_audit_response_option.rb +6 -0
  37. data/app/models/effective/cpd_audit_review.rb +222 -0
  38. data/app/models/effective/cpd_audit_review_item.rb +34 -0
  39. data/app/models/effective/cpd_category.rb +12 -3
  40. data/app/models/effective/cpd_cycle.rb +18 -2
  41. data/app/models/effective/cpd_rule.rb +23 -6
  42. data/app/models/effective/cpd_scorer.rb +23 -1
  43. data/app/models/effective/cpd_special_rule.rb +54 -0
  44. data/app/models/effective/cpd_special_rule_mate.rb +6 -0
  45. data/app/models/effective/cpd_statement.rb +25 -11
  46. data/app/views/admin/cpd_activities/_form.html.haml +1 -1
  47. data/app/views/admin/cpd_audit_level_questions/_form.html.haml +100 -0
  48. data/app/views/admin/cpd_audit_levels/_form.html.haml +24 -0
  49. data/app/views/admin/cpd_audit_levels/_form_content_audit.html.haml +15 -0
  50. data/app/views/admin/cpd_audit_levels/_form_content_audit_review.html.haml +15 -0
  51. data/app/views/admin/cpd_audit_levels/_form_cpd_audit_level.html.haml +52 -0
  52. data/app/views/admin/cpd_audit_levels/_form_cpd_audit_level_section.html.haml +10 -0
  53. data/app/views/admin/cpd_audit_reviews/_cpd_audit_review.html.haml +1 -0
  54. data/app/views/admin/cpd_audit_reviews/_form.html.haml +13 -0
  55. data/app/views/admin/cpd_audits/_audit_reviewer_fields.html.haml +2 -0
  56. data/app/views/admin/cpd_audits/_auditee_fields.html.haml +2 -0
  57. data/app/views/admin/cpd_audits/_form.html.haml +46 -0
  58. data/app/views/admin/cpd_audits/_form_conflict.html.haml +24 -0
  59. data/app/views/admin/cpd_audits/_form_determination.html.haml +10 -0
  60. data/app/views/admin/cpd_audits/_form_exemption.html.haml +24 -0
  61. data/app/views/admin/cpd_audits/_form_extension.html.haml +24 -0
  62. data/app/views/admin/cpd_audits/_form_new.html.haml +28 -0
  63. data/app/views/admin/cpd_audits/_status.html.haml +121 -0
  64. data/app/views/admin/cpd_categories/_form.html.haml +1 -1
  65. data/app/views/admin/cpd_cycles/_form.html.haml +6 -3
  66. data/app/views/admin/cpd_cycles/_form_content.html.haml +8 -0
  67. data/app/views/admin/cpd_cycles/_form_cpd_cycle.html.haml +3 -0
  68. data/app/views/admin/cpd_cycles/_form_cpd_rules.html.haml +12 -7
  69. data/app/views/admin/cpd_cycles/_form_cpd_special_rules.html.haml +4 -0
  70. data/app/views/admin/cpd_special_rules/_form.html.haml +18 -0
  71. data/app/views/admin/cpd_statements/_cpd_statement.html.haml +7 -0
  72. data/app/views/effective/cpd_audit_level_questions/_cpd_audit_level_question.html.haml +5 -0
  73. data/app/views/effective/cpd_audit_responses/_cpd_audit_response.html.haml +4 -0
  74. data/app/views/effective/cpd_audit_responses/_fields.html.haml +13 -0
  75. data/app/views/effective/cpd_audit_responses/fields/_choose_one.html.haml +1 -0
  76. data/app/views/effective/cpd_audit_responses/fields/_date.html.haml +2 -0
  77. data/app/views/effective/cpd_audit_responses/fields/_email.html.haml +2 -0
  78. data/app/views/effective/cpd_audit_responses/fields/_long_answer.html.haml +1 -0
  79. data/app/views/effective/cpd_audit_responses/fields/_number.html.haml +2 -0
  80. data/app/views/effective/cpd_audit_responses/fields/_select_all_that_apply.html.haml +1 -0
  81. data/app/views/effective/cpd_audit_responses/fields/_select_up_to_1.html.haml +1 -0
  82. data/app/views/effective/cpd_audit_responses/fields/_select_up_to_2.html.haml +1 -0
  83. data/app/views/effective/cpd_audit_responses/fields/_select_up_to_3.html.haml +1 -0
  84. data/app/views/effective/cpd_audit_responses/fields/_select_up_to_4.html.haml +1 -0
  85. data/app/views/effective/cpd_audit_responses/fields/_select_up_to_5.html.haml +1 -0
  86. data/app/views/effective/cpd_audit_responses/fields/_short_answer.html.haml +1 -0
  87. data/app/views/effective/cpd_audit_responses/fields/_upload_file.html.haml +1 -0
  88. data/app/views/effective/cpd_audit_responses/responses/_choose_one.html.haml +1 -0
  89. data/app/views/effective/cpd_audit_responses/responses/_date.html.haml +1 -0
  90. data/app/views/effective/cpd_audit_responses/responses/_email.html.haml +1 -0
  91. data/app/views/effective/cpd_audit_responses/responses/_long_answer.html.haml +1 -0
  92. data/app/views/effective/cpd_audit_responses/responses/_number.html.haml +1 -0
  93. data/app/views/effective/cpd_audit_responses/responses/_select_all_that_apply.html.haml +5 -0
  94. data/app/views/effective/cpd_audit_responses/responses/_select_up_to_1.html.haml +1 -0
  95. data/app/views/effective/cpd_audit_responses/responses/_select_up_to_2.html.haml +5 -0
  96. data/app/views/effective/cpd_audit_responses/responses/_select_up_to_3.html.haml +5 -0
  97. data/app/views/effective/cpd_audit_responses/responses/_select_up_to_4.html.haml +5 -0
  98. data/app/views/effective/cpd_audit_responses/responses/_select_up_to_5.html.haml +5 -0
  99. data/app/views/effective/cpd_audit_responses/responses/_short_answer.html.haml +1 -0
  100. data/app/views/effective/cpd_audit_responses/responses/_upload_file.html.haml +4 -0
  101. data/app/views/effective/cpd_audit_review_items/_cpd_audit_review_item.html.haml +6 -0
  102. data/app/views/effective/cpd_audit_review_items/_fields.html.haml +11 -0
  103. data/app/views/effective/cpd_audit_reviews/_conflict.html.haml +15 -0
  104. data/app/views/effective/cpd_audit_reviews/_cpd_audit_level_section.html.haml +23 -0
  105. data/app/views/effective/cpd_audit_reviews/_cpd_audit_review.html.haml +17 -0
  106. data/app/views/effective/cpd_audit_reviews/_cpd_statement.html.haml +15 -0
  107. data/app/views/effective/cpd_audit_reviews/_layout.html.haml +24 -0
  108. data/app/views/effective/cpd_audit_reviews/_recommendation.html.haml +9 -0
  109. data/app/views/effective/cpd_audit_reviews/_summary.html.haml +47 -0
  110. data/app/views/effective/cpd_audit_reviews/complete.html.haml +20 -0
  111. data/app/views/effective/cpd_audit_reviews/conflict.html.haml +21 -0
  112. data/app/views/effective/cpd_audit_reviews/cpd_audit_level_section.html.haml +43 -0
  113. data/app/views/effective/cpd_audit_reviews/cpd_statement.html.haml +35 -0
  114. data/app/views/effective/cpd_audit_reviews/information.html.haml +5 -0
  115. data/app/views/effective/cpd_audit_reviews/instructions.html.haml +10 -0
  116. data/app/views/effective/cpd_audit_reviews/questionnaire.html.haml +8 -0
  117. data/app/views/effective/cpd_audit_reviews/recommendation.html.haml +21 -0
  118. data/app/views/effective/cpd_audit_reviews/start.html.haml +10 -0
  119. data/app/views/effective/cpd_audit_reviews/statements.html.haml +29 -0
  120. data/app/views/effective/cpd_audit_reviews/submit.html.haml +14 -0
  121. data/app/views/effective/cpd_audit_reviews/waiting.html.haml +9 -0
  122. data/app/views/effective/cpd_audits/_conflict.html.haml +15 -0
  123. data/app/views/effective/cpd_audits/_cpd_audit.html.haml +12 -0
  124. data/app/views/effective/cpd_audits/_cpd_audit_level_section.html.haml +14 -0
  125. data/app/views/effective/cpd_audits/_exemption.html.haml +26 -0
  126. data/app/views/effective/cpd_audits/_extension.html.haml +30 -0
  127. data/app/views/effective/cpd_audits/_files.html.haml +12 -0
  128. data/app/views/effective/cpd_audits/_layout.html.haml +33 -0
  129. data/app/views/effective/cpd_audits/_summary.html.haml +54 -0
  130. data/app/views/effective/cpd_audits/_waiting.html.haml +6 -0
  131. data/app/views/effective/cpd_audits/complete.html.haml +24 -0
  132. data/app/views/effective/cpd_audits/conflict.html.haml +22 -0
  133. data/app/views/effective/cpd_audits/cpd_audit_level_section.html.haml +20 -0
  134. data/app/views/effective/cpd_audits/exemption.html.haml +19 -0
  135. data/app/views/effective/cpd_audits/extension.html.haml +21 -0
  136. data/app/views/effective/cpd_audits/files.html.haml +7 -0
  137. data/app/views/effective/cpd_audits/information.html.haml +5 -0
  138. data/app/views/effective/cpd_audits/instructions.html.haml +5 -0
  139. data/app/views/effective/cpd_audits/questionnaire.html.haml +5 -0
  140. data/app/views/effective/cpd_audits/start.html.haml +11 -0
  141. data/app/views/effective/cpd_audits/submit.html.haml +15 -0
  142. data/app/views/effective/cpd_audits/waiting.html.haml +24 -0
  143. data/app/views/effective/cpd_mailer/README.md +1 -0
  144. data/app/views/effective/cpd_mailer/cpd_audit_closed.liquid +15 -0
  145. data/app/views/effective/cpd_mailer/cpd_audit_conflict_resolved.liquid +15 -0
  146. data/app/views/effective/cpd_mailer/cpd_audit_conflicted.liquid +13 -0
  147. data/app/views/effective/cpd_mailer/cpd_audit_exemption_denied.liquid +13 -0
  148. data/app/views/effective/cpd_mailer/cpd_audit_exemption_granted.liquid +13 -0
  149. data/app/views/effective/cpd_mailer/cpd_audit_exemption_request.liquid +13 -0
  150. data/app/views/effective/cpd_mailer/cpd_audit_extension_denied.liquid +13 -0
  151. data/app/views/effective/cpd_mailer/cpd_audit_extension_granted.liquid +13 -0
  152. data/app/views/effective/cpd_mailer/cpd_audit_extension_request.liquid +13 -0
  153. data/app/views/effective/cpd_mailer/cpd_audit_opened.liquid +13 -0
  154. data/app/views/effective/cpd_mailer/cpd_audit_review_opened.liquid +15 -0
  155. data/app/views/effective/cpd_mailer/cpd_audit_review_ready.liquid +13 -0
  156. data/app/views/effective/cpd_mailer/cpd_audit_review_submitted.liquid +11 -0
  157. data/app/views/effective/cpd_mailer/cpd_audit_reviewed.liquid +13 -0
  158. data/app/views/effective/cpd_mailer/cpd_audit_submitted.liquid +13 -0
  159. data/app/views/effective/cpd_statement_activities/_cpd_statement_activity.html.haml +65 -0
  160. data/app/views/effective/cpd_statement_activities/_form.html.haml +10 -10
  161. data/app/views/effective/cpd_statements/_activities.html.haml +3 -64
  162. data/app/views/effective/cpd_statements/_activities_edit.html.haml +2 -0
  163. data/app/views/effective/cpd_statements/_activities_new.html.haml +3 -0
  164. data/app/views/effective/cpd_statements/_activities_table.html.haml +64 -0
  165. data/app/views/effective/cpd_statements/_agreements.html.haml +19 -5
  166. data/app/views/effective/cpd_statements/_cpd_statement.html.haml +6 -3
  167. data/app/views/effective/cpd_statements/_layout.html.haml +3 -4
  168. data/app/views/effective/cpd_statements/_summary.html.haml +26 -31
  169. data/app/views/effective/cpd_statements/activities.html.haml +1 -1
  170. data/app/views/effective/cpd_statements/agreements.html.haml +7 -1
  171. data/app/views/effective/cpd_statements/submit.html.haml +2 -3
  172. data/config/effective_cpd.rb +49 -10
  173. data/config/routes.rb +19 -1
  174. data/db/migrate/01_create_effective_cpd.rb.erb +175 -1
  175. data/db/seeds.rb +16 -13
  176. data/lib/effective_cpd.rb +42 -3
  177. data/lib/effective_cpd/engine.rb +7 -0
  178. data/lib/effective_cpd/version.rb +1 -1
  179. data/lib/generators/effective_cpd/install_generator.rb +18 -3
  180. metadata +175 -9
  181. data/app/datatables/effective_cpd_statements_datatable.rb +0 -23
  182. data/app/views/admin/cpd_statements/_form.html.haml +0 -6
@@ -0,0 +1,91 @@
1
+ module Effective
2
+ class CpdAuditLevelQuestion < ActiveRecord::Base
3
+ belongs_to :cpd_audit_level
4
+ belongs_to :cpd_audit_level_section
5
+
6
+ has_many :cpd_audit_level_question_options, -> { CpdAuditLevelQuestionOption.sorted }, inverse_of: :cpd_audit_level_question, dependent: :delete_all
7
+ accepts_nested_attributes_for :cpd_audit_level_question_options, reject_if: :all_blank, allow_destroy: true
8
+
9
+ has_many :cpd_audit_responses
10
+
11
+ has_one_attached :upload_file
12
+ has_rich_text :body
13
+
14
+ if respond_to?(:log_changes)
15
+ log_changes(to: :cpd_audit_level, except: [:cpd_audit_responses])
16
+ end
17
+
18
+ CATEGORIES = [
19
+ 'Choose one', # Radios
20
+ 'Select all that apply', # Checks
21
+ 'Select up to 1',
22
+ 'Select up to 2',
23
+ 'Select up to 3',
24
+ 'Select up to 4',
25
+ 'Select up to 5',
26
+ 'Date', # Date Field
27
+ 'Email', # Email Field
28
+ 'Number', # Numeric Field
29
+ 'Long Answer', # Text Area
30
+ 'Short Answer', # Text Field
31
+ 'Upload File' # File field
32
+ ]
33
+
34
+ WITH_OPTIONS_CATEGORIES = [
35
+ 'Choose one', # Radios
36
+ 'Select all that apply', # Checks
37
+ 'Select up to 1',
38
+ 'Select up to 2',
39
+ 'Select up to 3',
40
+ 'Select up to 4',
41
+ 'Select up to 5'
42
+ ]
43
+
44
+ effective_resource do
45
+ title :text
46
+ category :string
47
+ required :boolean
48
+
49
+ position :integer
50
+
51
+ timestamps
52
+ end
53
+
54
+ scope :deep, -> { with_rich_text_body.includes(:cpd_audit_level_section, :cpd_audit_level_question_options) }
55
+ scope :sorted, -> { order(:position) }
56
+
57
+ before_validation(if: -> { cpd_audit_level_section.present? }) do
58
+ self.cpd_audit_level = cpd_audit_level_section.cpd_audit_level
59
+ self.position ||= (cpd_audit_level_section.cpd_audit_level_questions.map(&:position).compact.max || -1) + 1
60
+ end
61
+
62
+ validates :title, presence: true
63
+ validates :category, presence: true, inclusion: { in: CATEGORIES }
64
+ validates :position, presence: true
65
+ validates :cpd_audit_level_question_options, presence: true, if: -> { question_option? }
66
+
67
+ before_destroy do
68
+ if (count = cpd_audit_responses.length) > 0
69
+ raise("#{count} audit responses belong to this question")
70
+ end
71
+ end
72
+
73
+ # Create choose_one? and select_all_that_apply? methods for each category
74
+ CATEGORIES.each do |category|
75
+ define_method(category.parameterize.underscore + '?') { self.category == category }
76
+ end
77
+
78
+ def question_option?
79
+ WITH_OPTIONS_CATEGORIES.include?(category)
80
+ end
81
+
82
+ def category_partial
83
+ category.to_s.parameterize.underscore
84
+ end
85
+
86
+ def to_s
87
+ title.presence || 'audit question'
88
+ end
89
+
90
+ end
91
+ end
@@ -0,0 +1,34 @@
1
+ module Effective
2
+ class CpdAuditLevelQuestionOption < ActiveRecord::Base
3
+ belongs_to :cpd_audit_level_question
4
+
5
+ has_many :cpd_audit_response_options
6
+
7
+ effective_resource do
8
+ title :text
9
+ position :integer
10
+
11
+ timestamps
12
+ end
13
+
14
+ before_validation(if: -> { cpd_audit_level_question.present? }) do
15
+ self.position ||= (cpd_audit_level_question.cpd_audit_level_question_options.map { |obj| obj.position }.compact.max || -1) + 1
16
+ end
17
+
18
+ scope :sorted, -> { order(:position) }
19
+
20
+ validates :title, presence: true
21
+ validates :position, presence: true
22
+
23
+ before_destroy do
24
+ if (count = cpd_audit_response_options.length) > 0
25
+ raise("#{count} audit response options belong to this question option")
26
+ end
27
+ end
28
+
29
+ def to_s
30
+ title.presence || 'New Audit Question Option'
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,50 @@
1
+ module Effective
2
+ class CpdAuditLevelSection < ActiveRecord::Base
3
+ belongs_to :cpd_audit_level
4
+
5
+ has_rich_text :top_content
6
+ has_rich_text :bottom_content
7
+
8
+ has_many :cpd_audit_level_questions, -> { CpdAuditLevelQuestion.sorted }, inverse_of: :cpd_audit_level_section, dependent: :destroy
9
+ accepts_nested_attributes_for :cpd_audit_level_questions, allow_destroy: true
10
+
11
+ has_many :cpd_audit_responses
12
+
13
+ if respond_to?(:log_changes)
14
+ log_changes(to: :cpd_audit_level, except: [:cpd_audit_responses])
15
+ end
16
+
17
+ effective_resource do
18
+ title :string
19
+ position :integer
20
+
21
+ timestamps
22
+ end
23
+
24
+ scope :deep, -> {
25
+ with_rich_text_top_content
26
+ .with_rich_text_bottom_content
27
+ .includes(cpd_audit_level_questions: [:rich_text_body])
28
+ }
29
+
30
+ scope :sorted, -> { order(:position) }
31
+
32
+ before_validation(if: -> { cpd_audit_level.present? }) do
33
+ self.position ||= (cpd_audit_level.cpd_audit_level_sections.map(&:position).compact.max || -1) + 1
34
+ end
35
+
36
+ validates :title, presence: true
37
+ validates :position, presence: true
38
+
39
+ before_destroy do
40
+ if (count = cpd_audit_responses.length) > 0
41
+ raise("#{count} audit responses belong to this section")
42
+ end
43
+ end
44
+
45
+ def to_s
46
+ title.presence || 'audit section'
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,86 @@
1
+ module Effective
2
+ class CpdAuditResponse < ActiveRecord::Base
3
+ belongs_to :cpd_audit
4
+ belongs_to :cpd_audit_level_section
5
+ belongs_to :cpd_audit_level_question
6
+
7
+ has_many :cpd_audit_response_options, dependent: :delete_all
8
+ has_many :cpd_audit_level_question_options, through: :cpd_audit_response_options
9
+
10
+ has_one_attached :upload_file
11
+
12
+ if respond_to?(:log_changes)
13
+ log_changes(to: :cpd_audit)
14
+ end
15
+
16
+ effective_resource do
17
+ # The response
18
+ date :date
19
+ email :string
20
+ number :integer
21
+ long_answer :text
22
+ short_answer :text
23
+
24
+ timestamps
25
+ end
26
+
27
+ scope :submitted, -> { where(cpd_audit: Effective::CpdAudit.where.not(submitted_at: nil)) }
28
+ scope :deep, -> { includes(:cpd_audit, :cpd_audit_level_question) }
29
+
30
+ before_validation(if: -> { cpd_audit_level_question.present? }) do
31
+ self.cpd_audit_level_section = cpd_audit_level_question.cpd_audit_level_section
32
+ end
33
+
34
+ validates :date, presence: true, if: -> { cpd_audit_level_question&.required? && cpd_audit_level_question.date? }
35
+ validates :email, presence: true, email: true, if: -> { cpd_audit_level_question&.required? && cpd_audit_level_question.email? }
36
+ validates :number, presence: true, if: -> { cpd_audit_level_question&.required? && cpd_audit_level_question.number? }
37
+ validates :long_answer, presence: true, if: -> { cpd_audit_level_question&.required? && cpd_audit_level_question.long_answer? }
38
+ validates :short_answer, presence: true, if: -> { cpd_audit_level_question&.required? && cpd_audit_level_question.short_answer? }
39
+ validates :upload_file, presence: true, if: -> { cpd_audit_level_question&.required? && cpd_audit_level_question.upload_file? }
40
+ validates :cpd_audit_level_question_option_ids, presence: true, if: -> { cpd_audit_level_question&.required? && cpd_audit_level_question.question_option? }
41
+
42
+ validates :cpd_audit_level_question_option_ids, if: -> { cpd_audit_level_question&.choose_one? },
43
+ length: { maximum: 1, message: 'please choose 1 option only' }
44
+
45
+ validates :cpd_audit_level_question_option_ids, if: -> { cpd_audit_level_question&.select_up_to_1? },
46
+ length: { maximum: 1, message: 'please select 1 option or fewer' }
47
+
48
+ validates :cpd_audit_level_question_option_ids, if: -> { cpd_audit_level_question&.select_up_to_2? },
49
+ length: { maximum: 2, message: 'please select 2 options or fewer' }
50
+
51
+ validates :cpd_audit_level_question_option_ids, if: -> { cpd_audit_level_question&.select_up_to_3? },
52
+ length: { maximum: 3, message: 'please select 3 options or fewer' }
53
+
54
+ validates :cpd_audit_level_question_option_ids, if: -> { cpd_audit_level_question&.select_up_to_4? },
55
+ length: { maximum: 4, message: 'please select 4 options or fewer' }
56
+
57
+ validates :cpd_audit_level_question_option_ids, if: -> { cpd_audit_level_question&.select_up_to_5? },
58
+ length: { maximum: 5, message: 'please select 5 options or fewer' }
59
+
60
+ def to_s
61
+ 'audit response'
62
+ end
63
+
64
+ def response
65
+ return nil unless cpd_audit_level_question.present?
66
+
67
+ return date if cpd_audit_level_question.date?
68
+ return email if cpd_audit_level_question.email?
69
+ return number if cpd_audit_level_question.number?
70
+ return long_answer if cpd_audit_level_question.long_answer?
71
+ return short_answer if cpd_audit_level_question.short_answer?
72
+ return upload_file if cpd_audit_level_question.upload_file?
73
+
74
+ return cpd_audit_level_question_options.first if cpd_audit_level_question.choose_one?
75
+ return cpd_audit_level_question_options.first if cpd_audit_level_question.select_up_to_1?
76
+ return cpd_audit_level_question_options if cpd_audit_level_question.question_option?
77
+
78
+ raise('unknown response for unexpected cpd audit question category')
79
+ end
80
+
81
+ def category_partial
82
+ cpd_audit_level_question&.category_partial
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,6 @@
1
+ module Effective
2
+ class CpdAuditResponseOption < ActiveRecord::Base
3
+ belongs_to :cpd_audit_response
4
+ belongs_to :cpd_audit_level_question_option
5
+ end
6
+ end
@@ -0,0 +1,222 @@
1
+ module Effective
2
+ class CpdAuditReview < ActiveRecord::Base
3
+ attr_accessor :current_user
4
+ attr_accessor :current_step
5
+
6
+ belongs_to :cpd_audit
7
+ belongs_to :cpd_audit_level
8
+ belongs_to :user, polymorphic: true # Auditor
9
+
10
+ has_many :cpd_audit_review_items, -> { CpdAuditReviewItem.sorted }, inverse_of: :cpd_audit_review
11
+ accepts_nested_attributes_for :cpd_audit_review_items, reject_if: :all_blank, allow_destroy: true
12
+
13
+ if respond_to?(:log_changes)
14
+ log_changes(to: :cpd_audit, except: :wizard_steps)
15
+ end
16
+
17
+ acts_as_email_form
18
+ acts_as_tokened
19
+
20
+ acts_as_wizard(
21
+ start: 'Start',
22
+ information: 'Information',
23
+ instructions: 'Instructions',
24
+
25
+ # Optional based on cpd_audit_level options
26
+ conflict: 'Conflict of Interest',
27
+
28
+ waiting: 'Waiting on Auditee Submission',
29
+
30
+ statements: 'Review CPD Statements',
31
+ # ... There will be one step per cpd_statement here. "statement1"
32
+
33
+ questionnaire: 'Review Questionnaire Responses',
34
+ # ... There will be one step per cpd_audit_level_sections here
35
+
36
+ recommendation: 'Recommendation',
37
+ submit: 'Confirm & Submit',
38
+ complete: 'Complete'
39
+ )
40
+
41
+ effective_resource do
42
+ token :string
43
+ due_date :date
44
+
45
+ # Auditor response
46
+ conflict_of_interest :boolean
47
+ conflict_of_interest_reason :text
48
+
49
+ # Rolling comments
50
+ comments :text
51
+
52
+ # Recommendation Step
53
+ recommendation :string
54
+
55
+ # Status Dates
56
+ submitted_at :datetime
57
+
58
+ # acts_as_statused
59
+ # I'm not using acts_as_statused yet, but I probably will later
60
+ status :string
61
+ status_steps :text
62
+
63
+ # Wizard Progress
64
+ wizard_steps :text
65
+
66
+ timestamps
67
+ end
68
+
69
+ scope :deep, -> { includes(:cpd_audit, :cpd_audit_level, :user) }
70
+ scope :sorted, -> { order(:id) }
71
+
72
+ scope :available, -> { where(submitted_at: nil) }
73
+ scope :completed, -> { where.not(submitted_at: nil) }
74
+
75
+ before_validation(if: -> { new_record? }) do
76
+ self.cpd_audit_level ||= cpd_audit&.cpd_audit_level
77
+ self.due_date ||= deadline_to_review()
78
+ end
79
+
80
+ validate(if: -> { recommendation.present? }) do
81
+ unless cpd_audit_level.recommendations.include?(recommendation)
82
+ self.errors.add(:recommendation, 'must exist in this audit level')
83
+ end
84
+ end
85
+
86
+ after_commit(on: :create) { send_email(:cpd_audit_review_opened) }
87
+ after_commit(on: :destroy) { cpd_audit.review! }
88
+
89
+ def to_s
90
+ 'audit review'
91
+ end
92
+
93
+ def to_s
94
+ persisted? ? "#{cpd_audit_level} Audit Review by #{user}" : 'audit review'
95
+ end
96
+
97
+ # Find or build
98
+ def cpd_audit_review_item(item)
99
+ unless item.kind_of?(CpdAuditResponse) || item.kind_of?(CpdStatementActivity)
100
+ raise("expected a cpd_audit_response or cpd_statement_activity")
101
+ end
102
+
103
+ cpd_audit_review_item = cpd_audit_review_items.find { |cari| cari.item == item }
104
+ cpd_audit_review_item ||= cpd_audit_review_items.build(item: item)
105
+ end
106
+
107
+ # The dynamic CPD Statement review steps
108
+ def auditee_cpd_statements
109
+ cpd_audit.user.cpd_statements.select do |cpd_statement|
110
+ cpd_statement.completed? && (submitted_at.blank? || cpd_statement.submitted_at < submitted_at)
111
+ end
112
+ end
113
+
114
+ def cpd_statement(wizard_step)
115
+ cpd_cycle_id = (wizard_step.to_s.split('statement').last.to_i rescue false)
116
+ auditee_cpd_statements.find { |cpd_statement| cpd_statement.cpd_cycle_id == cpd_cycle_id }
117
+ end
118
+
119
+ def dynamic_wizard_statement_steps
120
+ @statement_steps ||= auditee_cpd_statements.each_with_object({}) do |cpd_statement, h|
121
+ h["statement#{cpd_statement.cpd_cycle_id}".to_sym] = "#{cpd_statement.cpd_cycle.to_s} Activities"
122
+ end
123
+ end
124
+
125
+ # The dynamic CPD Audit Level Sections steps
126
+ def cpd_audit_level_section(wizard_step)
127
+ position = (wizard_step.to_s.split('section').last.to_i rescue false)
128
+ cpd_audit_level.cpd_audit_level_sections.find { |section| (section.position + 1) == position }
129
+ end
130
+
131
+ def dynamic_wizard_questionnaire_steps
132
+ @questionnaire_steps ||= cpd_audit_level.cpd_audit_level_sections.each_with_object({}) do |section, h|
133
+ h["section#{section.position+1}".to_sym] = section.title
134
+ end
135
+ end
136
+
137
+ def dynamic_wizard_steps
138
+ dynamic_wizard_statement_steps.merge(dynamic_wizard_questionnaire_steps)
139
+ end
140
+
141
+ def can_visit_step?(step)
142
+ return (step == :complete) if completed? # Can only view complete step once submitted
143
+ can_revisit_completed_steps(step)
144
+ end
145
+
146
+ def required_steps
147
+ steps = [:start, :information, :instructions]
148
+
149
+ steps << :conflict if cpd_audit_level.conflict_of_interest?
150
+
151
+ if conflict_of_interest?
152
+ return steps + [:submit, :complete]
153
+ end
154
+
155
+ steps += [:waiting] unless ready?
156
+
157
+ steps += [:statements] + dynamic_wizard_statement_steps.keys
158
+ steps += [:questionnaire] + dynamic_wizard_questionnaire_steps.keys
159
+ steps += [:recommendation, :submit, :complete]
160
+
161
+ steps
162
+ end
163
+
164
+ def wizard_step_title(step)
165
+ WIZARD_STEPS[step] || dynamic_wizard_steps.fetch(step)
166
+ end
167
+
168
+ # Called by CpdAudit.extension_granted
169
+ def extension_granted!
170
+ self.due_date = deadline_to_review()
171
+ end
172
+
173
+ # Called by CpdAudit.submit!
174
+ def ready!
175
+ send_email(:cpd_audit_review_ready)
176
+ end
177
+
178
+ # Called by review wizard submit step
179
+ def submit!
180
+ update!(submitted_at: Time.zone.now)
181
+ cpd_audit.review! # maybe go from submitted->removed
182
+
183
+ send_email(:cpd_audit_review_submitted)
184
+ end
185
+
186
+ # When ready, the applicant review wizard hides the waiting step
187
+ def ready?
188
+ cpd_audit&.was_submitted?
189
+ end
190
+
191
+ def in_progress?
192
+ submitted_at.blank?
193
+ end
194
+
195
+ def completed?
196
+ submitted_at.present?
197
+ end
198
+
199
+ def email_form_defaults(action)
200
+ { from: EffectiveCpd.mailer_sender }
201
+ end
202
+
203
+ def send_email(email)
204
+ EffectiveCpd.send_email(email, self, email_form_params) unless email_form_skip?
205
+ true
206
+ end
207
+
208
+ def deadline_to_conflict_of_interest
209
+ cpd_audit.deadline_to_conflict_of_interest
210
+ end
211
+
212
+ def deadline_to_review
213
+ return nil unless cpd_audit_level&.days_to_review.present?
214
+
215
+ date = cpd_audit.deadline_to_submit
216
+ return nil unless date.present?
217
+
218
+ EffectiveResources.advance_date(date, business_days: cpd_audit_level.days_to_review)
219
+ end
220
+
221
+ end
222
+ end