You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by dp...@apache.org on 2020/10/01 10:46:57 UTC
[incubator-superset] branch master updated: feat: CRUD REST API for
CSS Templates (#11114)
This is an automated email from the ASF dual-hosted git repository.
dpgaspar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push:
new fdb26f6 feat: CRUD REST API for CSS Templates (#11114)
fdb26f6 is described below
commit fdb26f61314ad03e18feb7b7a9004fc177dd7e7c
Author: Daniel Vaz Gaspar <da...@gmail.com>
AuthorDate: Thu Oct 1 11:46:25 2020 +0100
feat: CRUD REST API for CSS Templates (#11114)
* feat: CSS Template CRUD API
* fix API docs
* fix copy pasta
* lint
---
superset/app.py | 2 +
superset/css_templates/__init__.py | 16 ++
superset/css_templates/api.py | 133 +++++++++
superset/css_templates/commands/__init__.py | 16 ++
superset/css_templates/commands/bulk_delete.py | 53 ++++
superset/css_templates/commands/exceptions.py | 27 ++
superset/css_templates/dao.py | 45 ++++
superset/css_templates/filters.py | 40 +++
superset/css_templates/schemas.py | 33 +++
tests/css_templates/__init__.py | 16 ++
tests/css_templates/api_tests.py | 356 +++++++++++++++++++++++++
11 files changed, 737 insertions(+)
diff --git a/superset/app.py b/superset/app.py
index 1ef5b30..e5aa287 100644
--- a/superset/app.py
+++ b/superset/app.py
@@ -140,6 +140,7 @@ class SupersetAppInitializer:
TableColumnInlineView,
TableModelView,
)
+ from superset.css_templates.api import CssTemplateRestApi
from superset.dashboards.api import DashboardRestApi
from superset.databases.api import DatabaseRestApi
from superset.datasets.api import DatasetRestApi
@@ -197,6 +198,7 @@ class SupersetAppInitializer:
#
appbuilder.add_api(CacheRestApi)
appbuilder.add_api(ChartRestApi)
+ appbuilder.add_api(CssTemplateRestApi)
appbuilder.add_api(DashboardRestApi)
appbuilder.add_api(DatabaseRestApi)
appbuilder.add_api(DatasetRestApi)
diff --git a/superset/css_templates/__init__.py b/superset/css_templates/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/superset/css_templates/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/superset/css_templates/api.py b/superset/css_templates/api.py
new file mode 100644
index 0000000..4da895c
--- /dev/null
+++ b/superset/css_templates/api.py
@@ -0,0 +1,133 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import logging
+from typing import Any
+
+from flask import g, Response
+from flask_appbuilder.api import expose, protect, rison, safe
+from flask_appbuilder.models.sqla.interface import SQLAInterface
+from flask_babel import ngettext
+
+from superset.constants import RouteMethod
+from superset.css_templates.commands.bulk_delete import BulkDeleteCssTemplateCommand
+from superset.css_templates.commands.exceptions import (
+ CssTemplateBulkDeleteFailedError,
+ CssTemplateNotFoundError,
+)
+from superset.css_templates.filters import CssTemplateAllTextFilter
+from superset.css_templates.schemas import (
+ get_delete_ids_schema,
+ openapi_spec_methods_override,
+)
+from superset.models.core import CssTemplate
+from superset.views.base_api import BaseSupersetModelRestApi, statsd_metrics
+
+logger = logging.getLogger(__name__)
+
+
+class CssTemplateRestApi(BaseSupersetModelRestApi):
+ datamodel = SQLAInterface(CssTemplate)
+
+ include_route_methods = RouteMethod.REST_MODEL_VIEW_CRUD_SET | {
+ "bulk_delete", # not using RouteMethod since locally defined
+ }
+ class_permission_name = "CssTemplateModelView"
+ resource_name = "css_template"
+ allow_browser_login = True
+
+ show_columns = [
+ "created_by.first_name",
+ "created_by.id",
+ "created_by.last_name",
+ "css",
+ "id",
+ "template_name",
+ ]
+ list_columns = [
+ "changed_on_delta_humanized",
+ "created_on",
+ "created_by.first_name",
+ "created_by.id",
+ "created_by.last_name",
+ "css",
+ "id",
+ "template_name",
+ ]
+ add_columns = ["css", "template_name"]
+ edit_columns = add_columns
+ order_columns = ["template_name"]
+
+ search_filters = {"template_name": [CssTemplateAllTextFilter]}
+
+ apispec_parameter_schemas = {
+ "get_delete_ids_schema": get_delete_ids_schema,
+ }
+ openapi_spec_tag = "CSS Templates"
+ openapi_spec_methods = openapi_spec_methods_override
+
+ @expose("/", methods=["DELETE"])
+ @protect()
+ @safe
+ @statsd_metrics
+ @rison(get_delete_ids_schema)
+ def bulk_delete(self, **kwargs: Any) -> Response:
+ """Delete bulk CSS Templates
+ ---
+ delete:
+ description: >-
+ Deletes multiple css templates in a bulk operation.
+ parameters:
+ - in: query
+ name: q
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/get_delete_ids_schema'
+ responses:
+ 200:
+ description: CSS templates bulk delete
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ 401:
+ $ref: '#/components/responses/401'
+ 404:
+ $ref: '#/components/responses/404'
+ 422:
+ $ref: '#/components/responses/422'
+ 500:
+ $ref: '#/components/responses/500'
+ """
+ item_ids = kwargs["rison"]
+ try:
+ BulkDeleteCssTemplateCommand(g.user, item_ids).run()
+ return self.response(
+ 200,
+ message=ngettext(
+ "Deleted %(num)d css template",
+ "Deleted %(num)d css templates",
+ num=len(item_ids),
+ ),
+ )
+ except CssTemplateNotFoundError:
+ return self.response_404()
+ except CssTemplateBulkDeleteFailedError as ex:
+ return self.response_422(message=str(ex))
diff --git a/superset/css_templates/commands/__init__.py b/superset/css_templates/commands/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/superset/css_templates/commands/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/superset/css_templates/commands/bulk_delete.py b/superset/css_templates/commands/bulk_delete.py
new file mode 100644
index 0000000..40b2e80
--- /dev/null
+++ b/superset/css_templates/commands/bulk_delete.py
@@ -0,0 +1,53 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import logging
+from typing import List, Optional
+
+from flask_appbuilder.security.sqla.models import User
+
+from superset.commands.base import BaseCommand
+from superset.css_templates.commands.exceptions import (
+ CssTemplateBulkDeleteFailedError,
+ CssTemplateNotFoundError,
+)
+from superset.css_templates.dao import CssTemplateDAO
+from superset.dao.exceptions import DAODeleteFailedError
+from superset.models.core import CssTemplate
+
+logger = logging.getLogger(__name__)
+
+
+class BulkDeleteCssTemplateCommand(BaseCommand):
+ def __init__(self, user: User, model_ids: List[int]):
+ self._actor = user
+ self._model_ids = model_ids
+ self._models: Optional[List[CssTemplate]] = None
+
+ def run(self) -> None:
+ self.validate()
+ try:
+ CssTemplateDAO.bulk_delete(self._models)
+ return None
+ except DAODeleteFailedError as ex:
+ logger.exception(ex.exception)
+ raise CssTemplateBulkDeleteFailedError()
+
+ def validate(self) -> None:
+ # Validate/populate model exists
+ self._models = CssTemplateDAO.find_by_ids(self._model_ids)
+ if not self._models or len(self._models) != len(self._model_ids):
+ raise CssTemplateNotFoundError()
diff --git a/superset/css_templates/commands/exceptions.py b/superset/css_templates/commands/exceptions.py
new file mode 100644
index 0000000..d950822
--- /dev/null
+++ b/superset/css_templates/commands/exceptions.py
@@ -0,0 +1,27 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from flask_babel import lazy_gettext as _
+
+from superset.commands.exceptions import CommandException, DeleteFailedError
+
+
+class CssTemplateBulkDeleteFailedError(DeleteFailedError):
+ message = _("CSS template could not be deleted.")
+
+
+class CssTemplateNotFoundError(CommandException):
+ message = _("CSS template not found.")
diff --git a/superset/css_templates/dao.py b/superset/css_templates/dao.py
new file mode 100644
index 0000000..8f9d36b
--- /dev/null
+++ b/superset/css_templates/dao.py
@@ -0,0 +1,45 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+import logging
+from typing import List, Optional
+
+from sqlalchemy.exc import SQLAlchemyError
+
+from superset.dao.base import BaseDAO
+from superset.dao.exceptions import DAODeleteFailedError
+from superset.extensions import db
+from superset.models.core import CssTemplate
+
+logger = logging.getLogger(__name__)
+
+
+class CssTemplateDAO(BaseDAO):
+ model_cls = CssTemplate
+
+ @staticmethod
+ def bulk_delete(models: Optional[List[CssTemplate]], commit: bool = True) -> None:
+ item_ids = [model.id for model in models] if models else []
+ try:
+ db.session.query(CssTemplate).filter(CssTemplate.id.in_(item_ids)).delete(
+ synchronize_session="fetch"
+ )
+ if commit:
+ db.session.commit()
+ except SQLAlchemyError:
+ if commit:
+ db.session.rollback()
+ raise DAODeleteFailedError()
diff --git a/superset/css_templates/filters.py b/superset/css_templates/filters.py
new file mode 100644
index 0000000..e44382a
--- /dev/null
+++ b/superset/css_templates/filters.py
@@ -0,0 +1,40 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from typing import Any
+
+from flask_babel import lazy_gettext as _
+from sqlalchemy import or_
+from sqlalchemy.orm.query import Query
+
+from superset.models.core import CssTemplate
+from superset.views.base import BaseFilter
+
+
+class CssTemplateAllTextFilter(BaseFilter): # pylint: disable=too-few-public-methods
+ name = _("All Text")
+ arg_name = "css_template_all_text"
+
+ def apply(self, query: Query, value: Any) -> Query:
+ if not value:
+ return query
+ ilike_value = f"%{value}%"
+ return query.filter(
+ or_(
+ CssTemplate.template_name.ilike(ilike_value),
+ CssTemplate.css.ilike(ilike_value),
+ )
+ )
diff --git a/superset/css_templates/schemas.py b/superset/css_templates/schemas.py
new file mode 100644
index 0000000..e8243c2
--- /dev/null
+++ b/superset/css_templates/schemas.py
@@ -0,0 +1,33 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+openapi_spec_methods_override = {
+ "get": {"get": {"description": "Get a CSS template"}},
+ "get_list": {
+ "get": {
+ "description": "Get a list of CSS templates, use Rison or JSON "
+ "query parameters for filtering, sorting,"
+ " pagination and for selecting specific"
+ " columns and metadata.",
+ }
+ },
+ "post": {"post": {"description": "Create a CSS template"}},
+ "put": {"put": {"description": "Update a CSS template"}},
+ "delete": {"delete": {"description": "Delete CSS template"}},
+}
+
+get_delete_ids_schema = {"type": "array", "items": {"type": "integer"}}
diff --git a/tests/css_templates/__init__.py b/tests/css_templates/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/tests/css_templates/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
diff --git a/tests/css_templates/api_tests.py b/tests/css_templates/api_tests.py
new file mode 100644
index 0000000..095c246
--- /dev/null
+++ b/tests/css_templates/api_tests.py
@@ -0,0 +1,356 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# isort:skip_file
+"""Unit tests for Superset"""
+import json
+import pytest
+import prison
+from sqlalchemy.sql import func
+
+import tests.test_app
+from superset import db
+from superset.models.core import CssTemplate
+from superset.utils.core import get_example_database
+
+from tests.base_tests import SupersetTestCase
+
+
+CSS_TEMPLATES_FIXTURE_COUNT = 5
+
+
+class TestCssTemplateApi(SupersetTestCase):
+ def insert_css_template(
+ self, template_name: str, css: str, created_by_username: str = "admin",
+ ) -> CssTemplate:
+ admin = self.get_user(created_by_username)
+ css_template = CssTemplate(
+ template_name=template_name, css=css, created_by=admin
+ )
+ db.session.add(css_template)
+ db.session.commit()
+ return css_template
+
+ @pytest.fixture()
+ def create_css_templates(self):
+ with self.create_app().app_context():
+ css_templates = []
+ for cx in range(CSS_TEMPLATES_FIXTURE_COUNT):
+ css_templates.append(
+ self.insert_css_template(
+ template_name=f"template_name{cx}", css=f"css{cx}"
+ )
+ )
+ yield css_templates
+
+ # rollback changes
+ for css_template in css_templates:
+ db.session.delete(css_template)
+ db.session.commit()
+
+ @pytest.mark.usefixtures("create_css_templates")
+ def test_get_list_css_template(self):
+ """
+ CSS Template API: Test get list css template
+ """
+ css_templates = db.session.query(CssTemplate).all()
+
+ self.login(username="admin")
+ uri = f"api/v1/css_template/"
+ rv = self.get_assert_metric(uri, "get_list")
+ assert rv.status_code == 200
+ data = json.loads(rv.data.decode("utf-8"))
+ assert data["count"] == len(css_templates)
+ expected_columns = [
+ "changed_on_delta_humanized",
+ "created_on",
+ "created_by",
+ "template_name",
+ "css",
+ ]
+ for expected_column in expected_columns:
+ assert expected_column in data["result"][0]
+
+ @pytest.mark.usefixtures("create_css_templates")
+ def test_get_list_sort_css_template(self):
+ """
+ CSS Template API: Test get list and sort CSS Template
+ """
+ css_templates = (
+ db.session.query(CssTemplate)
+ .order_by(CssTemplate.template_name.asc())
+ .all()
+ )
+ self.login(username="admin")
+ query_string = {"order_column": "template_name", "order_direction": "asc"}
+ uri = f"api/v1/css_template/?q={prison.dumps(query_string)}"
+ rv = self.get_assert_metric(uri, "get_list")
+ assert rv.status_code == 200
+ data = json.loads(rv.data.decode("utf-8"))
+ assert data["count"] == len(css_templates)
+ for i, query in enumerate(css_templates):
+ assert query.template_name == data["result"][i]["template_name"]
+
+ @pytest.mark.usefixtures("create_css_templates")
+ def test_get_list_custom_filter_css_template(self):
+ """
+ CSS Template API: Test get list and custom filter
+ """
+ self.login(username="admin")
+
+ all_css_templates = (
+ db.session.query(CssTemplate).filter(CssTemplate.css.ilike("%css2%")).all()
+ )
+ query_string = {
+ "filters": [
+ {
+ "col": "template_name",
+ "opr": "css_template_all_text",
+ "value": "css2",
+ }
+ ],
+ }
+ uri = f"api/v1/css_template/?q={prison.dumps(query_string)}"
+ rv = self.get_assert_metric(uri, "get_list")
+ assert rv.status_code == 200
+ data = json.loads(rv.data.decode("utf-8"))
+ assert data["count"] == len(all_css_templates)
+
+ all_css_templates = (
+ db.session.query(CssTemplate)
+ .filter(CssTemplate.template_name.ilike("%template_name3%"))
+ .all()
+ )
+ query_string = {
+ "filters": [
+ {
+ "col": "template_name",
+ "opr": "css_template_all_text",
+ "value": "template_name3",
+ }
+ ],
+ }
+ uri = f"api/v1/css_template/?q={prison.dumps(query_string)}"
+ rv = self.get_assert_metric(uri, "get_list")
+ assert rv.status_code == 200
+ data = json.loads(rv.data.decode("utf-8"))
+ assert data["count"] == len(all_css_templates)
+
+ def test_info_css_template(self):
+ """
+ CssTemplate API: Test info
+ """
+ self.login(username="admin")
+ uri = f"api/v1/css_template/_info"
+ rv = self.get_assert_metric(uri, "info")
+ assert rv.status_code == 200
+
+ @pytest.mark.usefixtures("create_css_templates")
+ def test_get_css_template(self):
+ """
+ CSS Template API: Test get CSS Template
+ """
+ css_template = (
+ db.session.query(CssTemplate)
+ .filter(CssTemplate.template_name == "template_name1")
+ .one_or_none()
+ )
+ self.login(username="admin")
+ uri = f"api/v1/css_template/{css_template.id}"
+ rv = self.get_assert_metric(uri, "get")
+ assert rv.status_code == 200
+
+ expected_result = {
+ "id": css_template.id,
+ "template_name": "template_name1",
+ "css": "css1",
+ "created_by": {
+ "first_name": css_template.created_by.first_name,
+ "id": css_template.created_by.id,
+ "last_name": css_template.created_by.last_name,
+ },
+ }
+ data = json.loads(rv.data.decode("utf-8"))
+ for key, value in data["result"].items():
+ assert value == expected_result[key]
+
+ @pytest.mark.usefixtures("create_css_templates")
+ def test_get_css_template_not_found(self):
+ """
+ CSS Template API: Test get CSS Template not found
+ """
+ max_id = db.session.query(func.max(CssTemplate.id)).scalar()
+ self.login(username="admin")
+ uri = f"api/v1/css_template/{max_id + 1}"
+ rv = self.client.get(uri)
+ assert rv.status_code == 404
+
+ def test_create_css_template(self):
+ """
+ CSS Template API: Test create
+ """
+ post_data = {
+ "template_name": "template_name_create",
+ "css": "css_create",
+ }
+
+ self.login(username="admin")
+ uri = f"api/v1/css_template/"
+ rv = self.client.post(uri, json=post_data)
+ data = json.loads(rv.data.decode("utf-8"))
+ assert rv.status_code == 201
+
+ css_template_id = data.get("id")
+ model = db.session.query(CssTemplate).get(css_template_id)
+ for key in post_data:
+ assert getattr(model, key) == data["result"][key]
+
+ # Rollback changes
+ db.session.delete(model)
+ db.session.commit()
+
+ @pytest.mark.usefixtures("create_css_templates")
+ def test_update_css_template(self):
+ """
+ CSS Template API: Test update
+ """
+ css_template = (
+ db.session.query(CssTemplate)
+ .filter(CssTemplate.template_name == "template_name1")
+ .all()[0]
+ )
+
+ put_data = {
+ "template_name": "template_name_changed",
+ "css": "css_changed",
+ }
+
+ self.login(username="admin")
+ uri = f"api/v1/css_template/{css_template.id}"
+ rv = self.client.put(uri, json=put_data)
+ assert rv.status_code == 200
+
+ model = db.session.query(CssTemplate).get(css_template.id)
+ assert model.template_name == "template_name_changed"
+ assert model.css == "css_changed"
+
+ @pytest.mark.usefixtures("create_css_templates")
+ def test_update_css_template_not_found(self):
+ """
+ CSS Template API: Test update not found
+ """
+ max_id = db.session.query(func.max(CssTemplate.id)).scalar()
+ self.login(username="admin")
+
+ put_data = {
+ "template_name": "template_name_changed",
+ "css": "css_changed",
+ }
+
+ uri = f"api/v1/css_template/{max_id + 1}"
+ rv = self.client.put(uri, json=put_data)
+ assert rv.status_code == 404
+
+ @pytest.mark.usefixtures("create_css_templates")
+ def test_delete_css_template(self):
+ """
+ CSS Template API: Test delete
+ """
+ css_template = (
+ db.session.query(CssTemplate)
+ .filter(CssTemplate.template_name == "template_name1")
+ .one_or_none()
+ )
+
+ self.login(username="admin")
+ uri = f"api/v1/css_template/{css_template.id}"
+ rv = self.client.delete(uri)
+ assert rv.status_code == 200
+
+ model = db.session.query(CssTemplate).get(css_template.id)
+ assert model is None
+
+ @pytest.mark.usefixtures("create_css_templates")
+ def test_delete_css_template_not_found(self):
+ """
+ CSS Template API: Test delete not found
+ """
+ max_id = db.session.query(func.max(CssTemplate.id)).scalar()
+ self.login(username="admin")
+ uri = f"api/v1/css_template/{max_id + 1}"
+ rv = self.client.delete(uri)
+ assert rv.status_code == 404
+
+ @pytest.mark.usefixtures("create_css_templates")
+ def test_delete_bulk_css_templates(self):
+ """
+ CSS Template API: Test delete bulk
+ """
+ css_templates = db.session.query(CssTemplate).all()
+ css_template_ids = [css_template.id for css_template in css_templates]
+
+ self.login(username="admin")
+ uri = f"api/v1/css_template/?q={prison.dumps(css_template_ids)}"
+ rv = self.delete_assert_metric(uri, "bulk_delete")
+ assert rv.status_code == 200
+ response = json.loads(rv.data.decode("utf-8"))
+ expected_response = {
+ "message": f"Deleted {len(css_template_ids)} css templates"
+ }
+ assert response == expected_response
+ css_templates = db.session.query(CssTemplate).all()
+ assert css_templates == []
+
+ @pytest.mark.usefixtures("create_css_templates")
+ def test_delete_one_bulk_css_templates(self):
+ """
+ CSS Template API: Test delete one in bulk
+ """
+ css_template = db.session.query(CssTemplate).first()
+ css_template_ids = [css_template.id]
+
+ self.login(username="admin")
+ uri = f"api/v1/css_template/?q={prison.dumps(css_template_ids)}"
+ rv = self.delete_assert_metric(uri, "bulk_delete")
+ assert rv.status_code == 200
+ response = json.loads(rv.data.decode("utf-8"))
+ expected_response = {"message": f"Deleted {len(css_template_ids)} css template"}
+ assert response == expected_response
+ css_template_ = db.session.query(CssTemplate).get(css_template_ids[0])
+ assert css_template_ is None
+
+ def test_delete_bulk_css_template_bad_request(self):
+ """
+ CSS Template API: Test delete bulk bad request
+ """
+ css_template_ids = [1, "a"]
+ self.login(username="admin")
+ uri = f"api/v1/css_template/?q={prison.dumps(css_template_ids)}"
+ rv = self.delete_assert_metric(uri, "bulk_delete")
+ assert rv.status_code == 400
+
+ @pytest.mark.usefixtures("create_css_templates")
+ def test_delete_bulk_css_template_not_found(self):
+ """
+ CSS Template API: Test delete bulk not found
+ """
+ max_id = db.session.query(func.max(CssTemplate.id)).scalar()
+
+ css_template_ids = [max_id + 1, max_id + 2]
+ self.login(username="admin")
+ uri = f"api/v1/css_template/?q={prison.dumps(css_template_ids)}"
+ rv = self.delete_assert_metric(uri, "bulk_delete")
+ assert rv.status_code == 404