indentlabs/notebook

View on GitHub
app/controllers/application_controller.rb

Summary

Maintainability
B
5 hrs
Test Coverage
# frozen_string_literal: true
class ApplicationController < ActionController::Base
  protect_from_forgery

  before_action :set_universe_session
  before_action :set_universe_scope

  before_action :cache_most_used_page_information
  before_action :cache_forums_unread_counts

  before_action :set_metadata

  def content_type_from_controller(content_controller_name)
    content_controller_name.to_s.chomp('Controller').singularize.constantize
  end

  private

  def require_premium_plan
    unless user_signed_in? && current_user.on_premium_plan?
      return redirect_back(fallback_location: root_path, notice: "Doing that requires Premium access.")
    end
  end

  def require_admin_access
    unless user_signed_in? && current_user.site_administrator
      return redirect_to root_path, notice: "You don't have permission to view that!"
    end
  end

  def set_metadata
    @page_title       ||= ''
    @page_keywords    ||= %w[writing author nanowrimo novel character fiction fantasy universe creative dnd roleplay game design]
    @page_description ||= 'Notebook.ai is a set of tools for writers and roleplayers to create magnificent universes — and everything within them.'
  end

  def set_universe_session
    if params[:universe].present? && user_signed_in?
      if params[:universe] == 'all'
        session.delete(:universe_id)
      elsif params[:universe].is_a?(String) && params[:universe].to_i.to_s == params[:universe]
        found_universe = Universe.find_by(id: params[:universe])
        found_universe = nil unless found_universe.user_id == current_user.id || found_universe.contributors.pluck(:user_id).include?(current_user.id)
        session[:universe_id] = found_universe.id if found_universe
      end
    end
  end

  def set_universe_scope
    if user_signed_in? && session.key?(:universe_id)
      @universe_scope = Universe.find_by(id: session[:universe_id])

      if @universe_scope && @universe_scope.user_id != current_user.try(:id)
        # Verify the current user has access to this universe by looking up their
        # universe contributorship
        contributorship = Contributor.find_by(
          user:     current_user,
          universe: @universe_scope
        )

        if contributorship.nil?
          # If the user doesn't have current contributor access to this universe,
          # then revert back to unscoped universe actions
          @universe_scope = nil
        end
      end

    else
      @universe_scope = nil
    end
  end

  # Cache some super-common stuff we need for every page. For example, content lists for the side nav. This is a catch-all for most pages that render
  # UI, but methods are also free to skip this filter and call the individual cache methods they need instead.
  def cache_most_used_page_information
    return unless user_signed_in?

    cache_activated_content_types
    cache_current_user_content
    cache_notifications
    cache_recently_edited_pages
  end

  def cache_activated_content_types
    @activated_content_types ||= if user_signed_in?
      (
        # Use config to dictate order, but AND to only include what a user has turned on
        Rails.application.config.content_type_names[:all] & current_user.user_content_type_activators.pluck(:content_type)
      )
    else
      []
    end
  end

  def cache_current_user_content
    return if @current_user_content

    @current_user_content = {}
    return unless user_signed_in?

    cache_activated_content_types

    # We always want to cache Universes, even if they aren't explicitly turned on.
    @current_user_content = current_user.content(
      content_types: @activated_content_types + [Universe.name], 
      universe_id:   @universe_scope.try(:id)
    )

    # Due to the way we loop over @current_user_content (page, list) later, we want to make sure that we
    # at least have an empty list for all activated content types -- otherwise we may skip over the contributor
    # content injection for a type that a user doesn't have ANY pages for.
    @activated_content_types.each do |content_type|
      @current_user_content[content_type] ||= []
    end

    # Likewise, we should also always cache Timelines & Documents
    if @universe_scope
      @current_user_content['Timeline'] = current_user.timelines.where(universe_id: @universe_scope.try(:id)).to_a
      @current_user_content['Document'] = current_user.documents.where(universe_id: @universe_scope.try(:id)).order('updated_at DESC').to_a
    else
      @current_user_content['Timeline'] = current_user.timelines.to_a
      @current_user_content['Document'] = current_user.documents.order('updated_at DESC').to_a
    end
  end

  def cache_notifications
    @user_notifications ||= if user_signed_in?
      current_user.notifications.order('happened_at DESC').limit(100)
    else
      []
    end
  end

  def cache_recently_created_pages(amount=50)
    cache_current_user_content

    @recently_created_pages = if user_signed_in?
      @current_user_content.values.flatten
        .sort_by(&:created_at)
        .last(amount)
        .reverse
    else
      []
    end
  end

  def cache_recently_edited_pages(amount=50)
    cache_current_user_content

    @recently_edited_pages ||= if user_signed_in?
      @current_user_content.values.flatten
        .sort_by(&:updated_at)
        .last(amount)
        .reverse
    else
      []
    end
  end

  def cache_forums_unread_counts
    @unread_threads ||= if user_signed_in?
      Thredded::Topic.unread_followed_by(current_user).count
    else
      0
    end

    @unread_private_messages ||= if user_signed_in?
      Thredded::PrivateTopic
        .for_user(current_user)
        .unread(current_user)
        .count
    else
      0
    end
  end

  def cache_contributable_universe_ids
    cache_current_user_content

    @contributable_universe_ids ||= if user_signed_in?
      current_user.contributable_universe_ids
    else
      []
    end
  end

  def cache_linkable_content_for_each_content_type
    cache_contributable_universe_ids
    cache_current_user_content
    
    linkable_classes = @activated_content_types
    linkable_classes += %w(Document Timeline)

    @linkables_cache = {} # Cache is list of [[page_name, page_id], [page_name, page_id], ...]
    @linkables_raw   = {} # Raw is list of objects [#{page}, #{page}, ...]

    @current_user_content.each do |page_type, content_list|
      # We already have our own list of content by the current user in @current_user_content,
      # so all we need to grab is additional pages in contributable universes
      @linkables_raw[page_type] = @current_user_content[page_type]

      # Add contributor content
      if @contributable_universe_ids.any?
        existing_page_ids = @linkables_raw[page_type].map(&:id)

        pages_to_add = if page_type == Universe.name
          page_type.constantize.where(id: @contributable_universe_ids)
                               .where.not(id: existing_page_ids)
                               .where.not(user_id: current_user.id)
        else
          page_type.constantize.where(universe_id: @contributable_universe_ids)
                               .where.not(id: existing_page_ids)
                               .where.not(user_id: current_user.id)
        end

        # If we're scoped to a universe, also scope contributor content pulled to that
        # universe. If we're not, leave it as all contributor content.
        if @universe_scope && pages_to_add.klass.name != 'Universe'
          pages_to_add = pages_to_add.where(universe: @universe_scope)
        end

        filtered_fields = ContentPage.polymorphic_content_fields.map(&:to_s)
        filtered_fields.push 'universe_id' unless page_type == Universe.name
        pages_to_add.each do |page_data|
          filtered_page_data = page_data.attributes.slice(*filtered_fields)
          @linkables_raw[page_type].push ContentPage.new(filtered_page_data)
        end
      end

      # We can't properly display or @-mention content without a name set, so we explicitly
      # reject it here. However, this is a bit of a code-smell: why is there content without
      # a name set?
      @linkables_raw[page_type].reject!  { |page| page.name.nil? }

      # Finally, we want to sort our linkables cache once so we don't have to sort it again
      @linkables_raw[page_type].sort_by! { |page| page.name.downcase }.compact!

      # Lastly, build our name/id cache as well
      @linkables_cache[page_type] = @linkables_raw[page_type].map { |page| [page.name, page.id] }
    end
  end
end