concord-consortium/lara

View on GitHub
app/controllers/api/v1/interactive_pages_controller.rb

Summary

Maintainability
D
3 days
Test Coverage
C
74%
class Api::V1::InteractivePagesController < API::APIController
  layout false
  before_filter :set_interactive_page, except: [
    :get_library_interactives_list,
    :get_wrapping_plugins_list,
    :get_portal_list,
    :get_pages,
    :create_page
  ]
  skip_before_filter :verify_authenticity_token

  ## Queries:
  def get_sections
    render_page_sections_json
  end

  def get_preview_url
    page = @interactive_page
    activity = page.lightweight_activity
    render json: view_context.activity_preview_options(activity, page)
  end

  # This is identical to get_sections. Why use different names?
  # Because it expresses the intent. Its possible we will want to return
  # different responses for each in the future.
  def get_page
    render_page_sections_json
  end

  def get_pages
    activity = LightweightActivity.find(params[:activity_id])
    return error("Can't find activity #{params[:activity_id]}") unless activity
    pages = activity.pages.map do |page|
      generate_page_json page
    end
    render :json => pages
  end

  ## Mutations
  def copy_page
    activity = @interactive_page.lightweight_activity
    authorize! :update, activity
    return error("Can't find required parameter 'dest_index'") unless params[:dest_index]
    return error("Can't find activity for page") unless activity
    position = params[:dest_index]
    next_page = @interactive_page.duplicate
    next_page.lightweight_activity = activity
    next_page.set_list_position(position)
    activity.reload
    next_page.reload
    update_activity_changed_by(activity)
    render :json => generate_page_json(next_page)
  end

  def create_page
    activity = LightweightActivity.find(params[:activity_id])
    authorize! :update, activity
    return error("Can't find activity #{params[:activity_id]}") unless activity
    activity.pages.create()
    last_idx = activity.pages.length - 1
    prev_last_idx = activity.pages.length - 2
    if activity.pages[prev_last_idx].is_completion
      activity.pages[prev_last_idx].position = last_idx
      activity.pages[prev_last_idx].move_lower
    end

    pages = activity.reload.pages.map do |page|
      page.reload
      generate_page_json page
    end
    update_activity_changed_by(activity)
    render :json => pages
  end

  def delete_page
    activity = @interactive_page.lightweight_activity
    authorize! :update, activity
    @interactive_page.destroy
    update_activity_changed_by(activity)
    render :json => ({success: true})
  end

  def update_page
    activity = @interactive_page.lightweight_activity
    authorize! :update, activity, @interactive_page
    page_params = params['page']
    return error("Missing page parameter") if page_params.nil?

    if page_params
      change_keys = 'name is_completion is_hidden show_sidebar sidebar sidebar_title'.split
      # Limit the parameters we accept here, remove nil values, and convert
      # snake case to underscore.
      clean_params = page_params.map { |key, value| [key.to_s.underscore, value] }.to_h
      clean_params = clean_params.reject { |k, v| v.nil? }
      clean_params = clean_params.select { |k, v| change_keys.include?(k) }
      @interactive_page.update_attributes(clean_params)

      if page_params['isCompletion']
        @interactive_page.move_to_bottom
      end
    end
    @interactive_page.save!
    pages = activity.reload.pages.map do |page|
      generate_page_json page
    end
    update_activity_changed_by(activity)
    render :json => pages
  end

  def set_sections
    authorize! :update, @interactive_page
    param_sections = params['sections'] || [] # array of [{:id,:layout}]
    old_sections = @interactive_page.sections
    # Put them in the correct order:
    index = 1
    new_section_ids = param_sections.map { |s| s['id'] }
    new_sections = param_sections.map do |s|
      section = Section.find(s['id'])
      position = index
      if section.position != position
        section.position = position
        section.save!(validate: false)
      end
      index += 1
      section
    end

    # Remove deleted sections:
    old_sections.each do |section|
      unless (new_section_ids.include?(section.id.to_s))
        section.update_attribute(:interactive_page_id, nil)
      end
    end

    @interactive_page.sections = new_sections
    @interactive_page.save!
    update_activity_changed_by(@interactive_page.lightweight_activity)
    render_page_sections_json
  end

  def create_section
    @interactive_page.add_section
    render_page_sections_json
  end

  def copy_section
    authorize! :update, @interactive_page
    section_id = params['section_id']
    return error("Missing section parameter") if section_id.nil?

    section = @interactive_page.sections.find(section_id)
    section.duplicate
    update_activity_changed_by(@interactive_page.lightweight_activity)
    render_page_sections_json
  end

  def copy_page_item
    authorize! :update, @interactive_page
    item_id = params['page_item_id']
    return error("Missing page_item_id parameter") if item_id.nil?

    item = @interactive_page.page_items.find { |i| i.id == item_id.to_i }
    duplicate = item.duplicate
    update_activity_changed_by(@interactive_page.lightweight_activity)
    render json: generate_item_json(duplicate)
  end

  def update_section
    authorize! :update, @interactive_page
    section_params = params['section']

    return error("Missing section parameter") if section_params.nil?
    return error("Missing section[:id] parameter") if section_params['id'].nil?

    section_id = section_params.delete('id')
    section = Section.find(section_id)
    new_items = section_params.delete('items')
    new_item_ids = new_items&.map { |i| i['id'] }
    old_items = section.page_items
    section.update_attributes(section_params)

    # Its OK for update_section to come in without items...
    if new_items.present?
      new_item_ids.compact! # remove nil items
      # Usually we will just be reordering the page_items within the section:
      if section && new_items
        new_items.each do |pi|
          page_item = PageItem.find(pi.delete('id'))
          page_item&.update_attributes({
              column: pi['column'],
              position: pi['position'],
              section: section
            })
        end
      end
    end

    # remove any missing items...
    if new_item_ids && !new_item_ids.empty?
      old_items.each do |item|
        item.update_attributes({ section: nil }) unless new_item_ids.include?(item.id.to_s)
      end
    end
    update_activity_changed_by(@interactive_page.lightweight_activity)
    render_page_sections_json
  end

  def create_page_item
    authorize! :update, @interactive_page

    page_item_params = params["page_item"]
    return error("Missing page_item parameter") if page_item_params.nil?

    # verify the parameters
    section_id = page_item_params["section_id"]
    return error("Missing page_item[section_id] parameter") if section_id.nil?
    section = @interactive_page.sections.where(id: section_id).first
    return error("Invalid page_item[section_id] parameter") if section.nil?
    embeddable_type = page_item_params["embeddable"]
    return error("Missing page_item[embeddable] parameter") if embeddable_type.nil?
    position = page_item_params["position"]
    position = position.to_i unless position.nil?
    column = page_item_params["column"] || PageItem::COLUMN_PRIMARY

    # currently we only support library interactives, iframe interactives, and text blocks, this will change later
    case embeddable_type
    when /LibraryInteractive/
      library_interactive = LibraryInteractive.find_by_serializeable_id(embeddable_type)
      return error("Invalid page_item[embeddable] parameter") if library_interactive.nil?

      embeddable = ManagedInteractive.create!(library_interactive_id: library_interactive.id)
    when /MwInteractive/
      embeddable = MwInteractive.create!
    when /Embeddable::Xhtml/
      embeddable = Embeddable::Xhtml.create!
    when /Plugin_(\d+)::(.*)/
      # Parse the embeddable_type string to get the approved_script id.
      # For plugin items we need to create a new embeddable of type
      # Embeddable::Plugin and then associate it with the plugin.
      # 1. Create a plugin with the instance of the approved_script
      # 2. Create a Embeddable::EmbeddablePlugin
      # Example: "Plugin_10::windowShade"
      # IMPORTANT: The tip type, "windowShade" in the above example, as
      # the component_label is critical.
      tip_type = $2
      regex = /Plugin_(\d+)::#{tip_type}/
      script_id = embeddable_type.match(regex)[1]
      author_data = { tipType: tip_type }.to_json
      wrapped_embeddable_id = page_item_params["wrapped_embeddable_id"]
      wrapped_embeddable_type = page_item_params["wrapped_embeddable_type"]
      # I am following the convention I saw in interactive_pages_controller.rb
      embeddable = Embeddable::EmbeddablePlugin.create!
      embeddable.approved_script_id = script_id
      embeddable.author_data = author_data
      embeddable.component_label = tip_type
      embeddable.is_half_width = false
      if wrapped_embeddable_id && wrapped_embeddable_type
        embeddable.embeddable_id = wrapped_embeddable_id
        embeddable.embeddable_type = wrapped_embeddable_type
      end
      embeddable.save!
    else
      return error("Unknown embbeddable_type: #{embeddable_type}\nOnly library interactive embeddables, iFrame interactives, and text blocks are currently supported")
    end

    @interactive_page.add_embeddable(embeddable, position, section, column)
    @interactive_page.reload

    embeddable.reload
    pi = embeddable.p_item
    result = {
      id: pi.id.to_s,
      column: pi.column,
      position: pi.position,
      type: pi.embeddable_type,
      data: pi.embeddable.to_hash # using pi.embeddable.to_interactive here broke editing/saving by sending unnecessary/incorrect data back
    }
    update_activity_changed_by(@interactive_page.lightweight_activity)
    render json: result.to_json
  end

  def update_page_item
    authorize! :update, @interactive_page

    page_item_params = params["page_item"]
    return error("Missing page_item parameter") if page_item_params.nil?

    # verify the parameters
    page_item_id = page_item_params["id"]
    return error("Missing page_item[id] parameter") if page_item_id.nil?
    column = page_item_params["column"]
    return error("Missing page_item[column] parameter") if column.nil?
    position = page_item_params["position"]
    return error("Missing page_item[position] parameter") if position.nil?
    data = page_item_params["data"]
    return error("Missing page_item[data] parameter") if data.nil?
    type = page_item_params["type"]
    return error("Missing page_item[type] parameter") if type.nil?

    page_item = PageItem.find(page_item_id)
    if page_item
      new_attr = {
        column: column,
        position: position
      }
      page_item.update_attributes(new_attr)
      embeddable_type = type.constantize
      embeddable = embeddable_type.find(page_item.embeddable_id)
      # linked_interactives param follows ISetLinkedInteractives interface format. It isn't a regular attribute.
      # It requires special treatment and should be removed from params before .update_attributes is called.
      if data.has_key? :linked_interactives
        linked_interactives = data.delete :linked_interactives
        if linked_interactives.present?
          page_item.set_linked_interactives(JSON.parse(linked_interactives))
        end
      end
      if embeddable
        embeddable.update_attributes(data)
      end
    end
    @interactive_page.reload
    update_activity_changed_by(@interactive_page.lightweight_activity)
    render json: embeddable_to_edit_hash(embeddable).to_json
  end

  def delete_page_item
    authorize! :update, @interactive_page

    page_item_id = params["page_item_id"]
    return error("Missing page_item_id parameter") if page_item_id.nil?
    changed = false

    # The page_item must be in the current page:
    @interactive_page.page_items.each do |page_item|
      if page_item.id.to_s == page_item_id.to_s
        changed = true
        page_item.destroy
      end
    end
    @interactive_page.reload if changed
    update_activity_changed_by(@interactive_page.lightweight_activity) if changed
    render_page_sections_json
  end

  def get_portal_list
    portals = []
    Concord::AuthPortal.all.each_pair do |key, portal|
      name = portal.link_name
      path = user_omniauth_authorize_path(portal.strategy_name)
      portals.push({:name => name, :path => path})
    end

    render :json => {
      success: true,
      portals: portals
    }
  end

  def get_wrapping_plugins_list
    plugins = []
    available_plugins = ApprovedScript.authoring_menu_items("embeddable-decoration")
    available_plugins.each do |plugin|
      component_label = plugin.component_label
      component_name = plugin.component_name
      id = plugin.approved_script_id
      name = plugin.name
      plugins.push({
        :component_label => component_label,
        :component_name => component_name,
        :id => id,
        :name => name
      })
    end

    render :json => {
      success: true,
      plugins: plugins
    }
  end

  private
  def get_teacher_edition_plugins
    required_version = [3]
    required_label = ["teacherEditionTips"]
    ApprovedScript.where(:version => required_version, :label => required_label)
  end

  private
  def map_plugin_to_hash(plugin)
    {
      id: plugin.id,
      name: plugin.name,
      label: plugin.label,
      description: plugin.description,
      version: plugin.version,
      authoring_metadata: JSON.parse(plugin.authoring_metadata)
    }
  end

  public
  def get_library_interactives_list
    library_interactives = LibraryInteractive
      .select("library_interactives.*, CONCAT('LibraryInteractive_', library_interactives.id) as serializeable_id, count(managed_interactives.id) as query_use_count, UNIX_TIMESTAMP(library_interactives.created_at) as date_added")
      .joins("LEFT JOIN managed_interactives ON managed_interactives.library_interactive_id = library_interactives.id")
      .group('library_interactives.id')

    plugins = get_teacher_edition_plugins.map { |plugin| map_plugin_to_hash(plugin) }

    library_interactives_list = library_interactives.map do |library_interactive|
      li_json = library_interactive.as_json
      li_json["use_count"] = li_json["query_use_count"]
      li_json.delete("query_use_count")
      li_json
    end

    render :json => {
      success: true,
      library_interactives: library_interactives_list,
      plugins: plugins
    }
  end

  def get_interactive_list
    begin
      authorize! :read, @interactive_page

      scope = params[:scope] || "page"
      supports_snapshots = params[:supportsSnapshots]

      if scope != "page"
        raise "Invalid scope parameter: #{scope}"
      end

      interactives = @interactive_page
        .interactive_page_items
        .select do |pi|
          i = pi.embeddable
          case supports_snapshots
          when "true"
            !i.no_snapshots
          when "false"
            i.no_snapshots
          else
            # supportsSnapshots is optional
            true
          end
        end
        .map do |pi|
          i = pi.embeddable
          {
            id: "interactive_#{pi.id}",
            embeddableId: pi.embeddable_id,
            pageId: @interactive_page.id,
            name: i.name,
            section: pi.section != nil ? pi.section.title : Section::DEFAULT_SECTION_TITLE,
            url: i.url,
            thumbnailUrl: i.thumbnail_url,
            supportsSnapshots: !i.no_snapshots
          }
        end

      render :json => { :success => true, interactives: interactives}

    rescue CanCan::AccessDenied
      return render :json => { :success => false, :message => "You are not authorized to get the interactive list from the requested page"}
    rescue => error
      return render :json => { :success => false, :message => error.message}
    end
  end

  private
  def generate_item_json(page_item)
    embeddable = page_item.embeddable
    if (embeddable.nil?)
      return { error: "WARNING: page_item #{page_item.id} has no embeddable" }
    end
    embeddable_to_edit_hash(embeddable)
  end

  def generate_section_json(section)
    {
      can_collapse_small: section.can_collapse_small,
      id: section.id.to_s,
      items: section.page_items.map { |pi| generate_item_json(pi) },
      layout: section.layout,
      name: section.name,
      position: section.position,
      show: section.show
    }
  end

  def generate_page_json(page)
    sections = page.sections.map { |s| generate_section_json(s) }
    {
      id: page.id.to_s,
      name: page.name,
      isCompletion: page.is_completion,
      isHidden: page.is_hidden,
      showSidebar: page.show_sidebar,
      sidebar: page.sidebar,
      sidebarTitle: page.sidebar_title,
      position: page.position,
      sections: sections
    }
  end

  def render_page_sections_json(page=@interactive)
    render :json => generate_page_json(@interactive_page)
  end

  def set_interactive_page
    begin
      @interactive_page = InteractivePage.find(params['id'])
    rescue ActiveRecord::RecordNotFound
      render :json => { :success => false, :message => "Could not find interactive page ##{params['id']}"}
    end
  end

  def embeddable_to_edit_hash(embeddable)
    embeddable.reload
    pi = embeddable.p_item
    e = pi.embeddable
    # EM & NP ~2021: using pi.embeddable.to_interactive here broke editing/saving
    #   by sending unnecessary/incorrect data back
    data_hash = e.to_hash

    # NP 2022-05-27: we need to allow embeddables to customize their editing
    #   hash
    data_hash = e.to_editing_hash if e.respond_to?(:to_editing_hash)

    {
      id: pi.id.to_s,
      column: pi.column,
      position: pi.position,
      type: pi.embeddable_type,
      data: data_hash,
      authoring_api_urls: embeddable.respond_to?(:authoring_api_urls) ? embeddable.authoring_api_urls(request.protocol, request.host_with_port) : {}
    }
  end
end