diff --git a/.travis.yml b/.travis.yml index fd69637be..bdc4e9a39 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,4 @@ python: # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: pip install -r requirements.txt # command to run tests, e.g. python setup.py test -script: py.test +script: py.test \ No newline at end of file diff --git a/docx/__init__.py b/docx/__init__.py index 7083abe56..926c05934 100644 --- a/docx/__init__.py +++ b/docx/__init__.py @@ -16,6 +16,7 @@ from docx.parts.numbering import NumberingPart from docx.parts.settings import SettingsPart from docx.parts.styles import StylesPart +from docx.parts.header_footer import HeaderPart, FooterPart def part_class_selector(content_type, reltype): @@ -30,6 +31,8 @@ def part_class_selector(content_type, reltype): PartFactory.part_type_for[CT.WML_NUMBERING] = NumberingPart PartFactory.part_type_for[CT.WML_SETTINGS] = SettingsPart PartFactory.part_type_for[CT.WML_STYLES] = StylesPart +PartFactory.part_type_for[CT.WML_HEADER] = HeaderPart +PartFactory.part_type_for[CT.WML_FOOTER] = FooterPart del ( CT, CorePropertiesPart, DocumentPart, NumberingPart, PartFactory, diff --git a/docx/document.py b/docx/document.py index ba94a7990..c0275a900 100644 --- a/docx/document.py +++ b/docx/document.py @@ -87,7 +87,7 @@ def add_section(self, start_type=WD_SECTION.NEW_PAGE): """ new_sectPr = self._element.body.add_section_break() new_sectPr.start_type = start_type - return Section(new_sectPr) + return Section(new_sectPr, self._part) def add_table(self, rows, cols, style=None): """ @@ -147,7 +147,7 @@ def sections(self): A |Sections| object providing access to each section in this document. """ - return Sections(self._element) + return Sections(self._element, self._part) @property def settings(self): diff --git a/docx/enum/header_footer.py b/docx/enum/header_footer.py new file mode 100644 index 000000000..7ba61fce4 --- /dev/null +++ b/docx/enum/header_footer.py @@ -0,0 +1,42 @@ +# encoding: utf-8 + +""" +Enumerations related to the main document in WordprocessingML files +""" + +from __future__ import absolute_import, print_function, unicode_literals + +from .base import alias, XmlEnumeration, XmlMappedEnumMember + + +@alias('WD_HEADER_FOOTER') +class WD_HEADER_FOOTER(XmlEnumeration): + """ + alias: **WD_HEADER_FOOTER_INDEX** + + Specified header or footer in a document or section. + + Example:: + + from docx.enum.header_footer import WD_HEADER_FOOTER + + header = document.sections[-1].header + first_page_header = document.sections[-1].first_page_header + even_odd_header = document.sections[-1].even_odd_header + """ + + __ms_name__ = 'WdHeaderFooterIndex' + + __url__ = 'https://docs.microsoft.com/en-us/office/vba/api/word.wdheaderfooterindex' + + __members__ = ( + XmlMappedEnumMember( + 'PRIMARY', 1, 'default', 'Header or footer on all pages other than the first page of a document or section.' + ), + XmlMappedEnumMember( + 'FIRST_PAGE', 2, 'first', 'First header or footer in a document or section.' + ), + XmlMappedEnumMember( + 'EVEN_PAGE', 3, 'even', 'Headers or footers on even-numbered pages.' + ), + ) diff --git a/docx/header_footer.py b/docx/header_footer.py new file mode 100644 index 000000000..c70299b89 --- /dev/null +++ b/docx/header_footer.py @@ -0,0 +1,145 @@ +# encoding: utf-8 + +""" +|Header| and |Footer| objects +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from .blkcntnr import BlockItemContainer +from .shared import ElementProxy, Emu + + +class _HeaderFooter(ElementProxy): + + __slots__ = ('_type', '__body') + + def __init__(self, element, parent, hf_type): + super(_HeaderFooter, self).__init__(element, parent) + self._type = hf_type + self.__body = None + + def add_paragraph(self, text='', style=None): + """ + Return a paragraph, populated + with *text* and having paragraph style *style*. *text* can contain + tab (``\\t``) characters, which are converted to the appropriate XML + form for a tab. *text* can also include newline (``\\n``) or carriage + return (``\\r``) characters, each of which is converted to a line + break. + """ + return self._body.add_paragraph(text, style) + + @property + def paragraphs(self): + """ + A list of |Paragraph| instances corresponding to the paragraphs in + this object. Note that paragraphs within revision + marks such as ```` or ```` do not appear in this list. + """ + return self._body.paragraphs + + def add_table(self, rows, cols, style=None): + """ + Add a table having row and column counts of *rows* and *cols* + respectively and table style of *style*. *style* may be a paragraph + style object or a paragraph style name. If *style* is |None|, the + table inherits the default table style of the document. + """ + table = self._body.add_table(rows, cols, self._block_width) + table.style = style + return table + + @property + def tables(self): + """ + A list of |Table| instances corresponding to the tables in + this object. Note that only tables appearing at the + top level of the document appear in this list; a table nested inside + a table cell does not appear. A table within revision marks such as + ```` or ```` will also not appear in the list. + """ + + return self._body.tables + + @property + def _block_width(self): + """ + Return a |Length| object specifying the width of available "writing" + space between the margins of the last section of this document. + """ + section = self._parent + return Emu( + section.page_width - section.left_margin - section.right_margin + ) + + @property + def _body(self): + if self.__body is None: + ref = self._parent.sectPr.get_header_reference_of_type(self._type) + if ref is None: + return None + part = self.part.get_related_part(ref.rId) + if part is not None: + self.__body = _HeaderFooterBody(part._element, self) + + return self.__body + + +class Header(_HeaderFooter): + + @property + def is_linked_to_previous(self): + """ + + :return: True when is linked to previous header otherwise false + """ + if self._parent is None: + return True + return True if self._parent.sectPr.get_header_reference_of_type(self._type) is None else False + + @is_linked_to_previous.setter + def is_linked_to_previous(self, value): + """ + - if previous value is True and value is False then need to create new header + - if previous value is False and value is True then need to remove reference + """ + if self.is_linked_to_previous is True and value is False: + rId = self.part.add_header_part() + self._parent.sectPr.add_header_reference_of_type(rId, self._type) + self.add_paragraph('') + elif self.is_linked_to_previous is False and value is True: + self._parent.sectPr.remove_header_reference(self._type) + self.__body = None + + +class Footer(_HeaderFooter): + + @property + def is_linked_to_previous(self): + """ + + :return: True when is linked otherwise false + """ + if self._parent is None: + return True + return True if self._parent.sectPr.get_footer_reference_of_type(self._type) is None else False + + @is_linked_to_previous.setter + def is_linked_to_previous(self, value): + """ + - if previous value is True and value is False then need to create new footer + - if previous value is False and value is True then need to remove reference + """ + if self.is_linked_to_previous is True and value is False: + rId = self.part.add_footer_part(self._type) + self._parent.sectPr.add_footer_reference_of_type(rId, self._type) + elif self.is_linked_to_previous is False and value is True: + self._parent.sectPr.remove_footer_reference(self._type) + self.__body = None + + +class _HeaderFooterBody(BlockItemContainer): + pass diff --git a/docx/opc/part.py b/docx/opc/part.py index 928d3c183..b7c9acbc3 100644 --- a/docx/opc/part.py +++ b/docx/opc/part.py @@ -120,6 +120,14 @@ def part_related_by(self, reltype): """ return self.rels.part_with_reltype(reltype) + def parts_by_type(self, reltype): + """ + Return parts to which this part has a relationship of *reltype*. + Raises |KeyError| if no such relationship is found and |ValueError| + Provides ability to resolve implicitly related part, such as Slide -> SlideLayout. + """ + return self.rels.parts_with_reltype(reltype) + def relate_to(self, target, reltype, is_external=False): """ Return rId key of relationship of *reltype* to *target*, from an diff --git a/docx/opc/rel.py b/docx/opc/rel.py index 7dba2af8e..de7b5ae62 100644 --- a/docx/opc/rel.py +++ b/docx/opc/rel.py @@ -63,6 +63,14 @@ def part_with_reltype(self, reltype): rel = self._get_rel_of_type(reltype) return rel.target_part + def parts_with_reltype(self, reltype): + """ + Return target parts of rel with matching *reltype*, raising |KeyError| + if not found + """ + rels = self._get_rels_of_type(reltype) + return [rel.target_part for rel in rels] + @property def related_parts(self): """ @@ -119,6 +127,13 @@ def _get_rel_of_type(self, reltype): raise ValueError(tmpl % reltype) return matching[0] + def _get_rels_of_type(self, reltype): + matching = [rel for rel in self.values() if rel.reltype == reltype] + if len(matching) == 0: + tmpl = "no relationship of type '%s' in collection" + raise KeyError(tmpl % reltype) + return matching + @property def _next_rId(self): """ diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index 2731302e2..83d004886 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -86,11 +86,21 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('w:numbering', CT_Numbering) register_element_cls('w:startOverride', CT_DecimalNumber) -from .section import CT_PageMar, CT_PageSz, CT_SectPr, CT_SectType +from .section import CT_PageMar, CT_PageSz, CT_SectPr, CT_SectType, CT_HdrFtrRef, CT_HdrFtr register_element_cls('w:pgMar', CT_PageMar) register_element_cls('w:pgSz', CT_PageSz) register_element_cls('w:sectPr', CT_SectPr) register_element_cls('w:type', CT_SectType) +register_element_cls('w:headerReference', CT_HdrFtrRef) +register_element_cls('w:footerReference', CT_HdrFtrRef) +register_element_cls('w:hdr', CT_HdrFtr) +register_element_cls('w:ftr', CT_HdrFtr) +register_element_cls('w:titlePg', CT_OnOff) + + +from .settings import CT_Settings +register_element_cls('w:settings', CT_Settings) +register_element_cls('w:evenAndOddHeaders', CT_OnOff) from .shape import ( CT_Blip, CT_BlipFillProperties, CT_GraphicalObject, diff --git a/docx/oxml/base_complex_types.py b/docx/oxml/base_complex_types.py new file mode 100644 index 000000000..45b71150f --- /dev/null +++ b/docx/oxml/base_complex_types.py @@ -0,0 +1,15 @@ +# encoding: utf-8 + +""" +Simple complex types classes represent base elements +""" + +from .xmlchemy import BaseOxmlElement, RequiredAttribute +from .simpletypes import ST_RelationshipId + + +class CT_Rel(BaseOxmlElement): + """ + ```` element, defining the rId . + """ + rId = RequiredAttribute('r:id', ST_RelationshipId) diff --git a/docx/oxml/section.py b/docx/oxml/section.py index cf76b67ed..cd2510aae 100644 --- a/docx/oxml/section.py +++ b/docx/oxml/section.py @@ -9,8 +9,35 @@ from copy import deepcopy from ..enum.section import WD_ORIENTATION, WD_SECTION_START +from ..enum.header_footer import WD_HEADER_FOOTER from .simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure -from .xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrOne +from .xmlchemy import BaseOxmlElement, OptionalAttribute, ZeroOrOne, ZeroOrMore +from .base_complex_types import CT_Rel + + +class CT_HdrFtr(BaseOxmlElement): + """ + ```` and ````, the container element for the header and footer part. + """ + + p = ZeroOrMore('w:p', successors=()) + tbl = ZeroOrMore('w:tbl', successors=('w:sectPr',)) + + def clear_content(self): + """ + Remove all content child elements from this element. + """ + + content_elms = self[:] + for content_elm in content_elms: + self.remove(content_elm) + + +class CT_HdrFtrRef(CT_Rel): + """ + ```` and ````, elements, defining section header and footer reference. + """ + type = OptionalAttribute('w:type', WD_HEADER_FOOTER) class CT_PageMar(BaseOxmlElement): @@ -57,6 +84,9 @@ class CT_SectPr(BaseOxmlElement): pgMar = ZeroOrOne('w:pgMar', successors=( __child_sequence__[__child_sequence__.index('w:pgMar')+1:] )) + headerReference = ZeroOrMore('w:headerReference') + footerReference = ZeroOrMore('w:footerReference') + titlePg = ZeroOrOne('w:titlePg') @property def bottom_margin(self): @@ -239,6 +269,23 @@ def start_type(self, value): type = self.get_or_add_type() type.val = value + @property + def titlePg_val(self): + """ + The value of `evenAndOddHeaders/@val` or |None| if not present. + """ + titlePg = self.titlePg + if titlePg is None: + return False + return titlePg.val + + @titlePg_val.setter + def titlePg_val(self, value): + if value in [None, False]: + self._remove_titlePg() + else: + self.get_or_add_titlePg().val = value + @property def top_margin(self): """ @@ -256,6 +303,72 @@ def top_margin(self, value): pgMar = self.get_or_add_pgMar() pgMar.top = value + def get_footer_reference_of_type(self, type): + """ + + :param type: of footer refence + :return: "w:footerReference" object if exist, None otherwise + """ + for ref in self.footerReference_lst: + if ref.type == type: + return ref + return None + + def get_header_reference_of_type(self, type): + """ + + :param type: of header refence + :return: "w:headerReference" object if exist, None otherwise + """ + for ref in self.headerReference_lst: + if ref.type == type: + return ref + return None + + def add_header_reference_of_type(self, rId, type): + """ + Create new header reference with rId and type + :param type: of header reference + :param rId: relation id to related header file + :return: + """ + headerRef = self._add_headerReference() + headerRef.rId = rId + headerRef.type = type + + def remove_header_reference(self, type): + """ + Remove header reference from this section by type + :param type: of header reference + :param rId: relation id to related header file + :return: + """ + ref = self.get_header_reference_of_type(type) + if ref is not None: + self.remove(ref) + + def add_footer_reference_of_type(self, rId, type): + """ + Create new footer reference with rId and type + :param type: of footer reference + :param rId: relation id to related footer file + :return: + """ + footerRef = self._add_footerReference() + footerRef.rId = rId + footerRef.type = type + + def remove_footer_reference(self, type): + """ + Remove footer reference from this section by type + :param type: of footer reference + :param rId: relation id to related footer file + :return: + """ + ref = self.get_footer_reference_of_type(type) + if ref is not None: + self.remove(ref) + class CT_SectType(BaseOxmlElement): """ diff --git a/docx/oxml/settings.py b/docx/oxml/settings.py new file mode 100644 index 000000000..d6bb34e6d --- /dev/null +++ b/docx/oxml/settings.py @@ -0,0 +1,38 @@ +# encoding: utf-8 + +""" +Custom element classes related to the styles part +""" + +from .xmlchemy import ( + BaseOxmlElement, ZeroOrOne +) + + +class CT_Settings(BaseOxmlElement): + """ + `w:settings` element, defining behavior defaults for settings + and containing `w:evenAndOddHeaders` child elements that define even and odd headers + """ + _tag_seq = ( + 'w:evenAndOddHeaders' + ) + + evenAndOddHeaders = ZeroOrOne('w:evenAndOddHeaders') + + @property + def evenAndOddHeaders_val(self): + """ + The value of `evenAndOddHeaders/@val` or |None| if not present. + """ + evenAndOddHeaders = self.evenAndOddHeaders + if evenAndOddHeaders is None: + return False + return evenAndOddHeaders.val + + @evenAndOddHeaders_val.setter + def evenAndOddHeaders_val(self, value): + if value in [None, False]: + self._remove_evenAndOddHeaders() + else: + self.get_or_add_evenAndOddHeaders().val = value diff --git a/docx/parts/document.py b/docx/parts/document.py index 01266b3bd..b58a2716f 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -17,6 +17,7 @@ from ..shared import lazyproperty from .settings import SettingsPart from .styles import StylesPart +from .header_footer import HeaderPart, FooterPart class DocumentPart(XmlPart): @@ -28,6 +29,16 @@ class DocumentPart(XmlPart): inherited by many content objects provides access to this part object for that purpose. """ + + def add_relationship(self, part, relationship_type): + """ + Add relation to this document part + :param part: + :param relationship_type: + :return: rId + """ + return self.relate_to(part, relationship_type) + @property def core_properties(self): """ @@ -57,6 +68,14 @@ def get_or_add_image(self, image_descriptor): rId = self.relate_to(image_part, RT.IMAGE) return rId, image_part.image + def get_related_part(self, rId): + """ + + :param rId: of relationship + :return: relationship of rId + """ + return self.rels.related_parts[rId] + def get_style(self, style_id, style_type): """ Return the style in this document matching *style_id*. Returns the @@ -171,3 +190,29 @@ def _styles_part(self): styles_part = StylesPart.default(self.package) self.relate_to(styles_part, RT.STYLES) return styles_part + + def add_header_part(self): + """ + Creates an empty header part. + """ + headers_parts = self.parts_by_type(RT.HEADER) + last_header_number = 0 + for part in headers_parts: + header_num = int(part.partname.split('.')[-2][len('/word/header'):]) + if header_num > last_header_number: + last_header_number = header_num + return self.add_relationship(HeaderPart.new(headers_parts[-1].part.blob, self.package, last_header_number + 1), + RT.HEADER) + + def add_footer_part(self): + """ + Creates an empty footer part. + """ + footers_parts = self.parts_by_type(RT.FOOTER) + last_footer_number = 0 + for part in footers_parts: + footer_num = int(part.partname.split('.')[-2][len('/word/footer'):]) + if footer_num > last_footer_number: + last_footer_number = footer_num + return self.add_relationship(FooterPart.new(footers_parts[-1].part.blob, self.package, last_footer_number + 1), + RT.FOOTER) diff --git a/docx/parts/header_footer.py b/docx/parts/header_footer.py new file mode 100644 index 000000000..7f28763f4 --- /dev/null +++ b/docx/parts/header_footer.py @@ -0,0 +1,67 @@ +# encoding: utf-8 + +""" +|HeaderFooterPart| and closely related objects +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from ..opc.constants import CONTENT_TYPE as CT +from ..opc.packuri import PackURI +from ..oxml import parse_xml + +from ..opc.part import XmlPart + + +class _HeaderFooterPart(XmlPart): + pass + + +class HeaderPart(_HeaderFooterPart): + """ + Main header and footer part of a WordprocessingML (WML) package, aka a .docx file. + Acts as broker to other parts such as image, core properties, and style + parts. It also acts as a convenient delegate when a mid-document object + needs a service involving a remote ancestor. The `Parented.part` property + inherited by many content objects provides access to this part object for + that purpose. + """ + + @classmethod + def new(cls, blob, package, header_number): + """ + Return a newly created header part, containing a default + `w:hdr` element tree. + """ + partname = PackURI('/word/header%s.xml' % str(header_number)) + content_type = CT.WML_HEADER + element = parse_xml(blob) + header_part = cls(partname, content_type, element, package) + header_part.element.clear_content() + return header_part + + +class FooterPart(_HeaderFooterPart): + """ + Main header and footer part of a WordprocessingML (WML) package, aka a .docx file. + Acts as broker to other parts such as image, core properties, and style + parts. It also acts as a convenient delegate when a mid-document object + needs a service involving a remote ancestor. The `Parented.part` property + inherited by many content objects provides access to this part object for + that purpose. + """ + + @classmethod + def new(cls, blob, package, footer_number): + """ + Return a newly created footer part, containing a default + `w:ftr` element tree. + """ + partname = PackURI('/word/footer%s.xml' % str(footer_number)) + content_type = CT.WML_FOOTER + element = parse_xml(blob) + footer_part = cls(partname, content_type, element, package) + footer_part.element.clear_content() + return footer_part diff --git a/docx/section.py b/docx/section.py index 16221243b..cc545ff70 100644 --- a/docx/section.py +++ b/docx/section.py @@ -7,6 +7,8 @@ from __future__ import absolute_import, print_function, unicode_literals from collections import Sequence +from .header_footer import Header, Footer +from .enum.header_footer import WD_HEADER_FOOTER class Sections(Sequence): @@ -14,20 +16,21 @@ class Sections(Sequence): Sequence of |Section| objects corresponding to the sections in the document. Supports ``len()``, iteration, and indexed access. """ - def __init__(self, document_elm): + def __init__(self, document_elm, part): super(Sections, self).__init__() self._document_elm = document_elm + self._part = part def __getitem__(self, key): if isinstance(key, slice): sectPr_lst = self._document_elm.sectPr_lst[key] - return [Section(sectPr) for sectPr in sectPr_lst] + return [Section(sectPr, self._part) for sectPr in sectPr_lst] sectPr = self._document_elm.sectPr_lst[key] - return Section(sectPr) + return Section(sectPr, self._part) def __iter__(self): for sectPr in self._document_elm.sectPr_lst: - yield Section(sectPr) + yield Section(sectPr, self._part) def __len__(self): return len(self._document_elm.sectPr_lst) @@ -37,9 +40,10 @@ class Section(object): """ Document section, providing access to section and page setup settings. """ - def __init__(self, sectPr): + def __init__(self, sectPr, part): super(Section, self).__init__() self._sectPr = sectPr + self._part = part @property def bottom_margin(self): @@ -53,6 +57,34 @@ def bottom_margin(self): def bottom_margin(self, value): self._sectPr.bottom_margin = value + @property + def different_first_page_header_footer(self): + return self._sectPr.titlePg_val + + @different_first_page_header_footer.setter + def different_first_page_header_footer(self, value): + self._sectPr.titlePg_val = value + + @property + def even_odd_header(self): + return Header(self._sectPr, self, WD_HEADER_FOOTER.EVEN_PAGE) + + @property + def even_odd_footer(self): + return Footer(self._sectPr, self, WD_HEADER_FOOTER.EVEN_PAGE) + + @property + def first_page_footer(self): + return Footer(self._sectPr, self, WD_HEADER_FOOTER.FIRST_PAGE) + + @property + def first_page_header(self): + return Header(self._sectPr, self, WD_HEADER_FOOTER.FIRST_PAGE) + + @property + def footer(self): + return Footer(self._sectPr, self, WD_HEADER_FOOTER.PRIMARY) + @property def footer_distance(self): """ @@ -80,6 +112,10 @@ def gutter(self): def gutter(self, value): self._sectPr.gutter = value + @property + def header(self): + return Header(self._sectPr, self, WD_HEADER_FOOTER.PRIMARY) + @property def header_distance(self): """ @@ -146,6 +182,10 @@ def page_width(self): def page_width(self, value): self._sectPr.page_width = value + @property + def part(self): + return self._part + @property def right_margin(self): """ @@ -172,6 +212,10 @@ def start_type(self): def start_type(self, value): self._sectPr.start_type = value + @property + def sectPr(self): + return self._sectPr + @property def top_margin(self): """ diff --git a/docx/settings.py b/docx/settings.py index 737146697..3ee83d1fa 100644 --- a/docx/settings.py +++ b/docx/settings.py @@ -18,3 +18,15 @@ class Settings(ElementProxy): """ __slots__ = () + + @property + def odd_and_even_pages_header_footer(self): + """ + |Length| object representing the bottom margin for all pages in this + section in English Metric Units. + """ + return self._element.evenAndOddHeaders_val + + @odd_and_even_pages_header_footer.setter + def odd_and_even_pages_header_footer(self, value): + self._element.evenAndOddHeaders_val = value \ No newline at end of file diff --git a/features/ftr-access-props.feature b/features/ftr-access-props.feature new file mode 100644 index 000000000..7edf25427 --- /dev/null +++ b/features/ftr-access-props.feature @@ -0,0 +1,14 @@ +# Created by Ondrej at 18/11/2018 +Feature: Access and change footer properties + In order to discover and modify document footer behaviors + As a developer using python-docx + I need a way to get and set the properties of a footer + + Scenario Outline: Get is linked to previous property + Given a footer having is_linked_to_previous property + Then document.sections[-1].footer.is_linked_to_previous is + + Examples: Even and odd footers settings values + | a-or-no | value | + | a | True | + | no | False | \ No newline at end of file diff --git a/features/ftr-link-unlink.feature b/features/ftr-link-unlink.feature new file mode 100644 index 000000000..8ef41ec97 --- /dev/null +++ b/features/ftr-link-unlink.feature @@ -0,0 +1,15 @@ +Feature: Link footer of section + In order link footer to section + As a developer using python-docx + I need a way to link footer in one step + + + Scenario Outline: Link and unlink footer to section + Given a document section having footer + When I set footer to + Then a section footer is_linked to previous is + + Examples: Footer values + | value | + | True | + | False | diff --git a/features/hdr-access-props.feature b/features/hdr-access-props.feature new file mode 100644 index 000000000..7e19b4a8e --- /dev/null +++ b/features/hdr-access-props.feature @@ -0,0 +1,14 @@ +# Created by Ondrej at 18/11/2018 +Feature: Access and change header properties + In order to discover and modify document header behaviors + As a developer using python-docx + I need a way to get and set the properties of a header + + Scenario Outline: Get is linked to previous property + Given a header having is_linked_to_previous property + Then document.sections[-1].header.is_linked_to_previous is + + Examples: Even and odd headers settings values + | a-or-no | value | + | a | True | + | no | False | \ No newline at end of file diff --git a/features/hdr-link-unlink.feature b/features/hdr-link-unlink.feature new file mode 100644 index 000000000..da69425ef --- /dev/null +++ b/features/hdr-link-unlink.feature @@ -0,0 +1,15 @@ +Feature: Link header of section + In order link header to section + As a developer using python-docx + I need a way to link header in one step + + + Scenario Outline: Link and unlink header to section + Given a document section having header + When I set header to + Then a section header is_linked to previous is + + Examples: Header values + | value | + | True | + | False | diff --git a/features/sct-access-hdr-ftr.feature b/features/sct-access-hdr-ftr.feature new file mode 100644 index 000000000..814428caa --- /dev/null +++ b/features/sct-access-hdr-ftr.feature @@ -0,0 +1,13 @@ +# Created by Ondrej at 18/11/2018 +Feature: Access all types of headers and footers (default, first, even) + In order to discover and modify section header access + As a developer using python-docx + I need a way to get headers of sections + + Scenario: Access header of all types + Given a section with all header types + Then header, first_page_header, even_odd_header is present in document.section + + Scenario: Access footer of all types + Given a section with all footer types + Then footer, first_page_footer, even_odd_footer is present in document.section \ No newline at end of file diff --git a/features/sct-section-props.feature b/features/sct-section-props.feature index 412d93f02..5c5356d45 100644 --- a/features/sct-section-props.feature +++ b/features/sct-section-props.feature @@ -6,7 +6,7 @@ Feature: Access and change section properties Scenario Outline: Get section start type Given a section having start type - Then the reported section start type is + Then the reported section start type is Examples: Section start types | start-type | @@ -19,8 +19,8 @@ Feature: Access and change section properties Scenario Outline: Set section start type Given a section having start type - When I set the section start type to - Then the reported section start type is + When I set the section start type to + Then the reported section start type is Examples: Section start types | initial-start-type | new-start-type | reported-start-type | @@ -31,21 +31,21 @@ Feature: Access and change section properties Scenario: Get section page size Given a section having known page dimension - Then the reported page width is 8.5 inches - And the reported page height is 11 inches + Then the reported page width is 8.5 inches + And the reported page height is 11 inches Scenario: Set section page size Given a section having known page dimension - When I set the section page width to 11 inches - And I set the section page height to 8.5 inches - Then the reported page width is 11 inches - And the reported page height is 8.5 inches + When I set the section page width to 11 inches + And I set the section page height to 8.5 inches + Then the reported page width is 11 inches + And the reported page height is 8.5 inches Scenario Outline: Get section orientation Given a section known to have orientation - Then the reported page orientation is + Then the reported page orientation is Examples: Section page orientations | orientation | reported-orientation | @@ -55,38 +55,48 @@ Feature: Access and change section properties Scenario Outline: Set section orientation Given a section known to have orientation - When I set the section orientation to - Then the reported page orientation is + When I set the section orientation to + Then the reported page orientation is Examples: Section page orientations - | initial-orientation | new-orientation | reported-orientation | - | portrait | WD_ORIENT.LANDSCAPE | WD_ORIENT.LANDSCAPE | - | landscape | WD_ORIENT.PORTRAIT | WD_ORIENT.PORTRAIT | - | landscape | None | WD_ORIENT.PORTRAIT | + | initial-orientation | new-orientation | reported-orientation | + | portrait | WD_ORIENT.LANDSCAPE | WD_ORIENT.LANDSCAPE | + | landscape | WD_ORIENT.PORTRAIT | WD_ORIENT.PORTRAIT | + | landscape | None | WD_ORIENT.PORTRAIT | Scenario: Get section page margins Given a section having known page margins - Then the reported left margin is 1.0 inches - And the reported right margin is 1.25 inches - And the reported top margin is 1.5 inches - And the reported bottom margin is 1.75 inches - And the reported gutter margin is 0.25 inches - And the reported header margin is 0.5 inches - And the reported footer margin is 0.75 inches + Then the reported left margin is 1.0 inches + And the reported right margin is 1.25 inches + And the reported top margin is 1.5 inches + And the reported bottom margin is 1.75 inches + And the reported gutter margin is 0.25 inches + And the reported header margin is 0.5 inches + And the reported footer margin is 0.75 inches Scenario Outline: Set section page margins Given a section having known page margins - When I set the margin to inches - Then the reported margin is inches + When I set the margin to inches + Then the reported margin is inches Examples: Section margin settings | margin-type | length | - | left | 1.0 | - | right | 1.25 | - | top | 0.75 | - | bottom | 1.5 | - | header | 0.25 | - | footer | 0.5 | - | gutter | 0.25 | + | left | 1.0 | + | right | 1.25 | + | top | 0.75 | + | bottom | 1.5 | + | header | 0.25 | + | footer | 0.5 | + | gutter | 0.25 | + + + Scenario Outline: Get settings different first page header footer + Given a section having different first page header footer setting + Then the section.different_first_page_header_footer is + + Examples: Section margin settings + | a-or-no | value | + | a | True | + | no | False | \ No newline at end of file diff --git a/features/steps/footer.py b/features/steps/footer.py new file mode 100644 index 000000000..3d836d95c --- /dev/null +++ b/features/steps/footer.py @@ -0,0 +1,53 @@ +# encoding: utf-8 + +""" +Step implementations for document settings-related features +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from behave import given, then, when + +from docx import Document +from docx.settings import Settings + +from helpers import test_docx, tri_state_vals + + +# given ==================================================== + +@given('a footer having a is_linked_to_previous property') +def given_a_document_having_a_footer_linked_to_previous(context): + context.document = Document(test_docx('doc-word-linked_footer')) + + +@given('a footer having no is_linked_to_previous property') +def given_a_document_having_no_footer_linked_to_previous(context): + context.document = Document(test_docx('doc-word-no-linked_footer')) + + +@given('a document section having footer') +def given_a_document_section_having_footer(context): + context.document = Document(test_docx('a_footer_of_all_types')) + +# when ===================================================== + + +@when('I set footer to {value}') +def when_i_link_footer_to_previous_section(context, value): + document, value = context.document, tri_state_vals[value] + document.sections[-1].footer.is_linked_to_previous = value + +# then ===================================================== + +@then('document.sections[-1].footer.is_linked_to_previous is {value}') +def then_document_settings_even_and_odd_footer_is_value(context, value): + document, expected_value = context.document, tri_state_vals[value] + assert document.sections[-1].footer.is_linked_to_previous is expected_value + +@then('a section footer is_linked to previous is {value}') +def then_section_is_linked_to_previous_with_value(context, value): + document, expected_value = context.document, tri_state_vals[value] + assert document.sections[-1].footer.is_linked_to_previous is expected_value \ No newline at end of file diff --git a/features/steps/header.py b/features/steps/header.py new file mode 100644 index 000000000..178fbc299 --- /dev/null +++ b/features/steps/header.py @@ -0,0 +1,52 @@ +# encoding: utf-8 + +""" +Step implementations for document settings-related features +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from behave import given, then, when + +from docx import Document +from docx.settings import Settings + +from helpers import test_docx, tri_state_vals + + +# given ==================================================== + +@given('a header having a is_linked_to_previous property') +def given_a_document_having_a_header_linked_to_previous(context): + context.document = Document(test_docx('doc-word-linked_header')) + + +@given('a header having no is_linked_to_previous property') +def given_a_document_having_no_header_linked_to_previous(context): + context.document = Document(test_docx('doc-word-no-linked_header')) + + +@given('a document section having header') +def given_a_document_section_having_header(context): + context.document = Document(test_docx('a_header_of_all_types')) + +# when ===================================================== + +@when('I set header to {value}') +def when_i_link_header_to_previous_section(context, value): + document, value = context.document, tri_state_vals[value] + document.sections[-1].header.is_linked_to_previous = value + +# then ===================================================== + +@then('document.sections[-1].header.is_linked_to_previous is {value}') +def then_document_settings_even_and_odd_header_is_value(context, value): + document, expected_value = context.document, tri_state_vals[value] + assert document.sections[-1].header.is_linked_to_previous is expected_value + +@then('a section header is_linked to previous is {value}') +def then_section_is_linked_to_previous_with_value(context, value): + document, expected_value = context.document, tri_state_vals[value] + assert document.sections[-1].header.is_linked_to_previous is expected_value diff --git a/features/steps/section.py b/features/steps/section.py index 496e0f17f..5c31dda4a 100644 --- a/features/steps/section.py +++ b/features/steps/section.py @@ -13,7 +13,7 @@ from docx.section import Section from docx.shared import Inches -from helpers import test_docx +from helpers import test_docx, tri_state_vals # given ==================================================== @@ -58,6 +58,28 @@ def given_a_section_having_known_orientation(context, orientation): document = Document(test_docx('sct-section-props')) context.section = document.sections[section_idx] +@given('a section having a different first page header footer setting') +def given_a_section_having_a_different_first_page_header_footer_setting(context): + document = Document(test_docx('section_having_a_different_first_page_header_footer')) + context.section = document.sections[0] + + +@given('a section having no different first page header footer setting') +def given_a_section_having_no_different_first_page_header_footer_setting(context): + document = Document(test_docx('section_having_no_different_first_page_header_footer')) + context.section = document.sections[0] + + +@given('a section with all header types') +def given_a_header_of_type(context): + document = Document(test_docx('a_header_of_all_types')) + context.section = document.sections[0] + + +@given('a section with all footer types') +def given_a_header_of_type(context): + document = Document(test_docx('a_footer_of_all_types')) + context.section = document.sections[0] # when ===================================================== @@ -182,3 +204,21 @@ def then_the_reported_section_start_type_is_type(context, start_type): 'ODD_PAGE': WD_SECTION.ODD_PAGE, }[start_type] assert context.section.start_type == expected_start_type + +@then('the section.different_first_page_header_footer is {value}') +def then_section_different_first_page_header_footer_is_value(context, value): + assert context.section.different_first_page_header_footer is tri_state_vals[value] + +@then('header, first_page_header, even_odd_header is present in document.section') +def then_header_of_type_is_present_in_document_section(context): + assert context.section.header.is_linked_to_previous is False + assert context.section.first_page_header.is_linked_to_previous is False + assert context.section.even_odd_header.is_linked_to_previous is False + + +@then('footer, first_page_footer, even_odd_footer is present in document.section') +def then_header_of_type_is_present_in_document_section(context): + assert context.section.footer.is_linked_to_previous is False + assert context.section.first_page_footer.is_linked_to_previous is False + assert context.section.even_odd_footer.is_linked_to_previous is False + diff --git a/features/steps/settings.py b/features/steps/settings.py index b818487b5..e8d1b519c 100644 --- a/features/steps/settings.py +++ b/features/steps/settings.py @@ -13,7 +13,7 @@ from docx import Document from docx.settings import Settings -from helpers import test_docx +from helpers import test_docx, tri_state_vals # given ==================================================== @@ -28,9 +28,23 @@ def given_a_document_having_no_settings_part(context): context.document = Document(test_docx('set-no-settings-part')) +@given('a settings having a even and odd headers settings') +def given_a_even_and_odd_headers_settings(context): + context.document = Document(test_docx('have_a_even_and_odd_header_settings')) + +@given('a settings having no even and odd headers settings') +def given_no_even_and_odd_headers_settings(context): + context.document = Document(test_docx('have_no_even_and_odd_header_settings')) + + # then ===================================================== @then('document.settings is a Settings object') def then_document_settings_is_a_Settings_object(context): document = context.document assert type(document.settings) is Settings + +@then('document.settings.odd_and_even_pages_header_footer is {value}') +def then_document_settings_even_and_odd_header_is_value(context, value): + document, expected_value = context.document, tri_state_vals[value] + assert document.settings.odd_and_even_pages_header_footer is expected_value diff --git a/features/steps/test_files/a_footer_of_all_types.docx b/features/steps/test_files/a_footer_of_all_types.docx new file mode 100644 index 000000000..d33f81210 Binary files /dev/null and b/features/steps/test_files/a_footer_of_all_types.docx differ diff --git a/features/steps/test_files/a_header_of_all_types.docx b/features/steps/test_files/a_header_of_all_types.docx new file mode 100644 index 000000000..e92d8c447 Binary files /dev/null and b/features/steps/test_files/a_header_of_all_types.docx differ diff --git a/features/steps/test_files/doc-word-linked_footer.docx b/features/steps/test_files/doc-word-linked_footer.docx new file mode 100644 index 000000000..2c6babf05 Binary files /dev/null and b/features/steps/test_files/doc-word-linked_footer.docx differ diff --git a/features/steps/test_files/doc-word-linked_header.docx b/features/steps/test_files/doc-word-linked_header.docx new file mode 100644 index 000000000..3eb2fb32a Binary files /dev/null and b/features/steps/test_files/doc-word-linked_header.docx differ diff --git a/features/steps/test_files/doc-word-no-linked_footer.docx b/features/steps/test_files/doc-word-no-linked_footer.docx new file mode 100644 index 000000000..28df67f04 Binary files /dev/null and b/features/steps/test_files/doc-word-no-linked_footer.docx differ diff --git a/features/steps/test_files/doc-word-no-linked_header.docx b/features/steps/test_files/doc-word-no-linked_header.docx new file mode 100644 index 000000000..f8c6480c3 Binary files /dev/null and b/features/steps/test_files/doc-word-no-linked_header.docx differ diff --git a/features/steps/test_files/have_a_even_and_odd_header_settings.docx b/features/steps/test_files/have_a_even_and_odd_header_settings.docx new file mode 100644 index 000000000..077ff1671 Binary files /dev/null and b/features/steps/test_files/have_a_even_and_odd_header_settings.docx differ diff --git a/features/steps/test_files/have_no_even_and_odd_header_settings.docx b/features/steps/test_files/have_no_even_and_odd_header_settings.docx new file mode 100644 index 000000000..b020eac69 Binary files /dev/null and b/features/steps/test_files/have_no_even_and_odd_header_settings.docx differ diff --git a/features/steps/test_files/section_having_a_different_first_page_header_footer.docx b/features/steps/test_files/section_having_a_different_first_page_header_footer.docx new file mode 100644 index 000000000..ee5c6a682 Binary files /dev/null and b/features/steps/test_files/section_having_a_different_first_page_header_footer.docx differ diff --git a/features/steps/test_files/section_having_no_different_first_page_header_footer.docx b/features/steps/test_files/section_having_no_different_first_page_header_footer.docx new file mode 100644 index 000000000..802278426 Binary files /dev/null and b/features/steps/test_files/section_having_no_different_first_page_header_footer.docx differ diff --git a/features/sts-settings-props.feature b/features/sts-settings-props.feature new file mode 100644 index 000000000..b21e0a513 --- /dev/null +++ b/features/sts-settings-props.feature @@ -0,0 +1,14 @@ +# Created by Ondrej at 18/11/2018 +Feature: Access and change settings properties + In order to discover and modify document settings behaviors + As a developer using python-docx + I need a way to get and set the properties of a settings + + Scenario Outline: Get even and odd headers settings + Given a settings having even and odd headers settings + Then document.settings.odd_and_even_pages_header_footer is + + Examples: Even and odd headers settings values + | a-or-no | value | + | a | True | + | no | False | \ No newline at end of file diff --git a/tests/oxml/test_settings.py b/tests/oxml/test_settings.py new file mode 100644 index 000000000..d470b500d --- /dev/null +++ b/tests/oxml/test_settings.py @@ -0,0 +1,48 @@ +# encoding: utf-8 + +""" +Test suite for the docx.oxml.styles module. +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +import pytest + +from ..unitutil.cxml import element, xml + + +class DescribeCT_Settings(object): + + def it_can_add_evenAndOddHeaders_val(self, add_evenAndOddHeaders_val_fixture): + settings, expected_xml = add_evenAndOddHeaders_val_fixture + settings.evenAndOddHeaders_val = True + assert settings.xml == expected_xml + + def it_can_remove_evenAndOddHeaders_val(self, remove_evenAndOddHeaders_val_fixture): + settings, expected_xml = remove_evenAndOddHeaders_val_fixture + settings.evenAndOddHeaders_val = False + assert settings.xml == expected_xml + + # fixtures ------------------------------------------------------- + + @pytest.fixture(params=[ + ('w:settings', + 'w:settings/w:evenAndOddHeaders'), + ]) + def add_evenAndOddHeaders_val_fixture(self, request): + settings_cxml, expected_cxml = request.param + settings = element(settings_cxml) + expected_xml = xml(expected_cxml) + return settings, expected_xml + + @pytest.fixture(params=[ + ('w:settings/w:evenAndOddHeaders{w:val=1}', + 'w:settings'), + ]) + def remove_evenAndOddHeaders_val_fixture(self, request): + settings_cxml, expected_cxml = request.param + settings = element(settings_cxml) + expected_xml = xml(expected_cxml) + return settings, expected_xml \ No newline at end of file diff --git a/tests/test_document.py b/tests/test_document.py index c1cb060ec..8fffe4d6b 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -73,7 +73,7 @@ def it_can_add_a_section(self, add_section_fixture): assert document.element.xml == expected_xml sectPr = document.element.xpath('w:body/w:sectPr')[0] - Section_.assert_called_once_with(sectPr) + Section_.assert_called_once_with(sectPr, document._part) assert section is section_ def it_can_add_a_table(self, add_table_fixture): @@ -105,7 +105,7 @@ def it_provides_access_to_its_paragraphs(self, paragraphs_fixture): def it_provides_access_to_its_sections(self, sections_fixture): document, Sections_, sections_ = sections_fixture sections = document.sections - Sections_.assert_called_once_with(document._element) + Sections_.assert_called_once_with(document._element, document._part) assert sections is sections_ def it_provides_access_to_its_settings(self, settings_fixture): diff --git a/tests/test_header_footer.py b/tests/test_header_footer.py new file mode 100644 index 000000000..d552dd0bc --- /dev/null +++ b/tests/test_header_footer.py @@ -0,0 +1,223 @@ +# encoding: utf-8 + +""" +Test suite for the docx.settings module +""" + +from __future__ import absolute_import, print_function, unicode_literals + +import pytest +from .unitutil.mock import ( + instance_mock, method_mock +) + +from docx.section import Section + +from docx.opc.packuri import PackURI +from docx.opc.constants import RELATIONSHIP_TYPE as RT +from docx.parts.document import DocumentPart +from .unitutil.cxml import element, xml +from docx.parts.header_footer import HeaderPart, FooterPart + + +class DescribeSettings(object): + + def it_knows_its_default_header(self, section_with_default_header_fixture): + assert section_with_default_header_fixture.header.is_linked_to_previous is False + assert section_with_default_header_fixture.first_page_header.is_linked_to_previous is True + assert section_with_default_header_fixture.even_odd_header.is_linked_to_previous is True + + def it_knows_its_first_header(self, section_with_first_header_fixture): + assert section_with_first_header_fixture.header.is_linked_to_previous is True + assert section_with_first_header_fixture.first_page_header.is_linked_to_previous is False + assert section_with_first_header_fixture.even_odd_header.is_linked_to_previous is True + + def it_knows_its_even_header(self, section_with_even_header_fixture): + assert section_with_even_header_fixture.header.is_linked_to_previous is True + assert section_with_even_header_fixture.first_page_header.is_linked_to_previous is True + assert section_with_even_header_fixture.even_odd_header.is_linked_to_previous is False + + def it_can_change_header_set_is_linked(self, section_with_default_header_can_change_is_linked_fixture): + section, value, expected_xml = section_with_default_header_can_change_is_linked_fixture + section.header.is_linked_to_previous = value + assert section._sectPr.xml == expected_xml + + def it_knows_its_default_footer(self, section_with_default_footer_fixture): + assert section_with_default_footer_fixture.footer.is_linked_to_previous is False + assert section_with_default_footer_fixture.first_page_footer.is_linked_to_previous is True + assert section_with_default_footer_fixture.even_odd_footer.is_linked_to_previous is True + + def it_knows_its_first_footer(self, section_with_first_footer_fixture): + assert section_with_first_footer_fixture.footer.is_linked_to_previous is True + assert section_with_first_footer_fixture.first_page_footer.is_linked_to_previous is False + assert section_with_first_footer_fixture.even_odd_footer.is_linked_to_previous is True + + def it_knows_its_even_footer(self, section_with_even_footer_fixture): + assert section_with_even_footer_fixture.footer.is_linked_to_previous is True + assert section_with_even_footer_fixture.first_page_footer.is_linked_to_previous is True + assert section_with_even_footer_fixture.even_odd_footer.is_linked_to_previous is False + + def it_can_change__footer_set_is_linked(self, section_with_default_footer_can_change_is_linked_fixture): + section, value, expected_xml = section_with_default_footer_can_change_is_linked_fixture + section.footer.is_linked_to_previous = value + assert section._sectPr.xml == expected_xml + + # fixtures ------------------------------------------------------- + + @pytest.fixture(params=[ + ('w:sectPr/(w:footerReference{w:type=default,r:id=rId1}, r:id)', True, + 'w:sectPr/r:id'), + ('w:sectPr/r:id', False, + 'w:sectPr/(r:id, w:footerReference{w:type=default,r:id=rId1})') + ]) + def section_with_default_footer_can_change_is_linked_fixture(self, request, document_part_, + footer_rel_): + footer_reltype, footer_part, rId = footer_rel_ + sectPr_cxml, value, expected_xml = request.param + document_part_.load_rel(footer_reltype, footer_part, rId) + section = Section(element(sectPr_cxml), document_part_) + return section, value, xml(expected_xml) + + @pytest.fixture(params=[ + 'w:sectPr/w:footerReference{w:type=default,r:id=rId1}/r:id' + ]) + def section_with_default_footer_fixture(self, request, document_part_, + footer_rel_): + footer_reltype, footer_part, rId = footer_rel_ + sectPr_cxml = request.param + document_part_.load_rel(footer_reltype, footer_part, rId) + section = Section(element(sectPr_cxml), document_part_) + return section + + @pytest.fixture(params=[ + 'w:sectPr/w:footerReference{w:type=first,r:id=rId1}/r:id' + ]) + def section_with_first_footer_fixture(self, request, document_part_, + footer_rel_): + footer_reltype, footer_part, rId = footer_rel_ + sectPr_cxml = request.param + document_part_.load_rel(footer_reltype, footer_part, rId) + section = Section(element(sectPr_cxml), document_part_) + return section + + @pytest.fixture(params=[ + 'w:sectPr/w:footerReference{w:type=even,r:id=rId1}/r:id' + ]) + def section_with_even_footer_fixture(self, request, document_part_, + footer_rel_): + footer_reltype, footer_part, rId = footer_rel_ + sectPr_cxml = request.param + document_part_.load_rel(footer_reltype, footer_part, rId) + section = Section(element(sectPr_cxml), document_part_) + return section + + @pytest.fixture(params=[ + ('w:sectPr/(w:headerReference{w:type=default,r:id=rId1}, r:id)', True, + 'w:sectPr/r:id'), + ('w:sectPr/r:id', False, + 'w:sectPr/(r:id, w:headerReference{w:type=default,r:id=rId1})') + ]) + def section_with_default_header_can_change_is_linked_fixture(self, request, document_part_, + header_rel_): + header_reltype, header_part, rId = header_rel_ + sectPr_cxml, value, expected_xml = request.param + document_part_.load_rel(header_reltype, header_part, rId) + section = Section(element(sectPr_cxml), document_part_) + return section, value, xml(expected_xml) + + @pytest.fixture(params=[ + 'w:sectPr/w:headerReference{w:type=default,r:id=rId1}/r:id' + ]) + def section_with_default_header_fixture(self, request, document_part_, + header_rel_): + header_reltype, header_part, rId = header_rel_ + sectPr_cxml = request.param + document_part_.load_rel(header_reltype, header_part, rId) + section = Section(element(sectPr_cxml), document_part_) + return section + + @pytest.fixture(params=[ + 'w:sectPr/w:headerReference{w:type=first,r:id=rId1}/r:id' + ]) + def section_with_first_header_fixture(self, request, document_part_, + header_rel_): + header_reltype, header_part, rId = header_rel_ + sectPr_cxml = request.param + document_part_.load_rel(header_reltype, header_part, rId) + section = Section(element(sectPr_cxml), document_part_) + return section + + + @pytest.fixture(params=[ + 'w:sectPr/w:headerReference{w:type=even,r:id=rId1}/r:id' + ]) + def section_with_even_header_fixture(self, request, document_part_, + header_rel_): + header_reltype, header_part, rId = header_rel_ + sectPr_cxml = request.param + document_part_.load_rel(header_reltype, header_part, rId) + section = Section(element(sectPr_cxml), document_part_) + return section + + @pytest.fixture + def header_rel_(self, header_rId_, header_reltype_, header_part_): + return header_reltype_, header_part_, header_rId_ + + + @pytest.fixture + def footer_rel_(self, footer_rId_, footer_reltype_, footer_part_): + return footer_reltype_, footer_part_, footer_rId_ + + @pytest.fixture + def footer_rId_(self): + return 'rId1' + + @pytest.fixture + def header_rId_(self): + return 'rId1' + + @pytest.fixture + def document_part_(self, request, document_partname_, _add_header_part_, _add_footer_part_): + document_part = instance_mock(request, DocumentPart) + document_part.add_header_part = _add_header_part_ + document_part.add_footer_part = _add_footer_part_ + return document_part + + @pytest.fixture + def _add_header_part_(self, request, header_rId_): + return method_mock( + request, DocumentPart, 'add_header_part', + return_value=header_rId_ + ) + + @pytest.fixture + def _add_footer_part_(self, request, footer_rId_): + return method_mock( + request, DocumentPart, 'add_footer_part', + return_value=footer_rId_ + ) + + + @pytest.fixture + def header_part_(self, request): + return instance_mock(request, HeaderPart) + + @pytest.fixture + def footer_part_(self, request): + return instance_mock(request, FooterPart) + + @pytest.fixture + def header_reltype_(self): + return RT.HEADER + + @pytest.fixture + def footer_reltype_(self): + return RT.FOOTER + + @pytest.fixture + def document_partname_(self, _baseURI): + return PackURI(_baseURI) + + @pytest.fixture + def _baseURI(self): + return '/baseURI' diff --git a/tests/test_section.py b/tests/test_section.py index a497aa727..170f428db 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -10,7 +10,14 @@ from docx.enum.section import WD_ORIENT, WD_SECTION from docx.section import Section, Sections +from docx.parts.document import DocumentPart from docx.shared import Inches +from .unitutil.mock import ( + instance_mock +) +from docx.opc.packuri import PackURI +from docx.opc.constants import RELATIONSHIP_TYPE as RT +from docx.parts.header_footer import HeaderPart, FooterPart from .unitutil.cxml import element, xml @@ -38,18 +45,18 @@ def it_can_access_its_Section_instances_by_index(self, index_fixture): # fixtures ------------------------------------------------------- @pytest.fixture - def index_fixture(self, document_elm): - sections = Sections(document_elm) + def index_fixture(self, document_elm, document_part_): + sections = Sections(document_elm, document_part_) return sections, [0, 1] @pytest.fixture - def iter_fixture(self, document_elm): - sections = Sections(document_elm) + def iter_fixture(self, document_elm, document_part_): + sections = Sections(document_elm, document_part_) return sections, 2 @pytest.fixture - def len_fixture(self, document_elm): - sections = Sections(document_elm) + def len_fixture(self, document_elm, document_part_): + sections = Sections(document_elm, document_part_) return sections, 2 # fixture components --------------------------------------------- @@ -58,6 +65,10 @@ def len_fixture(self, document_elm): def document_elm(self): return element('w:document/w:body/(w:p/w:pPr/w:sectPr, w:sectPr)') + @pytest.fixture + def document_part_(self, request): + return instance_mock(request, DocumentPart) + class DescribeSection(object): @@ -109,6 +120,36 @@ def it_can_change_its_page_margins(self, margins_set_fixture): setattr(section, margin_prop_name, new_value) assert section._sectPr.xml == expected_xml + def it_knows_its_default_footer(self, section_with_default_footer_fixture): + assert section_with_default_footer_fixture.footer.is_linked_to_previous is False + assert section_with_default_footer_fixture.first_page_footer.is_linked_to_previous is True + assert section_with_default_footer_fixture.even_odd_footer.is_linked_to_previous is True + + def it_knows_its_first_footer(self, section_with_first_footer_fixture): + assert section_with_first_footer_fixture.footer.is_linked_to_previous is True + assert section_with_first_footer_fixture.first_page_footer.is_linked_to_previous is False + assert section_with_first_footer_fixture.even_odd_footer.is_linked_to_previous is True + + def it_knows_its_even_footer(self, section_with_even_footer_fixture): + assert section_with_even_footer_fixture.footer.is_linked_to_previous is True + assert section_with_even_footer_fixture.first_page_footer.is_linked_to_previous is True + assert section_with_even_footer_fixture.even_odd_footer.is_linked_to_previous is False + + def it_have_different_first_page_header_footer(self, section_with_different_first_page_header_footer_fixture): + assert section_with_different_first_page_header_footer_fixture.different_first_page_header_footer is True + + def it_have_no_different_first_page_header_footer(self, section_without_different_first_page_header_footer_fixture): + assert section_without_different_first_page_header_footer_fixture.different_first_page_header_footer is False + + def it_can_change_different_first_page_header_footer_option(self, section_can_change_different_first_page_header_footer_fixture): + section, expected_xml = section_can_change_different_first_page_header_footer_fixture + if section.different_first_page_header_footer is False: + section.different_first_page_header_footer = True + assert section._sectPr.xml == expected_xml + else: + section.different_first_page_header_footer = False + assert section._sectPr.xml == expected_xml + # fixtures ------------------------------------------------------- @pytest.fixture(params=[ @@ -122,9 +163,9 @@ def it_can_change_its_page_margins(self, margins_set_fixture): ('w:sectPr/w:pgMar', 'left_margin', None), ('w:sectPr', 'top_margin', None), ]) - def margins_get_fixture(self, request): + def margins_get_fixture(self, request, document_part_): sectPr_cxml, margin_prop_name, expected_value = request.param - section = Section(element(sectPr_cxml)) + section = Section(element(sectPr_cxml), document_part_) return section, margin_prop_name, expected_value @pytest.fixture(params=[ @@ -146,9 +187,9 @@ def margins_get_fixture(self, request): ('w:sectPr/w:pgMar{w:top=-360}', 'top_margin', Inches(0.6), 'w:sectPr/w:pgMar{w:top=864}'), ]) - def margins_set_fixture(self, request): + def margins_set_fixture(self, request, document_part_): sectPr_cxml, property_name, new_value, expected_cxml = request.param - section = Section(element(sectPr_cxml)) + section = Section(element(sectPr_cxml), document_part_) expected_xml = xml(expected_cxml) return section, property_name, new_value, expected_xml @@ -158,9 +199,9 @@ def margins_set_fixture(self, request): ('w:sectPr/w:pgSz', WD_ORIENT.PORTRAIT), ('w:sectPr', WD_ORIENT.PORTRAIT), ]) - def orientation_get_fixture(self, request): + def orientation_get_fixture(self, request, document_part_): sectPr_cxml, expected_orientation = request.param - section = Section(element(sectPr_cxml)) + section = Section(element(sectPr_cxml), document_part_) return section, expected_orientation @pytest.fixture(params=[ @@ -168,9 +209,9 @@ def orientation_get_fixture(self, request): (WD_ORIENT.PORTRAIT, 'w:sectPr/w:pgSz'), (None, 'w:sectPr/w:pgSz'), ]) - def orientation_set_fixture(self, request): + def orientation_set_fixture(self, request, document_part_): new_orientation, expected_cxml = request.param - section = Section(element('w:sectPr')) + section = Section(element('w:sectPr'), document_part_) expected_xml = xml(expected_cxml) return section, new_orientation, expected_xml @@ -179,18 +220,18 @@ def orientation_set_fixture(self, request): ('w:sectPr/w:pgSz', None), ('w:sectPr', None), ]) - def page_height_get_fixture(self, request): + def page_height_get_fixture(self, request, document_part_): sectPr_cxml, expected_page_height = request.param - section = Section(element(sectPr_cxml)) + section = Section(element(sectPr_cxml), document_part_) return section, expected_page_height @pytest.fixture(params=[ (None, 'w:sectPr/w:pgSz'), (Inches(2), 'w:sectPr/w:pgSz{w:h=2880}'), ]) - def page_height_set_fixture(self, request): + def page_height_set_fixture(self, request, document_part_): new_page_height, expected_cxml = request.param - section = Section(element('w:sectPr')) + section = Section(element('w:sectPr'), document_part_) expected_xml = xml(expected_cxml) return section, new_page_height, expected_xml @@ -199,18 +240,18 @@ def page_height_set_fixture(self, request): ('w:sectPr/w:pgSz', None), ('w:sectPr', None), ]) - def page_width_get_fixture(self, request): + def page_width_get_fixture(self, request, document_part_): sectPr_cxml, expected_page_width = request.param - section = Section(element(sectPr_cxml)) + section = Section(element(sectPr_cxml), document_part_) return section, expected_page_width @pytest.fixture(params=[ (None, 'w:sectPr/w:pgSz'), (Inches(4), 'w:sectPr/w:pgSz{w:w=5760}'), ]) - def page_width_set_fixture(self, request): + def page_width_set_fixture(self, request, document_part_): new_page_width, expected_cxml = request.param - section = Section(element('w:sectPr')) + section = Section(element('w:sectPr'), document_part_) expected_xml = xml(expected_cxml) return section, new_page_width, expected_xml @@ -223,9 +264,9 @@ def page_width_set_fixture(self, request): ('w:sectPr/w:type{w:val=evenPage}', WD_SECTION.EVEN_PAGE), ('w:sectPr/w:type{w:val=nextColumn}', WD_SECTION.NEW_COLUMN), ]) - def start_type_get_fixture(self, request): + def start_type_get_fixture(self, request, document_part_): sectPr_cxml, expected_start_type = request.param - section = Section(element(sectPr_cxml)) + section = Section(element(sectPr_cxml), document_part_) return section, expected_start_type @pytest.fixture(params=[ @@ -242,8 +283,97 @@ def start_type_get_fixture(self, request): ('w:sectPr/w:type', WD_SECTION.NEW_COLUMN, 'w:sectPr/w:type{w:val=nextColumn}'), ]) - def start_type_set_fixture(self, request): + def start_type_set_fixture(self, request, document_part_): initial_cxml, new_start_type, expected_cxml = request.param - section = Section(element(initial_cxml)) + section = Section(element(initial_cxml), document_part_) expected_xml = xml(expected_cxml) return section, new_start_type, expected_xml + + @pytest.fixture(params=[ + 'w:sectPr/w:footerReference{w:type=default,r:id=rId1}/r:id' + ]) + def section_with_default_footer_fixture(self, request, document_part_, + footer_rel_): + footer_reltype, footer_part, rId = footer_rel_ + sectPr_cxml = request.param + document_part_.load_rel(footer_reltype, footer_part, rId) + section = Section(element(sectPr_cxml), document_part_) + return section + + @pytest.fixture(params=[ + 'w:sectPr/w:footerReference{w:type=first,r:id=rId1}/r:id' + ]) + def section_with_first_footer_fixture(self, request, document_part_, + footer_rel_): + footer_reltype, footer_part, rId = footer_rel_ + sectPr_cxml = request.param + document_part_.load_rel(footer_reltype, footer_part, rId) + section = Section(element(sectPr_cxml), document_part_) + return section + + @pytest.fixture(params=[ + 'w:sectPr/w:footerReference{w:type=even,r:id=rId1}/r:id' + ]) + def section_with_even_footer_fixture(self, request, document_part_, + footer_rel_): + footer_reltype, footer_part, rId = footer_rel_ + sectPr_cxml = request.param + document_part_.load_rel(footer_reltype, footer_part, rId) + section = Section(element(sectPr_cxml), document_part_) + return section + + @pytest.fixture(params=[ + 'w:sectPr/w:titlePg' + ]) + def section_with_different_first_page_header_footer_fixture(self, request, document_part_): + sectPr_cxml= request.param + return Section(element(sectPr_cxml), document_part_) + + @pytest.fixture(params=[ + 'w:sectPr' + ]) + def section_without_different_first_page_header_footer_fixture(self, request, document_part_): + sectPr_cxml = request.param + return Section(element(sectPr_cxml), document_part_) + + @pytest.fixture(params=[ + ('w:sectPr/w:titlePg', 'w:sectPr'), + ('w:sectPr', 'w:sectPr/w:titlePg') + ]) + def section_can_change_different_first_page_header_footer_fixture(self, request, document_part_): + sectPr_cxml, expected_cxml = request.param + return Section(element(sectPr_cxml), document_part_), xml(expected_cxml) + + + @pytest.fixture + def footer_rel_(self, footer_rId_, footer_reltype_, footer_part_): + return footer_reltype_, footer_part_, footer_rId_ + + @pytest.fixture + def footer_rId_(self): + return 'rId2' + + @pytest.fixture + def document_part_(self, request, document_partname_): + return instance_mock(request, DocumentPart) + + @pytest.fixture + def footer_part_(self, request): + return instance_mock(request, FooterPart) + + @pytest.fixture + def footer_reltype_(self): + return RT.FOOTER + + @pytest.fixture + def footer_reltype_(self): + return RT.FOOTER + + @pytest.fixture + def document_partname_(self, _baseURI): + return PackURI(_baseURI) + + @pytest.fixture + def _baseURI(self): + return '/baseURI' + diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 000000000..0c4c5a3ef --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,48 @@ +# encoding: utf-8 + +""" +Test suite for the docx.settings module +""" + +from __future__ import absolute_import, print_function, unicode_literals + +import pytest + +from docx.settings import Settings + +from .unitutil.cxml import element, xml + + +class DescribeSettings(object): + + def it_can_remove_odd_and_even_pages_header_footer(self, remove_even_and_odd_headers_fixture): + settings, expected_xml = remove_even_and_odd_headers_fixture + settings.odd_and_even_pages_header_footer = False + assert settings._element.xml == expected_xml + + def it_can_add_odd_and_even_pages_header_footer(self, add_even_and_odd_headers_fixture): + settings, expected_xml = add_even_and_odd_headers_fixture + settings.odd_and_even_pages_header_footer = True + assert settings._element.xml == expected_xml + + # fixtures ------------------------------------------------------- + + @pytest.fixture(params=[ + ('w:settings/w:evenAndOddHeaders{w:val=1}', + 'w:settings'), + ]) + def remove_even_and_odd_headers_fixture(self, request): + initial_cxml, expected_cxml = request.param + settings = Settings(element(initial_cxml)) + expected_xml = xml(expected_cxml) + return settings, expected_xml + + @pytest.fixture(params=[ + ('w:settings', + 'w:settings/w:evenAndOddHeaders'), + ]) + def add_even_and_odd_headers_fixture(self, request): + initial_cxml, expected_cxml = request.param + settings = Settings(element(initial_cxml)) + expected_xml = xml(expected_cxml) + return settings, expected_xml \ No newline at end of file