AlchemyCMS/alchemy_cms

View on GitHub
app/controllers/alchemy/admin/pages_controller.rb

Summary

Maintainability
B
4 hrs
Test Coverage
A
94%
# frozen_string_literal: true

module Alchemy
  module Admin
    class PagesController < ResourcesController
      include OnPageLayout::CallbacksRunner

      helper "alchemy/pages"

      before_action :load_resource, except: [:index, :flush, :new, :create, :copy_language_tree, :link]

      authorize_resource class: Alchemy::Page, except: [:index, :tree]

      before_action only: [:index, :tree, :flush, :new, :create, :copy_language_tree] do
        authorize! :index, :alchemy_admin_pages
      end

      include Alchemy::Admin::CurrentLanguage

      before_action :set_translation,
        except: [:show]

      before_action :set_root_page,
        only: [:index, :show]

      before_action :set_preview_mode, only: [:show]

      before_action :load_languages_and_layouts,
        unless: -> { @page_root },
        only: [:index]

      before_action :set_view, only: [:index, :update]

      before_action :set_page_version, only: [:show, :edit]

      before_action :run_on_page_layout_callbacks,
        if: :run_on_page_layout_callbacks?,
        only: [:show]

      def index
        @query = @current_language.pages.contentpages.ransack(search_filter_params[:q])

        if @view == "list"
          @query.sorts = default_sort_order if @query.sorts.empty?
          items = @query.result

          if search_filter_params[:tagged_with].present?
            items = items.tagged_with(search_filter_params[:tagged_with])
          end

          if search_filter_params[:filter].present?
            items = apply_filters(items)
          end

          items = items.page(params[:page] || 1).per(items_per_page)
          @pages = items
        end
      end

      # Returns all pages as a tree from the root given by the id parameter
      #
      def tree
        render json: serialized_page_tree
      end

      # Used by page preview iframe in Page#edit view.
      #
      def show
        Current.preview_page = @page
        # Setting the locale to pages language, so the page content has it's correct translations.
        ::I18n.locale = @page.language.locale
        render(layout: Alchemy::Config.get(:admin_page_preview_layout) || "application")
      end

      def info
        render layout: !request.xhr?
      end

      def new
        @page ||= Page.new(layoutpage: params[:layoutpage] == "true", parent_id: params[:parent_id])
        @page_layouts = Page.layouts_for_select(@current_language.id, layoutpages: @page.layoutpage?)
        @clipboard = get_clipboard("pages")
        @clipboard_items = Page.all_from_clipboard_for_select(@clipboard, @current_language.id, layoutpages: @page.layoutpage?)
      end

      def create
        @page = paste_from_clipboard || Page.new(page_params)
        if @page.save
          flash[:notice] = Alchemy.t("Page created", name: @page.name)
          do_redirect_to(redirect_path_after_create_page)
        else
          new
          render :new
        end
      end

      # Edit the content of the page and all its elements and ingredients.
      #
      # Locks the page to current user to prevent other users from editing it meanwhile.
      #
      def edit
        # fetching page via before filter
        if page_is_locked?
          flash[:warning] = Alchemy.t("This page is locked", name: @page.locker_name)
          redirect_to admin_pages_path
        elsif page_needs_lock?
          @page.lock_to!(current_alchemy_user)
        end
        @preview_urls = Alchemy.preview_sources.map do |klass|
          [
            klass.model_name.human,
            klass.new(routes: Alchemy::Engine.routes).url_for(@page)
          ]
        end
        @preview_url = @preview_urls.first.last
        @layoutpage = @page.layoutpage?
        Alchemy::Current.language = @page.language
      end

      # Set page configuration like page names, meta tags and states.
      def configure
      end

      # Updates page
      #
      # * fetches page via before filter
      #
      def update
        @old_parent_id = @page.parent_id
        if @page.update(page_params)
          @notice = Alchemy.t("Page saved", name: @page.name)
          @while_page_edit = request.referer.include?("edit")

          if @view == "list"
            flash[:notice] = @notice
          end

          unless @while_page_edit
            @tree = serialized_page_tree
          end
        else
          render :configure, status: :unprocessable_entity
        end
      end

      # Fetches page via before filter, destroys it and redirects to page tree
      def destroy
        if @page.destroy
          flash[:notice] = Alchemy.t("Page deleted", name: @page.name)

          # Remove page from clipboard
          clipboard = get_clipboard("pages")
          clipboard.delete_if { |item| item["id"] == @page.id.to_s }
        else
          flash[:warning] = @page.errors.full_messages.to_sentence
        end

        respond_to do |format|
          format.js do
            @redirect_url = if @page.layoutpage?
              alchemy.admin_layoutpages_path
            else
              alchemy.admin_pages_path
            end

            render :redirect
          end
        end
      end

      def link
        render LinkDialog::Tabs.new(**link_dialog_params)
      end

      def fold
        # @page is fetched via before filter
        @page.fold!(current_alchemy_user.id, !@page.folded?(current_alchemy_user.id))
        render json: serialized_page_tree
      end

      # Leaves the page editing mode and unlocks the page for other users
      def unlock
        # fetching page via before filter
        @page.unlock!
        flash[:notice] = Alchemy.t(:unlocked_page, name: @page.name)
        @pages_locked_by_user = Page.from_current_site.locked_by(current_alchemy_user)
        respond_to do |format|
          format.js
          format.html do
            redirect_to(unlock_redirect_path, allow_other_host: true)
          end
        end
      end

      def unlock_redirect_path
        if params[:redirect_to].to_s.match?(/\A\/admin\/(layout_)?pages/)
          params[:redirect_to]
        else
          admin_pages_path
        end
      end

      # Sets the page public and updates the published_at attribute that is used as cache_key
      #
      def publish
        # fetching page via before filter
        @page.publish!

        flash[:notice] = Alchemy.t(:page_published, name: @page.name)
        redirect_back(fallback_location: admin_pages_path)
      end

      def copy_language_tree
        language_root_to_copy_from.copy_children_to(copy_of_language_root)
        flash[:notice] = Alchemy.t(:language_pages_copied)
        redirect_to admin_pages_path
      end

      def flush
        @current_language.pages.flushables.update_all(published_at: Time.current)
        # We need to ensure, that also all layoutpages get the +published_at+ timestamp set,
        # but not set to public true, because the cache_key for an element is +published_at+
        # and we don't want the layout pages to be present in +Page.published+ scope.
        @current_language.pages.flushable_layoutpages.update_all(published_at: Time.current)
        respond_to { |format| format.js }
      end

      private

      def resource_handler
        @_resource_handler ||= Alchemy::Resource.new(controller_path, alchemy_module, Alchemy::Page)
      end

      def set_view
        @view = params[:view] || session[:alchemy_pages_view] || "tree"
        session[:alchemy_pages_view] = @view
      end

      def copy_of_language_root
        Page.copy(
          language_root_to_copy_from,
          language_id: params[:languages][:new_lang_id],
          language_code: @current_language.code
        )
      end

      def language_root_to_copy_from
        Page.language_root_for(params[:languages][:old_lang_id])
      end

      # Returns a pair, the path that a given tree node should take, and the path its children should take
      #
      # This function will add a node's own slug into their ancestor's path
      # in order to create the full URL of a node
      #
      # @param [String]
      #   The node's ancestors path
      # @param [Hash]
      #   A children node
      #
      def process_url(ancestors_path, item)
        default_urlname = (ancestors_path.blank? ? "" : "#{ancestors_path}/") + item["slug"].to_s
        {my_urlname: default_urlname, children_path: default_urlname}
      end

      def load_resource
        @page = Page.includes(page_includes).find(params[:id])
      end

      def pages_from_raw_request
        request.raw_post.split("&").map do |i|
          parts = i.split("=")
          {
            parts[0].gsub(/[^0-9]/, "") => parts[1]
          }
        end
      end

      def redirect_path_after_create_page
        if @page.editable_by?(current_alchemy_user)
          params[:redirect_to] || edit_admin_page_path(@page)
        else
          admin_pages_path
        end
      end

      def page_params
        params.require(:page).permit(*secure_attributes)
      end

      def link_dialog_params
        params.permit([:url, :selected_tab, :link_title, :link_target])
      end

      def secure_attributes
        if can?(:create, Alchemy::Page)
          Page::PERMITTED_ATTRIBUTES + [:language_root, :parent_id, :language_id, :language_code]
        else
          Page::PERMITTED_ATTRIBUTES
        end
      end

      def page_is_locked?
        return false if !@page.locker.try(:logged_in?)
        return false if !current_alchemy_user.respond_to?(:id)

        @page.locked? && @page.locker.id != current_alchemy_user.id
      end

      def page_needs_lock?
        return true unless @page.locker

        @page.locker.try!(:id) != current_alchemy_user.try!(:id)
      end

      def paste_from_clipboard
        if params[:paste_from_clipboard]
          source = Page.find(params[:paste_from_clipboard])
          parent = Page.find_by(id: params[:page][:parent_id])
          Page.copy_and_paste(source, parent, params[:page][:name])
        end
      end

      def set_root_page
        @page_root = @current_language.root_page
      end

      def set_page_version
        @page_version = @page.draft_version
      end

      def serialized_page_tree
        PageTreeSerializer.new(
          @page,
          ability: current_ability,
          user: current_alchemy_user
        )
      end

      def load_languages_and_layouts
        @language = @current_language
        @languages_with_page_tree = Language.on_current_site.with_root_page
        @page_layouts = Page.layouts_for_select(@language.id)
      end

      def set_preview_mode
        @preview_mode = true
      end

      def page_includes
        Alchemy::EagerLoading.page_includes(version: :draft_version)
      end
    end
  end
end