448 lines
16 KiB
Ruby
448 lines
16 KiB
Ruby
require 'json'
|
|
require_dependency 'carto/export/layer_exporter'
|
|
require_dependency 'carto/export/data_import_exporter'
|
|
|
|
# Version History
|
|
# TODO: documentation at http://cartodb.readthedocs.org/en/latest/operations/exporting_importing_visualizations.html
|
|
# 2: export full visualization. Limitations:
|
|
# - No Odyssey support: export fails if any of parent_id / prev_id / next_id / slide_transition_options are set.
|
|
# - Privacy is exported, but permissions are not.
|
|
# 2.0.1: export Widget.source_id
|
|
# 2.0.2: export username
|
|
# 2.0.3: export state (Carto::State)
|
|
# 2.0.4: export legends (Carto::Legend)
|
|
# 2.0.5: export explicit widget order
|
|
# 2.0.6: export version
|
|
# 2.0.7: export map options
|
|
# 2.0.8: export widget style
|
|
# 2.0.9: export visualization id
|
|
# 2.1.0: export datasets: permissions, user_tables and syncs
|
|
# 2.1.1: export vizjson2 mark
|
|
# 2.1.2: export locked and password
|
|
# 2.1.3: export synchronization id
|
|
module Carto
|
|
module VisualizationsExportService2Configuration
|
|
CURRENT_VERSION = '2.1.3'.freeze
|
|
|
|
def compatible_version?(version)
|
|
version.to_i == CURRENT_VERSION.split('.')[0].to_i
|
|
end
|
|
end
|
|
|
|
module VisualizationsExportService2Validator
|
|
def check_valid_visualization(visualization)
|
|
raise 'Only derived or canonical visualizations can be exported' unless visualization.derived? ||
|
|
visualization.canonical? ||
|
|
visualization.remote?
|
|
end
|
|
end
|
|
|
|
module VisualizationsExportService2Importer
|
|
include VisualizationsExportService2Configuration
|
|
include LayerImporter
|
|
include DataImportImporter
|
|
|
|
def build_visualization_from_json_export(exported_json_string)
|
|
build_visualization_from_hash_export(parse_json(exported_json_string))
|
|
end
|
|
|
|
def build_visualization_from_hash_export(exported_hash)
|
|
raise 'Wrong export version' unless compatible_version?(exported_hash[:version])
|
|
|
|
build_visualization_from_hash(exported_hash[:visualization])
|
|
end
|
|
|
|
def marked_as_vizjson2_from_json_export?(exported_json_string)
|
|
marked_as_vizjson2_from_hash_export?(parse_json(exported_json_string))
|
|
end
|
|
|
|
def marked_as_vizjson2_from_hash_export?(exported_hash)
|
|
exported_hash[:visualization][:uses_vizjson2]
|
|
end
|
|
|
|
private
|
|
|
|
def parse_json(exported_json_string)
|
|
JSON.parse(exported_json_string, symbolize_names: true)
|
|
end
|
|
|
|
def build_visualization_from_hash(exported_visualization)
|
|
exported_layers = exported_visualization[:layers]
|
|
exported_overlays = exported_visualization[:overlays]
|
|
|
|
visualization = Carto::Visualization.new(
|
|
name: exported_visualization[:name],
|
|
description: exported_visualization[:description],
|
|
version: exported_visualization[:version] || 2,
|
|
type: exported_visualization[:type],
|
|
tags: exported_visualization[:tags],
|
|
privacy: exported_visualization[:privacy],
|
|
source: exported_visualization[:source],
|
|
license: exported_visualization[:license],
|
|
title: exported_visualization[:title],
|
|
kind: exported_visualization[:kind],
|
|
attributions: exported_visualization[:attributions],
|
|
bbox: exported_visualization[:bbox],
|
|
display_name: exported_visualization[:display_name],
|
|
map: build_map_from_hash(
|
|
exported_visualization[:map],
|
|
layers: build_layers_from_hash(exported_layers)),
|
|
overlays: build_overlays_from_hash(exported_overlays),
|
|
analyses: exported_visualization[:analyses].map { |a| build_analysis_from_hash(a) },
|
|
permission: build_permission_from_hash(exported_visualization[:permission]),
|
|
mapcaps: [build_mapcap_from_hash(exported_visualization[:mapcap])].compact,
|
|
external_source: build_external_source_from_hash(exported_visualization[:external_source]),
|
|
created_at: exported_visualization[:created_at],
|
|
updated_at: exported_visualization[:updated_at],
|
|
locked: exported_visualization[:locked] || false,
|
|
encrypted_password: exported_visualization[:encrypted_password],
|
|
password_salt: exported_visualization[:password_salt]
|
|
)
|
|
|
|
# This is optional as it was added in version 2.0.2
|
|
exported_user = exported_visualization[:user]
|
|
if exported_user
|
|
visualization.user = Carto::User.new(username: exported_user[:username])
|
|
end
|
|
|
|
# Added in version 2.0.3
|
|
visualization.state = build_state_from_hash(exported_visualization[:state])
|
|
|
|
active_layer_order = exported_layers.index { |l| l[:active_layer] }
|
|
if active_layer_order
|
|
visualization.active_layer = visualization.layers.find { |l| l.order == active_layer_order }
|
|
end
|
|
|
|
# Dataset-specific
|
|
user_table = build_user_table_from_hash(exported_visualization[:user_table])
|
|
visualization.map.user_table = user_table if user_table
|
|
visualization.synchronization = build_synchronization_from_hash(exported_visualization[:synchronization])
|
|
|
|
visualization.id = exported_visualization[:id] if exported_visualization[:id]
|
|
visualization
|
|
end
|
|
|
|
def build_map_from_hash(exported_map, layers:)
|
|
return nil unless exported_map
|
|
|
|
Carto::Map.new(
|
|
provider: exported_map[:provider],
|
|
bounding_box_sw: exported_map[:bounding_box_sw],
|
|
bounding_box_ne: exported_map[:bounding_box_ne],
|
|
center: exported_map[:center],
|
|
zoom: exported_map[:zoom],
|
|
view_bounds_sw: exported_map[:view_bounds_sw],
|
|
view_bounds_ne: exported_map[:view_bounds_ne],
|
|
scrollwheel: exported_map[:scrollwheel],
|
|
legends: exported_map[:legends],
|
|
layers: layers,
|
|
options: exported_map[:options]
|
|
)
|
|
end
|
|
|
|
def build_overlays_from_hash(exported_overlays)
|
|
return [] unless exported_overlays
|
|
|
|
exported_overlays.map.with_index.map do |overlay, i|
|
|
build_overlay_from_hash(overlay, order: (i + 1))
|
|
end
|
|
end
|
|
|
|
def build_overlay_from_hash(exported_overlay, order:)
|
|
Carto::Overlay.new(
|
|
order: order,
|
|
options: exported_overlay[:options],
|
|
type: exported_overlay[:type],
|
|
template: exported_overlay[:template]
|
|
)
|
|
end
|
|
|
|
def build_analysis_from_hash(exported_analysis)
|
|
return nil unless exported_analysis
|
|
|
|
Carto::Analysis.new(analysis_definition: exported_analysis[:analysis_definition])
|
|
end
|
|
|
|
def build_state_from_hash(exported_state)
|
|
Carto::State.new(json: exported_state ? exported_state[:json] : nil)
|
|
end
|
|
|
|
def build_permission_from_hash(exported_permission)
|
|
return nil unless exported_permission
|
|
|
|
Carto::Permission.new(access_control_list: JSON.dump(exported_permission[:access_control_list]))
|
|
end
|
|
|
|
def build_synchronization_from_hash(exported_synchronization)
|
|
return nil unless exported_synchronization
|
|
|
|
sync = Carto::Synchronization.new(
|
|
name: exported_synchronization[:name],
|
|
interval: exported_synchronization[:interval],
|
|
url: exported_synchronization[:url],
|
|
state: exported_synchronization[:state],
|
|
created_at: exported_synchronization[:created_at],
|
|
updated_at: exported_synchronization[:updated_at],
|
|
run_at: exported_synchronization[:run_at],
|
|
retried_times: exported_synchronization[:retried_times],
|
|
log: build_log_from_hash(exported_synchronization[:log]),
|
|
error_code: exported_synchronization[:error_code],
|
|
error_message: exported_synchronization[:error_message],
|
|
ran_at: exported_synchronization[:ran_at],
|
|
modified_at: exported_synchronization[:modified_at],
|
|
etag: exported_synchronization[:etag],
|
|
checksum: exported_synchronization[:checksum],
|
|
service_name: exported_synchronization[:service_name],
|
|
service_item_id: exported_synchronization[:service_item_id],
|
|
type_guessing: exported_synchronization[:type_guessing],
|
|
quoted_fields_guessing: exported_synchronization[:quoted_fields_guessing],
|
|
content_guessing: exported_synchronization[:content_guessing]
|
|
)
|
|
|
|
sync.id = exported_synchronization[:id]
|
|
sync
|
|
end
|
|
|
|
def build_user_table_from_hash(exported_user_table)
|
|
return nil unless exported_user_table
|
|
|
|
user_table = Carto::UserTable.new
|
|
user_table.name = exported_user_table[:name]
|
|
user_table.privacy = exported_user_table[:privacy]
|
|
user_table.tags = exported_user_table[:tags]
|
|
user_table.geometry_columns = exported_user_table[:geometry_columns]
|
|
user_table.rows_counted = exported_user_table[:rows_counted]
|
|
user_table.rows_estimated = exported_user_table[:rows_estimated]
|
|
user_table.indexes = exported_user_table[:indexes]
|
|
user_table.database_name = exported_user_table[:database_name]
|
|
user_table.description = exported_user_table[:description]
|
|
user_table.table_id = exported_user_table[:table_id]
|
|
user_table.data_import = build_data_import_from_hash(exported_user_table[:data_import])
|
|
|
|
user_table
|
|
end
|
|
|
|
def build_mapcap_from_hash(exported_mapcap)
|
|
return nil unless exported_mapcap
|
|
|
|
Carto::Mapcap.new(
|
|
ids_json: exported_mapcap[:ids_json],
|
|
export_json: exported_mapcap[:export_json],
|
|
created_at: exported_mapcap[:created_at]
|
|
)
|
|
end
|
|
|
|
def build_external_source_from_hash(exported_external_source)
|
|
return nil unless exported_external_source
|
|
|
|
es = Carto::ExternalSource.new(
|
|
import_url: exported_external_source[:import_url],
|
|
rows_counted: exported_external_source[:rows_counted],
|
|
size: exported_external_source[:size],
|
|
username: exported_external_source[:username],
|
|
geometry_types: exported_external_source[:geometry_types]
|
|
)
|
|
es.id = exported_external_source[:id]
|
|
|
|
es
|
|
end
|
|
end
|
|
|
|
module VisualizationsExportService2Exporter
|
|
include VisualizationsExportService2Configuration
|
|
include VisualizationsExportService2Validator
|
|
include LayerExporter
|
|
include DataImportExporter
|
|
|
|
def export_visualization_json_string(visualization_id, user, with_password: false)
|
|
export_visualization_json_hash(visualization_id, user, with_password: with_password).to_json
|
|
end
|
|
|
|
def export_visualization_json_hash(visualization_id, user, with_mapcaps: true, with_password: false)
|
|
{
|
|
version: CURRENT_VERSION,
|
|
visualization: export(Carto::Visualization.find(visualization_id), user,
|
|
with_mapcaps: with_mapcaps, with_password: with_password)
|
|
}
|
|
end
|
|
|
|
private
|
|
|
|
def export(visualization, user, with_mapcaps: true, with_password: false)
|
|
check_valid_visualization(visualization)
|
|
export_visualization(visualization, user, with_mapcaps: with_mapcaps, with_password: with_password)
|
|
end
|
|
|
|
def export_visualization(visualization, user, with_mapcaps: true, with_password: false)
|
|
layers = visualization.layers_with_data_readable_by(user)
|
|
active_layer_id = visualization.active_layer_id
|
|
layer_exports = layers.map do |layer|
|
|
export_layer(layer, active_layer: active_layer_id == layer.id)
|
|
end
|
|
|
|
export = {
|
|
id: visualization.id,
|
|
name: visualization.name,
|
|
description: visualization.description,
|
|
version: visualization.version,
|
|
type: visualization.type,
|
|
tags: visualization.tags,
|
|
privacy: visualization.privacy,
|
|
source: visualization.source,
|
|
license: visualization.license,
|
|
title: visualization.title,
|
|
kind: visualization.kind,
|
|
attributions: visualization.attributions,
|
|
bbox: visualization.bbox,
|
|
display_name: visualization.display_name,
|
|
map: export_map(visualization.map),
|
|
layers: layer_exports,
|
|
overlays: visualization.overlays.map { |o| export_overlay(o) },
|
|
analyses: visualization.analyses.map { |a| exported_analysis(a) },
|
|
user: export_user(visualization.user),
|
|
state: export_state(visualization.state),
|
|
permission: export_permission(visualization.permission),
|
|
synchronization: export_syncronization(visualization.synchronization),
|
|
user_table: export_user_table(visualization.map.try(:user_table)),
|
|
uses_vizjson2: visualization.uses_vizjson2?,
|
|
mapcap: with_mapcaps ? export_mapcap(visualization.latest_mapcap) : nil,
|
|
external_source: export_external_source(visualization.external_source),
|
|
created_at: visualization.created_at,
|
|
updated_at: visualization.updated_at,
|
|
locked: visualization.locked
|
|
}
|
|
|
|
if with_password
|
|
export[:encrypted_password] = visualization.encrypted_password
|
|
export[:password_salt] = visualization.password_salt
|
|
end
|
|
|
|
export
|
|
end
|
|
|
|
def export_user(user)
|
|
{
|
|
username: user.username
|
|
}
|
|
end
|
|
|
|
def export_map(map)
|
|
return nil unless map
|
|
|
|
{
|
|
provider: map.provider,
|
|
bounding_box_sw: map.bounding_box_sw,
|
|
bounding_box_ne: map.bounding_box_ne,
|
|
center: map.center,
|
|
zoom: map.zoom,
|
|
view_bounds_sw: map.view_bounds_sw,
|
|
view_bounds_ne: map.view_bounds_ne,
|
|
scrollwheel: map.scrollwheel,
|
|
legends: map.legends,
|
|
options: map.options
|
|
}
|
|
end
|
|
|
|
def export_overlay(overlay)
|
|
{
|
|
options: overlay.options,
|
|
type: overlay.type,
|
|
template: overlay.template
|
|
}
|
|
end
|
|
|
|
def exported_analysis(analysis)
|
|
{
|
|
analysis_definition: analysis.analysis_definition
|
|
}
|
|
end
|
|
|
|
def export_state(state)
|
|
{
|
|
json: state.json
|
|
}
|
|
end
|
|
|
|
def export_permission(permission)
|
|
{
|
|
access_control_list: JSON.parse(permission.access_control_list, symbolize_names: true)
|
|
}
|
|
end
|
|
|
|
def export_syncronization(synchronization)
|
|
return nil unless synchronization
|
|
{
|
|
id: synchronization.id,
|
|
name: synchronization.name,
|
|
interval: synchronization.interval,
|
|
url: synchronization.url,
|
|
state: synchronization.state,
|
|
created_at: synchronization.created_at,
|
|
updated_at: synchronization.updated_at,
|
|
run_at: synchronization.run_at,
|
|
retried_times: synchronization.retried_times,
|
|
log: export_log(synchronization.log),
|
|
error_code: synchronization.error_code,
|
|
error_message: synchronization.error_message,
|
|
ran_at: synchronization.ran_at,
|
|
modified_at: synchronization.modified_at,
|
|
etag: synchronization.etag,
|
|
checksum: synchronization.checksum,
|
|
service_name: synchronization.service_name,
|
|
service_item_id: synchronization.service_item_id,
|
|
type_guessing: synchronization.type_guessing,
|
|
quoted_fields_guessing: synchronization.quoted_fields_guessing,
|
|
content_guessing: synchronization.content_guessing
|
|
}
|
|
end
|
|
|
|
def export_user_table(user_table)
|
|
return nil unless user_table
|
|
|
|
{
|
|
name: user_table.name,
|
|
privacy: user_table.privacy,
|
|
tags: user_table.tags,
|
|
geometry_columns: user_table.geometry_columns,
|
|
rows_counted: user_table.rows_counted,
|
|
rows_estimated: user_table.rows_estimated,
|
|
indexes: user_table.indexes,
|
|
database_name: user_table.database_name,
|
|
description: user_table.description,
|
|
table_id: user_table.table_id,
|
|
data_import: export_data_import(user_table.data_import)
|
|
}
|
|
end
|
|
|
|
def export_external_source(external_source)
|
|
return nil unless external_source
|
|
|
|
{
|
|
id: external_source.id,
|
|
import_url: external_source.import_url,
|
|
rows_counted: external_source.rows_counted,
|
|
size: external_source.size,
|
|
username: external_source.username,
|
|
geometry_types: external_source.geometry_types
|
|
}
|
|
end
|
|
|
|
def export_mapcap(mapcap)
|
|
return nil unless mapcap
|
|
|
|
{
|
|
ids_json: mapcap.ids_json,
|
|
export_json: mapcap.export_json,
|
|
created_at: mapcap.created_at
|
|
}
|
|
end
|
|
end
|
|
|
|
# Both String and Hash versions are provided because `deep_symbolize_keys` won't symbolize through arrays
|
|
# and having separated methods make handling and testing much easier.
|
|
class VisualizationsExportService2
|
|
include VisualizationsExportService2Importer
|
|
include VisualizationsExportService2Exporter
|
|
end
|
|
end
|