From 218a411445c14749033f997cd334f55f83c28923 Mon Sep 17 00:00:00 2001 From: hoangvv Date: Tue, 4 Mar 2025 16:40:47 +0700 Subject: [PATCH 1/3] update --- exercise | 2 +- extra-addons | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/exercise b/exercise index 6baa85597..081505ce3 160000 --- a/exercise +++ b/exercise @@ -1 +1 @@ -Subproject commit 6baa855977686dfd47787b4b4b0f3a77ca15460f +Subproject commit 081505ce33ee5925763d9b5c8b42aad2ee594c36 diff --git a/extra-addons b/extra-addons index 9987dee07..6130cabb6 160000 --- a/extra-addons +++ b/extra-addons @@ -1 +1 @@ -Subproject commit 9987dee07d95818b87886bb7b368f9f934391053 +Subproject commit 6130cabb6b0f36246b480d8c80fd9d98a7fe1553 From 0782ace6b4dbfae85815092e177a2362b2a4fea7 Mon Sep 17 00:00:00 2001 From: hoangvv Date: Tue, 4 Mar 2025 16:58:29 +0700 Subject: [PATCH 2/3] update --- exercise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercise b/exercise index 081505ce3..4b8cd1223 160000 --- a/exercise +++ b/exercise @@ -1 +1 @@ -Subproject commit 081505ce33ee5925763d9b5c8b42aad2ee594c36 +Subproject commit 4b8cd12232cd226a7d3f952cd2644f31cea402a6 From 8bd3923a93bb663db60fc67828466668767ec2a8 Mon Sep 17 00:00:00 2001 From: hoangvv Date: Tue, 4 Mar 2025 18:12:43 +0700 Subject: [PATCH 3/3] update requirement package + add replace_attrs scripts --- exercise | 2 +- requirements.txt | 6 +- setup/replace_attrs.py | 244 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 setup/replace_attrs.py diff --git a/exercise b/exercise index 4b8cd1223..c06c5500e 160000 --- a/exercise +++ b/exercise @@ -1 +1 @@ -Subproject commit 4b8cd12232cd226a7d3f952cd2644f31cea402a6 +Subproject commit c06c5500ea2c47d9e6a8e1e84d183f17e4b9f29b diff --git a/requirements.txt b/requirements.txt index 646a7ffd7..1c6ebeb9e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -104,4 +104,8 @@ requests ; python_version > '3.10' google_auth ; python_version > '3.10' odoorpc ; python_version > '3.10' openai ; python_version > '3.10' -pysftp ; python_version > '3.10' \ No newline at end of file +pysftp ; python_version > '3.10' and sys_platform != 'win32' +unidecode ; python_version > '3.10' +pdfkit ; python_version > '3.10' +weasyprint +beautifulsoup4 \ No newline at end of file diff --git a/setup/replace_attrs.py b/setup/replace_attrs.py new file mode 100644 index 000000000..03a7df75f --- /dev/null +++ b/setup/replace_attrs.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- + +import re +from bs4 import formatter, BeautifulSoup as bs +from pathlib import Path + +xml_4indent_formatter = formatter.XMLFormatter(indent=4) +NEW_ATTRS = {'required', 'invisible', 'readonly', 'column_invisible'} +percent_d_regex = re.compile("%\('?\"?[\w\.\d_]+'?\"?\)d") + +def get_files_recursive(path): + return (str(p) for p in Path(path).glob('**/*.xml') if p.is_file()) + +root_dir = input('Enter root directory to check (empty for current directory) : ') +root_dir = root_dir or '.' +all_xml_files = get_files_recursive(root_dir) + +def normalize_domain(domain): + """Normalize Domain, taken from odoo/osv/expression.py -> just the part so that & operators are added where needed. + After that, we can use a part of the def parse() from the same file to manage parenthesis for and/or""" + 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): + stringify = '' + switcher = False + # Replace operators not supported in python (=, like, ilike) + operator = str(leaf[1]) + if operator == '=': + operator = '==' + elif 'like' in operator: + if 'not' in operator: + operator = 'not in' + else: + operator = 'in' + switcher = True + # Take left operand, never to add quotes (should be python object / field) + left_operand = leaf[0] + # Take care of right operand, don't add quotes if it's list/tuple/set/boolean/number, check if we have a true/false/1/0 string tho. + right_operand = leaf[2] + if right_operand in ('True', 'False', '1', '0') or type(right_operand) in (list, tuple, set, int, float, bool): + right_operand = str(right_operand) + else: + right_operand = "'"+right_operand+"'" + stringify = "%s %s %s" % (right_operand if switcher else left_operand, operator, left_operand if switcher else right_operand) + return stringify + +def stringify_attr(stack): + if stack in (True, False, 'True', 'False', 1, 0, '1', '0'): + return stack + last_parenthesis_index = max(index for index, item in enumerate(stack[::-1]) if item not in ('|', '!')) + 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 == '&' or leaf_or_operator == '|': + left = result.pop() + # In case of a single | or single & , we expect that it's a tag that have an attribute AND a state + # the state will be added as OR in states management + 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)) + result = result[0] + return result + +def get_new_attrs(attrs): + new_attrs = {} + attrs_dict = eval(attrs.strip()) + for attr in NEW_ATTRS: + if attr in attrs_dict.keys(): + new_attrs[attr] = stringify_attr(attrs_dict[attr]) + ordered_attrs = {attr: new_attrs[attr] for attr in NEW_ATTRS if attr in new_attrs} + return ordered_attrs + +# Prettify puts on three lines (1/ opening tag, 2/ text, 3/ closing tag), not very cool. +# Taken from https://stackoverflow.com/questions/55962146/remove-line-breaks-and-spaces-around-span-elements-with-python-regex +# And changed to avoid putting ALL one line, and only manage , as it's the only one messing stuff here +# Kinda ugly to use the 3 types of tags but tbh I keep it like this while I have no time for a regex replace keeping the name="x" :p +def prettify_output(html): + for attr in NEW_ATTRS: + html = re.sub(f'[ \n]+',f'', html) + html = re.sub(f'[ \n]+',f'', html) + html = re.sub(r'[ \n]+', r'', html) + html = re.sub(r'[ \n]+', r'', html) + return html + +autoreplace = input('Do you want to auto-replace attributes ? (y/n) (empty == no) (will not ask confirmation for each file) : ') or 'n' +nofilesfound = True +ok_files = [] +nok_files = [] + +for xml_file in all_xml_files: + try: + with open(xml_file, 'rb') as f: + contents = f.read().decode('utf-8') + f.close() + if not 'attrs' in contents and not 'states' in contents: + continue + counter_for_percent_d_replace = 1 + percent_d_results = {} + for percent_d in percent_d_regex.findall(contents): + contents = contents.replace(percent_d, "'REPLACEME%s'" % counter_for_percent_d_replace) + percent_d_results[counter_for_percent_d_replace] = percent_d + counter_for_percent_d_replace += 1 + soup = bs(contents, 'xml') + tags_with_attrs = soup.select('[attrs]') + attribute_tags_name_attrs = soup.select('attribute[name="attrs"]') + tags_with_states = soup.select('[states]') + attribute_tags_name_states = soup.select('attribute[name="states"]') + if not (tags_with_attrs or attribute_tags_name_attrs or\ + tags_with_states or attribute_tags_name_states): + continue + print('\n################################################################') + print('##### Taking care of file -> %s' % xml_file) + print('\n########### Current tags found ###\n') + for t in tags_with_attrs + attribute_tags_name_attrs + tags_with_states + attribute_tags_name_states: + print(t) + + nofilesfound = False + # Management of tags that have attrs="" + for tag in tags_with_attrs: + attrs = tag['attrs'] + new_attrs = get_new_attrs(attrs) + del tag['attrs'] + for new_attr in new_attrs.keys(): + tag[new_attr] = new_attrs[new_attr] + # Management of attributes name="attrs" + attribute_tags_after = [] + for attribute_tag in attribute_tags_name_attrs: + new_attrs = get_new_attrs(attribute_tag.text) + for new_attr in new_attrs.keys(): + new_tag = soup.new_tag('attribute') + new_tag['name'] = new_attr + new_tag.append(str(new_attrs[new_attr])) + attribute_tags_after.append(new_tag) + attribute_tag.insert_after(new_tag) + attribute_tag.decompose() + # Management ot tags that have states="" + for state_tag in tags_with_states: + base_invisible = '' + if 'invisible' in state_tag.attrs and state_tag['invisible']: + base_invisible = state_tag['invisible'] + if not (base_invisible.endswith('or') or base_invisible.endswith('and')): + base_invisible = base_invisible + ' or ' + else: + base_invisible = base_invisible + ' ' + invisible_attr = "state not in [%s]" % ','.join(("'" + state.strip() + "'") for state in state_tag['states'].split(',')) + state_tag['invisible'] = base_invisible + invisible_attr + del state_tag['states'] + # Management of attributes name="states" + attribute_tags_states_after = [] + for attribute_tag_states in attribute_tags_name_states: + states = attribute_tag_states.text + existing_invisible_tag = False + # I don't know why, looking for attribute[name="invisible"] does not work, + # but if it exists, I can find it with findAll attribute -> loop to name="invisible" + for tag in attribute_tag_states.parent.findAll('attribute'): + if tag['name'] == 'invisible': + existing_invisible_tag = tag + break + if not existing_invisible_tag: + existing_invisible_tag = soup.new_tag('attribute') + existing_invisible_tag['name'] = 'invisible' + if existing_invisible_tag.text: + states_to_add = 'state not in [%s]' % ( + ','.join(("'" + state.strip() + "'") for state in states.split(',')) + ) + if existing_invisible_tag.text.endswith('or') or existing_invisible_tag.text.endswith('and'): + new_invisible_text = '%s %s' % (existing_invisible_tag.text, states_to_add) + else: + new_invisible_text = ' or '.join([existing_invisible_tag.text, states_to_add]) + else: + new_invisible_text = 'state not in [%s]' % ( + ','.join(("'" + state.strip() + "'") for state in states.split(',')) + ) + existing_invisible_tag.string = new_invisible_text + attribute_tag_states.insert_after(existing_invisible_tag) + attribute_tag_states.decompose() + attribute_tags_states_after.append(existing_invisible_tag) + + print('\n########### Will be replaced by ###\n') + for t in tags_with_attrs + attribute_tags_after + tags_with_states + attribute_tags_states_after: + print(t) + print('################################################################\n') + if autoreplace.lower()[0] == 'n': + confirm = input('Do you want to replace? (y/n) (empty == no) : ') or 'n' + else: + confirm = 'y' + if confirm.lower()[0] == 'y': + with open(xml_file, 'wb') as rf: + html = soup.prettify(formatter=xml_4indent_formatter) + html = prettify_output(html) + for percent_d_result in percent_d_results.keys(): + html = html.replace("'REPLACEME%s'" % percent_d_result, percent_d_results[percent_d_result]) + rf.write(html.encode('utf-8')) + ok_files.append(xml_file) + except Exception as e: + nok_files.append((xml_file, e)) + +print('\n################################################') +print('################## Run Debug ##################') +print('################################################') + +if nofilesfound: + print('No XML Files with "attrs" or "states" found in dir " %s "' % root_dir) + +print('Succeeded on files') +for file in ok_files: + print(file) +if not ok_files: + print('No files') +print('') +print('Failed on files') +for file in nok_files: + print(file[0]) + print('Reason: ', file[1]) +if not nok_files: + print('No files')