Skip to content

Commit 2e83480

Browse files
author
robertbrodie
committed
fix: add notesMasterIdLst element when creating notes master
When python-pptx creates a notes master on first access to slide.notes_slide, it correctly creates the NotesMasterPart and adds the relationship in the .rels file, but omits the required <p:notesMasterIdLst> element from presentation.xml. This causes Apple Keynote (and potentially other consumers) to fail to recognize the notes master, breaking speaker notes import. This fix: - Adds CT_NotesMasterIdList and CT_NotesMasterIdListEntry element classes with proper ZeroOrOne/RequiredAttribute declarations - Registers both element classes in the oxml element class lookup - Adds a ZeroOrOne declaration for notesMasterIdLst on CT_Presentation with correct successor sequence - Updates PresentationPart.notes_master_part to populate the notesMasterIdLst element with the relationship ID after creating the notes master relationship Closes #1051
1 parent 278b47b commit 2e83480

4 files changed

Lines changed: 51 additions & 2 deletions

File tree

src/pptx/oxml/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,8 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
273273

274274

275275
from pptx.oxml.presentation import ( # noqa: E402
276+
CT_NotesMasterIdList,
277+
CT_NotesMasterIdListEntry,
276278
CT_Presentation,
277279
CT_SlideId,
278280
CT_SlideIdList,
@@ -281,6 +283,8 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
281283
CT_SlideSize,
282284
)
283285

286+
register_element_cls("p:notesMasterId", CT_NotesMasterIdListEntry)
287+
register_element_cls("p:notesMasterIdLst", CT_NotesMasterIdList)
284288
register_element_cls("p:presentation", CT_Presentation)
285289
register_element_cls("p:sldId", CT_SlideId)
286290
register_element_cls("p:sldIdLst", CT_SlideIdList)

src/pptx/oxml/presentation.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class CT_Presentation(BaseOxmlElement):
1717
get_or_add_sldSz: Callable[[], CT_SlideSize]
1818
get_or_add_sldIdLst: Callable[[], CT_SlideIdList]
1919
get_or_add_sldMasterIdLst: Callable[[], CT_SlideMasterIdList]
20+
get_or_add_notesMasterIdLst: Callable[[], CT_NotesMasterIdList]
2021

2122
sldMasterIdLst: CT_SlideMasterIdList | None = (
2223
ZeroOrOne( # pyright: ignore[reportAssignmentType]
@@ -30,6 +31,17 @@ class CT_Presentation(BaseOxmlElement):
3031
),
3132
)
3233
)
34+
notesMasterIdLst: CT_NotesMasterIdList | None = (
35+
ZeroOrOne( # pyright: ignore[reportAssignmentType]
36+
"p:notesMasterIdLst",
37+
successors=(
38+
"p:handoutMasterIdLst",
39+
"p:sldIdLst",
40+
"p:sldSz",
41+
"p:notesSz",
42+
),
43+
)
44+
)
3345
sldIdLst: CT_SlideIdList | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
3446
"p:sldIdLst", successors=("p:sldSz", "p:notesSz")
3547
)
@@ -115,6 +127,30 @@ class CT_SlideMasterIdListEntry(BaseOxmlElement):
115127
rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
116128

117129

130+
class CT_NotesMasterIdList(BaseOxmlElement):
131+
"""`p:notesMasterIdLst` element.
132+
133+
Child of `p:presentation` containing a reference to the notes master that belongs to the
134+
presentation.
135+
"""
136+
137+
_add_notesMasterId: Callable[..., CT_NotesMasterIdListEntry]
138+
notesMasterId = ZeroOrOne("p:notesMasterId")
139+
140+
def add_notesMasterId(self, rId: str) -> CT_NotesMasterIdListEntry:
141+
"""Create and return a new `p:notesMasterId` child element with r:id set to `rId`."""
142+
return self._add_notesMasterId(rId=rId)
143+
144+
145+
class CT_NotesMasterIdListEntry(BaseOxmlElement):
146+
"""`p:notesMasterId` element.
147+
148+
Child of `p:notesMasterIdLst` containing an `rId` reference to the notes master part.
149+
"""
150+
151+
rId: str = RequiredAttribute("r:id", XsdString) # pyright: ignore[reportAssignmentType]
152+
153+
118154
class CT_SlideSize(BaseOxmlElement):
119155
"""`p:sldSz` element.
120156

src/pptx/parts/presentation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ def notes_master_part(self) -> NotesMasterPart:
7272
return self.part_related_by(RT.NOTES_MASTER)
7373
except KeyError:
7474
notes_master_part = NotesMasterPart.create_default(self.package)
75-
self.relate_to(notes_master_part, RT.NOTES_MASTER)
75+
rId = self.relate_to(notes_master_part, RT.NOTES_MASTER)
76+
self._element.get_or_add_notesMasterIdLst().add_notesMasterId(rId)
7677
return notes_master_part
7778

7879
@lazyproperty

tests/parts/test_presentation.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,21 @@ def but_it_adds_a_notes_master_part_when_needed(
6565
NotesMasterPart_ = class_mock(request, "pptx.parts.presentation.NotesMasterPart")
6666
NotesMasterPart_.create_default.return_value = notes_master_part_
6767
part_related_by_.side_effect = KeyError
68-
prs_part = PresentationPart(None, None, package_, None)
68+
relate_to_.return_value = "rId42"
69+
prs_elm = element("p:presentation/p:sldMasterIdLst")
70+
prs_part = PresentationPart(None, None, package_, prs_elm)
6971

7072
notes_master_part = prs_part.notes_master_part
7173

7274
NotesMasterPart_.create_default.assert_called_once_with(package_)
7375
relate_to_.assert_called_once_with(prs_part, notes_master_part_, RT.NOTES_MASTER)
7476
assert notes_master_part is notes_master_part_
77+
# --- notesMasterIdLst element was added to presentation.xml ---
78+
notesMasterIdLst = prs_elm.notesMasterIdLst
79+
assert notesMasterIdLst is not None
80+
notesMasterId = notesMasterIdLst.notesMasterId
81+
assert notesMasterId is not None
82+
assert notesMasterId.rId == "rId42"
7583

7684
def it_provides_access_to_its_notes_master(self, request, notes_master_part_):
7785
notes_master_ = instance_mock(request, NotesMaster)

0 commit comments

Comments
 (0)