# 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
```