Module control.publish

Expand source code Browse git
from traceback import format_exception
from .mongo import Mongo

from .files import (
    dirContents,
    dirMake,
    dirRemove,
    dirCopy,
    fileCopy,
    fileExists,
    fileRemove,
    writeJson,
)
from .generic import AttrDict, deepdict, isonow
from .precheck import Precheck as PrecheckCls
from .static import Static as StaticCls


class Publish:
    def __init__(
        self, Settings, Messages, Viewers, Mongo: Mongo, Content, Tailwind, Handlebars
    ):
        """Publishing content as static pages.

        It is instantiated by a singleton object.

        Parameters
        ----------
        Settings: AttrDict
            App-wide configuration data obtained from
            `control.config.Config.Settings`.
        Messages: object
            Singleton instance of `control.messages.Messages`.
        Mongo: object
            Singleton instance of `control.mongo.Mongo`.
        Tailwind: object
            Singleton instance of `control.tailwind.Tailwind`.
        """
        self.Settings = Settings
        self.Messages = Messages
        self.Viewers = Viewers
        self.Mongo = Mongo
        self.Content = Content
        self.Tailwind = Tailwind
        self.Handlebars = Handlebars
        Messages.debugAdd(self)
        Content.addPublish(self)

        self.Precheck = (
            None
            if Content is None
            else PrecheckCls(Settings, Messages, Content, Viewers)
        )

    def getPubNums(self, project, edition):
        """Determine project and edition publication numbers.

        Those numbers are inside the project and edition records in the database
        if the project/edition has been published before;
        otherwise we pick an unused number for the project;
        and within the project an unused edition number.

        When we look for those numbers, we look in the database records,
        and we look on the filesystem, and we take the number one higher than
        the maximum number used in the database and on the file system.

        **N.B.:** This practice has the flaw that numbers used for publishing projects
        and editions may get reused when you unpublish and/or delete published editions.

        We store the used publishing numbers for projects and editions in the
        `site` record, as a dictionary named pubNums, keyed by project numbers, and
        valued by the maximum edition pubNum for that project.

        The `pubNums` dictionary will never lose keys, and its values will
        never be lowered when projects or editions are removed.

        So new projects and editions always get numbers that have never been used before
        for publishing.

        We also copy the `pubNum` field of a project or edition into the field
        `pubNumLast` when we unpublish such an item, thereby nulling the `pubNum` field.
        When we republish the item, its `pubNumLast` is restored to the `pubNum`
        field.
        """
        Mongo = self.Mongo
        Settings = self.Settings
        pubModeDir = Settings.pubModeDir
        projectDir = f"{pubModeDir}/project"

        pPubNumLast = project.pubNum
        ePubNumLast = edition.pubNum

        def getNum(kind, pubNumLast, condition, itemsDir):
            if pubNumLast is None:
                itemsDb = Mongo.getList(kind, condition)
                nDb = len(itemsDb)
                maxDb = 0 if nDb == 0 else max(r.pubNum or 0 for r in itemsDb)

                itemsFile = [int(n) for n in dirContents(itemsDir)[1] if n.isdecimal()]
                nFile = len(itemsFile)

                maxFile = 0 if nFile == 0 else max(itemsFile)
                pubNum = max((maxDb, maxFile)) + 1
            else:
                pubNum = pubNumLast

            return pubNum

        if pPubNumLast is None:
            # because there is only 1 site in the database,
            # we can retrieve it without paramaters
            site = Mongo.getRecord("site", {})

            if "publishedProjectCount" in site:
                pPubNum = site["publishedProjectCount"] + 1
            else:
                # Determine project publish number the old way,
                # to make sure no two project have the same pubNum
                pPubNum = getNum("project", pPubNumLast, {}, projectDir)

            Mongo.updateRecord("site", {}, {"publishedProjectCount": pPubNum})

        else:
            pPubNum = pPubNumLast

        if ePubNumLast is None:
            if "publishedEditionCount" in project:
                ePubNum = project["publishedEditionCount"] + 1
            else:
                # use get num for existing projects
                kind = "edition"
                pubNumLast = ePubNumLast
                condition = dict(projectId=project._id)
                itemsDir = f"{projectDir}/{pPubNum}/edition"

                ePubNum = getNum(kind, pubNumLast, condition, itemsDir)

            Mongo.updateRecord(
                "project", dict(_id=project._id), {"publishedEditionCount": ePubNum}
            )
        else:
            ePubNum = ePubNumLast

        return (pPubNum, ePubNum)

    def generatePages(self, pPubNum, ePubNum):
        Settings = self.Settings
        Messages = self.Messages
        Viewers = self.Viewers
        Content = self.Content
        Tailwind = self.Tailwind
        Handlebars = self.Handlebars

        site = Content.relevant()[-1]
        featured = Content.getValue("site", site, "featured", manner="logical")

        Static = StaticCls(Settings, Messages, Content, Viewers, Tailwind, Handlebars)

        try:
            self.addSiteFiles(site)
            good = Static.genPages(pPubNum, ePubNum, featured=featured)

        except Exception as e1:
            Messages.error(logmsg="".join(format_exception(e1)))
            good = False

        return good

    def updateEdition(self, site, project, edition, action, force=False, again=False):
        Settings = self.Settings
        Messages = self.Messages
        Mongo = self.Mongo
        Content = self.Content
        Precheck = self.Precheck

        if action not in {"add", "remove"}:
            Messages.error(msg=f"unknown action {action}")
            return

        processing = site.processing

        # quit early if another processing action is taking place

        if processing:
            Messages.warning(
                msg="Site is being published. Try again a minute later",
                logmsg=(
                    f"Refusing to publish {project._id}/{edition._id} "
                    "while site is being republished"
                ),
            )
            return

        # put a flag in the database that the site is publishing
        # this will prevent other publishing actions while this action is running

        last = site.lastPublished
        now = isonow()

        Mongo.updateRecord(
            "site", dict(_id=site._id), dict(processing=True, lastPublished=now)
        )

        # make sure that if something fails, the publishing flag will be reset

        pubModeDir = Settings.pubModeDir
        projectDir = f"{pubModeDir}/project"

        orig = dict(
            project=AttrDict(
                condition=dict(_id=project._id),
                updates={k: v for (k, v) in project.items() if k != "_id"},
            ),
            edition=AttrDict(
                condition=dict(_id=edition._id),
                updates={k: v for (k, v) in edition.items() if k != "_id"},
            ),
        )

        def restore(table):
            info = orig[table]
            Mongo.updateRecord(table, info.condition, info.updates)

        # quit early, without doing anything, if the action is not applicable

        good = True

        if action == "add":
            thisGood = Precheck.checkEdition(site, project, edition._id, edition)

            if thisGood:
                Messages.info("Edition validation OK")
            else:
                Messages.info("Edition validation not OK")
                good = False

                if force:
                    Messages.info("Continuing nevertheless")
                    good = True

            (pPubNum, ePubNum) = self.getPubNums(project, edition)

            if pPubNum is None:
                Messages.error(
                    msg="Could not find a publication number for project",
                    logmsg=f"Could not find a pubnum for project {project._id}",
                )
                good = False

            if ePubNum is None:
                Messages.error(
                    msg="Could not find a publication number for edition",
                    logmsg=f"Could not find a pubnum for {project._id}/{edition._id}",
                )
                good = False

            # if all went well, pPubNum and ePubNum are defined

        elif action == "remove":
            pPubNum = project.pubNum
            ePubNum = edition.pubNum
            pPubNumNew = pPubNum
            ePubNumNew = ePubNum

            if pPubNum is None:
                Messages.warning(
                    msg="Project is not a published one and cannot be unpublished",
                    logmsg=f"Project {project._id} has no pubnum",
                )
                good = False

            if ePubNum is None:
                Messages.warning(
                    msg="Edition is not a published one and cannot be unpublished",
                    logmsg=f"Edition {project._id}/{edition._id} has no pubnum",
                )
                good = False

        if good:
            fieldPaths = Content.fieldPaths
            datePublishedPath = fieldPaths["datePublished"]
            dateUnPublishedPath = fieldPaths["dateUnPublished"]

            thisProjectDir = f"{projectDir}/{pPubNum}"
            logmsg = None

            if action == "add":
                againRep = "Re-" if again else ""
                try:
                    stage = f"set pubnum for project to {pPubNum}"
                    update = dict(pubNum=pPubNum, isVisible=True)
                    Mongo.updateRecord("project", dict(_id=project._id), update)
                    project = Mongo.getRecord("project", dict(_id=project._id))

                    stage = f"set pubnum for edition to {ePubNum}"
                    update = {
                        "pubNum": ePubNum,
                        "isPublished": True,
                        datePublishedPath: now,
                    }
                    Mongo.updateRecord("edition", dict(_id=edition._id), update)
                    edition = Mongo.getRecord("edition", dict(_id=edition._id))

                    stage = "add site files"
                    self.addSiteFiles(site)

                    stage = f"add project files to {pPubNum}"
                    self.addProjectFiles(project, pPubNum)

                    stage = f"add edition files to {pPubNum}/{ePubNum}"
                    self.addEditionFiles(project, pPubNum, edition, ePubNum)

                    stage = f"generate static pages for {pPubNum}/{ePubNum}"

                    if self.generatePages(pPubNum, ePubNum):
                        Messages.info(
                            msg=f"{againRep}Published edition to {pPubNum}/{ePubNum}",
                            logmsg=(
                                f"{againRep}Published {project._id}/{edition._id} "
                                f"as {pPubNum}/{ePubNum}"
                            ),
                        )
                    else:
                        good = False

                except Exception as e:
                    good = False
                    logmsg = (
                        f"{againRep}Publishing {project._id}/{edition._id} "
                        f"as {pPubNum}/{ePubNum} failed with error {e}"
                        f"at stage '{stage}'"
                    )

                if not good:
                    Messages.error(
                        msg=f"{againRep}Publishing of edition failed", logmsg=logmsg
                    )
                    self.removeEditionFiles(pPubNum, ePubNum)
                    theseEditions = dirContents(f"{thisProjectDir}/edition")[1]

                    if len(theseEditions) == 0:
                        self.removeProjectFiles(pPubNum)

            elif action == "remove":
                try:
                    stage = f"unset pubnum for edition from {ePubNum} to None"
                    update = {
                        "isPublished": False,
                        dateUnPublishedPath: now,
                    }
                    Mongo.updateRecord("edition", dict(_id=edition._id), update)
                    edition = Mongo.getRecord("edition", dict(_id=edition._id))

                    stage = f"remove edition files {pPubNum}/{ePubNum}"
                    self.removeEditionFiles(pPubNum, ePubNum)
                    Messages.info(
                        msg=f"Unpublished edition {pPubNum}/{ePubNum}",
                        logmsg=(
                            f"Unpublished edition {pPubNum}/{ePubNum} = "
                            f"{project._id}/{edition._id}"
                        ),
                    )
                    ePubNumNew = None

                    # check whether there are other published editions in this project
                    # on the file system

                    stage = f"check remaining editions in project {pPubNum}"
                    theseEditions = dirContents(f"{thisProjectDir}/edition")[1]

                    if len(theseEditions) == 0:
                        stage = f"make project with {pPubNum} invisible"
                        update = dict(isVisible=False)
                        Mongo.updateRecord("project", dict(_id=project._id), update)
                        project = Mongo.getRecord("project", dict(_id=project._id))

                        stage = f"remove project files {pPubNum}"
                        self.removeProjectFiles(pPubNum)
                    else:
                        Messages.info(
                            msg=(
                                f"Project {pPubNum} still has {len(theseEditions)} "
                                "published editions"
                            ),
                        )

                    pNumRep = (
                        pPubNum if pPubNumNew == pPubNum else f"{pPubNum}=>{pPubNumNew}"
                    )
                    eNumRep = (
                        ePubNum if ePubNumNew == ePubNum else f"{ePubNum}=>{ePubNumNew}"
                    )

                    stage = f"regenerate static pages for {pNumRep}/{eNumRep}"

                    if self.generatePages(pPubNum, ePubNum):
                        Messages.info(
                            msg=f"Unpublished project {pPubNum}",
                            logmsg=(f"Unpublished project {pPubNum} = {project._id}"),
                        )
                    else:
                        good = False

                except Exception as e:
                    good = False
                    logmsg = (
                        f"Unpublishing edition {pPubNum}/{ePubNum} = "
                        f"{project._id}/{edition._id} failed with error {e}."
                        f"at stage '{stage}'"
                    )

                if not good:
                    Messages.error(msg="Unpublishing of edition failed", logmsg=logmsg)

        # finish off with unsetting the processing flag in the database

        if good:
            lastPublished = now
        else:
            restore("project")
            restore("edition")
            lastPublished = last

        Mongo.updateRecord(
            "site",
            dict(_id=site._id),
            dict(processing=False, lastPublished=lastPublished),
        )

    def addSiteFiles(self, site):
        Settings = self.Settings
        workingDir = Settings.workingDir
        pubModeDir = Settings.pubModeDir
        dbFile = Settings.dbFile

        dirMake(pubModeDir)

        (files, dirs) = dirContents(workingDir)

        for x in files:
            fileCopy(f"{workingDir}/{x}", f"{pubModeDir}/{x}")

        for x in dirs:
            if x in {"project", "db"}:
                continue

            dirCopy(f"{workingDir}/{x}", f"{pubModeDir}/{x}")

        dirMake(f"{pubModeDir}/project")
        writeJson(deepdict(site), asFile=f"{pubModeDir}/{dbFile}")

    def addProjectFiles(self, project, pPubNum):
        Settings = self.Settings
        workingDir = Settings.workingDir
        pubModeDir = Settings.pubModeDir
        dbFile = Settings.dbFile

        inDir = f"{workingDir}/project/{project._id}"
        outDir = f"{pubModeDir}/project/{pPubNum}"
        dirMake(outDir)

        (files, dirs) = dirContents(inDir)

        for x in files:
            fileCopy(f"{inDir}/{x}", f"{outDir}/{x}")

        for x in dirs:
            if x in {"edition", "db"}:
                continue

            dirCopy(f"{inDir}/{x}", f"{outDir}/{x}")

        writeJson(deepdict(project), asFile=f"{outDir}/{dbFile}")

    def addEditionFiles(self, project, pPubNum, edition, ePubNum):
        Settings = self.Settings
        workingDir = Settings.workingDir
        pubModeDir = Settings.pubModeDir
        tocFile = Settings.tocFile
        dbFile = Settings.dbFile

        inDir = f"{workingDir}/project/{project._id}/edition/{edition._id}"
        outDir = f"{pubModeDir}/project/{pPubNum}/edition/{ePubNum}"
        dirMake(outDir)
        tocPath = f"{outDir}/{tocFile}"

        if fileExists(tocPath):
            fileRemove(tocPath)

        (files, dirs) = dirContents(inDir)

        for x in files:
            fileCopy(f"{inDir}/{x}", f"{outDir}/{x}")

        for x in dirs:
            if x in {"db"}:
                continue

            dirCopy(f"{inDir}/{x}", f"{outDir}/{x}")

        writeJson(deepdict(edition), asFile=f"{outDir}/{dbFile}")

    def removeProjectFiles(self, pPubNum):
        Settings = self.Settings
        pubModeDir = Settings.pubModeDir

        outDir = f"{pubModeDir}/project/{pPubNum}"
        dirRemove(outDir)

    def removeEditionFiles(self, pPubNum, ePubNum):
        Settings = self.Settings
        pubModeDir = Settings.pubModeDir

        outDir = f"{pubModeDir}/project/{pPubNum}/edition/{ePubNum}"
        dirRemove(outDir)

