Skip to content
This repository has been archived by the owner on Apr 16, 2022. It is now read-only.

rewrite to support formatting and nesting fields #103

Open
wants to merge 63 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
8d6aa2e
reimplemented the base docx-mailmerge functionality, support for form…
iulica Mar 4, 2022
245c659
removed IF MergeElement and only update the inner MERGEFIELDS
iulica Mar 5, 2022
f11752a
added support for updateFields setting
iulica Mar 6, 2022
bb9c90c
cleaned up the code, removed support for IF, added implemented auto_u…
iulica Mar 6, 2022
fbcc2bc
more verbose error messages
iulica Mar 7, 2022
3dca645
fixed the complex fields when span over multiple paragraphs
iulica Mar 7, 2022
e7cfef4
complete tests, added a workaround to make tests happy for section ty…
iulica Mar 8, 2022
dd3f592
make more tests happy
iulica Mar 8, 2022
68b1327
make all tests happy
iulica Mar 8, 2022
256cf22
make tests happy
iulica Mar 8, 2022
59f2040
test_merge_template_with_rows
iulica Mar 8, 2022
f3fac1f
formatting tests and framework for easier testing
iulica Mar 8, 2022
783d28c
fixes for nested simple Fields and tests for nested fields
iulica Mar 8, 2022
076e191
added tests for auto fields udpate flag
iulica Mar 8, 2022
78c4f90
test for warnings (removing the warnings from the unittest run), impl…
iulica Mar 9, 2022
e0c9604
changed the documentation to reflect the changes
iulica Mar 9, 2022
a0ca1f6
Update README.rst
iulica Mar 13, 2022
e07d5d3
Update README.rst
iulica Mar 14, 2022
0d10293
Update README.rst
iulica Mar 22, 2022
914f811
Create auto_assign-issues.yml
iulica Mar 22, 2022
53b1b66
publish workflow
iulica Apr 1, 2022
af1a967
Update setup.py for version 0.6.0
iulica Apr 1, 2022
b32bf3b
Updated the package name and removed travis CI
iulica Apr 1, 2022
2e3ad7f
fix install command to docx-mailmerge2
iulica Apr 1, 2022
446c70c
Fixed number formatting when value is None
iulica Apr 5, 2022
c226d9f
refactoring to prepare the NEXT and NEXTIF fields implementation
iulica Apr 5, 2022
d30435d
gitignore
iulica Apr 6, 2022
5d33d00
(failed) test for issue #4
iulica Apr 6, 2022
5c4a5c9
add support for fixing the id duplicates issue #4
iulica Apr 6, 2022
443725b
Wish list update
iulica Apr 8, 2022
e53c4cc
added support for test output docx instead of temporary file for debu…
iulica Apr 15, 2022
f8e699c
added an workaround for IF fields containing text values with spaces …
iulica Apr 15, 2022
b12c7c0
added support for apostrophes for currency thousand separators with t…
iulica Apr 15, 2022
4f1190d
implemented date formatting support
iulica Apr 15, 2022
3740614
Release 0.6.2. Added support for Date formatting. Issues #5 #57
iulica Apr 16, 2022
e0c8e9a
implemented NEXT field #4
iulica Apr 16, 2022
bd7175c
fixed NEXT field #4
iulica Apr 18, 2022
55ce8ca
Update TODO list
iulica Apr 20, 2022
6b80124
Update README.rst
iulica Dec 18, 2022
5b1b69a
prepare release 0.6.3
iulica Oct 12, 2023
4945f33
added footnotes+xml to content_types_part
402Martin Oct 16, 2023
7939ebe
Merge pull request #13 from 402Martin/master
iulica Oct 16, 2023
3e45bf0
Revert "Added content type footnotes+xml so mail merge fields are sup…
iulica Oct 16, 2023
edbbac3
Merge pull request #14 from iulica/revert-13-master
iulica Oct 16, 2023
1e6fc8a
Adds test for merge footnotes
iulica Oct 24, 2023
1e256e0
adding support for endnotes and macro enabled documents
iulica Oct 24, 2023
4a313a0
Refactored MergeField class.
iulica Oct 26, 2023
3cbc7f2
Added support for keeping fields in the document.
iulica Oct 27, 2023
a57da5c
prepare release 0.7.0
iulica Oct 27, 2023
22dc108
Refactor duplicate_id_map to its own class UniqueIdsManager
iulica Nov 9, 2023
4ddc603
Added support for header/footer for merge_templates #17
iulica Nov 10, 2023
dae81c3
prepare v0.8.0
iulica Jan 16, 2024
9fb68e5
Update README.rst
iulica Jan 18, 2024
dc42458
Update README.rst
iulica Jan 23, 2024
42dc54d
ruffed
iulica Jul 19, 2024
7338d79
Added failed test for multiple tables with the same anchor
iulica Jul 19, 2024
544680b
Fix case where multiple table contain the same anchor. Issue #24
iulica Jul 19, 2024
f02a517
setup version 0.8.1
iulica Jul 19, 2024
1d2ad75
fix for empty fields, #25
iulica Sep 19, 2024
51a8db7
add test for empty field #25
iulica Sep 19, 2024
02cf82e
setup version 0.8.2
iulica Sep 19, 2024
d7206dc
fix for python 3.7
iulica Sep 27, 2024
f2f2a82
setup version 0.8.3
iulica Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
make all tests happy
iulica committed Mar 8, 2022
commit 68b1327f0d2ff09d2f241f0848bb9431d298b4f8
74 changes: 64 additions & 10 deletions mailmerge.py
Original file line number Diff line number Diff line change
@@ -198,7 +198,7 @@ def insert_into_tree(self):
for subelem in self._elements_to_add[1:]:
self.parent.remove(subelem)

