concord-consortium/lara

View on GitHub
app/models/interactive_page.rb

Summary

Maintainability
C
1 day
Test Coverage
B
87%
class InteractivePage < ActiveRecord::Base
  attr_accessible :lightweight_activity, :name, :position, :layout, :sidebar, :show_header,
                  :show_sidebar, :show_interactive, :show_info_assessment, :toggle_info_assessment,
                  :embeddable_display_mode, :sidebar_title, :is_hidden, :additional_sections, :is_completion

  serialize :additional_sections

  belongs_to :lightweight_activity, :class_name => 'LightweightActivity', :touch => true,
    :inverse_of => :pages

  acts_as_list :scope => :lightweight_activity

  LAYOUT_OPTIONS = [{ :name => 'Full Width',               :class_val => 'l-full-width' },
                    { :name => '60-40',                    :class_val => 'l-6040' },
                    { :name => '70-30',                    :class_val => 'l-7030' },
                    { :name => '60-40 (interactive left)', :class_val => 'r-4060' },
                    { :name => '70-30 (interactive left)', :class_val => 'r-3070' },
                    { :name => 'Responsive', :class_val => 'l-responsive' }]

  EMBEDDABLE_DISPLAY_OPTIONS = ['stacked','carousel']

  HEADER_BLOCK = Section::HEADER_BLOCK
  INTERACTIVE_BOX = Section::INTERACTIVE_BOX

  validates :sidebar_title, presence: true
  validates :layout, :inclusion => { :in => LAYOUT_OPTIONS.map { |l| l[:class_val] } }
  validates :embeddable_display_mode, :inclusion => { :in => EMBEDDABLE_DISPLAY_OPTIONS }

  # Reject invalid HTML inputs
  # See https://www.pivotaltracker.com/story/show/60459320
  validates :sidebar, :html => true

  # PageItem is a join model; if this is deleted, it should go too
  # has_many :page_items, :order => [:old_section, :position], :dependent => :destroy, :include => [:embeddable]

  has_many :sections, order: :position, include: [:page_items]
  has_many :legacy_page_items, :class_name => "PageItem", :conditions => "embeddable_type in ('Embeddable::MultipleChoice','Embeddable::OpenResponse','ImageInteractive','VideoInteractive','Embeddable::ImageQuestion')"

  # NP: 2021-09-01 TODO: This has-many was incorrectly ordering page_items.
  # I don't think it really matters B/C our sections include the page_items.
  # has_many :page_items, through: :sections, order: "sections.position, position"
  def page_items
    sections.map(&:page_items).flatten
  end

  def toggle_info_assessment
    self[:toggle_info_assessment].nil? ? true : self[:toggle_info_assessment]
  end

  @registered_sections = []
  def self.registered_sections
    @registered_sections
  end
  def self.register_section(s)
    @registered_sections.push(s)
  end

  # Interactive page can register additional page sections:
  #
  #  InteractivePage.register_page_section({name: 'FooBar', dir: 'foo_bar', label: 'Foo Bar'})
  #
  # Each section is required to provide two partials:
  # - #{dir}/_author.html.haml
  # - #{dir}/_runtime.html.haml
  #
  # Note that content of the section is totally flexible.
  # If you want to display some embeddables in section, you can provide 'section' argument to
  # .add_embeddable method and then obtain section embeddables using .section_embeddables method.
  @registered_additional_sections = []
  def self.registered_additional_sections
    @registered_additional_sections
  end

  def self.register_additional_section(s)
    @registered_sections.push(s)
    @registered_additional_sections.push(s)
    # Let client code use .update_attributes methods (and similar) to set show_<section_name>.
    attr_accessible "show_#{s[:name]}"
    # show_<section_name> getter:
    define_method("show_#{s[:name]}") do
      additional_sections && additional_sections[s[:name]]
    end
    # show_<section_name> setter:
    define_method("show_#{s[:name]}=") do |v|
      self.additional_sections ||= {}
      # Handle parameter values from Rails forms.
      if v == "1" || v == 1 || v == true
        additional_sections[s[:name]] = true
      else
        additional_sections.delete(s[:name])
      end
    end
  end

  # The order of these register_ calls matters. It determines the order of the embeddables.
  InteractivePage.register_section({name: HEADER_BLOCK, show_method: 'show_header'})
  InteractivePage.register_section({name: nil, show_method: 'show_info_assessment'})

  # Register additional sections of interactive page.
  # This code could (or should) to some config file or initializer, but as we
  # have only one additional section at the moment, let's keep it here.
  InteractivePage.register_additional_section({name:  CRater::ARG_SECTION_NAME,
                                               dir:   CRater::ARG_SECTION_DIR,
                                               label: CRater::ARG_SECTION_LABEL,
                                               show_method: 'show_' + CRater::ARG_SECTION_NAME})

  InteractivePage.register_section({name: INTERACTIVE_BOX, show_method: 'show_interactive'})

  # This is a sort of polymorphic has_many :through (which is forbidden in AR)
  def embeddables
    page_items.map(&:embeddable)
  end

  def interactives
    embeddables.select{ |e| Embeddable::is_interactive?(e) }
  end

  def interactive_page_items
    page_items.select{ |pi| Embeddable::is_interactive?(pi.embeddable) }
  end

  def section_embeddables(section)
    # For now, we continue to support passing a section name as a string to
    # this method, but this legacy feature will be removed in the future and
    # passing the section itself will become the only option.
    if !section.instance_of? Section
      section = sections.find { |s| s.title == section }
    end

    section_embeddables = []
    if section
      # If a section's primary column is on the right, we need to adjust the
      # embeddables' order so they're output in the same order they appear on
      # the activity page.
      primary_right_layouts = ["40-60", "30-70", "responsive-2-columns"]
      if primary_right_layouts.include? section.layout
        secondary_column_items = section.page_items.where(:column => "secondary").order(:position)
        primary_column_items = section.page_items.where(:column => "primary").order(:position)
        page_items = secondary_column_items + primary_column_items
        section_embeddables = page_items.map { |i| i.embeddable }
      else
        section_embeddables = section.page_items.map { |i| i.embeddable }
      end
    end
    section_embeddables
  end

  def visible_embeddables
    results = []
    sections.each do |s|
      results += section_visible_embeddables(s)
    end
    results
  end

  def visible_interactives
    interactives.select{ |e| !e.is_hidden }
  end

  def section_visible_embeddables(section)
    section_embeddables(section).select{ |e| !e.is_hidden }
  end

  def main_visible_embeddables
    # Visible embeddables that do not have section specified (nil section).
    section_visible_embeddables(Section::DEFAULT_SECTION_TITLE)
  end

  # This function will no longer work. There is no longer a header block
  # section, and passing a section title to section_embeddables is no
  # longer supported.
  def header_block_embeddables
    section_embeddables(HEADER_BLOCK)
  end

  # This function will no longer work. There is no longer a header block
  # section, and passing a section title to section_embeddables is no
  # longer supported.
  def header_block_visible_embeddables
    section_visible_embeddables(HEADER_BLOCK)
  end

  # This function will no longer work. There is no longer an interactive box
  # section, and passing a section title to section_embeddables is no
  # longer supported.
  def interactive_box_embeddables
    section_embeddables(INTERACTIVE_BOX)
  end

  # This function will no longer work. There is no longer an interactive box
  # section, and passing a section title to section_embeddables is no
  # longer supported.
  def interactive_box_visible_embeddables
    section_visible_embeddables(INTERACTIVE_BOX)
  end

  def reportable_items
    visible_embeddables.select { |item| item.reportable? }
  end

  # 2021-08-05 NP: Lets continue to add embeddables this way and
  ## look for a section identifier value
  def add_embeddable(embeddable, position = nil, section_identifier = nil, column = PageItem::COLUMN_PRIMARY)

    section_identifier ||= Section::DEFAULT_SECTION_TITLE

    # look for a section specified by instance or title
    if section_identifier.is_a?(Section)
      section = section_identifier
    else
      section = sections.find { |s| s.title == section_identifier }
      unless section
        section = sections.create(Section::DEFAULT_PARAMS.merge({title: section_identifier}))
        section.move_to_top if section.title == Section::HEADER_BLOCK
      end
    end

    section ||= self.sections.create(Section::DEFAULT_PARAMS)

    page_item = section.page_items.create!(embeddable: embeddable, position: position, column: column)

    if (position)
      page_item.insert_at(position)
    else
      page_item.move_to_bottom
    end
  end

  # 2021-08-05 NP: Add a new section.
  def add_section(position = nil, section_type = nil)
    section = self.sections.create(Section::DEFAULT_PARAMS)
    section.move_to_bottom
  end


  def add_interactive(interactive, position = nil, validate = true)
    self[:show_interactive] = true
    self.save!(validate: validate)
    add_embeddable(interactive, position, INTERACTIVE_BOX)
  end

  def next_visible_page
    lightweight_activity.visible_pages.where('position > ?', position).first
  end

  def prev_visible_page
    lightweight_activity.visible_pages.where('position < ?', position).last
  end

  def first_visible?
    !is_hidden && prev_visible_page == nil
  end

  def last_visible?
    !is_hidden && next_visible_page == nil
  end

  def visible_sections
    return [] unless additional_sections
    self.class.registered_additional_sections.select { |s| additional_sections[s[:name]] }
  end

  def to_hash
    # Intentionally leaving out:
    # - lightweight_activity association will be added there
    # - user will get the new user
    # - Associations will be done later
    {
      name: name,
      position: position,
      layout: layout,
      is_hidden: is_hidden,
      sidebar: sidebar,
      sidebar_title: sidebar_title,
      show_header: show_header,
      show_sidebar: show_sidebar,
      show_interactive: show_interactive,
      show_info_assessment: show_info_assessment,
      toggle_info_assessment: toggle_info_assessment,
      embeddable_display_mode: embeddable_display_mode,
      additional_sections: additional_sections,
      is_completion: is_completion
    }
  end

  def set_list_position(index)
    # Overloads the acts_as_list version
    self.position = index
    self.save!(:validate => false) # This is the part we need to override
  end

  def page_number
    lightweight_activity.visible_page_ids.index(self.id) + 1
  end

  def duplicate(helper=nil)
    helper = LaraDuplicationHelper.new if helper.nil?
    new_page = InteractivePage.new(to_hash)

    InteractivePage.transaction do
      new_page.save!(validate: false)
      new_sections = sections.map { |s| s.duplicate(helper, new_page.id) }
    end

    new_page.reload
  end

  def export
    page_json = self.as_json(only: [:id,
                                    :name,
                                    :position,
                                    :layout,
                                    :is_hidden,
                                    :sidebar,
                                    :sidebar_title,
                                    :show_header,
                                    :show_sidebar,
                                    :show_interactive,
                                    :show_info_assessment,
                                    :toggle_info_assessment,
                                    :embeddable_display_mode,
                                    :additional_sections,
                                    :is_completion])
    page_json[:sections] = []

    sections.each do |section|
      page_json[:sections] << section.export
    end

    page_json
  end

  def self.extract_from_hash(page_json_object)
    #pages = activity_json_object[:pages]
    import_simple_attributes = [
      :name,
      :position,
      :layout,
      :is_hidden,
      :sidebar,
      :sidebar_title,
      :show_header,
      :show_sidebar,
      :show_interactive,
      :show_info_assessment,
      :toggle_info_assessment,
      :embeddable_display_mode,
      :is_completion
    ]

    attributes = {}
    import_simple_attributes.each do |key|
      attributes[key] = page_json_object[key] if page_json_object.has_key?(key)
    end

    attributes
  end

  def self.import(page_json_object, helper=nil, version=nil)
    helper = LaraSerializationHelper.new if helper.nil?
    import_page = InteractivePage.new(self.extract_from_hash(page_json_object))

    InteractivePage.transaction do
      import_page.save!(validate: false)

      # import :additional_sections from page_json_object.
      # It will be a hash looking like this {:arg_block => true }
      # but we need string keys, eg: {"arg_block" => true }
      if page_json_object[:additional_sections]
        page_json_object[:additional_sections].each do |k,v|
          import_page.additional_sections ||= {}
          import_page.additional_sections[k.to_s] = v
        end
      end

      # if explicit version not set figure it out from the json
      version = version || (page_json_object.has_key?(:sections) ? 2 : 1)

      if version == 1 
        # First, import and cache all the embeddables.
        page_json_object[:embeddables].each do |embed_hash|
          embed = helper.import(embed_hash[:embeddable])
          section = embed_hash[:section]
          import_page.add_embeddable(embed, nil, section)
        end

        # Now when all the objects are created, setup references (e.g. question pointing to interactive, or
        # one embeddable pointing to another one).
        page_json_object[:embeddables].each do |embed_hash|
          helper.set_references(embed_hash[:embeddable])
        end
      elsif version == 2
        page_json_object[:sections].each do |s|
          # First create the section and add it to the page at the bottom to keep it in the same order as the export
          new_section = import_page.sections.create(Section::DEFAULT_PARAMS.merge({title: s[:title]}))
          new_section.move_to_bottom
    
          # Then, import and cache all the embeddables.
          s[:embeddables].each do |embed_json_obj|
            embed = helper.import(embed_json_obj.except(:column, :position))
            import_page.add_embeddable(embed, embed_json_obj[:position], new_section, embed_json_obj[:column])
          end
  
          # Now when all the objects are created, setup references (e.g. question pointing to interactive, or
          # one embeddable pointing to another one).
          s[:embeddables].each do |embed_json_obj|
            helper.set_references(embed_json_obj)
          end
        end
      end

      # For older export files, if page intro exists, add it as a new embeddable in header_block
      import_legacy_intro(import_page, page_json_object[:text])
    end

    import_page
  end

  def self.import_legacy_intro(import_page, intro_text)
    if intro_text
      helper = LaraSerializationHelper.new if helper.nil?
      intro_embeddable_hash = {
                         "content": intro_text,
                         "is_half_width": false,
                         "is_hidden": false,
                         "name": "",
                         "type": "Embeddable::Xhtml"
                        }
      intro_embeddable = helper.import(intro_embeddable_hash)
      import_page.show_header = true
      import_page.add_embeddable(intro_embeddable, 1, HEADER_BLOCK)
    end
  end
end