Classes

class Publish (Settings, Messages, Viewers, Mongo: Mongo, Content, Tailwind, Handlebars)

Publishing content as static pages.

It is instantiated by a singleton object.

Parameters

Settings : AttrDict
App-wide configuration data obtained from Config.Settings.
Messages : object
Singleton instance of Messages.
Mongo : object
Singleton instance of Mongo.
Tailwind : object
Singleton instance of Tailwind.
Expand source code Browse git
class Publish:
    def __init__(
        self, Settings, Messages, Viewers, Mongo: Mongo, Content, Tailwind, Handlebars
    ):
        """Publishing content as static pages.

        It is instantiated by a singleton object.

        Parameters
        ----------
        Settings: AttrDict
            App-wide configuration data obtained from
            `control.config.Config.Settings`.
        Messages: object
            Singleton instance of `control.messages.Messages`.
        Mongo: object
            Singleton instance of `control.mongo.Mongo`.
        Tailwind: object
            Singleton instance of `control.tailwind.Tailwind`.
        """
        self.Settings = Settings
        self.Messages = Messages
        self.Viewers = Viewers
        self.Mongo = Mongo
        self.Content = Content
        self.Tailwind = Tailwind
        self.Handlebars = Handlebars
        Messages.debugAdd(self)
        Content.addPublish(self)

        self.Precheck = (
            None
            if Content is None
            else PrecheckCls(Settings, Messages, Content, Viewers)
        )

    def getPubNums(self, project, edition):
        """Determine project and edition publication numbers.

        Those numbers are inside the project and edition records in the database
        if the project/edition has been published before;
        otherwise we pick an unused number for the project;
        and within the project an unused edition number.

        When we look for those numbers, we look in the database records,
        and we look on the filesystem, and we take the number one higher than
        the maximum number used in the database and on the file system.

        **N.B.:** This practice has the flaw that numbers used for publishing projects
        and editions may get reused when you unpublish and/or delete published editions.

        We store the used publishing numbers for projects and editions in the
        `site` record, as a dictionary named pubNums, keyed by project numbers, and
        valued by the maximum edition pubNum for that project.

        The `pubNums` dictionary will never lose keys, and its values will
        never be lowered when projects or editions are removed.

        So new projects and editions always get numbers that have never been used before
        for publishing.

        We also copy the `pubNum` field of a project or edition into the field
        `pubNumLast` when we unpublish such an item, thereby nulling the `pubNum` field.
        When we republish the item, its `pubNumLast` is restored to the `pubNum`
        field.
        """
        Mongo = self.Mongo
        Settings = self.Settings
        pubModeDir = Settings.pubModeDir
        projectDir = f"{pubModeDir}/project"

        pPubNumLast = project.pubNum
        ePubNumLast = edition.pubNum

        def getNum(kind, pubNumLast, condition, itemsDir):
            if pubNumLast is None:
                itemsDb = Mongo.getList(kind, condition)
                nDb = len(itemsDb)
                maxDb = 0 if nDb == 0 else max(r.pubNum or 0 for r in itemsDb)

                itemsFile = [int(n) for n in dirContents(itemsDir)[1] if n.isdecimal()]
                nFile = len(itemsFile)

                maxFile = 0 if nFile == 0 else max(itemsFile)
                pubNum = max((maxDb, maxFile)) + 1
            else:
                pubNum = pubNumLast

            return pubNum

        if pPubNumLast is None:
            # because there is only 1 site in the database,
            # we can retrieve it without paramaters
            site = Mongo.getRecord("site", {})

            if "publishedProjectCount" in site:
                pPubNum = site["publishedProjectCount"] + 1
            else:
                # Determine project publish number the old way,
                # to make sure no two project have the same pubNum
                pPubNum = getNum("project", pPubNumLast, {}, projectDir)

            Mongo.updateRecord("site", {}, {"publishedProjectCount": pPubNum})

        else:
            pPubNum = pPubNumLast

        if ePubNumLast is None:
            if "publishedEditionCount" in project:
                ePubNum = project["publishedEditionCount"] + 1
            else:
                # use get num for existing projects
                kind = "edition"
                pubNumLast = ePubNumLast
                condition = dict(projectId=project._id)
                itemsDir = f"{projectDir}/{pPubNum}/edition"

                ePubNum = getNum(kind, pubNumLast, condition, itemsDir)

            Mongo.updateRecord(
                "project", dict(_id=project._id), {"publishedEditionCount": ePubNum}
            )
        else:
            ePubNum = ePubNumLast

        return (pPubNum, ePubNum)

    def generatePages(self, pPubNum, ePubNum):
        Settings = self.Settings
        Messages = self.Messages
        Viewers = self.Viewers
        Content = self.Content
        Tailwind = self.Tailwind
        Handlebars = self.Handlebars

        site = Content.relevant()[-1]
        featured = Content.getValue("site", site, "featured", manner="logical")

        Static = StaticCls(Settings, Messages, Content, Viewers, Tailwind, Handlebars)

        try:
            self.addSiteFiles(site)
            good = Static.genPages(pPubNum, ePubNum, featured=featured)

        except Exception as e1:
            Messages.error(logmsg="".join(format_exception(e1)))
            good = False

        return good

    def updateEdition(self, site, project, edition, action, force=False, again=False):
        Settings = self.Settings
        Messages = self.Messages
        Mongo = self.Mongo
        Content = self.Content
        Precheck = self.Precheck

        if action not in {"add", "remove"}:
            Messages.error(msg=f"unknown action {action}")
            return

        processing = site.processing

        # quit early if another processing action is taking place

        if processing:
            Messages.warning(
                msg="Site is being published. Try again a minute later",
                logmsg=(
                    f"Refusing to publish {project._id}/{edition._id} "
                    "while site is being republished"
                ),
            )
            return

        # put a flag in the database that the site is publishing
        # this will prevent other publishing actions while this action is running

        last = site.lastPublished
        now = isonow()

        Mongo.updateRecord(
            "site", dict(_id=site._id), dict(processing=True, lastPublished=now)
        )

        # make sure that if something fails, the publishing flag will be reset

        pubModeDir = Settings.pubModeDir
        projectDir = f"{pubModeDir}/project"

        orig = dict(
            project=AttrDict(
                condition=dict(_id=project._id),
                updates={k: v for (k, v) in project.items() if k != "_id"},
            ),
            edition=AttrDict(
                condition=dict(_id=edition._id),
                updates={k: v for (k, v) in edition.items() if k != "_id"},
            ),
        )

        def restore(table):
            info = orig[table]
            Mongo.updateRecord(table, info.condition, info.updates)

        # quit early, without doing anything, if the action is not applicable

        good = True

        if action == "add":
            thisGood = Precheck.checkEdition(site, project, edition._id, edition)

            if thisGood:
                Messages.info("Edition validation OK")
            else:
                Messages.info("Edition validation not OK")
                good = False

                if force:
                    Messages.info("Continuing nevertheless")
                    good = True

            (pPubNum, ePubNum) = self.getPubNums(project, edition)

            if pPubNum is None:
                Messages.error(
                    msg="Could not find a publication number for project",
                    logmsg=f"Could not find a pubnum for project {project._id}",
                )
                good = False

            if ePubNum is None:
                Messages.error(
                    msg="Could not find a publication number for edition",
                    logmsg=f"Could not find a pubnum for {project._id}/{edition._id}",
                )
                good = False

            # if all went well, pPubNum and ePubNum are defined

        elif action == "remove":
            pPubNum = project.pubNum
            ePubNum = edition.pubNum
            pPubNumNew = pPubNum
            ePubNumNew = ePubNum

            if pPubNum is None:
                Messages.warning(
                    msg="Project is not a published one and cannot be unpublished",
                    logmsg=f"Project {project._id} has no pubnum",
                )
                good = False

            if ePubNum is None:
                Messages.warning(
                    msg="Edition is not a published one and cannot be unpublished",
                    logmsg=f"Edition {project._id}/{edition._id} has no pubnum",
                )
                good = False

        if good:
            fieldPaths = Content.fieldPaths
            datePublishedPath = fieldPaths["datePublished"]
            dateUnPublishedPath = fieldPaths["dateUnPublished"]

            thisProjectDir = f"{projectDir}/{pPubNum}"
            logmsg = None

            if action == "add":
                againRep = "Re-" if again else ""
                try:
                    stage = f"set pubnum for project to {pPubNum}"
                    update = dict(pubNum=pPubNum, isVisible=True)
                    Mongo.updateRecord("project", dict(_id=project._id), update)
                    project = Mongo.getRecord("project", dict(_id=project._id))

                    stage = f"set pubnum for edition to {ePubNum}"
                    update = {
                        "pubNum": ePubNum,
                        "isPublished": True,
                        datePublishedPath: now,
                    }
                    Mongo.updateRecord("edition", dict(_id=edition._id), update)
                    edition = Mongo.getRecord("edition", dict(_id=edition._id))

                    stage = "add site files"
                    self.addSiteFiles(site)

                    stage = f"add project files to {pPubNum}"
                    self.addProjectFiles(project, pPubNum)

                    stage = f"add edition files to {pPubNum}/{ePubNum}"
                    self.addEditionFiles(project, pPubNum, edition, ePubNum)

                    stage = f"generate static pages for {pPubNum}/{ePubNum}"

                    if self.generatePages(pPubNum, ePubNum):
                        Messages.info(
                            msg=f"{againRep}Published edition to {pPubNum}/{ePubNum}",
                            logmsg=(
                                f"{againRep}Published {project._id}/{edition._id} "
                                f"as {pPubNum}/{ePubNum}"
                            ),
                        )
                    else:
                        good = False

                except Exception as e:
                    good = False
                    logmsg = (
                        f"{againRep}Publishing {project._id}/{edition._id} "
                        f"as {pPubNum}/{ePubNum} failed with error {e}"
                        f"at stage '{stage}'"
                    )

                if not good:
                    Messages.error(
                        msg=f"{againRep}Publishing of edition failed", logmsg=logmsg
                    )
                    self.removeEditionFiles(pPubNum, ePubNum)
                    theseEditions = dirContents(f"{thisProjectDir}/edition")[1]

                    if len(theseEditions) == 0:
                        self.removeProjectFiles(pPubNum)

            elif action == "remove":
                try:
                    stage = f"unset pubnum for edition from {ePubNum} to None"
                    update = {
                        "isPublished": False,
                        dateUnPublishedPath: now,
                    }
                    Mongo.updateRecord("edition", dict(_id=edition._id), update)
                    edition = Mongo.getRecord("edition", dict(_id=edition._id))

                    stage = f"remove edition files {pPubNum}/{ePubNum}"
                    self.removeEditionFiles(pPubNum, ePubNum)
                    Messages.info(
                        msg=f"Unpublished edition {pPubNum}/{ePubNum}",
                        logmsg=(
                            f"Unpublished edition {pPubNum}/{ePubNum} = "
                            f"{project._id}/{edition._id}"
                        ),
                    )
                    ePubNumNew = None

                    # check whether there are other published editions in this project
                    # on the file system

                    stage = f"check remaining editions in project {pPubNum}"
                    theseEditions = dirContents(f"{thisProjectDir}/edition")[1]

                    if len(theseEditions) == 0:
                        stage = f"make project with {pPubNum} invisible"
                        update = dict(isVisible=False)
                        Mongo.updateRecord("project", dict(_id=project._id), update)
                        project = Mongo.getRecord("project", dict(_id=project._id))

                        stage = f"remove project files {pPubNum}"
                        self.removeProjectFiles(pPubNum)
                    else:
                        Messages.info(
                            msg=(
                                f"Project {pPubNum} still has {len(theseEditions)} "
                                "published editions"
                            ),
                        )

                    pNumRep = (
                        pPubNum if pPubNumNew == pPubNum else f"{pPubNum}=>{pPubNumNew}"
                    )
                    eNumRep = (
                        ePubNum if ePubNumNew == ePubNum else f"{ePubNum}=>{ePubNumNew}"
                    )

                    stage = f"regenerate static pages for {pNumRep}/{eNumRep}"

                    if self.generatePages(pPubNum, ePubNum):
                        Messages.info(
                            msg=f"Unpublished project {pPubNum}",
                            logmsg=(f"Unpublished project {pPubNum} = {project._id}"),
                        )
                    else:
                        good = False

                except Exception as e:
                    good = False
                    logmsg = (
                        f"Unpublishing edition {pPubNum}/{ePubNum} = "
                        f"{project._id}/{edition._id} failed with error {e}."
                        f"at stage '{stage}'"
                    )

                if not good:
                    Messages.error(msg="Unpublishing of edition failed", logmsg=logmsg)

        # finish off with unsetting the processing flag in the database

        if good:
            lastPublished = now
        else:
            restore("project")
            restore("edition")
            lastPublished = last

        Mongo.updateRecord(
            "site",
            dict(_id=site._id),
            dict(processing=False, lastPublished=lastPublished),
        )

    def addSiteFiles(self, site):
        Settings = self.Settings
        workingDir = Settings.workingDir
        pubModeDir = Settings.pubModeDir
        dbFile = Settings.dbFile

        dirMake(pubModeDir)

        (files, dirs) = dirContents(workingDir)

        for x in files:
            fileCopy(f"{workingDir}/{x}", f"{pubModeDir}/{x}")

        for x in dirs:
            if x in {"project", "db"}:
                continue

            dirCopy(f"{workingDir}/{x}", f"{pubModeDir}/{x}")

        dirMake(f"{pubModeDir}/project")
        writeJson(deepdict(site), asFile=f"{pubModeDir}/{dbFile}")

    def addProjectFiles(self, project, pPubNum):
        Settings = self.Settings
        workingDir = Settings.workingDir
        pubModeDir = Settings.pubModeDir
        dbFile = Settings.dbFile

        inDir = f"{workingDir}/project/{project._id}"
        outDir = f"{pubModeDir}/project/{pPubNum}"
        dirMake(outDir)

        (files, dirs) = dirContents(inDir)

        for x in files:
            fileCopy(f"{inDir}/{x}", f"{outDir}/{x}")

        for x in dirs:
            if x in {"edition", "db"}:
                continue

            dirCopy(f"{inDir}/{x}", f"{outDir}/{x}")

        writeJson(deepdict(project), asFile=f"{outDir}/{dbFile}")

    def addEditionFiles(self, project, pPubNum, edition, ePubNum):
        Settings = self.Settings
        workingDir = Settings.workingDir
        pubModeDir = Settings.pubModeDir
        tocFile = Settings.tocFile
        dbFile = Settings.dbFile

        inDir = f"{workingDir}/project/{project._id}/edition/{edition._id}"
        outDir = f"{pubModeDir}/project/{pPubNum}/edition/{ePubNum}"
        dirMake(outDir)
        tocPath = f"{outDir}/{tocFile}"

        if fileExists(tocPath):
            fileRemove(tocPath)

        (files, dirs) = dirContents(inDir)

        for x in files:
            fileCopy(f"{inDir}/{x}", f"{outDir}/{x}")

        for x in dirs:
            if x in {"db"}:
                continue

            dirCopy(f"{inDir}/{x}", f"{outDir}/{x}")

        writeJson(deepdict(edition), asFile=f"{outDir}/{dbFile}")

    def removeProjectFiles(self, pPubNum):
        Settings = self.Settings
        pubModeDir = Settings.pubModeDir

        outDir = f"{pubModeDir}/project/{pPubNum}"
        dirRemove(outDir)

    def removeEditionFiles(self, pPubNum, ePubNum):
        Settings = self.Settings
        pubModeDir = Settings.pubModeDir

        outDir = f"{pubModeDir}/project/{pPubNum}/edition/{ePubNum}"
        dirRemove(outDir)

