619 lines
22 KiB
Ruby
619 lines
22 KiB
Ruby
require 'json'
|
|
require 'carto/export/layer_exporter'
|
|
require 'carto/export/data_import_exporter'
|
|
require_dependency 'carto/export/connector_configuration_exporter'
|
|
|
|
# Not migrated
|
|
# snapshots -> difficult to do between clouds, not in use yet
|
|
# tags -> regenerated from tables
|
|
# visualization_export -> only purpose would be logging
|
|
|
|
# Version History
|
|
# 1.0.0: export user metadata
|
|
# 1.0.1: export search tweets
|
|
# 1.0.2: export user notifications
|
|
# 1.0.3: export rate limits
|
|
# 1.0.4: company and phone in users table
|
|
# 1.0.5: synchronization_oauths and connector configurations
|
|
# 1.0.6: client_applications & friends and sql_copy rate_limits
|
|
# 1.0.7: export password_reset_token and password_reset_sent_at user fields
|
|
# 1.0.8: user_multifactor_auths
|
|
# 1.0.9: oauth_apps, oauth_app_users and friends
|
|
# 1.0.10: public_map_quota
|
|
# 1.0.11: regular_api_key_quota
|
|
# 1.0.12: maintenance_mode
|
|
# 1.0.13: company_employees, use_case
|
|
|
|
module Carto
|
|
module UserMetadataExportServiceConfiguration
|
|
CURRENT_VERSION = '1.0.13'.freeze
|
|
EXPORTED_USER_ATTRIBUTES = %i(
|
|
email crypted_password database_name username admin enabled invite_token invite_token_date
|
|
map_enabled quota_in_bytes table_quota public_map_quota regular_api_key_quota account_type private_tables_enabled
|
|
period_end_date map_view_quota max_layers database_timeout user_timeout upgraded_at map_view_block_price
|
|
geocoding_quota dashboard_viewed_at sync_tables_enabled database_host geocoding_block_price api_key
|
|
notification organization_id created_at updated_at disqus_shortname id twitter_username website
|
|
description name avatar_url database_schema soft_geocoding_limit auth_token
|
|
twitter_datasource_enabled twitter_datasource_block_price twitter_datasource_block_size
|
|
twitter_datasource_quota soft_twitter_datasource_limit available_for_hire private_maps_enabled
|
|
google_sign_in last_password_change_date max_import_file_size max_import_table_row_count
|
|
max_concurrent_import_count last_common_data_update_date google_maps_key google_maps_private_key
|
|
enable_account_token location here_isolines_quota here_isolines_block_price soft_here_isolines_limit
|
|
obs_snapshot_quota obs_snapshot_block_price soft_obs_snapshot_limit mobile_xamarin
|
|
mobile_custom_watermark mobile_offline_maps mobile_gis_extension mobile_max_open_users
|
|
mobile_max_private_users obs_general_quota obs_general_block_price soft_obs_general_limit viewer
|
|
salesforce_datasource_enabled builder_enabled geocoder_provider isolines_provider routing_provider
|
|
github_user_id engine_enabled mapzen_routing_quota mapzen_routing_block_price soft_mapzen_routing_limit
|
|
no_map_logo org_admin last_name user_render_timeout database_render_timeout frontend_version
|
|
asset_host state company phone industry job_role password_reset_token password_reset_sent_at maintenance_mode
|
|
company_employees use_case
|
|
).freeze
|
|
|
|
BLANK_UUID = '00000000-0000-0000-0000-000000000000'.freeze
|
|
|
|
def compatible_version?(version)
|
|
version.to_i == CURRENT_VERSION.split('.')[0].to_i
|
|
end
|
|
end
|
|
|
|
module UserMetadataExportServiceImporter
|
|
include UserMetadataExportServiceConfiguration
|
|
include LayerImporter
|
|
include DataImportImporter
|
|
include ConnectorConfigurationImporter
|
|
|
|
def build_user_from_json_export(exported_json_string)
|
|
build_user_from_hash_export(parse_json(exported_json_string))
|
|
end
|
|
|
|
def build_user_from_hash_export(exported_hash)
|
|
raise 'Wrong export version' unless compatible_version?(exported_hash[:version])
|
|
|
|
build_user_from_hash(exported_hash[:user])
|
|
end
|
|
|
|
def build_search_tweets_from_json_export(exported_json_string)
|
|
build_search_tweets_from_hash_export(parse_json(exported_json_string))
|
|
end
|
|
|
|
def build_search_tweets_from_hash_export(exported_hash)
|
|
exported_hash[:user].fetch(:search_tweets, []).map { |st| build_search_tweet_from_hash(st) }
|
|
end
|
|
|
|
def save_imported_user(user)
|
|
user.save!
|
|
::User[user.id].after_save
|
|
|
|
client_application = user.client_applications.first
|
|
if client_application
|
|
client_application.access_tokens.each do |t|
|
|
# AR does not know about this, so it needs to be fixed
|
|
t.update_column(:type, 'AccessToken')
|
|
AccessToken[t.id].after_save
|
|
end
|
|
end
|
|
end
|
|
|
|
def save_imported_search_tweet(search_tweet, user)
|
|
if search_tweet.data_import
|
|
persisted_import = Carto::DataImport.where(id: search_tweet.data_import.id).first
|
|
search_tweet.data_import = persisted_import if persisted_import
|
|
search_tweet.table_id = search_tweet.data_import.table_id
|
|
else
|
|
# Some search tweets can be exported without data import if the FK point to a non-existent data import.
|
|
# However, this field is NOT NULL, so we cannot leave it empty.
|
|
# We could skip the import of the tweet, but instead, we keep it with an invalid ID (like in the source), so
|
|
# we can still correctly compute quota usage.
|
|
search_tweet.data_import_id = BLANK_UUID
|
|
end
|
|
search_tweet.user = user
|
|
search_tweet.save!
|
|
end
|
|
|
|
private
|
|
|
|
def parse_json(exported_json_string)
|
|
JSON.parse(exported_json_string, symbolize_names: true)
|
|
end
|
|
|
|
def build_user_from_hash(exported_user)
|
|
user = User.new(exported_user.slice(*EXPORTED_USER_ATTRIBUTES - [:id]))
|
|
|
|
user.feature_flags_user = exported_user[:feature_flags].map { |ff_name| build_feature_flag_from_name(ff_name) }
|
|
.compact
|
|
|
|
user.assets = exported_user[:assets].map { |asset| build_asset_from_hash(asset.symbolize_keys) }
|
|
|
|
user.layers = build_layers_from_hash(exported_user[:layers])
|
|
|
|
user.rate_limit = build_rate_limit_from_hash(exported_user[:rate_limit])
|
|
|
|
api_keys = exported_user[:api_keys] || []
|
|
user.api_keys += api_keys.map { |api_key| Carto::ApiKey.new_from_hash(api_key) }
|
|
|
|
user_multifactor_auths = exported_user[:user_multifactor_auths] || []
|
|
user.user_multifactor_auths += user_multifactor_auths.map { |uma| Carto::UserMultifactorAuth.new_from_hash(uma) }
|
|
|
|
if exported_user[:notifications]
|
|
user.static_notifications = Carto::UserNotification.create(notifications: exported_user[:notifications])
|
|
end
|
|
|
|
user.synchronization_oauths = build_synchronization_oauths_from_hash(exported_user[:synchronization_oauths])
|
|
|
|
user.connector_configurations = build_connector_configurations_from_hash(exported_user[:connector_configurations])
|
|
|
|
user.client_applications = build_client_applications_from_hash(exported_user[:client_application])
|
|
|
|
user.oauth_app_users = build_oauth_app_users_from_hash(exported_user[:oauth_app_users])
|
|
|
|
# Must be the last one to avoid attribute assignments to try to run SQL
|
|
user.id = exported_user[:id]
|
|
user
|
|
end
|
|
|
|
def build_feature_flag_from_name(ff_name)
|
|
ff = FeatureFlag.where(name: ff_name).first
|
|
if ff
|
|
Carto::FeatureFlagsUser.new(feature_flag_id: ff.id)
|
|
else
|
|
CartoDB::Logger.warning(message: 'Feature flag not found in user import', feature_flag: ff_name)
|
|
nil
|
|
end
|
|
end
|
|
|
|
def build_asset_from_hash(exported_asset)
|
|
Asset.new(
|
|
public_url: exported_asset[:public_url],
|
|
kind: exported_asset[:kind],
|
|
storage_info: exported_asset[:storage_info]
|
|
)
|
|
end
|
|
|
|
def build_search_tweet_from_hash(exported_search_tweet)
|
|
SearchTweet.new(
|
|
data_import: build_data_import_from_hash(exported_search_tweet[:data_import]),
|
|
service_item_id: exported_search_tweet[:service_item_id],
|
|
retrieved_items: exported_search_tweet[:retrieved_items],
|
|
state: exported_search_tweet[:state],
|
|
created_at: exported_search_tweet[:created_at],
|
|
updated_at: exported_search_tweet[:updated_at]
|
|
)
|
|
end
|
|
|
|
def build_rate_limit_from_hash(exported_hash)
|
|
return unless exported_hash
|
|
|
|
rate_limit = Carto::RateLimit.from_api_attributes(exported_hash[:limits])
|
|
rate_limit.id = exported_hash[:id]
|
|
|
|
rate_limit
|
|
end
|
|
|
|
def build_synchronization_oauths_from_hash(exported_array)
|
|
return [] unless exported_array.present?
|
|
|
|
exported_array.map { |so| build_synchronization_oauth_from_hash(so) }
|
|
end
|
|
|
|
def build_synchronization_oauth_from_hash(exported_hash)
|
|
SynchronizationOauth.new(
|
|
service: exported_hash[:service],
|
|
token: exported_hash[:token],
|
|
created_at: exported_hash[:created_at],
|
|
updated_at: exported_hash[:updated_at]
|
|
)
|
|
end
|
|
|
|
def build_oauth_token_fom_hash(exported_oauth_token)
|
|
Carto::OauthToken.new(
|
|
token: exported_oauth_token[:token],
|
|
secret: exported_oauth_token[:secret],
|
|
callback_url: exported_oauth_token[:callback_url],
|
|
verifier: exported_oauth_token[:verifier],
|
|
scope: exported_oauth_token[:scope],
|
|
authorized_at: exported_oauth_token[:authorized_at],
|
|
invalidated_at: exported_oauth_token[:invalidated_at],
|
|
valid_to: exported_oauth_token[:valid_to],
|
|
created_at: exported_oauth_token[:created_at],
|
|
updated_at: exported_oauth_token[:updated_at]
|
|
)
|
|
end
|
|
|
|
def build_client_applications_from_hash(client_app_hash)
|
|
return [] unless client_app_hash
|
|
|
|
client_application = Carto::ClientApplication.new(
|
|
name: client_app_hash[:name],
|
|
url: client_app_hash[:url],
|
|
support_url: client_app_hash[:support_url],
|
|
callback_url: client_app_hash[:callback_url],
|
|
key: client_app_hash[:key],
|
|
secret: client_app_hash[:secret],
|
|
created_at: client_app_hash[:created_at],
|
|
updated_at: client_app_hash[:updated_at],
|
|
oauth_tokens: client_app_hash[:oauth_tokens].map { |t| build_oauth_token_fom_hash(t) },
|
|
access_tokens: client_app_hash[:access_tokens].map { |t| build_oauth_token_fom_hash(t) }
|
|
)
|
|
|
|
[client_application]
|
|
end
|
|
|
|
def build_oauth_app_users_from_hash(oauth_app_users)
|
|
return [] unless oauth_app_users
|
|
oauth_app_users.map { |oau| build_oauth_app_user_from_hash(oau) }
|
|
end
|
|
|
|
def build_oauth_app_user_from_hash(oau_hash)
|
|
oau = Carto::OauthAppUser.new(
|
|
id: oau_hash[:id],
|
|
oauth_app_id: oau_hash[:oauth_app_id],
|
|
scopes: oau_hash[:scopes],
|
|
created_at: oau_hash[:created_at],
|
|
updated_at: oau_hash[:updated_at],
|
|
skip_role_setup: true
|
|
)
|
|
|
|
if oau_hash[:oauth_authorization_codes]
|
|
oau.oauth_authorization_codes = oau_hash[:oauth_authorization_codes].map do |oac_hash|
|
|
build_oauth_authorization_code_from_hash(oac_hash)
|
|
end
|
|
end
|
|
|
|
if oau_hash[:oauth_access_tokens]
|
|
oau.oauth_access_tokens = oau_hash[:oauth_access_tokens].map do |oat_hash|
|
|
build_oauth_access_token_from_hash(oat_hash)
|
|
end
|
|
end
|
|
|
|
if oau_hash[:oauth_refresh_tokens]
|
|
oau.oauth_refresh_tokens = oau_hash[:oauth_refresh_tokens].map do |ort_hash|
|
|
build_oauth_refresh_token_from_hash(ort_hash)
|
|
end
|
|
end
|
|
|
|
oau
|
|
end
|
|
|
|
def build_oauth_authorization_code_from_hash(oac_hash)
|
|
Carto::OauthAuthorizationCode.new(
|
|
scopes: oac_hash[:scopes],
|
|
code: oac_hash[:code],
|
|
redirect_uri: oac_hash[:redirect_uri],
|
|
created_at: oac_hash[:created_at]
|
|
)
|
|
end
|
|
|
|
def build_oauth_access_token_from_hash(oat_hash)
|
|
Carto::OauthAccessToken.new(
|
|
api_key_id: oat_hash[:api_key_id],
|
|
scopes: oat_hash[:scopes],
|
|
created_at: oat_hash[:created_at],
|
|
skip_api_key_creation: true
|
|
)
|
|
end
|
|
|
|
def build_oauth_refresh_token_from_hash(ort_hash)
|
|
Carto::OauthRefreshToken.new(
|
|
token: ort_hash[:token],
|
|
scopes: ort_hash[:scopes],
|
|
created_at: ort_hash[:created_at],
|
|
updated_at: ort_hash[:updated_at],
|
|
skip_token_regeneration: true
|
|
)
|
|
end
|
|
end
|
|
|
|
module UserMetadataExportServiceExporter
|
|
include UserMetadataExportServiceConfiguration
|
|
include LayerExporter
|
|
include DataImportExporter
|
|
include ConnectorConfigurationExporter
|
|
|
|
def export_user_json_string(user)
|
|
export_user_json_hash(user).to_json
|
|
end
|
|
|
|
def export_user_json_hash(user)
|
|
{
|
|
version: CURRENT_VERSION,
|
|
user: export(user)
|
|
}
|
|
end
|
|
|
|
private
|
|
|
|
def export(user)
|
|
user_hash = EXPORTED_USER_ATTRIBUTES.map { |att| [att, user.attributes[att.to_s]] }.to_h
|
|
|
|
user_hash[:feature_flags] = user.feature_flags_user.map(&:feature_flag).map(&:name)
|
|
|
|
user_hash[:assets] = user.assets.map { |a| export_asset(a) }
|
|
|
|
user_hash[:layers] = user.layers.map { |l| export_layer(l) }
|
|
|
|
user_hash[:search_tweets] = user.search_tweets.map { |st| export_search_tweet(st) }
|
|
|
|
user_hash[:api_keys] = user.api_keys.map { |api_key| export_api_key(api_key) }
|
|
|
|
user_hash[:user_multifactor_auths] = user.user_multifactor_auths.map { |uma| export_user_multifactor_auth(uma) }
|
|
|
|
user_hash[:rate_limit] = export_rate_limit(user.rate_limit)
|
|
|
|
user_hash[:notifications] = user.static_notifications.notifications
|
|
|
|
user_hash[:synchronization_oauths] = user.synchronization_oauths.map { |so| export_synchronization_oauth(so) }
|
|
|
|
user_hash[:connector_configurations] = user.connector_configurations.map do |cc|
|
|
export_connector_configuration(cc)
|
|
end
|
|
|
|
# Use Sequel models to export. Single table inheritance causes AR to try and create Sequel models -> fail.
|
|
user_hash[:client_application] = export_client_application(::User[user.id].client_application)
|
|
|
|
user_hash[:oauth_app_users] = user.oauth_app_users.map { |oau| export_oauth_app_user(oau) }
|
|
|
|
user_hash
|
|
end
|
|
|
|
def export_client_application(app)
|
|
return nil unless app
|
|
a_t_tokens = app.access_tokens.map(&:token)
|
|
{
|
|
name: app.name,
|
|
url: app.url,
|
|
support_url: app.support_url,
|
|
callback_url: app.callback_url,
|
|
key: app.key,
|
|
secret: app.secret,
|
|
created_at: app.created_at,
|
|
updated_at: app.updated_at,
|
|
oauth_tokens: app.oauth_tokens.reject { |t| a_t_tokens.include?(t.token) }.map { |ot| export_oauth_token(ot) },
|
|
access_tokens: app.access_tokens.map { |ot| export_oauth_token(ot) }
|
|
}
|
|
end
|
|
|
|
def export_oauth_token(oauth_token)
|
|
{
|
|
token: oauth_token.token,
|
|
secret: oauth_token.secret,
|
|
callback_url: oauth_token.callback_url,
|
|
verifier: oauth_token.verifier,
|
|
scope: oauth_token.scope,
|
|
authorized_at: oauth_token.authorized_at,
|
|
invalidated_at: oauth_token.invalidated_at,
|
|
valid_to: oauth_token.valid_to,
|
|
created_at: oauth_token.created_at,
|
|
updated_at: oauth_token.updated_at
|
|
}
|
|
end
|
|
|
|
def export_asset(asset)
|
|
{
|
|
public_url: asset.public_url,
|
|
kind: asset.kind,
|
|
storage_info: asset.storage_info
|
|
}
|
|
end
|
|
|
|
def export_search_tweet(search_tweet)
|
|
{
|
|
data_import: export_data_import(search_tweet.data_import),
|
|
service_item_id: search_tweet.service_item_id,
|
|
retrieved_items: search_tweet.retrieved_items,
|
|
state: search_tweet.state,
|
|
created_at: search_tweet.created_at,
|
|
updated_at: search_tweet.updated_at
|
|
}
|
|
end
|
|
|
|
def export_api_key(api_key)
|
|
{
|
|
id: api_key.id,
|
|
created_at: api_key.created_at,
|
|
db_password: api_key.db_password,
|
|
db_role: api_key.db_role,
|
|
name: api_key.name,
|
|
token: api_key.token,
|
|
type: api_key.type,
|
|
updated_at: api_key.updated_at,
|
|
grants: api_key.grants,
|
|
user_id: api_key.user_id
|
|
}
|
|
end
|
|
|
|
def export_user_multifactor_auth(user_multifactor_auth)
|
|
exported_keys = %i(user_id type shared_secret enabled last_login created_at updated_at)
|
|
user_multifactor_auth.to_h.slice(*exported_keys)
|
|
end
|
|
|
|
def export_rate_limit(rate_limit)
|
|
return unless rate_limit
|
|
|
|
{
|
|
id: rate_limit.id,
|
|
limits: rate_limit.api_attributes
|
|
}
|
|
end
|
|
|
|
def export_synchronization_oauth(sync_oauth)
|
|
{
|
|
service: sync_oauth.service,
|
|
token: sync_oauth.token,
|
|
created_at: sync_oauth.created_at,
|
|
updated_at: sync_oauth.updated_at
|
|
}
|
|
end
|
|
|
|
def export_oauth_app_user(oau)
|
|
oauth_authorization_codes = oau.oauth_authorization_codes.map { |oac| export_oauth_authorization_code(oac) }
|
|
oauth_access_tokens = oau.oauth_access_tokens.map { |oat| export_oauth_access_token(oat) }
|
|
oauth_refresh_tokens = oau.oauth_refresh_tokens.map { |ort| export_oauth_refresh_token(ort) }
|
|
|
|
{
|
|
id: oau.id,
|
|
oauth_app_id: oau.oauth_app_id,
|
|
scopes: oau.scopes,
|
|
created_at: oau.created_at,
|
|
updated_at: oau.updated_at,
|
|
oauth_authorization_codes: oauth_authorization_codes,
|
|
oauth_access_tokens: oauth_access_tokens,
|
|
oauth_refresh_tokens: oauth_refresh_tokens
|
|
}
|
|
end
|
|
|
|
def export_oauth_authorization_code(oac)
|
|
{
|
|
scopes: oac.scopes,
|
|
code: oac.code,
|
|
redirect_uri: oac.redirect_uri,
|
|
created_at: oac.created_at
|
|
}
|
|
end
|
|
|
|
def export_oauth_access_token(oat)
|
|
{
|
|
api_key_id: oat.api_key_id,
|
|
scopes: oat.scopes,
|
|
created_at: oat.created_at
|
|
}
|
|
end
|
|
|
|
def export_oauth_refresh_token(ort)
|
|
{
|
|
token: ort.token,
|
|
scopes: ort.scopes,
|
|
created_at: ort.created_at,
|
|
updated_at: ort.updated_at
|
|
}
|
|
end
|
|
end
|
|
|
|
class UserAlreadyExists < RuntimeError; 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 UserMetadataExportService
|
|
include UserMetadataExportServiceImporter
|
|
include UserMetadataExportServiceExporter
|
|
|
|
def export_to_directory(user, path)
|
|
root_dir = Pathname.new(path)
|
|
root_dir.mkpath
|
|
|
|
user_json = export_user_json_string(user)
|
|
root_dir.join("user_#{user.id}.json").open('w') { |file| file.write(user_json) }
|
|
|
|
redis_json = Carto::RedisExportService.new.export_user_json_string(user)
|
|
root_dir.join("redis_user_#{user.id}.json").open('w') { |file| file.write(redis_json) }
|
|
|
|
export_user_visualizations_to_directory(user, Carto::Visualization::TYPE_REMOTE, path)
|
|
export_user_visualizations_to_directory(user, Carto::Visualization::TYPE_CANONICAL, path)
|
|
export_user_visualizations_to_directory(user, Carto::Visualization::TYPE_DERIVED, path)
|
|
end
|
|
|
|
def import_from_directory(path)
|
|
user = user_from_file(path)
|
|
|
|
raise UserAlreadyExists.new if ::Carto::User.exists?(id: user.id)
|
|
save_imported_user(user)
|
|
|
|
Carto::RedisExportService.new.restore_redis_from_json_export(redis_user_file(path))
|
|
|
|
user
|
|
end
|
|
|
|
def rollback_import_from_directory(path)
|
|
user = user_from_file(path)
|
|
return unless user
|
|
|
|
user = ::User[user.id]
|
|
return unless user
|
|
|
|
carto_user = Carto::User.find(user.id)
|
|
carto_user.assets.each(&:delete)
|
|
carto_user.destroy
|
|
user.before_destroy(skip_table_drop: true)
|
|
|
|
Carto::RedisExportService.new.remove_redis_from_json_export(redis_user_file(path))
|
|
rescue ActiveRecord::RecordNotFound
|
|
# User was not created so not found and no redis removal needed
|
|
end
|
|
|
|
def import_user_visualizations_from_directory(user, type, meta_path)
|
|
with_non_viewer_user(user) do
|
|
Dir["#{meta_path}/#{type}_*#{Carto::VisualizationExporter::EXPORT_EXTENSION}"].each do |fname|
|
|
imported_vis = Carto::VisualizationsExportService2.new.build_visualization_from_json_export(File.read(fname))
|
|
Carto::VisualizationsExportPersistenceService.new.save_import(user, imported_vis, full_restore: true)
|
|
if Carto::VisualizationsExportService2.new.marked_as_vizjson2_from_json_export?(File.read(fname))
|
|
imported_vis.mark_as_vizjson2
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def import_metadata_from_directory(user, meta_path)
|
|
import_user_visualizations_from_directory(user, Carto::Visualization::TYPE_REMOTE, meta_path)
|
|
import_user_visualizations_from_directory(user, Carto::Visualization::TYPE_CANONICAL, meta_path)
|
|
import_user_visualizations_from_directory(user, Carto::Visualization::TYPE_DERIVED, meta_path)
|
|
|
|
import_search_tweets_from_directory(user, meta_path)
|
|
end
|
|
|
|
def import_search_tweets_from_directory(user, meta_path)
|
|
user_file = user_file_dir(meta_path)
|
|
search_tweets = build_search_tweets_from_json_export(File.read(user_file))
|
|
search_tweets.each { |st| save_imported_search_tweet(st, user) }
|
|
end
|
|
|
|
private
|
|
|
|
def user_from_file(path)
|
|
build_user_from_json_export(File.read(user_file_dir(path)))
|
|
end
|
|
|
|
def user_file_dir(path)
|
|
Dir["#{path}/user_*.json"].first
|
|
end
|
|
|
|
def redis_user_file(path)
|
|
File.read(Dir["#{path}/redis_user_*.json"].first)
|
|
end
|
|
|
|
def export_user_visualizations_to_directory(user, type, path)
|
|
root_dir = Pathname.new(path)
|
|
user.visualizations.where(type: type).each do |visualization|
|
|
next if visualization.canonical? && should_skip_canonical_viz_export(visualization)
|
|
next if !visualization.remote? && visualization.map.nil?
|
|
|
|
visualization_export = Carto::VisualizationsExportService2.new.export_visualization_json_string(
|
|
visualization.id, user, with_password: true
|
|
)
|
|
filename = "#{visualization.type}_#{visualization.id}#{Carto::VisualizationExporter::EXPORT_EXTENSION}"
|
|
root_dir.join(filename).open('w') { |file| file.write(visualization_export) }
|
|
end
|
|
end
|
|
|
|
def should_skip_canonical_viz_export(viz)
|
|
return true if viz.table.nil?
|
|
|
|
viz.user.visualizations.where(type: viz.type,
|
|
name: viz.name).all.sort_by(&:updated_at).last.id != viz.id
|
|
end
|
|
|
|
def with_non_viewer_user(user)
|
|
was_viewer = user.viewer
|
|
if user.viewer
|
|
user.update_attributes(viewer: false)
|
|
::User[user.id].reload
|
|
end
|
|
|
|
yield
|
|
ensure
|
|
if was_viewer
|
|
user.update_attributes(viewer: true)
|
|
::User[user.id].reload
|
|
end
|
|
end
|
|
end
|
|
end
|