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