# EffectiveCpdUser # # Mark your user model with effective_cpd_user to get a few helpers # And user specific point required scores module EffectiveCpdAuditReview extend ActiveSupport::Concern module Base def effective_cpd_audit_review include ::EffectiveCpdAuditReview end end module ClassMethods def effective_cpd_audit_review?; true; end end included do acts_as_email_form acts_as_tokened acts_as_reportable if respond_to?(:acts_as_reportable) log_changes(to: :cpd_audit, except: :wizard_steps) if respond_to?(:log_changes) acts_as_wizard( start: 'Start', information: 'Information', instructions: 'Instructions', # Optional based on cpd_audit_level options conflict: 'Conflict of Interest', waiting: 'Waiting on Auditee Submission', files: 'Review Files', statements: 'Review CPD Statements', # ... There will be one step per cpd_statement here. "statement1" questionnaire: 'Review Questionnaire Responses', # ... There will be one step per cpd_audit_level_sections here recommendation: 'Recommendation', submit: 'Submit', submitted: 'Submitted' ) attr_accessor :current_user attr_accessor :current_step # App scoped belongs_to :cpd_audit, polymorphic: true belongs_to :cpd_audit_level, polymorphic: true belongs_to :user, polymorphic: true # Auditor # Effective scoped has_many :cpd_audit_review_items, -> { Effective::CpdAuditReviewItem.sorted }, class_name: 'Effective::CpdAuditReviewItem', inverse_of: :cpd_audit_review, dependent: :destroy accepts_nested_attributes_for :cpd_audit_review_items, reject_if: :all_blank, allow_destroy: true effective_resource do due_date :date, permitted: false # Anonymous Name Mode anonymous_name :string, permitted: false # Required when cpd_audit_level.anonymous? anonymous_number :integer, permitted: false # A unique value # Auditor response conflict_of_interest :boolean conflict_of_interest_reason :text # Rolling comments - internal, reviewer/admin only comments :text # Comments for the auditee to see feedback :text # Recommendation Step recommendation :string # Status Dates submitted_at :datetime, permitted: false # acts_as_statused # I'm not using acts_as_statused yet, but I probably will later status :string, permitted: false status_steps :text, permitted: false # Acts as tokyyened token :string, permitted: false # Wizard Progress wizard_steps :text, permitted: false timestamps end scope :deep, -> { includes(:cpd_audit, :cpd_audit_level, :user) } scope :sorted, -> { order(:id) } scope :available, -> { where(submitted_at: nil) } scope :completed, -> { where.not(submitted_at: nil) } scope :done, -> { where.not(submitted_at: nil) } # effective_reports def reportable_scopes { available: nil, done: nil } end before_validation(if: -> { new_record? }) do self.cpd_audit_level ||= cpd_audit&.cpd_audit_level self.due_date ||= deadline_to_review() end validate(if: -> { recommendation.present? }) do unless cpd_audit_level.recommendations.include?(recommendation) self.errors.add(:recommendation, 'must exist in this audit level') end end with_options(if: -> { cpd_audit_level&.anonymous? }) do before_validation { assign_anonymous_name_and_number } validates :anonymous_name, presence: true validates :anonymous_number, presence: true end after_commit(on: :create) { send_email(cpd_audit.was_completed? ? :cpd_audit_review_ready : :cpd_audit_review_opened) } after_commit(on: :destroy) { cpd_audit.try_review! } def dynamic_wizard_statement_steps @statement_steps ||= auditee_cpd_statements.each_with_object({}) do |cpd_statement, h| h["statement#{cpd_statement.cpd_cycle_id}".to_sym] = "#{cpd_statement.cpd_cycle.to_s} Activities" end end def dynamic_wizard_questionnaire_steps @questionnaire_steps ||= cpd_audit_level.cpd_audit_level_sections.reject(&:skip_review?).each_with_object({}) do |section, h| h["section#{section.position+1}".to_sym] = section.title end end def dynamic_wizard_steps dynamic_wizard_statement_steps.merge(dynamic_wizard_questionnaire_steps) end def wizard_step_title(step) self.class::WIZARD_STEPS[step] || dynamic_wizard_steps.fetch(step) end def required_steps steps = [:start, :information, :instructions] steps << :conflict if cpd_audit_level.conflict_of_interest? if conflict_of_interest? return steps + [:submit, :submitted] end steps += [:waiting] unless ready? steps += [:files] steps += [:statements] + dynamic_wizard_statement_steps.keys steps += [:questionnaire] + dynamic_wizard_questionnaire_steps.keys steps += [:recommendation, :submit, :submitted] steps end def can_visit_step?(step) return (step == :submitted) if completed? # Can only view complete step once submitted can_revisit_completed_steps(step) end end def to_s persisted? ? "#{cpd_audit_level} #{model_name.human} by #{name}" : model_name.human end def name anonymous_name.presence || user.to_s end def anonymous? cpd_audit_level&.anonymous? end # The dynamic CPD Audit Level Sections steps def cpd_audit_level_section(wizard_step) position = (wizard_step.to_s.split('section').last.to_i rescue false) cpd_audit_level.cpd_audit_level_sections.find { |section| (section.position + 1) == position } end # Find or build def cpd_audit_review_item(item) unless item.kind_of?(Effective::CpdAuditResponse) || item.kind_of?(Effective::CpdStatementActivity) raise("expected a cpd_audit_response or cpd_statement_activity") end cpd_audit_review_item = cpd_audit_review_items.find { |cari| cari.item == item } cpd_audit_review_item ||= cpd_audit_review_items.build(item: item) end # The dynamic CPD Statement review steps def auditee_cpd_statements cpd_audit.user.cpd_statements.select do |cpd_statement| cpd_statement.completed? && (submitted_at.blank? || cpd_statement.submitted_at < submitted_at) end end def cpd_statement(wizard_step) cpd_cycle_id = (wizard_step.to_s.split('statement').last.to_i rescue false) auditee_cpd_statements.find { |cpd_statement| cpd_statement.cpd_cycle_id == cpd_cycle_id } end # Called by CpdAudit.extension_granted def extension_granted! self.due_date = deadline_to_review() end # Called by CpdAudit.submit! def ready! send_email(:cpd_audit_review_ready) end # Called by review wizard submit step def submit! update!(submitted_at: Time.zone.now) cpd_audit.try_review! # audit might go from completed->reviewed send_email(:cpd_audit_review_submitted) end # When ready, the applicant review wizard hides the waiting step def ready? cpd_audit&.ready_to_review? end def draft? submitted_at.blank? end def in_progress? submitted_at.blank? end def completed? submitted_at.present? end def done? submitted_at.present? end def email_form_defaults(action) { from: EffectiveCpd.mailer_sender } end def send_email(email) EffectiveCpd.send_email(email, self, email_form_params) unless email_form_skip? true end def deadline_to_conflict_of_interest cpd_audit&.deadline_to_conflict_of_interest end def deadline_to_review return nil unless cpd_audit_level&.days_to_review.present? date = cpd_audit&.deadline_to_submit return nil unless date.present? EffectiveResources.advance_date(date, business_days: cpd_audit_level.days_to_review) end # The name pattern is A23XXX where XXX is an autoincrement def assign_anonymous_name_and_number return if anonymous_name.present? || anonymous_number.present? return if cpd_audit_level.blank? prefix = cpd_audit_level.anonymous_audit_reviews_prefix raise('expected cpd audit level to have an anonymous audit review prefix') unless prefix.present? # Where starts with prefix number = cpd_audit.cpd_audit_reviews.map { |ar| ar.anonymous_number }.compact.max if cpd_audit.new_record? number ||= (self.class.all.where('anonymous_name LIKE ?', "#{prefix}%").maximum('anonymous_number') || 0) number = number + 1 # The next number # Apply prefix and pad number to 3 digits name = prefix + number.to_s.rjust(3, '0') assign_attributes(anonymous_number: number, anonymous_name: name) end end