effective_cpd 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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