replacement_element = Element("MergeField", name=self.key)
replacement_element = Element("MergeField", merge_key=self.key, name=self.name)
self.parent.replace(self._elements_to_add[0], replacement_element)
return replacement_element

@@ -211,10 +211,11 @@ class MergeData(object):
"MERGEFIELD": MergeField,
}

def __init__(self):
def __init__(self, remove_empty_tables=False):
self._merge_field_map = {} # merge_field.key: MergeField()
self._merge_field_next_id = 0
self.has_nested_fields = False
self.remove_empty_tables = remove_empty_tables

def get_merge_fields(self, key):
merge_obj = self.get_field_obj(key)
@@ -226,7 +227,7 @@ def get_instr_text(self, elements, recursive=False):
text
for elem in elements
for text in elem.xpath('w:instrText/text()', namespaces=NAMESPACES) + [
"{{{}}}".format(obj_name) if not recursive else self.get_field_obj(obj_name).instr for obj_name in elem.xpath('@name')]
"{{{}}}".format(obj_name) if not recursive else self.get_field_obj(obj_name).instr for obj_name in elem.xpath('@merge_key')]
])

@classmethod
@@ -287,16 +288,52 @@ def _get_next_key(self):

def replace(self, body, row):
""" replaces in the body xml tree the MergeField elements with values from the row """
all_tables = {
key: value
for key, value in row.items()
if isinstance(value, list)
}

for anchor, table_rows in all_tables.items():
self.replace_table_rows(body, anchor, table_rows)

merge_fields = body.findall('.//MergeField')
for field_element in merge_fields:
filled_field = self.fill_field(field_element, row)
for text_element in reversed(filled_field.filled_elements):
field_element.addnext(text_element)
field_element.getparent().remove(field_element)
if filled_field:
for text_element in reversed(filled_field.filled_elements):
field_element.addnext(text_element)
field_element.getparent().remove(field_element)

def replace_table_rows(self, body, anchor, rows):
""" replace the rows of a table with the values from the rows list """
table, idx, template = self.__find_row_anchor(body, anchor)
if table is not None:
if len(rows) > 0:
del table[idx]
for i, row_data in enumerate(rows):
row = deepcopy(template)
self.replace(row, row_data)
table.insert(idx + i, row)
else:
# if there is no data for a given table
# we check whether table needs to be removed
if self.remove_empty_tables:
parent = table.getparent()
parent.remove(table)

def __find_row_anchor(self, body, field):
for table in body.findall('.//{%(w)s}tbl' % NAMESPACES):
for idx, row in enumerate(table):
if row.find('.//MergeField[@name="%s"]' % field) is not None:
return table, idx, row
return None, None, None

def fill_field(self, field_element, row):
"""" fills the corresponding MergeField python object with data from row """
field_key = field_element.get('name')
if field_element.get('name') not in row:
return None
field_key = field_element.get('merge_key')
field_obj = self._merge_field_map[field_key]
field_obj.fill_data(self, row)
return field_obj
@@ -414,9 +451,9 @@ def __init__(self, file, remove_empty_tables=False, auto_update_fields_on_open="
self.parts = {} # part: ElementTree
self.settings = None
self._settings_info = None
self.merge_data = MergeData(remove_empty_tables=remove_empty_tables)
self.remove_empty_tables = remove_empty_tables
self.auto_update_fields_on_open = auto_update_fields_on_open
self.merge_data = MergeData()

try:
self.__fill_parts()
@@ -431,6 +468,12 @@ def __init__(self, file, remove_empty_tables=False, auto_update_fields_on_open="
self.zip.close()
raise


def __setattr__(self, __name, __value):
super(MailMerge, self).__setattr__(__name, __value)
if __name == 'remove_empty_tables':
self.merge_data.remove_empty_tables = __value

@classmethod
def parse_instr(cls, instr):
args = shlex.split(instr, posix=False)
@@ -568,7 +611,12 @@ def __get_tree_of_file(self, file):
zi = self.zip.getinfo(fn)
return zi, etree.parse(self.zip.open(zi))

def write(self, file):
def write(self, file, empty_value=''):
if empty_value is not None:
self.merge(**{
field: empty_value
for field in self.get_merge_fields()
})

with ZipFile(file, 'w', ZIP_DEFLATED) as output:
for zi in self.zip.filelist:
@@ -588,7 +636,7 @@ def get_merge_fields(self, parts=None):
fields = set()
for part in parts:
for mf in part.findall('.//MergeField'):
for name in self.merge_data.get_merge_fields(mf.attrib['name']):
for name in self.merge_data.get_merge_fields(mf.attrib['merge_key']):
fields.add(name)
return fields

@@ -637,6 +685,12 @@ def merge(self, parts=None, **replacements):
for part in parts:
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():
self.merge_data.replace_table_rows(part, anchor, rows)

def __enter__(self):
return self