Module control.publish

Expand source code Browse git
from datetime import datetime as dt
from traceback import format_exception

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


class Publish:
    def __init__(
        self, Settings, Messages, Viewers, 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, 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.
        """
        Mongo = self.Mongo
        Settings = self.Settings
        pubModeDir = Settings.pubModeDir
        projectDir = f"{pubModeDir}/project"

        pPubNumLast = project.pubNum
        ePubNumLast = edition.pubNum

        def getNum(kind, item, pubNumLast, condition, itemsDir):
            if pubNumLast is None:
                itemsDb = Mongo.getList(kind, stop=False, **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

        kind = "project"
        item = project
        pubNumLast = pPubNumLast
        condition = {}
        itemsDir = projectDir

        pPubNum = getNum(kind, item, pubNumLast, condition, itemsDir)

        kind = "edition"
        item = edition
        pubNumLast = ePubNumLast
        condition = dict(projectId=project._id)
        itemsDir = f"{projectDir}/{pPubNum}/edition"

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

        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, Viewers, Tailwind, Handlebars)

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

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

        return good

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

        if action not in {"add", "remove"}:
            Messages.error(msg=f"unknown action {action}", stop=False)
            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 = dt.utcnow().isoformat(timespec="seconds").replace(":", "-")

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

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

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

        def restore(table, record):
            key = "isVisible" if table == "project" else "isPublished"
            Mongo.updateRecord(
                table,
                {
                    "pubNum": record.pubNum,
                    "lastPublished": record.lastPublished,
                    key: record[key] or False,
                },
                _id=record._id,
            )

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

        good = True

        if action == "add":
            thisGood = Precheck.checkEdition(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}",
                    stop=False,
                )
                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}",
                    stop=False,
                )
                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:
            thisProjectDir = f"{projectDir}/{pPubNum}"
            logmsg = None

            if action == "add":
                try:
                    stage = f"set pubnum for project to {pPubNum}"
                    update = dict(pubNum=pPubNum, lastPublished=now, isVisible=True)
                    Mongo.updateRecord("project", update, _id=project._id)

                    stage = f"set pubnum for edition to {ePubNum}"
                    update = dict(pubNum=ePubNum, lastPublished=now, isPublished=True)
                    Mongo.updateRecord("edition", update, _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"Published edition to {pPubNum}/{ePubNum}",
                            logmsg=(
                                f"Published {project._id}/{edition._id} "
                                f"as {pPubNum}/{ePubNum}"
                            ),
                        )
                    else:
                        good = False

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

                if not good:
                    Messages.error(
                        msg="Publishing of edition failed",
                        logmsg=logmsg,
                        stop=False,
                    )
                    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 = dict(pubNum=None, isPublished=False)
                    Mongo.updateRecord("edition", update, _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"unset pubnum for project from {pPubNum} to None"
                        update = dict(pubNum=None, isVisible=False)
                        Mongo.updateRecord("project", update, _id=project._id)

                        stage = f"remove project files {pPubNum}"
                        self.removeProjectFiles(pPubNum)

                        pPubNumNew = None

                    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,
                        stop=False,
                    )

        # finish off with unsetting the processing flag in the database

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

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

    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", "meta"}:
                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", "meta"}:
                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 {"meta"}:
                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, 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, 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, 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.
        """
        Mongo = self.Mongo
        Settings = self.Settings
        pubModeDir = Settings.pubModeDir
        projectDir = f"{pubModeDir}/project"

        pPubNumLast = project.pubNum
        ePubNumLast = edition.pubNum

        def getNum(kind, item, pubNumLast, condition, itemsDir):
            if pubNumLast is None:
                itemsDb = Mongo.getList(kind, stop=False, **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

        kind = "project"
        item = project
        pubNumLast = pPubNumLast
        condition = {}
        itemsDir = projectDir

        pPubNum = getNum(kind, item, pubNumLast, condition, itemsDir)

        kind = "edition"
        item = edition
        pubNumLast = ePubNumLast
        condition = dict(projectId=project._id)
        itemsDir = f"{projectDir}/{pPubNum}/edition"

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

        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, Viewers, Tailwind, Handlebars)

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

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

        return good

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

        if action not in {"add", "remove"}:
            Messages.error(msg=f"unknown action {action}", stop=False)
            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 = dt.utcnow().isoformat(timespec="seconds").replace(":", "-")

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

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

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

        def restore(table, record):
            key = "isVisible" if table == "project" else "isPublished"
            Mongo.updateRecord(
                table,
                {
                    "pubNum": record.pubNum,
                    "lastPublished": record.lastPublished,
                    key: record[key] or False,
                },
                _id=record._id,
            )

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

        good = True

        if action == "add":
            thisGood = Precheck.checkEdition(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}",
                    stop=False,
                )
                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}",
                    stop=False,
                )
                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:
            thisProjectDir = f"{projectDir}/{pPubNum}"
            logmsg = None

            if action == "add":
                try:
                    stage = f"set pubnum for project to {pPubNum}"
                    update = dict(pubNum=pPubNum, lastPublished=now, isVisible=True)
                    Mongo.updateRecord("project", update, _id=project._id)

                    stage = f"set pubnum for edition to {ePubNum}"
                    update = dict(pubNum=ePubNum, lastPublished=now, isPublished=True)
                    Mongo.updateRecord("edition", update, _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"Published edition to {pPubNum}/{ePubNum}",
                            logmsg=(
                                f"Published {project._id}/{edition._id} "
                                f"as {pPubNum}/{ePubNum}"
                            ),
                        )
                    else:
                        good = False

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

                if not good:
                    Messages.error(
                        msg="Publishing of edition failed",
                        logmsg=logmsg,
                        stop=False,
                    )
                    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 = dict(pubNum=None, isPublished=False)
                    Mongo.updateRecord("edition", update, _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"unset pubnum for project from {pPubNum} to None"
                        update = dict(pubNum=None, isVisible=False)
                        Mongo.updateRecord("project", update, _id=project._id)

                        stage = f"remove project files {pPubNum}"
                        self.removeProjectFiles(pPubNum)

                        pPubNumNew = None

                    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,
                        stop=False,
                    )

        # finish off with unsetting the processing flag in the database

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

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

    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", "meta"}:
                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", "meta"}:
                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 {"meta"}:
                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)
