- Create Economist SubmissionTracking package correctly: * mainArticle = full blog post content * coverLetter = 216-word SIR— letter * Links to blog post via blogPostId - Archive 'Letter to The Economist' from blog posts (it's the cover letter) - Fix date display on article cards (use published_at) - Target publication already displaying via blue badge Database changes: - Make blogPostId optional in SubmissionTracking model - Economist package ID: 68fa85ae49d4900e7f2ecd83 - Le Monde package ID: 68fa2abd2e6acd5691932150 Next: Enhanced modal with tabs, validation, export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
188 lines
5.9 KiB
Python
188 lines
5.9 KiB
Python
"""OPC-local oxml module to handle OPC-local concerns like relationship parsing."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Callable, cast
|
|
|
|
from lxml import etree
|
|
|
|
from pptx.opc.constants import NAMESPACE as NS
|
|
from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM
|
|
from pptx.oxml import parse_xml, register_element_cls
|
|
from pptx.oxml.simpletypes import (
|
|
ST_ContentType,
|
|
ST_Extension,
|
|
ST_TargetMode,
|
|
XsdAnyUri,
|
|
XsdId,
|
|
)
|
|
from pptx.oxml.xmlchemy import (
|
|
BaseOxmlElement,
|
|
OptionalAttribute,
|
|
RequiredAttribute,
|
|
ZeroOrMore,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from pptx.opc.packuri import PackURI
|
|
|
|
nsmap = {
|
|
"ct": NS.OPC_CONTENT_TYPES,
|
|
"pr": NS.OPC_RELATIONSHIPS,
|
|
"r": NS.OFC_RELATIONSHIPS,
|
|
}
|
|
|
|
|
|
def oxml_to_encoded_bytes(
|
|
element: BaseOxmlElement,
|
|
encoding: str = "utf-8",
|
|
pretty_print: bool = False,
|
|
standalone: bool | None = None,
|
|
) -> bytes:
|
|
return etree.tostring(
|
|
element, encoding=encoding, pretty_print=pretty_print, standalone=standalone
|
|
)
|
|
|
|
|
|
def oxml_tostring(
|
|
elm: BaseOxmlElement,
|
|
encoding: str | None = None,
|
|
pretty_print: bool = False,
|
|
standalone: bool | None = None,
|
|
):
|
|
return etree.tostring(elm, encoding=encoding, pretty_print=pretty_print, standalone=standalone)
|
|
|
|
|
|
def serialize_part_xml(part_elm: BaseOxmlElement) -> bytes:
|
|
"""Produce XML-file bytes for `part_elm`, suitable for writing directly to a `.xml` file.
|
|
|
|
Includes XML-declaration header.
|
|
"""
|
|
return etree.tostring(part_elm, encoding="UTF-8", standalone=True)
|
|
|
|
|
|
class CT_Default(BaseOxmlElement):
|
|
"""`<Default>` element.
|
|
|
|
Specifies the default content type to be applied to a part with the specified extension.
|
|
"""
|
|
|
|
extension: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
|
|
"Extension", ST_Extension
|
|
)
|
|
contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
|
|
"ContentType", ST_ContentType
|
|
)
|
|
|
|
|
|
class CT_Override(BaseOxmlElement):
|
|
"""`<Override>` element.
|
|
|
|
Specifies the content type to be applied for a part with the specified partname.
|
|
"""
|
|
|
|
partName: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
|
|
"PartName", XsdAnyUri
|
|
)
|
|
contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
|
|
"ContentType", ST_ContentType
|
|
)
|
|
|
|
|
|
class CT_Relationship(BaseOxmlElement):
|
|
"""`<Relationship>` element.
|
|
|
|
Represents a single relationship from a source to a target part.
|
|
"""
|
|
|
|
rId: str = RequiredAttribute("Id", XsdId) # pyright: ignore[reportAssignmentType]
|
|
reltype: str = RequiredAttribute("Type", XsdAnyUri) # pyright: ignore[reportAssignmentType]
|
|
target_ref: str = RequiredAttribute( # pyright: ignore[reportAssignmentType]
|
|
"Target", XsdAnyUri
|
|
)
|
|
targetMode: str = OptionalAttribute( # pyright: ignore[reportAssignmentType]
|
|
"TargetMode", ST_TargetMode, default=RTM.INTERNAL
|
|
)
|
|
|
|
@classmethod
|
|
def new(
|
|
cls, rId: str, reltype: str, target_ref: str, target_mode: str = RTM.INTERNAL
|
|
) -> CT_Relationship:
|
|
"""Return a new `<Relationship>` element.
|
|
|
|
`target_ref` is either a partname or a URI.
|
|
"""
|
|
relationship = cast(CT_Relationship, parse_xml(f'<Relationship xmlns="{nsmap["pr"]}"/>'))
|
|
relationship.rId = rId
|
|
relationship.reltype = reltype
|
|
relationship.target_ref = target_ref
|
|
relationship.targetMode = target_mode
|
|
return relationship
|
|
|
|
|
|
class CT_Relationships(BaseOxmlElement):
|
|
"""`<Relationships>` element, the root element in a .rels file."""
|
|
|
|
relationship_lst: list[CT_Relationship]
|
|
_insert_relationship: Callable[[CT_Relationship], CT_Relationship]
|
|
|
|
relationship = ZeroOrMore("pr:Relationship")
|
|
|
|
def add_rel(
|
|
self, rId: str, reltype: str, target: str, is_external: bool = False
|
|
) -> CT_Relationship:
|
|
"""Add a child `<Relationship>` element with attributes set as specified."""
|
|
target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL
|
|
relationship = CT_Relationship.new(rId, reltype, target, target_mode)
|
|
return self._insert_relationship(relationship)
|
|
|
|
@classmethod
|
|
def new(cls) -> CT_Relationships:
|
|
"""Return a new `<Relationships>` element."""
|
|
return cast(CT_Relationships, parse_xml(f'<Relationships xmlns="{nsmap["pr"]}"/>'))
|
|
|
|
@property
|
|
def xml_file_bytes(self) -> bytes:
|
|
"""Return XML bytes, with XML-declaration, for this `<Relationships>` element.
|
|
|
|
Suitable for saving in a .rels stream, not pretty printed and with an XML declaration at
|
|
the top.
|
|
"""
|
|
return oxml_to_encoded_bytes(self, encoding="UTF-8", standalone=True)
|
|
|
|
|
|
class CT_Types(BaseOxmlElement):
|
|
"""`<Types>` element.
|
|
|
|
The container element for Default and Override elements in [Content_Types].xml.
|
|
"""
|
|
|
|
default_lst: list[CT_Default]
|
|
override_lst: list[CT_Override]
|
|
|
|
_add_default: Callable[..., CT_Default]
|
|
_add_override: Callable[..., CT_Override]
|
|
|
|
default = ZeroOrMore("ct:Default")
|
|
override = ZeroOrMore("ct:Override")
|
|
|
|
def add_default(self, ext: str, content_type: str) -> CT_Default:
|
|
"""Add a child `<Default>` element with attributes set to parameter values."""
|
|
return self._add_default(extension=ext, contentType=content_type)
|
|
|
|
def add_override(self, partname: PackURI, content_type: str) -> CT_Override:
|
|
"""Add a child `<Override>` element with attributes set to parameter values."""
|
|
return self._add_override(partName=partname, contentType=content_type)
|
|
|
|
@classmethod
|
|
def new(cls) -> CT_Types:
|
|
"""Return a new `<Types>` element."""
|
|
return cast(CT_Types, parse_xml(f'<Types xmlns="{nsmap["ct"]}"/>'))
|
|
|
|
|
|
register_element_cls("ct:Default", CT_Default)
|
|
register_element_cls("ct:Override", CT_Override)
|
|
register_element_cls("ct:Types", CT_Types)
|
|
|
|
register_element_cls("pr:Relationship", CT_Relationship)
|
|
register_element_cls("pr:Relationships", CT_Relationships)
|