effective_cpd 0.0.1

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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +117 -0
  4. data/Rakefile +18 -0
  5. data/app/assets/config/effective_cpd_manifest.js +2 -0
  6. data/app/assets/javascripts/effective_cpd.js +2 -0
  7. data/app/assets/javascripts/effective_cpd/activities.js +49 -0
  8. data/app/assets/javascripts/effective_cpd/activities_new.js +21 -0
  9. data/app/assets/stylesheets/effective_cpd.scss +1 -0
  10. data/app/assets/stylesheets/effective_cpd/_activities.scss +19 -0
  11. data/app/controllers/admin/cpd_activities_controller.rb +13 -0
  12. data/app/controllers/admin/cpd_categories_controller.rb +13 -0
  13. data/app/controllers/admin/cpd_cycles_controller.rb +13 -0
  14. data/app/controllers/admin/cpd_rules_controller.rb +13 -0
  15. data/app/controllers/admin/cpd_statements_controller.rb +13 -0
  16. data/app/controllers/effective/cpd_cycles_controller.rb +19 -0
  17. data/app/controllers/effective/cpd_statement_activities_controller.rb +27 -0
  18. data/app/controllers/effective/cpd_statements_controller.rb +71 -0
  19. data/app/datatables/admin/effective_cpd_activities_datatable.rb +31 -0
  20. data/app/datatables/admin/effective_cpd_categories_datatable.rb +24 -0
  21. data/app/datatables/admin/effective_cpd_cycles_datatable.rb +22 -0
  22. data/app/datatables/admin/effective_cpd_rules_datatable.rb +43 -0
  23. data/app/datatables/admin/effective_cpd_statements_datatable.rb +30 -0
  24. data/app/datatables/effective_cpd_datatable.rb +30 -0
  25. data/app/datatables/effective_cpd_statements_datatable.rb +23 -0
  26. data/app/helpers/effective_cpd_helper.rb +37 -0
  27. data/app/mailers/effective/cpd_mailer.rb +7 -0
  28. data/app/models/effective/cpd_activity.rb +41 -0
  29. data/app/models/effective/cpd_category.rb +35 -0
  30. data/app/models/effective/cpd_cycle.rb +113 -0
  31. data/app/models/effective/cpd_rule.rb +108 -0
  32. data/app/models/effective/cpd_scorer.rb +158 -0
  33. data/app/models/effective/cpd_statement.rb +95 -0
  34. data/app/models/effective/cpd_statement_activity.rb +78 -0
  35. data/app/views/admin/cpd_activities/_form.html.haml +20 -0
  36. data/app/views/admin/cpd_categories/_form.html.haml +21 -0
  37. data/app/views/admin/cpd_categories/_form_cpd_category.html.haml +5 -0
  38. data/app/views/admin/cpd_cycles/_form.html.haml +17 -0
  39. data/app/views/admin/cpd_cycles/_form_content.html.haml +32 -0
  40. data/app/views/admin/cpd_cycles/_form_cpd_cycle.html.haml +21 -0
  41. data/app/views/admin/cpd_cycles/_form_cpd_rules.html.haml +56 -0
  42. data/app/views/admin/cpd_statements/_form.html.haml +6 -0
  43. data/app/views/effective/cpd_statement_activities/_form.html.haml +70 -0
  44. data/app/views/effective/cpd_statements/_activities.html.haml +64 -0
  45. data/app/views/effective/cpd_statements/_activities_new.html.haml +39 -0
  46. data/app/views/effective/cpd_statements/_agreements.html.haml +6 -0
  47. data/app/views/effective/cpd_statements/_cpd_statement.html.haml +5 -0
  48. data/app/views/effective/cpd_statements/_layout.html.haml +37 -0
  49. data/app/views/effective/cpd_statements/_summary.html.haml +36 -0
  50. data/app/views/effective/cpd_statements/activities.html.haml +25 -0
  51. data/app/views/effective/cpd_statements/agreements.html.haml +14 -0
  52. data/app/views/effective/cpd_statements/complete.html.haml +13 -0
  53. data/app/views/effective/cpd_statements/start.html.haml +13 -0
  54. data/app/views/effective/cpd_statements/submit.html.haml +20 -0
  55. data/app/views/layouts/effective_cpd_mailer_layout.html.haml +7 -0
  56. data/config/effective_cpd.rb +29 -0
  57. data/config/routes.rb +28 -0
  58. data/db/migrate/01_create_effective_cpd.rb.erb +98 -0
  59. data/db/seeds.rb +472 -0
  60. data/lib/effective_cpd.rb +18 -0
  61. data/lib/effective_cpd/engine.rb +11 -0
  62. data/lib/effective_cpd/version.rb +3 -0
  63. data/lib/generators/effective_cpd/install_generator.rb +46 -0
  64. data/lib/generators/templates/effective_cpd_mailer_preview.rb +4 -0
  65. data/lib/tasks/effective_cpd_tasks.rake +6 -0
  66. metadata +233 -0
