CartoDB/cartodb20

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

Summary

Maintainability
D
2 days
Test Coverage
require 'active_support/inflector'
require 'carto/api/vizjson3_presenter'

require_relative '../../models/table'
require_relative '../../models/visualization/member'
require_relative '../../models/visualization/collection'

class Admin::PagesController < Admin::AdminController
  include Carto::HtmlSafe

  include CartoDB
  include VisualizationsControllerHelper

  DATASETS_PER_PAGE = 9
  MAPS_PER_PAGE = 9
  USER_TAGS_LIMIT = 100
  PAGE_NUMBER_PLACEHOLDER = 'PAGENUMBERPLACEHOLDER'

  # TODO logic as done client-side, how and where to encapsulate this better?
  GEOMETRY_MAPPING = {
    'st_multipolygon'    => 'polygon',
    'st_polygon'         => 'polygon',
    'st_multilinestring' => 'line',
    'st_linestring'      => 'line',
    'st_multipoint'      => 'point',
    'st_point'           => 'point'
  }


  ssl_required :common_data, :public, :datasets, :maps, :user_feed
  ssl_allowed :index, :sitemap, :datasets_for_user, :datasets_for_organization, :maps_for_user, :maps_for_organization,
              :render_not_found

  before_filter :login_required, :except => [:public, :datasets, :maps, :sitemap, :index, :user_feed]
  before_filter :load_viewed_entity
  before_filter :set_new_dashboard_flag
  before_filter :ensure_organization_correct
  skip_before_filter :browser_is_html5_compliant?, only: [:public, :datasets, :maps, :user_feed]
  skip_before_filter :ensure_user_organization_valid, only: [:public]

  helper_method :named_map_vizjson3

  # Just an entrypoint to dispatch to different places according to
  def index
    if current_user
      # I am logged in, visiting my subdomain -> my dashboard
      redirect_to CartoDB.url(self, 'dashboard', user: current_user)
    elsif CartoDB.extract_subdomain(request).present?
      # I am visiting another user subdomain -> other user public pages
      redirect_to CartoDB.url(self, 'public_user_feed_home')
    elsif current_viewer
      # I am logged in but did not specify a subdomain -> my dashboard
      redirect_to CartoDB.url(self, 'dashboard', user: current_viewer)
    else
      # I am not logged in and did not specify a subdomain -> login
      # Avoid using CartoDB.url helper, since we cannot get any user information from domain, path or session
      redirect_to login_url
    end
  end

  def common_data
    redirect_to CartoDB.url(self, 'datasets_library')
  end

  def sitemap
    if @viewed_user.nil?
      username = CartoDB.extract_subdomain(request)
      org = get_organization_if_exists(username)
      render_404 and return if org.nil?
      visualizations = public_builder(organization_id: org.id).build
    else
      # Redirect to org url if has only user
      if eligible_for_redirect?(@viewed_user)
        redirect_to CartoDB.base_url(@viewed_user.organization.name) << CartoDB.path(self, 'public_sitemap') and return
      end

      visualizations = public_builder(user_id: @viewed_user.id).with_prefetch_user(true).build
    end

    @urls = visualizations.map { |vis|
      case vis.type
      when Carto::Visualization::TYPE_DERIVED
        {
          loc: CartoDB.url(self, 'public_visualizations_public_map', params: { id: vis.id }, user: vis.user),
          lastfreq: vis.updated_at.strftime("%Y-%m-%dT%H:%M:%S%:z")
        }
      when Carto::Visualization::TYPE_CANONICAL
        {
          loc: CartoDB.url(self, 'public_table', params: { id: vis.name }, user: vis.user),
          lastfreq: vis.updated_at.strftime("%Y-%m-%dT%H:%M:%S%:z")
        }
      end
    }.compact
    render :formats => [:xml]
  end

  def datasets
    render_403 && return if public_profile_disabled?

    datasets = CartoDB::ControllerFlows::Public::Datasets.new(self)
    content = CartoDB::ControllerFlows::Public::Content.new(self, request, datasets)
    content.render()
  end

  def maps
    render_403 && return if public_profile_disabled?

    maps = CartoDB::ControllerFlows::Public::Maps.new(self)
    content = CartoDB::ControllerFlows::Public::Content.new(self, request, maps)
    content.render()
  end

  def public
    if current_user
      index
    else
      user_feed
    end
  end

  def user_feed
    # The template of this endpoint get the user_feed data calling
    # to another endpoint in the front-end part
    render_403 && return if public_profile_disabled?

    if @viewed_user.nil?
      username = CartoDB.extract_subdomain(request).strip.downcase
      org = get_organization_if_exists(username)
      unless org.nil?
        redirect_to CartoDB.url(self, 'public_maps_home') and return
      end
      render_404
    else

      set_layout_vars_for_user(@viewed_user, 'feed')

      dataset_builder = user_datasets_public_builder(@viewed_user)
      maps_builder = user_maps_public_builder(@viewed_user)

      @name                = @viewed_user.name_or_username
      @avatar_url          = @viewed_user.avatar
      @tables_num          = dataset_builder.count
      @maps_count          = maps_builder.count
      @website             = website_url(@viewed_user.website)
      @website_clean       = @website ? @website.gsub(/https?:\/\//, "") : ""

      if eligible_for_redirect?(@viewed_user)
        # redirect username.host.ext => org-name.host.ext/u/username
        redirect_to CartoDB.base_url(@viewed_user.organization.name, @viewed_user.username) <<
                            CartoDB.path(self, 'public_user_feed_home') and return
      end

      description = @name.dup

      # TODO: move to helper
      if @maps_count == 0 && @tables_num == 0
        description << " uses CARTO to transform location intelligence into dynamic renderings that enable discovery of trends and patterns"
      else
        description << " has"

        unless @maps_count == 0
          description << " created #{@maps_count} #{'map'.pluralize(@maps_count)}"
        end

        unless @maps_count == 0 || @tables_num == 0
          description << " and"
        end

        unless @tables_num == 0
          description << " published #{@tables_num} public #{'dataset'.pluralize(@tables_num)}"
        end

        description << " · View #{@name} CARTO profile for the latest activity and contribute to Open Data by creating an account in CARTO"
      end

      @page_description = description

      respond_to do |format|
        format.html { render 'user_feed', layout: 'public_user_feed' }
      end
    end
  end

  def datasets_for_user(user)
    set_layout_vars_for_user(user, 'datasets')
    render_datasets(user_datasets_public_builder(user), user)
  end

  def datasets_for_organization(org)
    set_layout_vars_for_organization(org, 'datasets')
    render_datasets(org_datasets_public_builder(org))
  end

  def maps_for_user(user)
    set_layout_vars_for_user(user, 'maps')
    render_maps(user_maps_public_builder(user), user)
  end

  def maps_for_organization(org)
    set_layout_vars_for_organization(org, 'maps')
    render_maps(org_maps_public_builder(org))
  end

  def render_not_found
    render_404
  end

  protected

  def eligible_for_redirect?(user)
    return false if CartoDB.subdomainless_urls?
    user.has_organization? && CartoDB.subdomain_from_request(request) != user.organization.name
  end

  def render_datasets(vis_query_builder, user = nil)
    home = CartoDB.url(self, 'public_datasets_home', params: { page: PAGE_NUMBER_PLACEHOLDER }, user: user)
    set_pagination_vars(total_count: vis_query_builder.count,
                        per_page: DATASETS_PER_PAGE,
                        first_page_url: CartoDB.url(self, 'public_datasets_home', user: user),
                        numbered_page_url: home)

    @datasets = []

    vis_list = vis_query_builder.build_paged(current_page, DATASETS_PER_PAGE).map do |v|
      Carto::Admin::VisualizationPublicMapAdapter.new(v, current_user, self)
    end

    vis_list.each do |vis|
      @datasets << process_dataset_render(vis)
    end

    @datasets.compact!

    description = @name.dup

    # TODO: move to helper
    if @datasets.size == 0
      description << " uses CARTO to transform location intelligence into dynamic renderings that enable discovery of trends and patterns"
    else
      description << " has published #{@datasets.size} public #{'dataset'.pluralize(@datasets.size)}"
    end

    description << " · View #{@name} CARTO profile for the latest activity and contribute to Open Data by creating an account in CARTO"

    @page_description = description

    respond_to do |format|
      format.html { render 'public_datasets', layout: 'public_dashboard' }
    end
  end

  def render_maps(vis_query_builder, user=nil)
    set_pagination_vars(
      total_count: vis_query_builder.count,
      per_page:    MAPS_PER_PAGE,
      first_page_url: CartoDB.url(self, 'public_maps_home', user: user),
      numbered_page_url: CartoDB.url(self, 'public_maps_home', params: { page: PAGE_NUMBER_PLACEHOLDER }, user: user)
    )

    vis_list = vis_query_builder.build_paged(current_page, MAPS_PER_PAGE).map do |v|
      Carto::Admin::VisualizationPublicMapAdapter.new(v, current_user, self)
    end

    @visualizations = []
    vis_list.each do |vis|
      @visualizations << process_map_render(vis)
    end

    @visualizations.compact!

    description = @name.dup

    # TODO: move to helper
    if @visualizations.size == 0 && @tables_num == 0
      description << " uses CARTO to transform location intelligence into dynamic renderings that enable discovery of trends and patterns"
    else
      description << " has"

      unless @visualizations.size == 0
        description << " created #{@visualizations.size} #{'map'.pluralize(@visualizations.size)}"
      end

      unless @visualizations.size == 0 || @tables_num == 0
        description << " and"
      end

      unless @tables_num == 0
        description << " published #{@tables_num} public #{'dataset'.pluralize(@tables_num)}"
      end

      description << " · View #{@name} CARTO profile for the latest activity and contribute to Open Data by creating an account in CARTO"
    end

    @page_description = description

    respond_to do |format|
      format.html { render 'public_maps', layout: 'public_dashboard' }
    end
  end

  def set_new_dashboard_flag
    ff_user = @viewed_user || @viewed_org.try(:owner)

    unless ff_user.nil?
      @has_new_dashboard = ff_user.builder_enabled?
    end
  end

  def set_layout_vars_for_user(user, content_type)
    builder = user_maps_public_builder(user, visualization_version)
    most_viewed = builder.with_order(:mapviews, :desc).build_paged(1, 1).first

    set_layout_vars({
        most_viewed_vis_map: most_viewed ? Carto::Admin::VisualizationPublicMapAdapter.new(most_viewed, current_user, self) : nil,
        content_type: content_type,
        default_fallback_basemap: user.default_basemap,
        user: user,
        base_url: user.public_url(nil, request.protocol == "https://" ? "https" : "http")
      })
    set_shared_layout_vars(user, {
        name:       user.name_or_username,
        avatar_url: user.avatar,
      }, {
        available_for_hire: user.available_for_hire,
        email:              user.email,
        user: user
      })
  end

  def set_layout_vars_for_organization(org, content_type)
    most_viewed_vis_map = org.public_vis_by_type(Carto::Visualization::TYPE_DERIVED,
                                                 1,
                                                 1,
                                                 nil,
                                                 'mapviews',
                                                 visualization_version).first
    set_layout_vars(most_viewed_vis_map: most_viewed_vis_map,
                    content_type: content_type,
                    default_fallback_basemap: org.owner ? org.owner.default_basemap : nil,
                    base_url: '')
    set_shared_layout_vars(org,
                           name: org.display_name.blank? ? org.name : org.display_name,
                           avatar_url: org.avatar_url)
  end

  def set_layout_vars(required)
    @most_viewed_vis_map = required.fetch(:most_viewed_vis_map)
    @content_type        = required.fetch(:content_type)
    @maps_url            = CartoDB.url(view_context, 'public_maps_home', user: required.fetch(:user, nil))
    @datasets_url        = CartoDB.url(view_context, 'public_datasets_home', user: required.fetch(:user, nil))
    @default_fallback_basemap = required.fetch(:default_fallback_basemap, {})
    @base_url            = required.fetch(:base_url, {})
  end

  def set_pagination_vars(required)
    # Force all number pagination vars to be integers avoiding problems with
    # undesired strings
    @total_count  = required.fetch(:total_count, 0).to_i
    @per_page     = required.fetch(:per_page, 9).to_i
    @current_page = current_page.to_i
    @first_page_url = required.fetch(:first_page_url)
    @numbered_page_url = required.fetch(:numbered_page_url)
    @page_number_placeholder = PAGE_NUMBER_PLACEHOLDER
  end

  # Shared as in shared for both new and old layout
  def set_shared_layout_vars(model, required, optional = {})
    @twitter_username   = model.twitter_username
    @location           = model.location
    @description        = model.description
    @website            = website_url(model.website)
    @website_clean      = @website ? @website.gsub(/https?:\/\//, "") : ""
    @name               = required.fetch(:name)
    @avatar_url         = required.fetch(:avatar_url)
    @email              = optional.fetch(:email, nil)
    @available_for_hire = optional.fetch(:available_for_hire, false)
    @user               = optional.fetch(:user, nil)
    @is_org             = model.is_a? Carto::Organization
    @tables_num = (@is_org ? org_datasets_public_builder(model) : user_datasets_public_builder(model)).count
    @maps_count = (@is_org ? org_maps_public_builder(model) : user_maps_public_builder(model)).count

    @needs_gmaps_lib = @most_viewed_vis_map.try(:map).try(:provider) == 'googlemaps'
    @needs_gmaps_lib ||= @default_fallback_basemap['className'] == 'googlemaps'

    gmaps_user = @most_viewed_vis_map.try(:user) || @viewed_user
    @gmaps_query_string = gmaps_user ? gmaps_user.google_maps_query_string : @viewed_org.google_maps_key
  end

  def user_datasets_public_builder(user)
    public_builder(user_id: user.id, vis_type: Carto::Visualization::TYPE_CANONICAL)
  end

  def user_maps_public_builder(user, version = nil)
    public_builder(user_id: user.id, vis_type: Carto::Visualization::TYPE_DERIVED, version: version)
  end

  def org_datasets_public_builder(org)
    public_builder(vis_type: Carto::Visualization::TYPE_CANONICAL, organization_id: org.id)
  end

  def org_maps_public_builder(org)
    public_builder(vis_type: Carto::Visualization::TYPE_DERIVED, organization_id: org.id)
  end

  def public_builder(user_id: nil, vis_type: nil, organization_id: nil, version: nil)
    tags = tag_or_nil.nil? ? nil : [tag_or_nil]

    builder = Carto::VisualizationQueryBuilder.new
                                              .with_privacy(Carto::Visualization::PRIVACY_PUBLIC)
                                              .with_published
                                              .without_raster
                                              .with_order(:updated_at, :desc)
                                              .with_user_id(user_id)
                                              .with_type(vis_type)
                                              .with_tags(tags)
                                              .with_organization_id(organization_id)
                                              .with_version(version)

    builder
  end

  def visualization_version
    @has_new_dashboard ? Carto::Visualization::VERSION_BUILDER : nil
  end

  def named_map_vizjson3(visualization)
    generate_named_map_vizjson3(Carto::Visualization.find(visualization.id))
  end

  def get_organization_if_exists(name)
    Carto::Organization.where(name: name).first
  end

  def current_page
    params[:page].to_i > 0 ? params[:page] : 1
  end

  def tag_or_nil
    params[:tag]
  end

  def ensure_organization_correct
    return if CartoDB.subdomainless_urls?

    user_or_org_domain = CartoDB.subdomain_from_request(request)
    user_domain = CartoDB.extract_subdomain(request)
    user = ::User.where(username: user_domain).first

    unless user.nil?
      if user.username != user_or_org_domain and not user.belongs_to_organization?(get_organization_if_exists(user_or_org_domain))
        render_404
      end
    end
  end

  def process_dataset_render(dataset)
    geometry_type = dataset.kind
    if geometry_type != 'raster'
      table_geometry_types = dataset.table.geometry_types
      geometry_type = GEOMETRY_MAPPING.fetch(table_geometry_types.first.try(&:downcase), '')
    end

    vis_item(dataset).merge(
      rows_count: dataset.table.rows_counted,
      size_in_bytes: dataset.table.table_size,
      geometry_type: geometry_type,
      source: markdown_html_safe(dataset.source)
    )
  rescue StandardError => e
    # A dataset might be invalid. For example, having the table deleted and not yet cleaned.
    # We don't want public page to be broken, but error must be traced.
    CartoDB.notify_exception(e, vis: dataset)
    nil
  end

  def process_map_render(map)
    vis_item(map)
  end

  def vis_item(vis)
    return {
      id:          vis.id,
      title:       vis.name,
      description: markdown_html_safe(vis.description),
      tags:        vis.tags,
      updated_at:  vis.updated_at,
      owner:       vis.user,
      map_zoom:    vis.map.zoom
    }
  end

  def load_viewed_entity
    username = CartoDB.extract_subdomain(request)
    @viewed_user = ::User.where(username: username).first

    if @viewed_user.nil?
      username = username.strip.downcase
      @viewed_org = get_organization_if_exists(username)
    end
  end


  def website_url(url)
    if url.blank?
      ""
    else
      !url.blank? && url[/^https?:\/\//].nil? ? "http://#{url}" : url
    end
  end

  private

  def public_profile_disabled?
    if !@viewed_user.nil? && current_user != @viewed_user
      user = Carto::User.find_by(id: @viewed_user.id)
      user.has_feature_flag?('disable_public_profile_page')
    end
  end

end