---
# System prepended metadata

title: Code snippets for Railwaymen

---

# Code snippets for Railwaymen
Marcel Wojdyło, 6/01/2022

## Content container model for an e-learning app

This code comes from an application whose main use was to store and deliver e-learning content.

The model below is the basic building block of an e-learning course. It may take on a few different types depending on the type of content that the user wants to use it for. It may contain plain text, have a file attached, contain a SCORM course, a video, etc. Apart from methods that handle the various attachment types, this model also contains methods to trigger various workers that interact with the API used for handling SCORM files. 

(**SCORM** is the standard file format for interactive presentations used in  e-learning solutions.)

```rb
class Element < ApplicationRecord
  acts_as_list scope: :activity
  belongs_to :activity
  belongs_to :last_edited_by, optional: true, class_name: 'User', foreign_key: :last_edited_by_id
  delegate :learning_journey, to: :activity
  delegate :phase, to: :activity

  enum element_type: [:plain_text, :scorm, :file_container, :video, :task, :learning_guide]
  ELEMENT_TYPES = element_types.to_h { |k, _v| [k.to_sym, k] }

  def self.get_element_types
    ELEMENT_TYPES
  end

  def self.get_human_element_types
    ELEMENT_TYPES.to_h { |k, _v| [Element.human_enum_name(:element_types, k), k] }
  end

  has_many :registrations, dependent: :destroy
  has_many :users, through: :registrations
  has_many :element_completions, as: :completeable, dependent: :destroy
  has_many :users, through: :completions
  has_one_attached :file
  has_one_attached :video
  has_one_attached :scorm_zip
  has_one_attached :learning_guide
  validates_presence_of :element_type
  validates_presence_of :plain_text_content, if: proc { |element| element.element_type.eql? 'plain_text' }
  validates_presence_of :file, if: proc { |element| element.element_type.eql? 'file_container' }
  validates_presence_of :video, if: proc { |element| element.element_type.eql? 'video' }
  validates_presence_of :scorm_zip, if: proc { |element| element.element_type.eql? 'scorm' }
  validates_presence_of :learning_guide, if: proc { |element| element.element_type.eql? 'learning_guide' }
  validate :type_for_plain_text_content
  validate :extensions_in_guides_and_templates
  validates :video, content_type: { in: [:mp4], message: I18n.t('errors.element.video_attachment_type_invalid') }
  validates :scorm_zip, content_type: { in: ELEMENT_SCORM_ZIP_ACCEPT_FORMATS, message: I18n.t('errors.element.scorm_attachment_type_invalid') }
  validate :type_not_changed

  enum status: [:pending, :uploading, :upload_failed, :uploaded, :setting_course_id, :setting_course_id_failed, :ready]
  STATUSES = statuses.to_h { |k, _v| [k.to_sym, k] }.freeze
  def self.get_statuses
    STATUSES
  end

  def self.get_human_statuses
    STATUSES.to_h { |k, _v| [Element.human_enum_name(:statuses, k), k] }
  end

  def type_not_completable?
    NOT_COMPLETABLE_ELEMENT_TYPES.include? element_type
  end

  def allows_attachments?
    element_type.in?([Element.get_element_types[:scorm], Element.get_element_types[:file_container], Element.get_element_types[:video], Element.get_element_types[:learning_guide]])
  end

  def can_create_course?
    status.in?([Element.get_statuses[:pending], Element.get_statuses[:upload_failed]])
  end

  def failed_scorm_import?
    status.eql?(Element.get_statuses[:setting_course_id_failed])
  end

  def display_name
    "#{I18n.t('activerecord.models.element.one')} #{id}"
  end

  def type_for_plain_text_content
    errors.add(:base, I18n.t('errors.element.type_for_plain_text_content')) if plain_text_content.present? && element_type != 'plain_text'
  end

  def extensions_in_guides_and_templates
    allowed_extensions = %r{\.(pdf|doc|docx|xls|xlsx|ppt|pptx)$}i
    case element_type
    when 'file_container'
      return unless file.attached?

      errors.add(:file, I18n.t('errors.element.file_attachment_type_invalid')) if file.filename.to_s !~ allowed_extensions
    when 'learning_guide'
      return unless learning_guide.attached?

      errors.add(:learning_guide, I18n.t('errors.element.file_attachment_type_invalid')) if learning_guide.filename.to_s !~ allowed_extensions
    end
  end

  def create_course
    update(status: Element.statuses[:uploading])
    ScormCreateCourseWorker.perform_async(id)
  end

  def set_course
    ScormSetCourseWorker.perform_async(id)
  end

  def create_registration(user_id)
    registration = Registration.create_or_find_by(element_id: id, user_id: user_id)
    registration.update(status: Registration.statuses[:creating_cloud_registration])
    ScormRegistrationWorker.perform_async(id, user_id)
  end

  def generate_launch_link(user_id)
    ScormRegistrationLinkService.new(id, user_id).generate_link
  end

  def upload_scorm_course
    update(import_job_id: ScormCloud::CourseService.new(self).create)
  end

  def type_not_changed
    errors.add(:element_type, I18n.t('errors.element.type_not_changed')) if element_type_changed? && persisted?
  end
end
```

