diff --git a/bi_sql_editor/demo/bi_sql_view_demo.xml b/bi_sql_editor/demo/bi_sql_view_demo.xml index d9cc095b6..669cd38e3 100644 --- a/bi_sql_editor/demo/bi_sql_view_demo.xml +++ b/bi_sql_editor/demo/bi_sql_view_demo.xml @@ -20,6 +20,7 @@ ORDER BY unexisting_field Partners View partners_view + - - - diff --git a/bi_sql_editor/models/bi_sql_view.py b/bi_sql_editor/models/bi_sql_view.py index 53026da22..f8f08282d 100644 --- a/bi_sql_editor/models/bi_sql_view.py +++ b/bi_sql_editor/models/bi_sql_view.py @@ -8,7 +8,7 @@ from datetime import datetime, timedelta from psycopg2 import ProgrammingError from odoo import SUPERUSER_ID, _, api, fields, models -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError from odoo.tools import sql, table_columns from odoo.tools.safe_eval import safe_eval @@ -169,12 +169,6 @@ class BiSQLView(models.Model): sequence = fields.Integer(string="sequence") - option_context_field = fields.Boolean( - string="Use Context Field", - help="Check this box if you want to add a context column in the field list view." - " Custom Context will be inserted in the created views.", - ) - # Constrains Section @api.constrains("is_materialized") def _check_index_materialized(self): @@ -269,17 +263,24 @@ class BiSQLView(models.Model): def copy(self, default=None): self.ensure_one() default = dict(default or {}) - default.update( - { - "name": _("%s (Copy)") % self.name, - "technical_name": "%s_copy" % self.technical_name, - } - ) + if "name" not in default: + default["name"] = _("%s (Copy)") % self.name + if "technical_name" not in default: + default["technical_name"] = f"{self.technical_name}_copy" return super().copy(default=default) # Action Section def button_create_sql_view_and_model(self): for sql_view in self.filtered(lambda x: x.state == "sql_valid"): + # Check if many2one fields are correctly + bad_fields = sql_view.bi_sql_view_field_ids.filtered( + lambda x: x.ttype == "many2one" and not x.many2one_model_id.id + ) + if bad_fields: + raise ValidationError( + _("Please set related models on the following fields %s") + % ",".join(bad_fields.mapped("name")) + ) # Create ORM and access sql_view._create_model_and_fields() sql_view._create_model_access() @@ -297,28 +298,33 @@ class BiSQLView(models.Model): sql_view.cron_id.active = True sql_view.state = "model_valid" + def button_reset_to_model_valid(self): + views = self.filtered(lambda x: x.state == "ui_valid") + views.mapped("tree_view_id").unlink() + views.mapped("graph_view_id").unlink() + views.mapped("pivot_view_id").unlink() + views.mapped("search_view_id").unlink() + views.mapped("action_id").unlink() + views.mapped("menu_id").unlink() + return views.write({"state": "model_valid"}) + + def button_reset_to_sql_valid(self): + self.button_reset_to_model_valid() + views = self.filtered(lambda x: x.state == "model_valid") + for sql_view in views: + # Drop SQL View (and indexes by cascade) + if sql_view.is_materialized: + sql_view._drop_view() + if sql_view.cron_id: + sql_view.cron_id.active = False + # Drop ORM + sql_view._drop_model_and_fields() + return views.write({"state": "sql_valid"}) + def button_set_draft(self): - for sql_view in self.filtered(lambda x: x.state != "draft"): - sql_view.menu_id.unlink() - sql_view.action_id.unlink() - sql_view.tree_view_id.unlink() - sql_view.graph_view_id.unlink() - sql_view.pivot_view_id.unlink() - sql_view.search_view_id.unlink() - - if sql_view.state in ("model_valid", "ui_valid"): - # Drop SQL View (and indexes by cascade) - if sql_view.is_materialized: - sql_view._drop_view() - - if sql_view.cron_id: - sql_view.cron_id.active = False - - # Drop ORM - sql_view._drop_model_and_fields() - - super(BiSQLView, sql_view).button_set_draft() - return True + self.button_reset_to_model_valid() + self.button_reset_to_sql_valid() + return super().button_set_draft() def button_create_ui(self): self.tree_view_id = self.env["ir.ui.view"].create(self._prepare_tree_view()).id diff --git a/bi_sql_editor/models/bi_sql_view_field.py b/bi_sql_editor/models/bi_sql_view_field.py index 2a89e291f..7444d3463 100644 --- a/bi_sql_editor/models/bi_sql_view_field.py +++ b/bi_sql_editor/models/bi_sql_view_field.py @@ -70,31 +70,40 @@ class BiSQLViewField(models.Model): string="SQL View", comodel_name="bi.sql.view", ondelete="cascade" ) + state = fields.Selection(related="bi_sql_view_id.state", store=True) + is_index = fields.Boolean( help="Check this box if you want to create" " an index on that field. This is recommended for searchable and" " groupable fields, to reduce duration", + states={"model_valid": [("readonly", True)], "ui_valid": [("readonly", True)]}, ) is_group_by = fields.Boolean( string="Is Group by", help="Check this box if you want to create" " a 'group by' option in the search view", + states={"ui_valid": [("readonly", True)]}, ) index_name = fields.Char(compute="_compute_index_name") - graph_type = fields.Selection(selection=_GRAPH_TYPE_SELECTION) + graph_type = fields.Selection( + selection=_GRAPH_TYPE_SELECTION, + states={"ui_valid": [("readonly", True)]}, + ) tree_visibility = fields.Selection( selection=_TREE_VISIBILITY_SELECTION, default="available", required=True, + states={"ui_valid": [("readonly", True)]}, ) field_description = fields.Char( help="This will be used as the name of the Odoo field, displayed for users", required=True, + states={"model_valid": [("readonly", True)], "ui_valid": [("readonly", True)]}, ) ttype = fields.Selection( @@ -104,6 +113,7 @@ class BiSQLViewField(models.Model): " Odoo field that will be created. Keep empty if you don't want to" " create a new field. If empty, this field will not be displayed" " neither available for search or group by function", + states={"model_valid": [("readonly", True)], "ui_valid": [("readonly", True)]}, ) selection = fields.Text( @@ -113,24 +123,28 @@ class BiSQLViewField(models.Model): " List of options, specified as a Python expression defining a list of" " (key, label) pairs. For example:" " [('blue','Blue'), ('yellow','Yellow')]", + states={"model_valid": [("readonly", True)], "ui_valid": [("readonly", True)]}, ) many2one_model_id = fields.Many2one( comodel_name="ir.model", string="Model", help="For 'Many2one' Odoo field.\n" " Comodel of the field.", + states={"model_valid": [("readonly", True)], "ui_valid": [("readonly", True)]}, ) group_operator = fields.Selection( selection=_GROUP_OPERATOR_SELECTION, help="By default, Odoo will sum the values when grouping. If you wish " "to alter the behaviour, choose an alternate Group Operator", + states={"model_valid": [("readonly", True)], "ui_valid": [("readonly", True)]}, ) field_context = fields.Char( default="{}", help="Context value that will be inserted for this field in all the views." " Important note : please write a context with single quote.", + states={"ui_valid": [("readonly", True)]}, ) # Constrains Section @@ -188,6 +202,16 @@ class BiSQLViewField(models.Model): ) return super().create(vals_list) + def unlink(self): + if self.filtered(lambda x: x.state in ("model_valid", "ui_valid")): + raise UserError( + _( + "Impossible to delete fields if the view" + " is in the state 'Model Valid' or 'UI Valid'." + ) + ) + return super().unlink() + # Custom Section @api.model def _model_mapping(self): @@ -233,12 +257,12 @@ class BiSQLViewField(models.Model): if self.tree_visibility == "invisible": visibility_text = 'invisible="1"' elif self.tree_visibility == "optional_hide": - visibility_text = 'option="hide"' + visibility_text = 'optional="hide"' elif self.tree_visibility == "optional_show": - visibility_text = 'option="show"' + visibility_text = 'optional="show"' return ( - f"""\n""" ) diff --git a/bi_sql_editor/tests/test_bi_sql_view.py b/bi_sql_editor/tests/test_bi_sql_view.py index 02622d331..d7999ebfa 100644 --- a/bi_sql_editor/tests/test_bi_sql_view.py +++ b/bi_sql_editor/tests/test_bi_sql_view.py @@ -1,7 +1,7 @@ # Copyright 2017 Onestein () # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo.exceptions import AccessError, UserError +from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tests import tagged from odoo.tests.common import SingleTransactionCase @@ -10,85 +10,107 @@ from odoo.tests.common import SingleTransactionCase class TestBiSqlViewEditor(SingleTransactionCase): @classmethod def setUpClass(cls): - super(TestBiSqlViewEditor, cls).setUpClass() + super().setUpClass() - cls.res_partner = cls.env["res.partner"] - cls.res_users = cls.env["res.users"] cls.bi_sql_view = cls.env["bi.sql.view"] - cls.group_bi_user = cls.env.ref( + cls.group_bi_manager = cls.env.ref( "sql_request_abstract.group_sql_request_manager" ) - cls.group_user = cls.env.ref("base.group_user") - cls.view = cls.bi_sql_view.create( - { - "name": "Partners View 2", - "is_materialized": True, - "technical_name": "partners_view_2", - "query": "SELECT name as x_name, street as x_street," - "company_id as x_company_id FROM res_partner " - "ORDER BY name", - } - ) - cls.company = cls.env.ref("base.main_company") - # Create bi user - cls.bi_user = cls._create_user("bi_user", cls.group_bi_user, cls.company) - cls.no_bi_user = cls._create_user("no_bi_user", cls.group_user, cls.company) + cls.group_bi_no_access = cls.env.ref("base.group_user") + cls.demo_user = cls.env.ref("base.user_demo") + cls.view = cls.env.ref("bi_sql_editor.partner_sql_view") @classmethod - def _create_user(cls, login, groups, company): - """Create a user.""" - user = cls.res_users.create( - { - "name": login, - "login": login, - "password": "demo", - "email": "example@yourcompany.com", - "company_id": company.id, - "groups_id": [(6, 0, groups.ids)], - } - ) - return user + def _get_user(cls, access_level=False): + if access_level == "manager": + cls.demo_user.write({"groups_id": [(6, 0, cls.group_bi_manager.ids)]}) + else: + cls.demo_user.write({"groups_id": [(6, 0, cls.group_bi_no_access.ids)]}) + return cls.demo_user def test_process_view(self): - view = self.view - self.assertEqual(view.state, "draft", "state not draft") - view.button_validate_sql_expression() - self.assertEqual(view.state, "sql_valid", "state not sql_valid") - view.button_create_sql_view_and_model() - self.assertEqual(view.state, "model_valid", "state not model_valid") - view.button_create_ui() - self.assertEqual(view.state, "ui_valid", "state not ui_valid") - view.button_update_model_access() - self.assertEqual(view.has_group_changed, False, "has_group_changed not False") - cron_res = view.cron_id.method_direct_trigger() - self.assertEqual(cron_res, True, "something went wrong with the cron") + copy_view = self.view.copy(default={"technical_name": "test_process_view"}) + self.assertEqual(copy_view.state, "draft") + copy_view.button_validate_sql_expression() + self.assertEqual(copy_view.state, "sql_valid") + + field_lines = copy_view.bi_sql_view_field_ids + self.assertEqual(len(field_lines), 3) + field_lines.filtered(lambda x: x.name == "x_company_id").is_index = True + + copy_view.button_create_sql_view_and_model() + self.assertEqual(copy_view.state, "model_valid") + + field_lines.filtered(lambda x: x.name == "x_name").tree_visibility = "invisible" + field_lines.filtered( + lambda x: x.name == "x_street" + ).tree_visibility = "optional_hide" + field_lines.filtered( + lambda x: x.name == "x_company_id" + ).tree_visibility = "optional_show" + + field_lines.filtered(lambda x: x.name == "x_company_id").is_group_by = True + + field_lines.filtered(lambda x: x.name == "x_company_id").graph_type = "row" + + copy_view.button_create_ui() + self.assertEqual(copy_view.state, "ui_valid") + copy_view.button_update_model_access() + self.assertEqual(copy_view.has_group_changed, False) + # Check that cron works correctly + copy_view.cron_id.method_direct_trigger() def test_copy(self): - copy_view = self.view.copy() - self.assertEqual(copy_view.name, "Partners View 2 (Copy)", "Wrong name") + copy_view = self.view.copy(default={"technical_name": "test_copy"}) + self.assertEqual(copy_view.name, f"{self.view.name} (Copy)") def test_security(self): with self.assertRaises(AccessError): - self.bi_sql_view.with_user(self.no_bi_user.id).search( - [("name", "=", "Partners View 2")] + self.bi_sql_view.with_user(self._get_user()).search( + [("name", "=", self.view.name)] ) - bi = self.bi_sql_view.with_user(self.bi_user.id).search( - [("name", "=", "Partners View 2")] + bi = self.bi_sql_view.with_user(self._get_user("manager")).search( + [("name", "=", self.view.name)] ) self.assertEqual( - len(bi), 1, "Bi user should not have access to " "bi %s" % self.view.name + len(bi), 1, "Bi Manager should have access to bi %s" % self.view.name ) def test_unlink(self): - self.assertEqual(self.view.state, "ui_valid", "state not ui_valid") - with self.assertRaises(UserError): - self.view.unlink() - self.view.button_set_draft() - self.assertNotEqual( - self.view.cron_id, - False, - "Set to draft materialized view should" " not unlink cron", + copy_view = self.view.copy( + default={ + "name": "Test Unlink", + "technical_name": "test_unlink", + } ) - self.view.unlink() - res = self.bi_sql_view.search([("name", "=", "Partners View 2")]) + view_name = copy_view.name + copy_view.button_validate_sql_expression() + copy_view.button_create_sql_view_and_model() + copy_view.button_create_ui() + self.assertEqual(copy_view.state, "ui_valid") + with self.assertRaises(UserError): + copy_view.unlink() + copy_view.button_set_draft() + self.assertNotEqual( + copy_view.cron_id, + False, + "Set to draft materialized view should not unlink cron", + ) + copy_view.unlink() + res = self.bi_sql_view.search([("name", "=", view_name)]) self.assertEqual(len(res), 0, "View not deleted") + + def test_many2one_not_found(self): + copy_view = self.view.copy( + default={"technical_name": "test_many2one_not_found"} + ) + + copy_view.query = "SELECT parent_id as x_weird_name_id FROM res_partner;" + copy_view.button_validate_sql_expression() + field_lines = copy_view.bi_sql_view_field_ids + self.assertEqual(len(field_lines), 1) + self.assertEqual(field_lines[0].ttype, "many2one") + self.assertEqual(field_lines[0].many2one_model_id.id, False) + + with self.assertRaises(ValidationError): + copy_view.button_create_sql_view_and_model() diff --git a/bi_sql_editor/views/view_bi_sql_view.xml b/bi_sql_editor/views/view_bi_sql_view.xml index 207440b4e..4f82187f6 100644 --- a/bi_sql_editor/views/view_bi_sql_view.xml +++ b/bi_sql_editor/views/view_bi_sql_view.xml @@ -34,14 +34,20 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).