313 lines
13 KiB
Python
313 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import re
|
|
from bs4 import BeautifulSoup as bs
|
|
from ast import literal_eval
|
|
import logging
|
|
|
|
# Set up logging
|
|
logging.basicConfig(level=logging.INFO)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Constants
|
|
NEW_ATTRS = ['invisible', 'required', 'readonly', 'column_invisible']
|
|
|
|
def upgrade(file_manager):
|
|
"""Upgrade XML files by converting 'attrs' and 'states' into new attributes."""
|
|
# Filter files to only XML files
|
|
files = [file for file in file_manager if file.path.suffix == '.xml']
|
|
if not files:
|
|
return
|
|
|
|
# Helper functions
|
|
def normalize_domain(domain):
|
|
"""
|
|
Normalize Domain, taken from odoo/osv/expression.py -> just the part so that & operators are added where needed.
|
|
"""
|
|
if len(domain) == 1:
|
|
return domain
|
|
result = []
|
|
expected = 1 # expected number of expressions
|
|
op_arity = {'!': 1, '&': 2, '|': 2}
|
|
for token in domain:
|
|
if expected == 0: # more than expected, like in [A, B]
|
|
result[0:0] = ['&'] # put an extra '&' in front
|
|
expected = 1
|
|
if isinstance(token, (list, tuple)): # domain term
|
|
expected -= 1
|
|
token = tuple(token)
|
|
else:
|
|
expected += op_arity.get(token, 0) - 1
|
|
result.append(token)
|
|
return result
|
|
|
|
def stringify_leaf(leaf):
|
|
"""
|
|
Convert a domain leaf to python expression string
|
|
"""
|
|
operator = str(leaf[1])
|
|
left_operand = leaf[0]
|
|
right_operand = leaf[2]
|
|
|
|
# Handle '=?'
|
|
if operator == '=?':
|
|
if isinstance(right_operand, str):
|
|
right_operand = f"'{right_operand}'"
|
|
return f"({right_operand} in [None, False] or {left_operand} == {right_operand})"
|
|
|
|
# Handle '='
|
|
elif operator == '=':
|
|
if right_operand in (False, []): # Check for False or empty list
|
|
return f"not {left_operand}"
|
|
elif right_operand == True: # Check for True
|
|
return left_operand
|
|
operator = '=='
|
|
|
|
# Handle '!='
|
|
elif operator == '!=':
|
|
if right_operand in (False, []): # Check for False or empty list
|
|
return left_operand
|
|
elif right_operand == True: # Check for True
|
|
return f"not {left_operand}"
|
|
|
|
# Handle 'like' operators
|
|
elif 'like' in operator:
|
|
case_insensitive = 'ilike' in operator
|
|
if isinstance(right_operand, str) and re.search('[_%]', right_operand):
|
|
# Since wildcards won't work/be recognized after conversion we throw an error
|
|
raise Exception("Script doesn't support 'like' domains with wildcards")
|
|
|
|
if operator in ['=like', '=ilike']:
|
|
operator = '=='
|
|
else:
|
|
if 'not' in operator:
|
|
operator = 'not in'
|
|
else:
|
|
operator = 'in'
|
|
# Switch operands for 'in' operations
|
|
left_operand, right_operand = right_operand, left_operand
|
|
|
|
# Format right operand
|
|
if isinstance(right_operand, str):
|
|
right_operand = f"'{right_operand}'"
|
|
|
|
# Handle case insensitive operations
|
|
if 'like' in str(leaf[1]) and 'ilike' in str(leaf[1]):
|
|
return f"{left_operand}.lower() {operator} {right_operand}.lower()"
|
|
else:
|
|
return f"{left_operand} {operator} {right_operand}"
|
|
|
|
def stringify_attr(stack):
|
|
"""
|
|
Convert domain stack to python expression string
|
|
"""
|
|
if stack in (True, False, 'True', 'False', 1, 0, '1', '0'):
|
|
return str(stack)
|
|
|
|
try:
|
|
last_parenthesis_index = max(index for index, item in enumerate(stack[::-1]) if item not in ('|', '!'))
|
|
except ValueError:
|
|
last_parenthesis_index = 0
|
|
|
|
stack = normalize_domain(stack)
|
|
stack = stack[::-1]
|
|
result = []
|
|
|
|
for index, leaf_or_operator in enumerate(stack):
|
|
if leaf_or_operator == '!':
|
|
expr = result.pop()
|
|
result.append('(not (%s))' % expr)
|
|
elif leaf_or_operator in ['&', '|']:
|
|
left = result.pop()
|
|
try:
|
|
right = result.pop()
|
|
except IndexError:
|
|
res = left + ('%s' % ' and' if leaf_or_operator == '&' else ' or')
|
|
result.append(res)
|
|
continue
|
|
form = '(%s %s %s)'
|
|
if index > last_parenthesis_index:
|
|
form = '%s %s %s'
|
|
result.append(form % (left, 'and' if leaf_or_operator == '&' else 'or', right))
|
|
else:
|
|
result.append(stringify_leaf(leaf_or_operator))
|
|
|
|
return result[0] if result else ''
|
|
|
|
def get_new_attrs(attrs):
|
|
"""
|
|
Parse attrs string and return dictionary of new attributes
|
|
"""
|
|
new_attrs = {}
|
|
try:
|
|
# Temporarily replace dynamic variables in leafs
|
|
escaped_operators = ['=', '!=', '>', '>=', '<', '<=', '=\\?', '=like', 'like', 'not like', 'ilike', 'not ilike', '=ilike', 'in', 'not in', 'child_of', 'parent_of']
|
|
|
|
# Handle HTML entities
|
|
attrs = re.sub("<", "<", attrs)
|
|
attrs = re.sub(">", ">", attrs)
|
|
|
|
# Replace dynamic variables with placeholder strings
|
|
attrs = re.sub(f"([\"'](?:{'|'.join(escaped_operators)})[\"']\\s*,\\s*)(?!False|True)([\\w\\.]+)(?=\\s*[\\]\\)])", r"\1'__dynamic_variable__.\2'", attrs)
|
|
attrs = re.sub(r"(%\([\w\.]+\)d)", r"'__dynamic_variable__.\1'", attrs)
|
|
|
|
attrs = attrs.strip()
|
|
if re.search("^{.*}$", attrs, re.DOTALL):
|
|
attrs_dict = literal_eval(attrs.strip())
|
|
for attr, attr_value in attrs_dict.items():
|
|
if attr not in NEW_ATTRS:
|
|
continue
|
|
stringified_attr = stringify_attr(attr_value)
|
|
if isinstance(stringified_attr, str):
|
|
# Convert dynamic variable strings back to their original form
|
|
stringified_attr = re.sub(r"'__dynamic_variable__\.([^']+)'", r"\1", stringified_attr)
|
|
new_attrs[attr] = stringified_attr
|
|
return new_attrs
|
|
except Exception as e:
|
|
logger.error(f"Failed to parse attrs '{attrs}': {e}")
|
|
return {}
|
|
|
|
def get_combined_invisible_condition(invisible_attribute, states_attribute):
|
|
"""
|
|
Combine invisible attribute condition with states attribute
|
|
"""
|
|
invisible_attribute = invisible_attribute.strip() if invisible_attribute else ''
|
|
states_attribute = states_attribute.strip() if states_attribute else ''
|
|
|
|
if not states_attribute:
|
|
return invisible_attribute
|
|
|
|
states_list = [f"'{s.strip()}'" for s in states_attribute.split(',')]
|
|
states_to_add = f"state not in [{','.join(states_list)}]"
|
|
|
|
if invisible_attribute:
|
|
if invisible_attribute.endswith(('or', 'and')):
|
|
combined_invisible_condition = f"{invisible_attribute} {states_to_add}"
|
|
else:
|
|
combined_invisible_condition = f"{invisible_attribute} or {states_to_add}"
|
|
else:
|
|
combined_invisible_condition = states_to_add
|
|
|
|
return combined_invisible_condition
|
|
|
|
# Process each file
|
|
for fileno, file in enumerate(files, start=1):
|
|
try:
|
|
content = file.content
|
|
|
|
# Skip if no attrs or states
|
|
if not ('attrs' in content or 'states' in content):
|
|
continue
|
|
|
|
# Parse XML with BeautifulSoup
|
|
soup = bs(content, 'xml')
|
|
tags_with_attrs = soup.select('[attrs]')
|
|
attr_tags = soup.select('attribute[name="attrs"]')
|
|
tags_with_states = soup.select('[states]')
|
|
state_tags = soup.select('attribute[name="states"]')
|
|
|
|
if not (tags_with_attrs or attr_tags or tags_with_states or state_tags):
|
|
continue
|
|
|
|
# Process tags with attrs
|
|
for tag in tags_with_attrs:
|
|
attrs_value = tag.get('attrs', '')
|
|
new_attrs = get_new_attrs(attrs_value)
|
|
|
|
if new_attrs:
|
|
# Remove the original attrs attribute
|
|
del tag['attrs']
|
|
|
|
# Add new attributes, handling existing ones
|
|
for attr_name, attr_value in new_attrs.items():
|
|
if attr_name in tag.attrs:
|
|
# Combine with existing attribute
|
|
old_value = tag[attr_name]
|
|
if old_value in ['True', '1', True, 1]:
|
|
new_value = f"True or ({attr_value})"
|
|
elif old_value in ['False', '0', False, 0]:
|
|
new_value = f"False or ({attr_value})"
|
|
else:
|
|
new_value = f"({old_value}) or ({attr_value})"
|
|
tag[attr_name] = new_value
|
|
else:
|
|
tag[attr_name] = attr_value
|
|
|
|
# Process <attribute name="attrs">
|
|
for attr_tag in attr_tags:
|
|
attrs_value = attr_tag.get_text(strip=True) if attr_tag.get_text() else ''
|
|
new_attrs = get_new_attrs(attrs_value)
|
|
|
|
if new_attrs:
|
|
parent = attr_tag.parent
|
|
|
|
# Create new attribute tags
|
|
for attr_name, attr_value in new_attrs.items():
|
|
# Check if attribute already exists
|
|
existing_attr = parent.find('attribute', {'name': attr_name})
|
|
if existing_attr:
|
|
# Combine with existing attribute
|
|
old_value = existing_attr.get_text(strip=True) if existing_attr.get_text() else ''
|
|
if old_value in ['True', '1']:
|
|
new_value = f"True or ({attr_value})"
|
|
elif old_value in ['False', '0']:
|
|
new_value = f"False or ({attr_value})"
|
|
else:
|
|
new_value = f"({old_value}) or ({attr_value})"
|
|
existing_attr.string = new_value
|
|
else:
|
|
# Create new attribute tag
|
|
new_tag = soup.new_tag('attribute', name=attr_name)
|
|
new_tag.string = str(attr_value)
|
|
attr_tag.insert_before(new_tag)
|
|
|
|
# Remove the original attrs attribute tag
|
|
attr_tag.decompose()
|
|
|
|
# Process tags with states
|
|
for tag in tags_with_states:
|
|
states_value = tag.get('states', '')
|
|
invisible_value = tag.get('invisible', '')
|
|
|
|
new_invisible = get_combined_invisible_condition(invisible_value, states_value)
|
|
|
|
# Update attributes
|
|
if new_invisible:
|
|
tag['invisible'] = new_invisible
|
|
elif 'invisible' in tag.attrs:
|
|
del tag['invisible']
|
|
|
|
# Remove states attribute
|
|
del tag['states']
|
|
|
|
# Process <attribute name="states">
|
|
for state_tag in state_tags:
|
|
states_value = state_tag.get_text(strip=True) if state_tag.get_text() else ''
|
|
parent = state_tag.parent
|
|
|
|
# Find existing invisible attribute
|
|
invisible_attr = parent.find('attribute', {'name': 'invisible'})
|
|
invisible_value = invisible_attr.get_text(strip=True) if invisible_attr and invisible_attr.get_text() else ''
|
|
|
|
new_invisible = get_combined_invisible_condition(invisible_value, states_value)
|
|
|
|
if invisible_attr:
|
|
# Update existing invisible attribute
|
|
invisible_attr.string = new_invisible
|
|
else:
|
|
# Create new invisible attribute
|
|
new_tag = soup.new_tag('attribute', name='invisible')
|
|
new_tag.string = new_invisible
|
|
state_tag.insert_before(new_tag)
|
|
|
|
# Remove the states attribute tag
|
|
state_tag.decompose()
|
|
|
|
# Update file content
|
|
file.content = str(soup)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing file {file.path}: {e}")
|
|
continue
|
|
|
|
file_manager.print_progress(fileno, len(files)) |