@@ -0,0 +1,31 @@
1
+ module Admin
2
+ class EffectiveCpdActivitiesDatatable < Effective::Datatable
3
+ datatable do
4
+
5
+ if attributes[:cpd_category_id]
6
+ reorder :position
7
+ end
8
+
9
+ col :updated_at, visible: false
10
+ col :created_at, visible: false
11
+ col :id, visible: false
12
+
13
+ col :cpd_category
14
+ col :position, visible: false
15
+
16
+ col :title do |cpd_activity|
17
+ content_tag(:div, cpd_activity.title) + raw(cpd_activity.body)
18
+ end
19
+
20
+ col :amount_label, label: 'Amount'
21
+ col :amount2_label, label: 'Amount2'
22
+ col :require_file, label: 'Upload'
23
+
24
+ actions_col
25
+ end
26
+
27
+ collection do
28
+ Effective::CpdActivity.all.deep
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,24 @@
1
+ module Admin
2
+ class EffectiveCpdCategoriesDatatable < Effective::Datatable
3
+ datatable do
4
+ length :all
5
+ reorder :position
6
+
7
+ col :id, visible: false
8
+ col :created_at, visible: false
9
+ col :updated_at, visible: false
10
+
11
+ col :title do |cpd_category|
12
+ content_tag(:div, cpd_category.title) + raw(cpd_category.body)
13
+ end
14
+
15
+ col :cpd_activities, label: 'Activities'
16
+
17
+ actions_col
18
+ end
19
+
20
+ collection do
21
+ Effective::CpdCategory.all.deep
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ module Admin
2
+ class EffectiveCpdCyclesDatatable < Effective::Datatable
3
+ datatable do
4
+ order :start_at, :desc
5
+
6
+ col :id, visible: false
7
+ col :created_at, visible: false
8
+ col :updated_at, visible: false
9
+
10
+ col :title
11
+ col :start_at
12
+ col :end_at
13
+ col :required_score
14
+
15
+ actions_col
16
+ end
17
+
18
+ collection do
19
+ Effective::CpdCycle.all.deep
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,43 @@
1
+ module Admin
2
+ class EffectiveCpdRulesDatatable < Effective::Datatable
3
+ filters do
4
+ scope :categories
5
+ scope :activities
6
+ scope :unavailable
7
+ scope :all
8
+ end
9
+
10
+ datatable do
11
+ col :id, visible: false
12
+ col :created_at, visible: false
13
+ col :updated_at, visible: false
14
+
15
+ col :cpd_cycle,label: cpd_cycle_label.titleize
16
+
17
+ col :ruleable, label: 'Category or Activity', search: {
18
+ collection: {
19
+ 'Categories' => Effective::CpdCategory.sorted,
20
+ 'Activities' => Effective::CpdActivity.sorted,
21
+ },
22
+ polymorphic: true
23
+ }
24
+
25
+ col :formula do |cpd_activity|
26
+ content_tag(:div, cpd_activity.formula, style: 'white-space: nowrap;')
27
+ end
28
+
29
+ col :credit_description
30
+
31
+ col :max_credits_per_cycle, label: 'Max ' + cpd_credits_label.titleize
32
+ col :max_cycles_can_carry_forward, label: 'Max ' + cpd_cycles_label.titleize
33
+
34
+ col :unavailable
35
+
36
+ actions_col
37
+ end
38
+
39
+ collection do
40
+ Effective::CpdRule.all.deep
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ module Admin
2
+ class EffectiveCpdStatementsDatatable < Effective::Datatable
3
+ filters do
4
+ scope :completed
5
+ scope :draft
6
+ scope :all
7
+ end
8
+
9
+ datatable do
10
+ order :updated_at
11
+
12
+ col :id, visible: false
13
+ col :token, visible: false
14
+ col :created_at, visible: false
15
+ col :updated_at, visible: false
16
+
17
+ col :cpd_cycle, label: cpd_cycle_label.titleize
18
+ col :user
19
+ col :completed_at
20
+ col :score
21
+ col :carry_forward
22
+
23
+ actions_col
24
+ end
25
+
26
+ collection do
27
+ Effective::CpdStatement.all.deep
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # Displays available cpd_cycles that the current_user may complete
2
+
3
+ class EffectiveCpdDatatable < Effective::Datatable
4
+ datatable do
5
+ order :start_at
6
+
7
+ col :start_at, visible: false
8
+
9
+ col(:title, label: cpd_cycle_label.titleize)
10
+ col :available_date, label: 'Available'
11
+
12
+ actions_col(actions: []) do |cpd_cycle|
13
+ statement = cpd_cycle.cpd_statements.where(user: current_user).first
14
+
15
+ if statement.blank?
16
+ dropdown_link_to('Start', effective_cpd.cpd_cycle_cpd_statement_build_path(cpd_cycle, :new, :start))
17
+ elsif statement.completed?
18
+ 'Complete'
19
+ else
20
+ dropdown_link_to('Continue', effective_cpd.cpd_cycle_cpd_statement_build_path(cpd_cycle, statement, statement.next_step))
21
+ end
22
+ end
23
+ end
24
+
25
+ collection do
26
+ raise('expected a current_user') unless current_user.present?
27
+ Effective::CpdCycle.available
28
+ end
29
+
30
+ end
@@ -0,0 +1,23 @@
1
+ # Displays past cpd statements that were completed by the user
2
+
3
+ class EffectiveCpdStatementsDatatable < Effective::Datatable
4
+ datatable do
5
+ order :cpd_cycle_id
6
+
7
+ col(:cpd_cycle_id, label: cpd_cycle_label.titleize) do |statement|
8
+ statement.cpd_cycle.to_s
9
+ end
10
+
11
+ col :completed_at, label: 'Completed'
12
+
13
+ actions_col(actions: []) do |cpd_statement|
14
+ dropdown_link_to('Show', effective_cpd.cpd_cycle_cpd_statement_build_path(cpd_statement.cpd_cycle, cpd_statement, cpd_statement.last_completed_step))
15
+ end
16
+ end
17
+
18
+ collection do
19
+ raise('expected a current_user') unless current_user.present?
20
+ Effective::CpdStatement.completed.where(user: current_user).includes(:cpd_cycle)
21
+ end
22
+
23
+ end
@@ -0,0 +1,37 @@
1
+ module EffectiveCpdHelper
2
+
3
+ def cpd_cycle_label
4
+ (EffectiveCpd.cycle_label || '').to_s.html_safe
5
+ end
6
+
7
+ def cpd_cycles_label
8
+ (EffectiveCpd.cycle_label || '').to_s.pluralize.html_safe
9
+ end
10
+
11
+ def cpd_credit_label
12
+ (EffectiveCpd.credit_label || '').to_s.html_safe
13
+ end
14
+
15
+ def cpd_credits_label
16
+ (EffectiveCpd.credit_label || '').to_s.pluralize.html_safe
17
+ end
18
+
19
+ def cpd_rule_formula_hint(cpd_activity)
20
+ raise('expected a CPDActivity') unless cpd_activity.kind_of?(Effective::CpdActivity)
21
+
22
+ if cpd_activity.amount_label.present? && cpd_activity.amount2_label.present?
23
+ "must include 'amount' and 'amount2'. Something like (amount + (amount2 * 10))"
24
+ elsif cpd_activity.amount_label.present?
25
+ "must include 'amount'. Something like (10 * amount)"
26
+ elsif cpd_activity.amount2_label.present?
27
+ "must include 'amount2'. Something like (10 * amount2)"
28
+ else
29
+ "must be a number and may not include 'amount'. Something like 10"
30
+ end
31
+ end
32
+
33
+ def effective_cpd_categories
34
+ @effective_cpd_categories ||= Effective::CpdCategory.deep.sorted
35
+ end
36
+
37
+ end
@@ -0,0 +1,7 @@
1
+ module Effective
2
+ class CpdMailer < ActionMailer::Base
3
+ default from: EffectiveCpd.mailer[:default_from]
4
+ layout EffectiveCpd.mailer[:layout].presence || 'effective_cpd_mailer_layout'
5
+
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ module Effective
2
+ class CpdActivity < ActiveRecord::Base
3
+ belongs_to :cpd_category
4
+
5
+ has_rich_text :body
6
+ log_changes(to: :cpd_category) if respond_to?(:log_changes)
7
+
8
+ # has_many :rules, -> { order(cycle_id: :desc) }, as: :ruleable, dependent: :delete_all
9
+ # accepts_nested_attributes_for :rules, allow_destroy: true
10
+
11
+ effective_resource do
12
+ title :string
13
+ position :integer
14
+
15
+ # The formula can include amount and amount2
16
+ # This is the description of the amount unit
17
+ amount_label :string # 'hours of committee work', 'hours or work'
18
+ amount2_label :string # 'CEUs'
19
+
20
+ # Whether the user must attach supporting documents
21
+ requires_upload_file :boolean
22
+
23
+ timestamps
24
+ end
25
+
26
+ scope :deep, -> { with_rich_text_body.includes(:cpd_category) }
27
+ scope :sorted, -> { order(:position) }
28
+
29
+ before_validation(if: -> { cpd_category.present? }) do
30
+ self.position ||= (cpd_category.cpd_activities.map(&:position).compact.max || -1) + 1
31
+ end
32
+
33
+ validates :title, presence: true
34
+ validates :position, presence: true
35
+
36
+ def to_s
37
+ title.presence || 'activity'
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,35 @@
1
+ module Effective
2
+ class CpdCategory < ActiveRecord::Base
3
+ has_rich_text :body
4
+ log_changes if respond_to?(:log_changes)
5
+
6
+ has_many :cpd_activities, -> { order(:position) }, inverse_of: :cpd_category, dependent: :destroy
7
+ accepts_nested_attributes_for :cpd_activities, allow_destroy: true
8
+
9
+ # has_many :rules, -> { order(cycle_id: :desc) }, as: :ruleable, dependent: :delete_all
10
+ # accepts_nested_attributes_for :rules, allow_destroy: true
11
+
12
+ effective_resource do
13
+ title :string
14
+ position :integer
15
+
16
+ timestamps
17
+ end
18
+
19
+ scope :deep, -> { with_rich_text_body.includes(cpd_activities: [:rich_text_body]) }
20
+ scope :sorted, -> { order(:position) }
21
+
22
+ before_validation do
23
+ self.position ||= (CpdCategory.all.maximum(:position) || -1) + 1
24
+ end
25
+
26
+ validates :title, presence: true
27
+ validates :position, presence: true
28
+ validates :body, presence: true
29
+
30
+ def to_s
31
+ title.presence || 'category'
32
+ end
33
+
34
+ end
35
+ end
@@ -0,0 +1,113 @@
1
+ module Effective
2
+ class CpdCycle < ActiveRecord::Base
3
+ has_rich_text :all_steps_content # Update build_from_cycle() below if these change
4
+ has_rich_text :start_content
5
+ has_rich_text :activities_content
6
+ has_rich_text :agreements_content
7
+ has_rich_text :submit_content
8
+ has_rich_text :complete_content
9
+
10
+ has_many :cpd_rules, dependent: :delete_all
11
+ accepts_nested_attributes_for :cpd_rules, allow_destroy: true
12
+
13
+ has_many :cpd_statements
14
+
15
+ if respond_to?(:log_changes)
16
+ log_changes(except: [:cpd_statements])
17
+ end
18
+
19
+ effective_resource do
20
+ title :string # 2021 Continuing Professional Development
21
+
22
+ start_at :datetime
23
+ end_at :datetime
24
+
25
+ required_score :integer # If set, a user must fill out this many activities to finish a statement
26
+
27
+ timestamps
28
+ end
29
+
30
+ scope :deep, -> {
31
+ with_rich_text_all_steps_content
32
+ .with_rich_text_start_content
33
+ .with_rich_text_activities_content
34
+ .with_rich_text_submit_content
35
+ .with_rich_text_complete_content
36
+ .includes(:cpd_rules)
37
+ }
38
+
39
+ scope :sorted, -> { order(:id) }
40
+
41
+ scope :upcoming, -> { where('start_at > ?', Time.zone.now) }
42
+ scope :available, -> { where('start_at <= ? AND (end_at > ? OR end_at IS NULL)', Time.zone.now, Time.zone.now) }
43
+ scope :completed, -> { where('end_at < ?', Time.zone.now) }
44
+
45
+ before_validation(if: -> { new_record? && cpd_rules.blank? }) do
46
+ cycle = CpdCycle.latest_cycle
47
+ build_from_cycle(cycle: cycle) if cycle
48
+ end
49
+
50
+ validates :title, presence: true
51
+ validates :start_at, presence: true
52
+
53
+ validate(if: -> { start_at.present? && end_at.present? }) do
54
+ self.errors.add(:end_at, 'must be after the start date') unless end_at > start_at
55
+ end
56
+
57
+ def self.latest_cycle
58
+ order(id: :desc).first
59
+ end
60
+
61
+ def to_s
62
+ title.presence || 'New CPD Cycle'
63
+ end
64
+
65
+ def build_from_cycle(cycle:)
66
+ raise('expected a CpdCycle') unless cycle.kind_of?(CpdCycle)
67
+
68
+ # Cycle
69
+ attributes = cycle.dup.attributes.except('title', 'token', 'start_at', 'end_at')
70
+ assign_attributes(attributes)
71
+
72
+ [:all_steps_content, :start_content, :activities_content, :submit_content, :complete_content].each do |rich_text|
73
+ self.send("#{rich_text}=", cycle.send(rich_text))
74
+ end
75
+
76
+ cycle.cpd_rules.each do |rule|
77
+ attributes = rule.dup.attributes.except('cpd_cycle_id')
78
+ self.cpd_rules.build(attributes)
79
+ end
80
+
81
+ self
82
+ end
83
+
84
+ # Find or build
85
+ def rule_for(ruleable)
86
+ raise('expected a CpdCategory or CpdActivity') unless ruleable.kind_of?(CpdActivity) || ruleable.kind_of?(CpdCategory)
87
+
88
+ rule = cpd_rules.find { |rule| rule.ruleable_id == ruleable.id && rule.ruleable_type == ruleable.class.name }
89
+ rule ||= cpd_rules.build(ruleable: ruleable)
90
+ end
91
+
92
+ def available?
93
+ started? && !ended?
94
+ end
95
+
96
+ def started?
97
+ start_at_was.present? && Time.zone.now >= start_at_was
98
+ end
99
+
100
+ def ended?
101
+ end_at.present? && end_at < Time.zone.now
102
+ end
103
+
104
+ def available_date
105
+ if start_at && end_at
106
+ "#{start_at.strftime('%F')} to #{end_at.strftime('%F')}"
107
+ elsif start_at
108
+ "#{start_at.strftime('%F')}"
109
+ end
110
+ end
111
+
112
+ end
113
+ end