# -*- coding: utf-8 -*- from odoo import models, fields, api, exceptions, tools import json import re from odoo.tools.misc import file_path from odoo.tools.config import config from .anita_utility import apply_alpha try: import sass as libsass except ImportError: libsass = None import logging _logger = logging.getLogger(__name__) class AnitaThemeMode(models.Model): ''' user theme style setting ''' _name = 'anita_theme_setting.theme_mode' _description = 'anita theme mode' name = fields.Char(string="Name", required=True) sequence = fields.Integer(string="Mode Sequence", default=0) theme_styles = fields.One2many( string="Theme Styles", comodel_name="anita_theme_setting.theme_style", inverse_name="theme_mode") mode_template = fields.Many2one( comodel_name="anita_theme_setting.mode_template", string="Mode Template") template_var_vals = fields.One2many( comodel_name="anita_theme_setting.template_var_val", inverse_name="theme_mode", string="Template Vars") # if owner is False, so the style own to global owner = fields.Reference( string="Owner", selection=[('res.company', 'res.company'), ('res.users', 'res.users')], default=False, help="owner which this theme is create by, if it is false, it mean it is owned to the system!") mode_style_css = fields.Text(string="Mode Style Css") compiled_mode_style_css = fields.Text(string="Compiled mode style css") is_default = fields.Boolean(string="Is Default", default=False) version = fields.Char( string="Version", help="use version to check update or not") _sql_constraints = [('theme_mode_name_unique', 'UNIQUE(name, owner)', "Theme mode name and owner must unique")] def get_mode_preview_data(self, style_id=False): """ get mode preview data :return: """ if self.mode_template: if style_id: theme_style = self.env['anita_theme_setting.theme_style'].browse( style_id) if theme_style.is_semi_transparent(): result = theme_style.get_mode_style_css() else: result = self.get_mode_style_css() else: result = self.get_mode_style_css() else: result = "" mode_name = 'body.{name}'.format(name=self.name) # replace mode_name with body.preview result = result.replace(mode_name, 'body.preview') return result @api.onchange('mode_template') def on_mode_template_changed(self): """ compute mode style :return: """ # check id is instance of NewId if self.mode_template: scss_vars = self.mode_template.get_color_vars() template_var_vals = [(5, 0, 0)] for group_name in scss_vars: var_items = scss_vars[group_name] for var_item in var_items: template_var_vals.append((0, 0, { "name": var_item["name"], "group": group_name, "value": var_item["value"], "disable_opacity": var_item.get("disable_opacity", False), "description": var_item["description"] })) self.template_var_vals = template_var_vals else: self.template_var_vals = [(5, 0, 0)] def check_template_vars(self): """ check style css :return: """ return @tools.ormcache('self.id') def get_scss_vars_cache(self): """ get scss vars :return: """ records = self.template_var_vals.read(['name', 'value']) return {record['name']: record['value'] for record in records} @tools.ormcache('self.id') def get_scss_var_record_cache(self): """ get scss vars :return: """ records = self.template_var_vals return {record.name: { "name": record.name, "value": record.value, "disable_opacity": record.disable_opacity, } for record in records} def _compile_scss(self, alpha=False): """ This code will compile valid scss into css. Simply copied and adapted slightly """ self.ensure_one() scss_source = self.mode_template.template scss_vars = [] var_val_cache = self.get_scss_var_record_cache() for var_name in var_val_cache: var_record = var_val_cache[var_name] name = var_record.get("name") if alpha and alpha < 1: color = apply_alpha(var_record.get('name'), var_record.get('value'), alpha) else: color = var_record.get('value') # new color scss_vars.append('{name}: {value};'.format( name=name, value=color)) # origin color, some element disable alpha scss_vars.append('{name}: {value};'.format( name=name.replace('$', '$_'), value=var_record.get('value'))) precision = 8 output_style = 'expanded' bootstrap_scss_path = file_path('web/static/lib/bootstrap/scss') mode_selectors = "$mode_selectors: 'body.{name}';".format(name=self.name) extra_vars = [ mode_selectors, "$compile_normal: 1;", "$semi_transparent: {semi_trans};".format(semi_trans=True if alpha < 1.0 else False), ] total_vars = extra_vars + scss_vars normal_source = '\n'.join(total_vars) + '\n' + scss_source if scss_source else '' # compile normal results = [] try: result = libsass.compile( string=normal_source, include_paths=[bootstrap_scss_path], output_style=output_style, precision=precision) # remove the import bootstrap index = result.find("body.{name}".format(name=self.name)) result = result[index:] results.append(result) except libsass.CompileError as e: _logger.error(e.args[0]) # compile for background if alpha < 1.0: scss_vars = [] for var_name in var_val_cache: var_record = var_val_cache[var_name] name = var_record.get("name") # new color scss_vars.append('{name}: {value};'.format(name=name, value=var_record.get('value'))) # origin color scss_vars.append('{name}: {value};'.format(name=name.replace('$', '$_'), value=var_record.get('value'))) mode_selectors = "$mode_selectors: 'body.{name} .o_dialog_container';".format(name=self.name) extra_vars = [ mode_selectors, "$compile_normal: 0;", "$semi_transparent: {semi_trans};".format(semi_trans=1 if alpha < 1.0 else 0), ] total_vars = extra_vars + scss_vars normal_source = '\n'.join(total_vars) + '\n' + str(scss_source) try: result = libsass.compile( string=normal_source, include_paths=[bootstrap_scss_path], output_style=output_style, precision=precision) # remove the import bottstrap index = result.find('body.{name} .o_dialog_container'.format(name=self.name)) result = result[index:] results.append(result) except libsass.CompileError as e: _logger.error(e.args[0]) return '\n'.join(results) @api.model def compute_mode_style_url(self): """ compute mode style url :return: """ for record in self: record.mode_style_url = \ '/web/image/anita_theme_setting.anita_theme_setting/{var_id}/icon128x128'.format( var_id=record.id) def get_default_mode_style_text(self, theme_mode_info): """ get default mode data :return: """ if self.name == 'normal': return color_vars = theme_mode_info["template_vars"] mode_template = theme_mode_info["mode_template"] if not mode_template: raise exceptions.ValidationError('Can not find the theme template!') template = mode_template.template # css template data template = template.replace('$mode_name', self.name) # replace the var for var_nam in color_vars: template = str(template).replace(var_nam, color_vars[var_nam]) return template @api.model def check_default_mode_data(self, owner=False, force = False): ''' check default mode data :return: ''' modes = self.search([('owner', '=', owner)]) olde_mode_cache = {mode.name: mode for mode in modes} default_modes = \ self.env["anita_theme_setting.default_mode_data"].search([]) # create new mode if the mode do not exits for default_mode in default_modes: if not default_mode.data: continue # need to reload the mode data mode_data = default_mode.get_mode_data() if default_mode.name not in olde_mode_cache: mode_data["name"] = default_mode["name"] # create new mode use the mode data record = self.create_mode(mode_data, owner) if not record.mode_template: continue # check the templates templates = self.env["anita_theme_setting.mode_template"].get_templates( ) template = mode_data.get('mode_template', False) if template and template not in templates: raise exceptions.ValidationError( '{template} do not exits!'.format(template=mode_data["template"])) template = templates[mode_data["mode_template"]] record.mode_template = template.id # check vars are defined in the template vars record.check_template_vars() else: # check version, if the version is different mode = olde_mode_cache[default_mode.name] if force or mode.version != mode_data["version"]: # update template vars temlate_var_vals = mode_data["template_var_vals"] mode.update_template_var_values(temlate_var_vals) # update template mode_templates = self.env["anita_theme_setting.mode_template"].get_templates( ) if mode_data.get("mode_template", False): template_name = mode_data["mode_template"] mode.mode_template = \ mode_templates.get(template_name, False) mode.version = mode_data["version"] mode.check_mode_data() # recompile the template mode.compiled_mode_style_css = False # unlink the invalid mode for default_mode in default_modes: if not default_mode.data: default_mode.unlink() def create_mode(self, mode_config, owner=False, is_default=True): ''' create mode :param mode style txt: :param mode_data: :param owner: :param is_default: :return: ''' mode_templates = \ self.env["anita_theme_setting.mode_template"].get_templates() mode_template = mode_config.get('mode_template', False) if mode_template and mode_template not in mode_templates: raise exceptions.ValidationError('Can not find mode template {mode_template}'.format( mode_template=mode_template)) # convert to cache data template_var_vals = mode_config.get('template_var_vals', {}) var_val_cache = {} for group in template_var_vals: var_items = template_var_vals[group] for var_item in var_items: var_val_cache[var_item["name"]] = var_item["value"] theme_styles = mode_config['theme_styles'] theme_style_array = [] for theme_style in theme_styles: groups = theme_style["groups"] group_datas = [] for group_index, var_items in enumerate(groups): sub_groups = var_items["sub_groups"] sub_group_array = [] for sub_group_index, sub_group in enumerate(sub_groups): # create sub group item_array = [] style_items = sub_group["style_items"] for item_index, style_item in enumerate(style_items): var_val_array = [] var_infos = style_item["vars"] for var_index, var_info in enumerate(var_infos): color = var_info.get("color", False) ref_var = var_info.get("ref_var", False) if ref_var: if ref_var not in var_val_cache: color = False _logger.info( "Can not find ref var {ref_var}".format(ref_var=ref_var)) else: color = var_val_cache[ref_var] var_val_array.append((0, 0, { "name": var_info["name"], "type": var_info["type"], "sequence": var_index, "is_default": is_default, "color": color, "image": var_info.get('image', False), "image_url": var_info.get('image_url', False), "svg": var_info.get('svg', False), "identity": var_info.get('identity', False) })) item_array.append((0, 0, { "name": style_item["name"], "is_default": is_default, "sequence": item_index, "val_template": style_item["val_template"], "sub_group": sub_group["name"], "vars": var_val_array, "selectors": json.dumps(style_item["selectors"]) })) sub_group_array.append((0, 0, { "name": sub_group["name"], "sequence": sub_group_index, "style_items": item_array, "is_default": is_default })) group_datas.append((0, 0, { "name": var_items["name"], "sequence": group_index, "is_default": is_default, "sub_groups": sub_group_array })) theme_style_array.append((0, 0, { "name": theme_style["name"], "is_default": is_default, "groups": group_datas })) vals = [] template_var_vals = mode_config.get('template_var_vals', {}) for group_name in template_var_vals: var_items = template_var_vals[group_name] for var_item in var_items: vals.append((0, 0, { "name": var_item["name"], "group": group_name, "value": var_item["value"], "disable_opacity": var_item.get("disable_opacity", False), "description": var_item["description"] })) mode_template = mode_config.get("mode_template", False) return self.env["anita_theme_setting.theme_mode"].create([{ "name": mode_config["name"], "theme_styles": theme_style_array, "is_default": is_default, "version": mode_config["version"], "mode_template": mode_templates[mode_template].id if mode_template else False, "owner": owner, "template_var_vals": vals }]) def get_mode_data(self): ''' get the mode data :return: ''' self.ensure_theme_style() rst = [] for record in self: tmp_data = record.read( ['name', 'is_default', 'sequence', 'version'])[0] tmp_data['theme_styles'] = record.theme_styles.get_styles() rst.append(tmp_data) return rst def delete_mode(self): """ delete the mode :return: """ self.unlink() def check_style_css(self): """ check style css :return: """ if self.mode_template and not self.compiled_mode_style_css: self.compiled_mode_style_css = self._compile_scss() def update_template_var_values(self, template_var_vals): """ update template var values :param temlate_var_vals: :return: """ self.ensure_one() vals = [(5, 0, 0)] for group_name in template_var_vals: var_items = template_var_vals[group_name] for var_item in var_items: vals.append((0, 0, { "name": var_item["name"], "group": group_name, "value": var_item["value"], "description": var_item.get("description") })) self.template_var_vals = vals def get_mode_style_css(self): """ get mode style css """ if config.get('debug', False) or not self.compiled_mode_style_css: self.compiled_mode_style_css = self._compile_scss() return self.compiled_mode_style_css def check_mode_data(self): ''' check the mode data :return: ''' self.ensure_one() default_modes = self.env[ "anita_theme_setting.default_mode_data"].search([]) default_mode_cache = { default_mode.name: default_mode for default_mode in default_modes} mode_name = self.name if mode_name not in default_mode_cache: return theme_styles = self.theme_styles theme_style_cache = { theme_style.name: theme_style for theme_style in theme_styles if theme_style.is_default} default_mode = default_mode_cache[mode_name] default_mode_data = default_mode.get_mode_data() theme_styles = default_mode_data['theme_styles'] for theme_style in theme_styles: # create new style style_name = theme_style['name'] if style_name not in theme_style_cache: self.env["anita_theme_setting.theme_style"].create_style( self.id, theme_style) else: # check the style data, may be one more style with the same name tmp_style = theme_style_cache[style_name] tmp_style.check_groups(default_mode, theme_style["groups"]) def get_mode_edit_action(self, style_id): """ get theme mode edit action """ self.ensure_one() form_id = self.env.ref("anita_theme_setting.edit_theme_mode_form").id var_vals = [] for var_val in self.template_var_vals: var_vals.append((0, 0, { "name": var_val.name, "group": var_val.group, "value": var_val.value, "disable_opacity": var_val.disable_opacity, "description": var_val.description })) return { "type": "ir.actions.act_window", "res_model": "anita_theme_setting.theme_mode", "views": [[form_id, "form"]], "res_id": self.id, "view_mode": "form", "target": "new", "context": { "default_name": self.name, "default_mode_template": self.mode_template.id, "sequnce": self.sequence, "template_var_vals": var_vals, "default_style_id": style_id } } @api.model def get_new_mode_action(self): """ get create new mode action """ form_id = self.env.ref("anita_theme_setting.new_theme_mode_form").id return { "type": "ir.actions.act_window", "res_model": "anita_theme_setting.theme_mode", "views": [[form_id, "form"]], "view_mode": "form", "target": "new" } def ensure_theme_style(self): """ create a empty theme style """ if not self.theme_styles: self.env["anita_theme_setting.theme_style"].create_style( self.id, { "name": "style1", "is_default": False, "groups": [] })