effective_cpd 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +117 -0
- data/Rakefile +18 -0
- data/app/assets/config/effective_cpd_manifest.js +2 -0
- data/app/assets/javascripts/effective_cpd.js +2 -0
- data/app/assets/javascripts/effective_cpd/activities.js +49 -0
- data/app/assets/javascripts/effective_cpd/activities_new.js +21 -0
- data/app/assets/stylesheets/effective_cpd.scss +1 -0
- data/app/assets/stylesheets/effective_cpd/_activities.scss +19 -0
- data/app/controllers/admin/cpd_activities_controller.rb +13 -0
- data/app/controllers/admin/cpd_categories_controller.rb +13 -0
- data/app/controllers/admin/cpd_cycles_controller.rb +13 -0
- data/app/controllers/admin/cpd_rules_controller.rb +13 -0
- data/app/controllers/admin/cpd_statements_controller.rb +13 -0
- data/app/controllers/effective/cpd_cycles_controller.rb +19 -0
- data/app/controllers/effective/cpd_statement_activities_controller.rb +27 -0
- data/app/controllers/effective/cpd_statements_controller.rb +71 -0
- data/app/datatables/admin/effective_cpd_activities_datatable.rb +31 -0
- data/app/datatables/admin/effective_cpd_categories_datatable.rb +24 -0
- data/app/datatables/admin/effective_cpd_cycles_datatable.rb +22 -0
- data/app/datatables/admin/effective_cpd_rules_datatable.rb +43 -0
- data/app/datatables/admin/effective_cpd_statements_datatable.rb +30 -0
- data/app/datatables/effective_cpd_datatable.rb +30 -0
- data/app/datatables/effective_cpd_statements_datatable.rb +23 -0
- data/app/helpers/effective_cpd_helper.rb +37 -0
- data/app/mailers/effective/cpd_mailer.rb +7 -0
- data/app/models/effective/cpd_activity.rb +41 -0
- data/app/models/effective/cpd_category.rb +35 -0
- data/app/models/effective/cpd_cycle.rb +113 -0
- data/app/models/effective/cpd_rule.rb +108 -0
- data/app/models/effective/cpd_scorer.rb +158 -0
- data/app/models/effective/cpd_statement.rb +95 -0
- data/app/models/effective/cpd_statement_activity.rb +78 -0
- data/app/views/admin/cpd_activities/_form.html.haml +20 -0
- data/app/views/admin/cpd_categories/_form.html.haml +21 -0
- data/app/views/admin/cpd_categories/_form_cpd_category.html.haml +5 -0
- data/app/views/admin/cpd_cycles/_form.html.haml +17 -0
- data/app/views/admin/cpd_cycles/_form_content.html.haml +32 -0
- data/app/views/admin/cpd_cycles/_form_cpd_cycle.html.haml +21 -0
- data/app/views/admin/cpd_cycles/_form_cpd_rules.html.haml +56 -0
- data/app/views/admin/cpd_statements/_form.html.haml +6 -0
- data/app/views/effective/cpd_statement_activities/_form.html.haml +70 -0
- data/app/views/effective/cpd_statements/_activities.html.haml +64 -0
- data/app/views/effective/cpd_statements/_activities_new.html.haml +39 -0
- data/app/views/effective/cpd_statements/_agreements.html.haml +6 -0
- data/app/views/effective/cpd_statements/_cpd_statement.html.haml +5 -0
- data/app/views/effective/cpd_statements/_layout.html.haml +37 -0
- data/app/views/effective/cpd_statements/_summary.html.haml +36 -0
- data/app/views/effective/cpd_statements/activities.html.haml +25 -0
- data/app/views/effective/cpd_statements/agreements.html.haml +14 -0
- data/app/views/effective/cpd_statements/complete.html.haml +13 -0
- data/app/views/effective/cpd_statements/start.html.haml +13 -0
- data/app/views/effective/cpd_statements/submit.html.haml +20 -0
- data/app/views/layouts/effective_cpd_mailer_layout.html.haml +7 -0
- data/config/effective_cpd.rb +29 -0
- data/config/routes.rb +28 -0
- data/db/migrate/01_create_effective_cpd.rb.erb +98 -0
- data/db/seeds.rb +472 -0
- data/lib/effective_cpd.rb +18 -0
- data/lib/effective_cpd/engine.rb +11 -0
- data/lib/effective_cpd/version.rb +3 -0
- data/lib/generators/effective_cpd/install_generator.rb +46 -0
- data/lib/generators/templates/effective_cpd_mailer_preview.rb +4 -0
- data/lib/tasks/effective_cpd_tasks.rake +6 -0
- 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,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
|