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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 96dbf47b702128c10b5b8f1ee07f9fb4798efdd77220b506b75124737e55e8ec
4
+ data.tar.gz: a5e0a5a658c43ffef382a8becb65b7a84b61a24a01f701dbb7daaecf63aa391f
5
+ SHA512:
6
+ metadata.gz: 3ee41586315d02240cbdaa075b91aec80122db0c6820db8f5c79595af3e45a617fcabbe5284d67ef149209bd998870dd9684bf3d3bce0c88ddfec836e7e82e6b
7
+ data.tar.gz: ab8a10bd1aed88b3971d596a22bae191a6e9d77c20ba1f7343bc8888d9930feb57ab8d57cc2cb3aded11b938d5c83e8689ed2c54f85bd28cdd76291921c7fc7e
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Code and Effect Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # Effective CPD
2
+
3
+ Continuing professional development.
4
+
5
+ An admin creates a set of categories, activites and rules. User enters number of hours or work done to have a scored statement. Audits.
6
+
7
+ Works with action_text for content bodies, and active_storage for file uploads.
8
+
9
+ ## Getting Started
10
+
11
+ This requires Rails 6+ and Twitter Bootstrap 4 and just works with Devise.
12
+
13
+ Please first install the [effective_datatables](https://github.com/code-and-effect/effective_datatables) gem.
14
+
15
+ Please download and install the [Twitter Bootstrap4](http://getbootstrap.com)
16
+
17
+ Add to your Gemfile:
18
+
19
+ ```ruby
20
+ gem 'haml-rails' # or try using gem 'hamlit-rails'
21
+ gem 'effective_cpd'
22
+ ```
23
+
24
+ Run the bundle command to install it:
25
+
26
+ ```console
27
+ bundle install
28
+ ```
29
+
30
+ Then run the generator:
31
+
32
+ ```ruby
33
+ rails generate effective_cpd:install
34
+ ```
35
+
36
+ The generator will install an initializer which describes all configuration options and creates a database migration.
37
+
38
+ If you want to tweak the table names, manually adjust both the configuration file and the migration now.
39
+
40
+ Then migrate the database:
41
+
42
+ ```ruby
43
+ rake db:migrate
44
+ ```
45
+
46
+ Render the "available statements for current_user" datatable on your user dashboard:
47
+
48
+ ```haml
49
+ %h2 Continuing Professional Development
50
+
51
+ %p Please submit a CPD statement for the following available #{cpd_cycles_label}:
52
+ = render_datatable(EffectiveCpdDatatable.new, simple: true)
53
+
54
+ - datatable = EffectiveCpdStatementsDatatable.new(self)
55
+ - if datatable.present?
56
+ .mt-4
57
+ %p You completed these statements:
58
+ = render_datatable(datatable, simple: true)
59
+
60
+ ```
61
+
62
+ Add a link to the admin menu:
63
+
64
+ ```haml
65
+ - if can? :admin, :effective_cpd
66
+ = link_to 'CPD Categories', effective_cpd.admin_cpd_categories_path
67
+ = link_to 'CPD Cycles', effective_cpd.admin_cpd_cycles_path
68
+ = link_to 'CPD Statements', effective_cpd.admin_cpd_statements_path
69
+ ```
70
+
71
+ ## Authorization
72
+
73
+ All authorization checks are handled via the effective_resources gem found in the `config/initializers/effective_resources.rb` file.
74
+
75
+ ## Permissions
76
+
77
+ The permissions you actually want to define are as follows (using CanCan):
78
+
79
+ ```ruby
80
+ # Regular signed up user. Guest users not supported.
81
+ if user.persisted?
82
+ can :new, Effective::CpdStatement
83
+ can [:index, :show, :update], Effective::CpdStatement, user_id: user.id
84
+ can [:index, :show], Effective::CpdCycle
85
+ can([:create, :update, :destroy], Effective::CpdStatementActivity) { |sa| sa.cpd_statement.user_id == user.id }
86
+ end
87
+
88
+ if user.admin?
89
+ can :admin, :effective_cpd
90
+ can :manage, Effective::CpdActivity
91
+ can :manage, Effective::CpdCategory
92
+ can :manage, Effective::CpdCycle
93
+ can :manage, Effective::CpdRule
94
+ can :manage, Effective::CpdStatement
95
+ end
96
+ ```
97
+
98
+ ## License
99
+
100
+ MIT License. Copyright [Code and Effect Inc.](http://www.codeandeffect.com/)
101
+
102
+ ## Testing
103
+
104
+ Run tests by:
105
+
106
+ ```ruby
107
+ rails test
108
+ ```
109
+
110
+ ## Contributing
111
+
112
+ 1. Fork it
113
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
114
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
115
+ 4. Push to the branch (`git push origin my-new-feature`)
116
+ 5. Bonus points for test coverage
117
+ 6. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
9
+
10
+ require "rake/testtask"
11
+
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'test'
14
+ t.pattern = 'test/**/*_test.rb'
15
+ t.verbose = false
16
+ end
17
+
18
+ task default: :test
@@ -0,0 +1,2 @@
1
+ //= link_directory ../javascripts .js
2
+ //= link_directory ../stylesheets .css
@@ -0,0 +1,2 @@
1
+ //= require effective_cpd/activities
2
+ //= require effective_cpd/activities_new
@@ -0,0 +1,49 @@
1
+ const initialize_effective_cpd_activities = function() {
2
+ const $obj = $('.cpd-statement-activities:not(.initialized)')
3
+
4
+ if($obj.length > 0) {
5
+ $(document).on('mousedown touchstart', function(event) { collapse_effective_cpd_activities() })
6
+
7
+ // When we click outside the New Activity or Edit Activity
8
+ $(document).on('mousedown touchstart', '.activities-new', function(event) { event.stopPropagation() });
9
+ $(document).on('mousedown touchstart', '.statement-activity', function(event) { event.stopPropagation() });
10
+
11
+ $obj.addClass('initialized')
12
+ }
13
+ };
14
+
15
+ // Collapse the New Activity and all Edit Activity forms
16
+ const collapse_effective_cpd_activities = function() {
17
+ const $activities = $('#cpd-statement-activities')
18
+ $activities.children('.activities-new').hide()
19
+ $activities.children('.activities-index').show()
20
+
21
+ const $statementActivities = $('.cpd-statement-activities')
22
+ $statementActivities.find('.statement-activity-content').show()
23
+ $statementActivities.find('.statement-activity-form').hide()
24
+ };
25
+
26
+ // When we click the New Activity button, use the whole screen
27
+ $(document).on('click', '[data-cpd-new-activity]', function(event) {
28
+ event.preventDefault()
29
+ event.stopPropagation()
30
+
31
+ const $statement = $('#cpd-statement-activities')
32
+ $statement.children('.activities-index').hide()
33
+ $statement.children('.activities-new').show()
34
+ });
35
+
36
+ // When we click the Edit Activity
37
+ $(document).on('click', '[data-cpd-edit-activity]', function(event) {
38
+ event.preventDefault()
39
+
40
+ collapse_effective_cpd_activities()
41
+
42
+ const $activity = $(event.currentTarget).closest('.statement-activity')
43
+ $activity.children('.statement-activity-content').hide()
44
+ $activity.children('.statement-activity-form').show()
45
+ });
46
+
47
+ // Initializers
48
+ $(document).ready(function() { initialize_effective_cpd_activities() });
49
+ $(document).on('turbolinks:load', function() { initialize_effective_cpd_activities() });
@@ -0,0 +1,21 @@
1
+ // In the New Activity Form
2
+ $(document).on('click', '[data-cpd-show-activity]', function(event) {
3
+ event.preventDefault()
4
+
5
+ let $obj = $(event.currentTarget)
6
+ let $form = $obj.siblings('.statement-activity-form')
7
+ let $tabContent = $obj.closest('.tab-content').children('.statement-activity-content')
8
+
9
+ $tabContent.html($form.html())
10
+ $tabContent.siblings('.active').removeClass('active')
11
+ $tabContent.addClass('active')
12
+ });
13
+
14
+ $(document).on('click', '[data-cpd-back-activity]', function(event) {
15
+ event.preventDefault()
16
+
17
+ let $activities = $(event.currentTarget).closest('.activities-new')
18
+ let anchor = $activities.find('.nav[role=tablist]').find('a.nav-link.active').first().attr('href')
19
+
20
+ $(anchor).siblings('.active').removeClass('active').end().addClass('active')
21
+ });
@@ -0,0 +1 @@
1
+ @import 'effective_cpd/activities';
@@ -0,0 +1,19 @@
1
+ .cpd-statement-activities {
2
+ .statement-activity-category {
3
+ .progress { height: 3px; }
4
+ }
5
+
6
+ .statement-activity-content {
7
+ a { color: inherit; }
8
+ }
9
+
10
+ .carry-forward {
11
+ text-align: right;
12
+ padding-right: 2rem;
13
+ }
14
+
15
+ .score {
16
+ text-align: right;
17
+ }
18
+
19
+ }
@@ -0,0 +1,13 @@
1
+ module Admin
2
+ class CpdActivitiesController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_cpd) }
5
+
6
+ include Effective::CrudController
7
+
8
+ def permitted_params
9
+ params.require(:effective_cpd_activity).permit!
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Admin
2
+ class CpdCategoriesController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_cpd) }
5
+
6
+ include Effective::CrudController
7
+
8
+ def permitted_params
9
+ params.require(:effective_cpd_category).permit!
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Admin
2
+ class CpdCyclesController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_cpd) }
5
+
6
+ include Effective::CrudController
7
+
8
+ def permitted_params
9
+ params.require(:effective_cpd_cycle).permit!
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Admin
2
+ class CpdRulesController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_cpd) }
5
+
6
+ include Effective::CrudController
7
+
8
+ def permitted_params
9
+ params.require(:effective_cpd_rule).permit!
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Admin
2
+ class CpdStatementsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+ before_action { EffectiveResources.authorize!(self, :admin, :effective_cpd) }
5
+
6
+ include Effective::CrudController
7
+
8
+ # def permitted_params
9
+ # params.require(:effective_cpd_statement).permit!
10
+ # end
11
+
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module Effective
2
+ class CpdCyclesController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+
5
+ def show
6
+ cycle = Effective::CpdCycle.find(params[:id])
7
+ EffectiveResources.authorize!(self, :show, cycle)
8
+
9
+ statement = Effective::CpdStatement.where(cpd_cycle: cycle, user: current_user).first
10
+
11
+ if statement.present?
12
+ redirect_to effective_cpd.cpd_cycle_cpd_statement_build_path(cycle, statement, statement.next_step)
13
+ else
14
+ redirect_to effective_cpd.cpd_cycle_cpd_statement_build_path(cycle, :new, :start)
15
+ end
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ module Effective
2
+ class CpdStatementActivitiesController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+
5
+ include Effective::CrudController
6
+
7
+ resource_scope -> { CpdStatement.find(params[:cpd_statement_id]).cpd_statement_activities }
8
+
9
+ # Score all statements when we change any activity
10
+ after_save do
11
+ CpdScorer.new(user: resource.cpd_statement.user, from: resource.cpd_statement).score!
12
+ end
13
+
14
+ # Redirect the remote form back to the activities page
15
+ on :save, redirect: -> {
16
+ statement = resource.cpd_statement
17
+ effective_cpd.cpd_cycle_cpd_statement_build_path(statement.cpd_cycle, statement, :activities)
18
+ }
19
+
20
+ def permitted_params
21
+ params.require(:effective_cpd_statement_activity).permit(
22
+ :id, :cpd_category_id, :cpd_activity_id, :amount, :amount2, :description, files: []
23
+ )
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,71 @@
1
+ module Effective
2
+ class CpdStatementsController < ApplicationController
3
+ before_action(:authenticate_user!) if defined?(Devise)
4
+
5
+ include Effective::WizardController
6
+
7
+ resource_scope do
8
+ cycle = Effective::CpdCycle.find(params[:cpd_cycle_id])
9
+ Effective::CpdStatement.deep.where(cpd_cycle: cycle, user: current_user)
10
+ end
11
+
12
+ after_save(if: -> { step == :start }) do
13
+ CpdScorer.new(user: resource.user).score!
14
+ end
15
+
16
+ # Enforce one statement per user per cycle. Redirect them to an existing statement for this cycle.
17
+ before_action(only: [:new, :show]) do
18
+ cycle = Effective::CpdCycle.find(params[:cpd_cycle_id])
19
+ existing = Effective::CpdStatement.where(cpd_cycle: cycle, user: current_user).where.not(id: resource).first
20
+
21
+ if existing&.completed?
22
+ flash[:danger] = "You have already completed a statement for this #{cpd_cycle_label}."
23
+ redirect_to(root_path)
24
+ elsif existing.present?
25
+ flash[:success] = "You have been redirected to the #{resource_wizard_step_title(existing.next_step)} step."
26
+ redirect_to effective_cpd.cpd_cycle_cpd_statement_build_path(existing.cpd_cycle, existing, existing.next_step)
27
+ end
28
+ end
29
+
30
+ # Enforce cycle availability
31
+ before_action(only: [:show, :update]) do
32
+ cycle = resource.cpd_cycle
33
+
34
+ unless cycle.available?
35
+ flash[:danger] = begin
36
+ if cycle.ended?
37
+ "This #{cpd_cycle_label} has ended"
38
+ elsif !cycle.started?
39
+ "This #{cpd_cycle_label} has not yet started"
40
+ else
41
+ "This #{cpd_cycle_label} is unavailable"
42
+ end
43
+ end
44
+
45
+ redirect_to(root_path)
46
+ end
47
+ end
48
+
49
+ def permitted_params
50
+ case step
51
+ when :start
52
+ params.require(:effective_cpd_statement).permit(:current_step)
53
+ when :activities
54
+ params.require(:effective_cpd_statement).permit(:current_step)
55
+ when :agreements
56
+ params.require(:effective_cpd_statement).permit(
57
+ :current_step, :confirm_read, :confirm_factual, files: []
58
+ )
59
+ when :submit
60
+ params.require(:effective_cpd_statement).permit(
61
+ :current_step, :confirm_readonly
62
+ )
63
+ when :complete
64
+ raise('unexpected post to complete')
65
+ else
66
+ raise('unexpected step')
67
+ end
68
+ end
69
+
70
+ end
71
+ end