Methods

def addEditionFiles(self, project, pPubNum, edition, ePubNum)
def addProjectFiles(self, project, pPubNum)
def addSiteFiles(self, site)
def generatePages(self, pPubNum, ePubNum)
def getPubNums(self, project, edition)

Determine project and edition publication numbers.

Those numbers are inside the project and edition records in the database if the project/edition has been published before; otherwise we pick an unused number for the project; and within the project an unused edition number.

When we look for those numbers, we look in the database records, and we look on the filesystem, and we take the number one higher than the maximum number used in the database and on the file system.

N.B.: This practice has the flaw that numbers used for publishing projects and editions may get reused when you unpublish and/or delete published editions.

We store the used publishing numbers for projects and editions in the site record, as a dictionary named pubNums, keyed by project numbers, and valued by the maximum edition pubNum for that project.

The pubNums dictionary will never lose keys, and its values will never be lowered when projects or editions are removed.

So new projects and editions always get numbers that have never been used before for publishing.

We also copy the pubNum field of a project or edition into the field pubNumLast when we unpublish such an item, thereby nulling the pubNum field. When we republish the item, its pubNumLast is restored to the pubNum field.

def removeEditionFiles(self, pPubNum, ePubNum)
def removeProjectFiles(self, pPubNum)
def updateEdition(self, site, project, edition, action, force=False, again=False)