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.
- 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
|