def test_export_chart_command(self, mock_g): mock_g.user = security_manager.find_user("admin") example_chart = db.session.query(Slice).all()[0] command = ExportChartsCommand([example_chart.id]) contents = dict(command.run()) expected = [ "metadata.yaml", "charts/energy_sankey.yaml", "datasets/examples/energy_usage.yaml", "databases/examples.yaml", ] assert expected == list(contents.keys()) metadata = yaml.safe_load(contents["charts/energy_sankey.yaml"]) assert metadata == { "slice_name": "Energy Sankey", "viz_type": "sankey", "params": { "collapsed_fieldsets": "", "groupby": ["source", "target",], "metric": "sum__value", "row_limit": "5000", "slice_name": "Energy Sankey", "viz_type": "sankey", }, "cache_timeout": None, "dataset_uuid": str(example_chart.table.uuid), "uuid": str(example_chart.uuid), "version": "1.0.0", }
def test_export_chart_command_invalid_dataset(self, mock_g): """Test that an error is raised when exporting an invalid dataset""" mock_g.user = security_manager.find_user("admin") command = ExportChartsCommand([-1]) contents = command.run() with self.assertRaises(ChartNotFoundError): next(contents)
def test_export_chart_command_no_access(self, mock_g): """Test that users can't export datasets they don't have access to""" mock_g.user = security_manager.find_user("gamma") example_chart = db.session.query(Slice).all()[0] command = ExportChartsCommand([example_chart.id]) contents = command.run() with self.assertRaises(ChartNotFoundError): next(contents)
def _export(model: Dashboard) -> Iterator[Tuple[str, str]]: dashboard_slug = secure_filename(model.dashboard_title) file_name = f"dashboards/{dashboard_slug}.yaml" payload = model.export_to_dict( recursive=False, include_parent_ref=False, include_defaults=True, export_uuids=True, ) # TODO (betodealmeida): move this logic to export_to_dict once this # becomes the default export endpoint for key, new_name in JSON_KEYS.items(): if key in payload: value = payload.pop(key) try: payload[new_name] = json.loads(value) except (TypeError, json.decoder.JSONDecodeError): logger.info("Unable to decode `%s` field: %s", key, value) payload[new_name] = {} payload["version"] = EXPORT_VERSION file_content = yaml.safe_dump(payload, sort_keys=False) yield file_name, file_content chart_ids = [chart.id for chart in model.slices] yield from ExportChartsCommand(chart_ids).run()
def test_export_chart_command_no_related(self, mock_g): """ Test that only the chart is exported when export_related=False. """ mock_g.user = security_manager.find_user("admin") example_chart = (db.session.query(Slice).filter_by( slice_name="Energy Sankey").one()) command = ExportChartsCommand([example_chart.id], export_related=False) contents = dict(command.run()) expected = [ "metadata.yaml", f"charts/Energy_Sankey_{example_chart.id}.yaml", ] assert expected == list(contents.keys())
def _export(model: Dashboard) -> Iterator[Tuple[str, str]]: dashboard_slug = secure_filename(model.dashboard_title) file_name = f"dashboards/{dashboard_slug}.yaml" payload = model.export_to_dict( recursive=False, include_parent_ref=False, include_defaults=True, export_uuids=True, ) # TODO (betodealmeida): move this logic to export_to_dict once this # becomes the default export endpoint for key, new_name in JSON_KEYS.items(): value: Optional[str] = payload.pop(key, None) if value: try: payload[new_name] = json.loads(value) except (TypeError, json.decoder.JSONDecodeError): logger.info("Unable to decode `%s` field: %s", key, value) payload[new_name] = {} # the mapping between dashboard -> charts is inferred from the position # attributes, so if it's not present we need to add a default config if not payload.get("position"): payload["position"] = default_position(model.dashboard_title, model.slices) payload["version"] = EXPORT_VERSION file_content = yaml.safe_dump(payload, sort_keys=False) yield file_name, file_content chart_ids = [chart.id for chart in model.slices] yield from ExportChartsCommand(chart_ids).run()
def _export( model: Dashboard, export_related: bool = True ) -> Iterator[Tuple[str, str]]: dashboard_slug = secure_filename(model.dashboard_title) file_name = f"dashboards/{dashboard_slug}.yaml" payload = model.export_to_dict( recursive=False, include_parent_ref=False, include_defaults=True, export_uuids=True, ) # TODO (betodealmeida): move this logic to export_to_dict once this # becomes the default export endpoint for key, new_name in JSON_KEYS.items(): value: Optional[str] = payload.pop(key, None) if value: try: payload[new_name] = json.loads(value) except (TypeError, json.decoder.JSONDecodeError): logger.info("Unable to decode `%s` field: %s", key, value) payload[new_name] = {} # Extract all native filter datasets and replace native # filter dataset references with uuid for native_filter in payload.get("metadata", {}).get( "native_filter_configuration", [] ): for target in native_filter.get("targets", []): dataset_id = target.pop("datasetId", None) if dataset_id is not None: dataset = DatasetDAO.find_by_id(dataset_id) target["datasetUuid"] = str(dataset.uuid) if export_related: yield from ExportDatasetsCommand([dataset_id]).run() # the mapping between dashboard -> charts is inferred from the position # attribute, so if it's not present we need to add a default config if not payload.get("position"): payload["position"] = get_default_position(model.dashboard_title) # if any charts or not referenced in position, we need to add them # in a new row referenced_charts = find_chart_uuids(payload["position"]) orphan_charts = { chart for chart in model.slices if str(chart.uuid) not in referenced_charts } if orphan_charts: payload["position"] = append_charts(payload["position"], orphan_charts) payload["version"] = EXPORT_VERSION file_content = yaml.safe_dump(payload, sort_keys=False) yield file_name, file_content if export_related: chart_ids = [chart.id for chart in model.slices] yield from ExportChartsCommand(chart_ids).run()
def test_export_chart_command_key_order(self, mock_g): """Test that they keys in the YAML have the same order as export_fields""" mock_g.user = security_manager.find_user("admin") example_chart = db.session.query(Slice).all()[0] command = ExportChartsCommand([example_chart.id]) contents = dict(command.run()) metadata = yaml.safe_load(contents["charts/energy_sankey.yaml"]) assert list(metadata.keys()) == [ "slice_name", "viz_type", "params", "cache_timeout", "uuid", "version", "dataset_uuid", ]
def export(self, **kwargs: Any) -> Response: """Export charts --- get: description: >- Exports multiple charts and downloads them as YAML files parameters: - in: query name: q content: application/json: schema: $ref: '#/components/schemas/get_export_ids_schema' responses: 200: description: A zip file with chart(s), dataset(s) and database(s) as YAML content: application/zip: schema: type: string format: binary 400: $ref: '#/components/responses/400' 401: $ref: '#/components/responses/401' 404: $ref: '#/components/responses/404' 500: $ref: '#/components/responses/500' """ token = request.args.get("token") requested_ids = kwargs["rison"] timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") root = f"chart_export_{timestamp}" filename = f"{root}.zip" buf = BytesIO() with ZipFile(buf, "w") as bundle: try: for file_name, file_content in ExportChartsCommand( requested_ids).run(): with bundle.open(f"{root}/{file_name}", "w") as fp: fp.write(file_content.encode()) except ChartNotFoundError: return self.response_404() buf.seek(0) response = send_file( buf, mimetype="application/zip", as_attachment=True, attachment_filename=filename, ) if token: response.set_cookie(token, "done", max_age=600) return response
def test_export_chart_with_query_context(self, mock_g): """Test that charts that have a query_context are exported correctly""" mock_g.user = security_manager.find_user("alpha") example_chart = db.session.query(Slice).filter_by( slice_name="Heatmap").one() command = ExportChartsCommand([example_chart.id]) contents = dict(command.run()) expected = [ "metadata.yaml", f"charts/Heatmap_{example_chart.id}.yaml", "datasets/examples/energy_usage.yaml", "databases/examples.yaml", ] assert expected == list(contents.keys()) metadata = yaml.safe_load( contents[f"charts/Heatmap_{example_chart.id}.yaml"]) assert metadata == { "slice_name": "Heatmap", "viz_type": "heatmap", "params": { "all_columns_x": "source", "all_columns_y": "target", "canvas_image_rendering": "pixelated", "collapsed_fieldsets": "", "linear_color_scheme": "blue_white_yellow", "metric": "sum__value", "normalize_across": "heatmap", "slice_name": "Heatmap", "viz_type": "heatmap", "xscale_interval": "1", "yscale_interval": "1", }, "cache_timeout": None, "dataset_uuid": str(example_chart.table.uuid), "uuid": str(example_chart.uuid), "version": "1.0.0", }