From 22dc108942ce3af68c8c6ac5af6e5e46227f2169 Mon Sep 17 00:00:00 2001 From: Iulian Ciorascu Date: Thu, 9 Nov 2023 10:04:18 +0100 Subject: [PATCH] Refactor duplicate_id_map to its own class UniqueIdsManager Tests for UniqueIdsManager Refactor Mailmerge.parts to be more flexible for handling different categories of parts Refactor settings with the new category of parts --- mailmerge.py | 109 ++++++++++++++----------- tests/test_footnote_header_footer.docx | Bin 19360 -> 18219 bytes tests/test_keep_fields.py | 6 +- tests/test_nested_fields.py | 10 +-- tests/test_unique_id.py | 24 ++++++ tests/test_winword2010.py | 2 +- tests/utils.py | 1 - 7 files changed, 95 insertions(+), 57 deletions(-) create mode 100644 tests/test_unique_id.py diff --git a/mailmerge.py b/mailmerge.py index e8519ba..bbc1bc9 100644 --- a/mailmerge.py +++ b/mailmerge.py @@ -30,16 +30,15 @@ for tag in ATTACHMENT_TAGS } -CONTENT_TYPES_PARTS = ( - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml', - 'application/vnd.ms-word.document.macroEnabled.main+xml', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml', -) - -CONTENT_TYPE_SETTINGS = 'application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml' +CONTENT_TYPES_PARTS = { + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml':'main', + 'application/vnd.ms-word.document.macroEnabled.main+xml':'main', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml':'rel_part', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml':'rel_part', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml':'notes', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml':'notes', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml':'settings' +} VALID_SEPARATORS = { 'page_break', 'column_break', 'textWrapping_break', @@ -359,6 +358,23 @@ class NextField(MergeField): def fill_data(self, merge_data, row): raise NextRecord() +class UniqueIdsManager(object): + """ handles different counters for various ids in the document """ + + def __init__(self): + self.id_type_map = {} # type of id -> {'max': max_id, 'ids': set(existing_ids)} + + def register_id(self, id_type, obj_id=None): + """ registers an new object id or creates a new id for the type """ + type_id_value = self.id_type_map.setdefault(id_type, {"max": 0, "ids": set()}) + new_obj_id = None + if obj_id is None or obj_id in type_id_value['ids']: + obj_id = type_id_value['max'] + 1 + new_obj_id = obj_id + type_id_value['ids'].add(obj_id) + type_id_value['max'] = max(type_id_value['max'], obj_id) + return new_obj_id + class MergeData(object): """ prepare the MergeField objects and the data """ @@ -371,7 +387,7 @@ class MergeData(object): def __init__(self, remove_empty_tables=False, keep_fields="none"): self._merge_field_map = {} # merge_field.key: MergeField() self._merge_field_next_id = 0 - self.duplicate_id_map = {} # tag: {'max': max_id, 'ids': set(existing_ids)} + self.unique_id_manager = UniqueIdsManager() self.has_nested_fields = False self.remove_empty_tables = remove_empty_tables self.keep_fields = keep_fields @@ -400,21 +416,14 @@ def is_first(self): def get_new_element_id(self, element): """ Returns None if the existing id is new otherwise a new id """ - tag = element.tag - id = element.get('id') - if id is None: + # tag = element.tag + elem_id = element.get('id') + if elem_id is None: return None - id = int(id) - id_data = self.duplicate_id_map.setdefault(tag, {'max': id, 'values': set()}) - if id in id_data['values']: - # it already exists - id = id_data['max'] + 1 - id_data['values'].add(id) - id_data['max'] = id - return str(id) - - id_data['values'].add(id) - id_data['max'] = max(id, id_data['max']) + elem_id = int(elem_id) + new_id = self.unique_id_manager.register_id("id", elem_id) + if new_id: + return str(new_id) return None def get_merge_fields(self, key): @@ -707,9 +716,8 @@ def __init__(self, file, remove_empty_tables=False, auto_update_fields_on_open=" keep_fields : none - merge all fields even if no data, some - keep fields with no data, all - keep all fields """ self.zip = ZipFile(file) - self.parts = {} # part: ElementTree - self.settings = None - self._settings_info = None + self.parts = {} # zi_part: ElementTree + self.categories = {} # category: [zi, ...] self.merge_data = MergeData(remove_empty_tables=remove_empty_tables, keep_fields=keep_fields) self.remove_empty_tables = remove_empty_tables self.auto_update_fields_on_open = auto_update_fields_on_open @@ -719,7 +727,7 @@ def __init__(self, file, remove_empty_tables=False, auto_update_fields_on_open=" try: self.__fill_parts() - for part in self.parts.values(): + for part in self.get_parts().values(): self.__fill_simple_fields(part) self.__fill_complex_fields(part) @@ -727,6 +735,21 @@ def __init__(self, file, remove_empty_tables=False, auto_update_fields_on_open=" self.zip.close() raise + def get_parts(self, categories=None): + """ return all the parts based on categories """ + if categories is None: + categories = ["main", "rel_part", "notes"] + elif isinstance(categories, str): + categories = [categories] + return { + zi: self.parts[zi] + for category in categories + for zi in self.categories.get(category, []) + } + + def get_settings(self): + """ returns the settings part """ + return list(self.get_parts(['settings']).values())[0] def __setattr__(self, __name, __value): super(MailMerge, self).__setattr__(__name, __value) @@ -736,11 +759,11 @@ def __setattr__(self, __name, __value): def __fill_parts(self): content_types = etree.parse(self.zip.open('[Content_Types].xml')) for file in content_types.findall('{%(ct)s}Override' % NAMESPACES): - type = file.attrib['ContentType' % NAMESPACES] - if type in CONTENT_TYPES_PARTS: + part_type = file.attrib['ContentType' % NAMESPACES] + category = CONTENT_TYPES_PARTS.get(part_type) + if category: zi, self.parts[zi] = self.__get_tree_of_file(file) - elif type == CONTENT_TYPE_SETTINGS: - self._settings_info, self.settings = self.__get_tree_of_file(file) + self.categories.setdefault(category, []).append(zi) def __fill_simple_fields(self, part): for fld_simple_elem in part.findall('.//{%(w)s}fldSimple' % NAMESPACES): @@ -842,8 +865,8 @@ def __fill_complex_fields(self, part): def __fix_settings(self): - if self.settings: - settings_root = self.settings.getroot() + for settings in self.get_parts(categories=['settings']).values(): + settings_root = settings.getroot() if not self._has_unmerged_fields: mail_merge = settings_root.find('{%(w)s}mailMerge' % NAMESPACES) if mail_merge is not None: @@ -888,15 +911,12 @@ def write(self, file, empty_value=''): if zi in self.parts: xml = etree.tostring(self.parts[zi].getroot(), encoding='UTF-8', xml_declaration=True) output.writestr(zi.filename, xml) - elif zi == self._settings_info: - xml = etree.tostring(self.settings.getroot(), encoding='UTF-8', xml_declaration=True) - output.writestr(zi.filename, xml) else: output.writestr(zi.filename, self.zip.read(zi)) def get_merge_fields(self, parts=None): if not parts: - parts = self.parts.values() + parts = self.get_parts().values() fields = set() for part in parts: @@ -926,13 +946,8 @@ def merge_templates(self, replacements, separator): # Duplicate template. Creates a copy of the template, does a merge, and separates them by a new paragraph, a new break or a new section break. #GET ROOT - WORK WITH DOCUMENT - for part in self.parts.values(): + for part in self.get_parts(["main"]).values(): root = part.getroot() - tag = root.tag - - # ignore header, footer, footnotes, endnotes, etc - if tag in ATTACHMENT_TAGS_WITH_NAMESPACE: - continue # the mailmerge is done with the help of the MergeDocument class # that handles the document duplication @@ -964,13 +979,13 @@ def merge(self, **replacements): self._merge(replacements) def _merge(self, replacements): - for part in self.parts.values(): + for part in self.get_parts().values(): self.merge_data.replace(part, replacements) def merge_rows(self, anchor, rows): """ anchor is one of the fields in the table """ - for part in self.parts.values(): + for part in self.get_parts().values(): self.merge_data.replace_table_rows(part, anchor, rows) def __enter__(self): diff --git a/tests/test_footnote_header_footer.docx b/tests/test_footnote_header_footer.docx index 782976944f5d24aab833a396be409e55cf2e70a9..72178f22101f4b99362372e8737a96d5236c2693 100644 GIT binary patch delta 7756 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 delta 8744 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 wA>ZUMpKDJF3lf6#$