cartodb-4.29/app/controllers/admin/pages_controller.rb
2020-06-15 10:58:47 +08:00

506 lines
18 KiB
Ruby

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
datasets = CartoDB::ControllerFlows::Public::Datasets.new(self)
content = CartoDB::ControllerFlows::Public::Content.new(self, request, datasets)
content.render()
end
def maps
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
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.build.count
@maps_count = maps_builder.build.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.build.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.build.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? Organization
@tables_num = (@is_org ? org_datasets_public_builder(model) : user_datasets_public_builder(model)).build.count
@maps_count = (@is_org ? org_maps_public_builder(model) : user_maps_public_builder(model)).build.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)
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
end