Exporting Plone content to Excel#

I have customers who want to export some contents of a Plone site to Excel. Since this is a database-like site, there are different areas resulting in different Excel files.

OpenPyXL#

I found the OpenPyXL library for Python very useful as a base for my export. With OpenPyXL you create an Workbook, add Sheets and fill them with data.

Base Class#

Because I need this in different parts of my project, I created a base class for the export. It contains the logic needed to create the Excel file and to send it to the browser. The actual data is provided by the sheet_info_factory method generator. It needs to be implemented by the subclass and returns a generator of “sheets”: each a dict with the sheet name, the header and the rows.

from Products.Five.browser import BrowserView
from plone import api
from tempfile import NamedTemporaryFile

import openpyxml

class ExcelGenericView(BrowserView):
    def sheet_info_factory(self):
        """Generator returning dicts with:
        - name: callable returning string
        - header: callable expecting view as parameter returning a list of strings (first row)
        - rows: callable expecting view as parameter returning an iterable with lists of data
        """
        raise NotImplemented("implement me")

    @property
    def fileprefix(self):
        return "export"

    @property
    def filename(self):
        return f"{self.fileprefix}-{self.now():%Y-%m-%d %H:%M}.xslx"

    def __call__(self):
        workbook = openpyxl.Workbook(write_only=True)
        workbook.iso_dates = True
        for sheet_info in self.sheet_info_factory():
            sheet = workbook.create_sheet(sheet_info["name"]())
            sheet.append(sheet_info["header"](self))
            for row in sheet_info["rows"](self):
                sheet.append(row)
        self.request.response.setHeader(
            "Content-Type",
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
        )
        self.request.response.setHeader(
            "Content-Disposition", f"attachment;filename={self.filename}"
        )
        with NamedTemporaryFile() as tmp:
            workbook.save(tmp.name)
            tmp.seek(0)
            self.request.response.write(tmp.read())

Example subclasses#

Here is a simple example of a subclass for an export of items in a folder with title, description and type:

class ExcelExportView(ExcelGenericView):
    def sheet_info_factory(self):
        yield {
            "name": lambda: f"Folder {self.context.id}",
            "header": lambda view: ["Title", "Description", "Type"]
            "rows": lambda view: (
                [brain.Title, brain.Description, brain.portal_type]
                for item in api.content.find(context=self.context, depth=1)
            ),
        }

Here is an example from a project:

def kurs_header(view):
    return [
        "Nachname",
        "Vorname",
        "Titel (vor)",
        "Titel (nach)",
        "Strasse",
        "PLZ",
        "Ort",
        "Land",
        "E-Mail",
        "Geburtsdatum",
        "Newsletter",
        "Bezahlt",
        "Teilgenommen",
        "Prüfungsanmeldung",
        "Stripe Invoice Status",
        "Stripe Customer ID",
        "Stripe Invoice ID",
        "Stripe Invoice URL",
    ]


def kurs_rows(view):
    with api.env.adopt_roles(["Manager"]):
        for anmeldung_ref in api.relation.get(target=view.context, relationship="kurs"):
            anmeldung = anmeldung_ref.from_object
            tn = aq_parent(aq_inner(anmeldung))
            yield [
                tn.nachname,
                tn.vorname,
                tn.titel_vorn,
                tn.titel_hinten,
                tn.strasse,
                tn.plz,
                tn.ort,
                tn.land,
                anmeldung.email,
                tn.geburtsdatum.strftime("%d.%m.%Y"),
                "Ja" if anmeldung.newsletter else "Nein",
                (
                    "Bar"
                    if anmeldung.barzahlung
                    and anmeldung.stripe_status not in ("paid", "void")
                    else (
                        "Bezahlt"
                        if anmeldung.stripe_status == "paid"
                        else (
                            "Storniert"
                            if anmeldung.stripe_status == "void"
                            else "Unbezahlt"
                        )
                    )
                ),
                anmeldung.teilgenommen,
                "Ja" if anmeldung.pruefungsanmeldung else "Nein",
                anmeldung.stripe_status,
                tn.stripe_customer,
                anmeldung.stripe_invoice_number,
                anmeldung.stripe_hosted_invoice_url,
            ]


class ExcelKursExportView(ExcelGenericView):
    @property
    def fileprefix(self):
        basicadapter = IKursBasicBehavior(self.context)
        return f"kurs-{basicadapter.title.replace(' ', '')}"

    def sheet_info_factory(self):
        yield {
            "name": lambda: "Kursteilnehmer_innen",
            "header": kurs_header,
            "rows": kurs_rows,
        }

This can be implemented much more dynamic, as the header and rows can be constructed from the context and the view if needed. But you get the idea.