Effective CPD

Continuing professional development.

An admin creates a set of categories, activities and rules. User enters number of hours or work done to have a scored statement. Audits.

Works with action_text for content bodies, and active_storage for file uploads.

Getting Started

This requires Rails 6+ and Twitter Bootstrap 4 and just works with Devise.

Please first install the effective_datatables gem.

Please download and install the Twitter Bootstrap4

Add to your Gemfile:

gem 'haml-rails' # or try using gem 'hamlit-rails'
gem 'effective_cpd'

Run the bundle command to install it:

bundle install

Then run the generator:

rails generate effective_cpd:install

The generator will install an initializer which describes all configuration options and creates a database migration.

If you want to tweak the table names, manually adjust both the configuration file and the migration now.

Then migrate the database:

rake db:migrate

Please add the following to your User model:

has_many :cpd_statements, -> { Effective::CpdStatement.sorted }, class_name: 'Effective::CpdStatement'

Use the following datatables to display to your user their statements and audits and audit reviews:

%h2 Continuing Professional Development

- # Auditee datatables (4)
- auditing = EffectiveCpdAvailableAuditsDatatable.new(self)
- if auditing.present?
  .mt-4
    %p You have been selected for audit:
    = render_datatable(auditing, simple: true)

- audited = EffectiveCpdCompletedAuditsDatatable.new(self)
- if audited.present?
  .mt-4
    %p You have completed these past audits:
    = render_datatable(audited, simple: true)

- available = EffectiveCpdAvailableCyclesDatatable.new(self)
- if available.present?
  .mt-4
    %p Please submit a CPD statement for the following available #{cpd_cycles_label}:
    = render_datatable(available, simple: true)

- completed = EffectiveCpdCompletedStatementsDatatable.new(self)
- if completed.present?
  .mt-4
    %p You have completed these past statements:
    = render_datatable(completed, simple: true)

- # Auditor / Audit reviewer datatables (2)
- reviewing = EffectiveCpdAvailableAuditReviewsDatatable.new(self)
- if reviewing.present?
  .mt-4
    %p You have been selected to review the following audits:
    = render_datatable(reviewing, simple: true)

- reviewed = EffectiveCpdCompletedAuditReviewsDatatable.new(self)
- if reviewed.present?
  .mt-4
    %p You have completed these past audit reviews:
    = render_datatable(reviewed, simple: true)

On the Admin::Users#edit, you can use the following datatables as well:

%h2 CPD Statements
- datatable = Admin::EffectiveCpdStatementsDatatable.new(user_id: user.id, user_type: user.class.name)
= render_datatable(datatable, inline: true)

%h2 CPD Audits
- datatable = Admin::EffectiveCpdAuditsDatatable.new(user_id: user.id, user_type: user.class.name)
= render_datatable(datatable)

Add a link to the admin menu:

- if can? :admin, :effective_cpd
  - if can? :index, Effective::CpdCategory
    = nav_link_to 'CPD Categories', effective_cpd.admin_cpd_categories_path

  - if can? :index, Effective::CpdCycle
    = nav_link_to 'CPD Cycles', effective_cpd.admin_cpd_cycles_path

  - if can? :index, Effective::CpdAuditLevel
    = nav_link_to 'CPD Audit Levels', effective_cpd.admin_cpd_audit_levels_path

  = nav_divider

  - if can? :index, Effective::CpdStatement
    = nav_link_to 'CPD Statements', effective_cpd.admin_cpd_statements_path

  - if can? :index, Effective::CpdAudit
    = nav_link_to 'CPD Audits', effective_cpd.admin_cpd_audits_path

Configuration

As an admin, visit the CPD Categories, then CPD Cycles, and CPD Audit levels.

Once all these 3 areas have been configured, users can submit statements and audits can be performed.

Required Score

You can specify the required score in the CPD Cycle.

You can also programmatically do it. Add the following to your user class.

# This is an ActiveRecord concern to add the has_many :cpd_statements
effective_cpd_user

# We require 100 points in the last 3 years.
def cpd_statement_required_score(cpd_statement)
  # We always consider the 3 year window, of the passed cpd_statement and the last two statements
  last_two_statements = cpd_statements.select do |statement|
    statement.completed? && statement.cpd_cycle_id < cpd_statement.cpd_cycle_id
  end.last(2)

  # They can submit 0 0 100
  return 0 if last_two_statements.length < 2

  # 100 points in the last 3 years.
  required_score = 100

  # Score so far
  existing_score = last_two_statements.sum { |statement| statement.score }
  raise('expected existing_score to be >= 0') if existing_score < 0

  # Required score minus previous
  return 0 if existing_score >= required_score
  (required_score - existing_score)
end

Custom Score Rounding

In your effective_cpd_statement model you can override the #round_amount method to perform specific rounding on the score that will be applied to amount and amount2.

Example:

  # Rounding to the nearest 0.5
  def round_amount(amount)
    return 0 if amount.blank?

    (amount * 2.0).round / 2.0
  end

Rake Tasks

You can use

bundle exec rake effective_cpd:notify_auditees

to send the cpd_audit_opened notification to all opened (but not yet started) auditees.

Authorization

All authorization checks are handled via the effective_resources gem found in the config/initializers/effective_resources.rb file.

Permissions

The permissions you actually want to define are as follows (using CanCanCan):

# Regular signed up user. Guest users not supported.
if user.persisted?
  can :new, Effective::CpdStatement
  can [:index, :show, :update], Effective::CpdStatement, user_id: user.id
  can [:index, :show], Effective::CpdCycle
  can([:create, :update, :destroy], Effective::CpdStatementActivity) { |sa| sa.cpd_statement.user_id == user.id }
  can [:index, :show, :update], Effective::CpdAudit, user_id: user.id
end

if user.reviewer?
  can [:index], EffectiveCpd.CpdAudit
  can [:index, :show, :update], EffectiveCpd.CpdAuditReview, user_id: user.id
end

if user.admin?
  can :admin, :effective_cpd
  can :manage, Effective::CpdActivity
  can :manage, Effective::CpdCategory
  can :manage, Effective::CpdCycle
  can :manage, Effective::CpdRule

  can([:index, :show], EffectiveCpd.CpdStatement)
  can(:unsubmit, EffectiveCpd.CpdStatement) { |statement| statement.completed? }

  can :manage, Effective::CpdAuditLevel
  can :manage, Effective::CpdAuditLevelSection
  can :manage, Effective::CpdAuditLevelQuestion

  can(crud - [:destroy], EffectiveCpd.CpdAudit)
  can(:destroy, EffectiveCpd.CpdAudit) { |audit| audit.draft? }

  can(:missing, EffectiveCpd.CpdAudit) { |audit| audit. && !audit.missing_info? }
  can(:complete, EffectiveCpd.CpdAudit) { |audit| audit. && !audit.was_completed? }
  can(:close, EffectiveCpd.CpdAudit) { |audit| audit. && !audit.closed? }

  can(crud - [:edit, :update], EffectiveCpd.CpdAuditReview)
end

License

MIT License. Copyright Code and Effect Inc.

Testing

Run tests by:

rails test

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Bonus points for test coverage
  6. Create new Pull Request