Expand source code Browse git
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 {"meta"}:
            continue

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

    writeJson(deepdict(edition), asFile=f"{outDir}/{dbFile}")
def addProjectFiles(self, project, pPubNum)
Expand source code Browse git
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", "meta"}:
            continue

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

    writeJson(deepdict(project), asFile=f"{outDir}/{dbFile}")
def addSiteFiles(self, site)
Expand source code Browse git
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", "meta"}:
            continue

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

    dirMake(f"{pubModeDir}/project")
    writeJson(deepdict(site), asFile=f"{pubModeDir}/{dbFile}")
def generatePages(self, pPubNum, ePubNum)
Expand source code Browse git
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, Viewers, Tailwind, Handlebars)

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

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

    return good
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.

Expand source code Browse git
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.
    """
    Mongo = self.Mongo
    Settings = self.Settings
    pubModeDir = Settings.pubModeDir
    projectDir = f"{pubModeDir}/project"

    pPubNumLast = project.pubNum
    ePubNumLast = edition.pubNum

    def getNum(kind, item, pubNumLast, condition, itemsDir):
        if pubNumLast is None:
            itemsDb = Mongo.getList(kind, stop=False, **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

    kind = "project"
    item = project
    pubNumLast = pPubNumLast
    condition = {}
    itemsDir = projectDir

    pPubNum = getNum(kind, item, pubNumLast, condition, itemsDir)

    kind = "edition"
    item = edition
    pubNumLast = ePubNumLast
    condition = dict(projectId=project._id)
    itemsDir = f"{projectDir}/{pPubNum}/edition"

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

    return (pPubNum, ePubNum)
def removeEditionFiles(self, pPubNum, ePubNum)
Expand source code Browse git
def removeEditionFiles(self, pPubNum, ePubNum):
    Settings = self.Settings
    pubModeDir = Settings.pubModeDir

    outDir = f"{pubModeDir}/project/{pPubNum}/edition/{ePubNum}"
    dirRemove(outDir)
def removeProjectFiles(self, pPubNum)
Expand source code Browse git
def removeProjectFiles(self, pPubNum):
    Settings = self.Settings
    pubModeDir = Settings.pubModeDir

    outDir = f"{pubModeDir}/project/{pPubNum}"
    dirRemove(outDir)
def updateEdition(self, site, project, edition, action, force=False, again=False)
Expand source code Browse git
def updateEdition(self, site, project, edition, action, force=False, again=False):
    Settings = self.Settings
    Messages = self.Messages
    Mongo = self.Mongo
    Precheck = self.Precheck

    if action not in {"add", "remove"}:
        Messages.error(msg=f"unknown action {action}", stop=False)
        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 = dt.utcnow().isoformat(timespec="seconds").replace(":", "-")

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

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

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

    def restore(table, record):
        key = "isVisible" if table == "project" else "isPublished"
        Mongo.updateRecord(
            table,
            {
                "pubNum": record.pubNum,
                "lastPublished": record.lastPublished,
                key: record[key] or False,
            },
            _id=record._id,
        )

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

    good = True

    if action == "add":
        thisGood = Precheck.checkEdition(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}",
                stop=False,
            )
            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}",
                stop=False,
            )
            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:
        thisProjectDir = f"{projectDir}/{pPubNum}"
        logmsg = None

        if action == "add":
            try:
                stage = f"set pubnum for project to {pPubNum}"
                update = dict(pubNum=pPubNum, lastPublished=now, isVisible=True)
                Mongo.updateRecord("project", update, _id=project._id)

                stage = f"set pubnum for edition to {ePubNum}"
                update = dict(pubNum=ePubNum, lastPublished=now, isPublished=True)
                Mongo.updateRecord("edition", update, _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"Published edition to {pPubNum}/{ePubNum}",
                        logmsg=(
                            f"Published {project._id}/{edition._id} "
                            f"as {pPubNum}/{ePubNum}"
                        ),
                    )
                else:
                    good = False

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

            if not good:
                Messages.error(
                    msg="Publishing of edition failed",
                    logmsg=logmsg,
                    stop=False,
                )
                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 = dict(pubNum=None, isPublished=False)
                Mongo.updateRecord("edition", update, _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"unset pubnum for project from {pPubNum} to None"
                    update = dict(pubNum=None, isVisible=False)
                    Mongo.updateRecord("project", update, _id=project._id)

                    stage = f"remove project files {pPubNum}"
                    self.removeProjectFiles(pPubNum)

                    pPubNumNew = None

                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,
                    stop=False,
                )

    # finish off with unsetting the processing flag in the database

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

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