## ApplicationController for records in an e-learning app
This is the controller that was used for the Element records outlined in the code snippet above. It uses CanCanCan to authorize the various actions appropriately. It allows users of various types to access CRUD functionality for these Elements. It also contains logic for triggering the importing of SCORM files into the API that was used to handle them.

```rb
class ElementsController < ApplicationController
  load_and_authorize_resource :learning_journey
  load_and_authorize_resource :phase, through: :learning_journey
  load_and_authorize_resource :activity, through: :phase
  load_and_authorize_resource through: :activity
  skip_load_and_authorize_resource only: :scorm_course_import_status

  before_action :elements, only: [:destroy, :create_scorm_course]

  def show
    if @element.video?
      respond_to do |format|
        format.js { render 'elements/video_player.js.erb', layout: false }
      end
    else
      respond_to do |format|
        format.js { render 'elements/show.js.erb', layout: false }
      end
    end
  end

  def new
    respond_to do |format|
      format.js { render 'elements/new.js.erb', layout: false }
    end
  end

  def create
    @element ||= Element.new(element_params)
    @element.activity = @activity
    if @element.save
      @element.create_course if @element.scorm?
      elements
      if @element.allows_attachments?
        @action = 'new_after_upload'
        respond_to do |format|
          format.js { render 'elements/new.js.erb', layout: false }
        end
      else
        respond_to do |format|
          format.js { render 'activities/edit.js.erb', layout: false }
        end
      end
    else
      respond_to do |format|
        format.js { render 'elements/new.js.erb', layout: false }
      end
    end
  end

  def edit
    respond_to do |format|
      format.js { render 'elements/edit.js.erb', layout: false }
    end
  end

  def update
    if @element.update(element_params)
      elements
      # When retrying SCORM upload and file is a valid ZIP
      if @element.scorm? && @element.setting_course_id_failed?
        @action = 'new_after_upload'
        @element.create_course
        partial_to_render = 'elements/new.js.erb'
      else
        partial_to_render = 'activities/edit.js.erb'
      end
    # When retrying SCORM upload and file is not a valid ZIP
    elsif @element.scorm? && @element.setting_course_id_failed?
      @action = 'new_after_upload'
      # Reset Element status to trigger correct UI in partial
      @element.update(status: Element.get_statuses[:pending])
      partial_to_render = 'elements/new.js.erb'
    else
      partial_to_render = 'elements/edit.js.erb'
    end
    respond_to do |format|
      format.js { render partial_to_render, layout: false }
    end
  end

  def destroy
    @element.destroy
    respond_to do |format|
      format.js { render 'activities/edit.js.erb', layout: false }
    end
  end

  def cancel_element_creation
    @element.destroy
    respond_to do |format|
      format.js { render js: "window.location = '#{learning_journey_path(@learning_journey)}';" }
    end
  end

  def sort
    params[:element].each_with_index do |id, index|
      Element.where(id: id).update_all(position: index + 1)
    end

    head :ok
  end

  def create_scorm_course
    @element.create_course
    respond_to do |format|
      format.js { render 'activities/edit.js.erb', layout: false }
    end
  end

  def set_course
    @element.set_course
    redirect_to learning_journey_phase_activity_path(@learning_journey, @phase, @activity), notice: t('.notice')
  end

  def scorm_course_import_status
    @element = Element.find(params[:element_id])
    authorize! :scorm_course_import_status, @element
    render json: { status: @element.status }
  end

  def video_player
    respond_to do |format|
      format.js { render 'elements/video_player.js.erb', layout: false }
    end
  end

  private

  def element_params
    params.require(:element).permit(:element_type, :title, :description, :plain_text_content, :file, :video, :scorm_zip, :learning_guide).merge(last_edited_by_param)
  end

  def elements
    @elements ||= Element.where(activity_id: @activity.id).order('position')
  end
end
```

## External API service for comparing faces
The code below comes from an application used for verifying ID documents and comparing them with a provided portrait. This service contacts the Face++ API to compare the faces in two provided images, a selfie and the portrait extracted from the supplied ID document.

```rb
class FaceppService
  def initialize(verification_attempt)
    @verification_attempt = verification_attempt
    @selfie = Base64.encode64(verification_attempt.selfie.download)
    @portrait = Base64.encode64(verification_attempt.document_portrait.download)
  rescue Module::DelegationError
    @no_attachments = true
  end

  def compare
    return true if @no_attachments

    connection = Faraday.new('https://api-us.faceplusplus.com/facepp/v3/')
    api_endpoint = 'compare'

    response = connection.post(
      api_endpoint,
      api_key: FACEPP_API_KEY,
      api_secret: FACEPP_API_SECRET,
      image_base64_1: @selfie,
      image_base64_2: @portrait
        ) do |req|
      req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
    end

    error = JSON.parse(response.body)['error_message']
    confidence = JSON.parse(response.body)['confidence'].to_f
    raise FaceppService.new response.status, response.body, error if error

    confidence > FACEPP_CONFIDENCE_THRESHOLD
  end

  class FaceppService < StandardError
    def initialize(http_status, response_body, response_status)
      msg = "
          HTTP status: #{http_status}
          Response status: #{response_status}
          Response body: #{response_body}
        "
      super(msg)
    end
  end
end
```

