Example #1
0
    def system(self):
        """
        Reference to `Doc` object for job's publishing control document
        """

        if not hasattr(self, "_system"):
            opts = dict(id=None, before=self.started)
            if self.id:
                query = Query("pub_proc", "pub_system")
                query.where(query.Condition("id", self.id))
                row = query.execute(self.cursor).fetchone()
                if not row:
                    raise Exception("Job {} not found".format(self.id))
                opts["id"] = row.pub_system
            else:
                name = self.__opts.get("system")
                if name:
                    query = Query("document d", "d.id")
                    query.join("doc_type t", "t.id = d.doc_type")
                    query.where("t.name = 'PublishingSystem'")
                    query.where(query.Condition("d.title", name))
                    rows = query.execute(self.cursor).fetchall()
                    if len(rows) > 1:
                        raise Exception(f"multiple {name} docs")
                    if not rows:
                        message = "Publishing system {!r} not found"
                        raise Exception(message.format(name))
                    row = rows[0]
                    opts["id"] = row.id
                    opts["title"] = name
            if opts["id"]:
                self._system = Doc(self.session, **opts)
            else:
                self._system = None
        return self._system
Example #2
0
    def subsystem(self):
        """
        Reference to the `Job.Subsystem` object controlling this job
        """

        if not hasattr(self, "_subsystem"):
            self._subsystem = None
            if self.system:
                if self.system.root is None:
                    doc = self.system
                    args = doc.cdr_id, doc.version, doc.title
                    doc = "{}V{} ({})".format(*args)
                    message = "{} can't be parsed".format(doc)
                    raise Exception(message)
                name = self.__opts.get("subsystem")
                if not name and self.id:
                    query = Query("pub_proc", "pub_subset")
                    query.where(query.Condition("id", self.id))
                    row = query.execute(self.cursor).fetchone()
                    if not row:
                        raise Exception("Job {} not found".format(self.id))
                    name = row.pub_subset
                if name:
                    path = "SystemSubset/SubsetName"
                    for node in self.system.root.findall(path):
                        if Doc.get_text(node) == name:
                            parent = node.getparent()
                            self._subsystem = Job.Subsystem(parent, name)
                            break
        return self._subsystem
Example #3
0
    def _values_for_path(self):
        """
        Find values located in document at specified path

        Parameters:
          DocId - required string indicating which document to look in
          Path - required string for location of values to be returned
          Pub - if present (regardless of value) look for values in
                the latest publishable version of the document

        Return:
          XML document node with the following structure:
            ReportBody
              ReportName
              Value*
                @Loc
        """

        doc_id = self.__opts.get("DocId")
        if not doc_id:
            raise Exception("Missing required 'DocId' parameter")
        path = self.__opts.get("Path")
        if not doc_id:
            raise Exception("Missing required 'Path' parameter")
        table = "query_term_pub" if "Pub" in self.__opts else "query_term"
        query = Query(table, "value", "node_loc").order("node_loc")
        query.where(query.Condition("doc_id", Doc(self.session, id=doc_id).id))
        query.where(query.Condition("path", path))
        body = self.__start_body()
        for value, loc in query.execute(self.cursor).fetchall():
            etree.SubElement(body, "Value", Loc=loc).text = value
        return body
Example #4
0
    def _glossary_term_names(self):
        """
        Find GlossaryTermName documents linked to a GlossaryTermConcept doc

        Parameters:
          ConceptID - required CDR ID for the master concept document

        Return:
          XML document node with the following structure:
            ReportBody
              ReportName
              GlossaryTermName*
                @ref
        """

        concept_id = self.__opts.get("ConceptId")
        if not concept_id:
            raise Exception("Missing required 'ConceptId' parameter")
        path = "/GlossaryTermName/GlossaryTermConcept/@cdr:ref"
        doc = Doc(self.session, id=concept_id)
        query = Query("document d", "d.id", "d.title")
        query.join("query_term n", "n.doc_id = d.id")
        query.where(query.Condition("n.path", path))
        query.where(query.Condition("n.int_val", doc.id))
        body = self.__start_body()
        for id, title in query.execute(self.cursor).fetchall():
            cdr_id = Doc.normalize_id(id)
            etree.SubElement(body, "GlossaryTermName", ref=cdr_id).text = title
        return body
Example #5
0
    def _translated_summary(self):
        """
        Find corresponding translation of English summary

        Parameters:
          EnglishSummary - required CDR ID for the document for which
                           we want to find the Spanish translation

        Return:
          XML document node with the following structure:
            ReportBody
              ReportName
              TranslatedSummary
        """

        english_id = self.__opts.get("EnglishSummary")
        if not english_id:
            raise Exception("Missing required 'EnglishSummary' parameter")
        doc = Doc(self.session, id=english_id)
        query = Query("query_term", "doc_id")
        query.where("path = '/Summary/TranslationOf/@cdr:ref'")
        query.where(query.Condition("int_val", doc.id))
        row = query.execute(self.cursor).fetchone()
        if not row:
            message = "No translated summary found for {}".format(english_id)
            raise Exception(message)
        body = self.__start_body()
        spanish_id = Doc.normalize_id(row.doc_id)
        etree.SubElement(body, "TranslatedSummary").text = spanish_id
        return body
Example #6
0
    def _patient_summary(self):
        """
        Find corresponding patient version for HP summary

        Parameters:
          HPSummary - required CDR ID for the Health Professional
                      for which we want to find the patient verion

        Return:
          XML document node with the following structure:
            ReportBody
              ReportName
              PatientSummary
        """

        hp_id = self.__opts.get("HPSummary")
        if not hp_id:
            raise Exception("Missing required 'HPSummary' parameter")
        doc = Doc(self.session, id=hp_id)
        query = Query("query_term", "doc_id")
        query.where("path = '/Summary/PatientVersionOf/@cdr:ref'")
        query.where(query.Condition("int_val", doc.id))
        row = query.execute(self.cursor).fetchone()
        if not row:
            message = "No patient summary found for {}".format(hp_id)
            raise Exception(message)
        body = self.__start_body()
        patient_id = Doc.normalize_id(row.doc_id)
        etree.SubElement(body, "PatientSummary").text = patient_id
        return body
Example #7
0
    def _genetics_syndromes(self):
        """
        Find Term documents used to represent genetics syndromes

        Parameters:
          TitlePattern - optional string narrowing report to syndrome
                         names which match the pattern

        Return:
          XML document node with the following structure:
            ReportBody
              ReportName
              ReportRow*
                DocId
                DocTitle
        """

        pattern = self.__opts.get("TitlePattern")
        query = Query("document d", "d.id", "d.title")
        query.unique().order("d.title")
        query.join("query_term t", "t.doc_id = d.id")
        query.join("query_term s", "s.doc_id = t.doc_id",
                   "LEFT(s.node_loc, 8) = LEFT(t.node_loc, 8)")
        query.where("t.path = '/Term/MenuInformation/MenuItem/MenuType'")
        query.where("s.path = '/Term/MenuInformation/MenuItem/MenuStatus'")
        query.where("s.value = 'Online'")
        query.where("t.value = 'Genetics Professionals--GeneticSyndrome'")
        if pattern:
            query.where(query.Condition("d.title", pattern, "LIKE"))
        body = self.__start_body()
        for id, title in query.execute(self.cursor).fetchall():
            wrapper = etree.SubElement(body, "ReportRow")
            etree.SubElement(wrapper, "DocId").text = Doc.normalize_id(id)
            etree.SubElement(wrapper, "DocTitle").text = title
        return body
Example #8
0
    def _dated_actions(self):
        """
        Find the to-do list for documents of a particular type

        Parameters:
          DocType - required string naming the document type for
                    which documents should be included

        Return:
          XML document node with the following structure:
            ReportBody
              ReportName
              ReportRow*
                DocId
                DocTitle
        """

        doctype = self.__opts.get("DocType")
        if not doctype:
            raise Exception("Missing required 'DocType' parameter")
        path = "/{}/DatedAction/ActionDate".format(doctype)
        query = Query("document d", "d.id", "d.title").unique()
        query.join("query_term a", "a.doc_id = d.id")
        query.where(query.Condition("a.path", path))
        rows = query.execute(self.cursor).fetchall()
        body = self.__start_body()
        for id, title in rows:
            wrapper = etree.SubElement(body, "ReportRow")
            etree.SubElement(wrapper, "DocId").text = Doc.normalize_id(id)
            etree.SubElement(wrapper, "DocTitle").text = title
            result = Doc(self.session, id=id).filter("name:Dated Actions")
            wrapper.append(result.result_tree.getroot())
        return body
Example #9
0
    def _board_member(self):
        """
        Find corresponding person document linked from board member document

        Parameters:
          PersonID - required CDR ID for the Person document for which
                     we want to find the corresponding PDQBoardMemeberInfo
                     document

        Return:
          XML document node with the following structure:
            ReportBody
              ReportName
              BoardMember
        """

        person_id = self.__opts.get("PersonId")
        if not person_id:
            raise Exception("Missing required 'PersonId' parameter")
        doc = Doc(self.session, id=person_id)
        query = Query("query_term", "doc_id")
        query.where("path = '/PDQBoardMemberInfo/BoardMemberName/@cdr:ref'")
        query.where(query.Condition("int_val", doc.id))
        row = query.execute(self.cursor).fetchone()
        if not row:
            message = "No board member found for {}".format(person_id)
            raise Exception(message)
        body = self.__start_body()
        member_id = Doc.normalize_id(row.doc_id)
        etree.SubElement(body, "BoardMember").text = member_id
        return body
Example #10
0
    def parms(self):
        """
        Dictionary of parameters for the job

        Default values from the control document. Some may be overridden
        for this job.

        Have to strip unwanted space from the values; see
        https://sourceforge.net/p/adodbapi/bugs/27/
        """

        if not hasattr(self, "_parms"):
            if self.id:
                query = Query("pub_proc_parm", "parm_name", "parm_value")
                query.where(query.Condition("pub_proc", self.id))
                self._parms = {}
                for name, value in query.execute(self.cursor).fetchall():
                    if value is not None:
                        value = value.strip()  # bug in adodbapi
                    self._parms[name] = value
            else:
                if not self.subsystem:
                    raise Exception("Missing publishing subsystem")
                requested = self.__opts.get("parms") or {}
                defined = self.subsystem.parms.copy()
                undefined = set(requested) - set(defined)
                if undefined:
                    messages = "Paramater(s) {} undefined"
                    #raise Exception(messages.format(", ".join(undefined)))
                    raise Exception(messages.format(undefined))
                defined.update(requested)
                self.session.logger.info("job parms: %r", defined)
                self._parms = defined
        return self._parms
Example #11
0
    def docs(self):
        """
        List of documents requested or published for this job

        For a job which is being created, this will be populated from the
        `docs` argument passed into the constructor. For a job which is
        already in the database and which has been published, the list
        will be pulled from the `pub_proc_doc` table. For a job which is
        in the database, queued but not yet run, the list will have
        the documents explicitly requested at job creation time, but will
        not yet have the documents which will be picked up by the control
        document's query logic for this job type.

        It might seem redundant to create new Doc objects for an unsaved
        job's documents, but in most cases the Doc objects passed to the
        constructor will only have document IDs, and no version information.
        We can't really get the date/time cutoff used to pick the right
        version without creating a new object.
        """

        if not hasattr(self, "_docs"):
            docs = []
            if self.id:
                query = Query("pub_proc_doc", "doc_id", "doc_version")
                query.where(query.Condition("pub_proc", self.id))
                for doc_id, version in query.execute(self.cursor).fetchall():
                    docs.append(Doc(self.session, id=doc_id, version=version))
            else:
                cutoff = self.parms.get("MaxDocUpdatedDate")
                if not cutoff or cutoff == "JobStartDateTime":
                    cutoff = self.started
                opts = dict(before=cutoff)
                for requested in self.__opts.get("docs", []):
                    opts["id"] = requested.id
                    if self.force:
                        opts["version"] = "last"
                    elif requested.version:
                        if not (self.permissive or requested.publishable):
                            args = requested.cdr_id, requested.version
                            message = "{}V{} is not publishable".format(*args)
                            raise Exception(message)
                        opts["version"] = requested.version
                    else:
                        opts["version"] = "lastp"
                    try:
                        doc = Doc(self.session, **opts)
                        if not self.force and doc.active_status != "A":
                            raise Exception("{} is blocked".format(doc.id))
                        docs.append(doc)
                    except Exception as e:
                        raise Exception("{}: {}".format(requested.cdr_id, e))
            self._docs = docs
        return self._docs
Example #12
0
    def __init__(self):
        """Assemble the dictionary of all control value groups."""

        groups = {}
        query = Query("ctl", "grp", "name", "val", "comment")
        query.where("inactivated IS NULL")
        for row in query.execute().fetchall():
            key = row.grp.lower()
            group = groups.get(key)
            if group is None:
                group = groups[key] = self.Group(row.grp, key)
            value = self.Value(row.name, row.val, row.comment)
            group.values[value.key] = value
        self.groups = groups
Example #13
0
 def build_tables(self):
     """Show the documents the user has locked."""
     fields = "c.dt_out", "t.name", "d.id", "d.title"
     query = Query("usr u", *fields).order(*fields[:3])
     query.join("checkout c", "c.usr = u.id")
     query.join("document d", "d.id = c.id")
     query.join("doc_type t", "t.id = d.doc_type")
     query.where("c.dt_in IS NULL")
     query.where(query.Condition("u.id", self.user.id))
     rows = []
     for dt_out, doc_type, doc_id, title in query.execute(self.cursor):
         doc_id = Reporter.Cell(doc_id, center=True)
         rows.append([str(dt_out)[:19], doc_type, doc_id, title])
     caption = f"Checked out by {self.user.fullname or self.user.name}"
     return Reporter.Table(rows, caption=caption, columns=self.COLUMNS)
Example #14
0
    def output_dir(self):
        """
        Destination location for the exported documents
        """

        if not hasattr(self, "_output_dir"):
            if self.id:
                query = Query("pub_proc", "output_dir")
                query.where(query.Condition("id", self.id))
                row = query.execute(self.cursor).fetchone()
                if not row:
                    raise Exception("Job {} not found".format(self.id))
                self._output_dir = row.output_dir.replace("\\", "/")
            else:
                self._output_dir = None
        return self._output_dir
Example #15
0
 def lockers(self):
     """Get sequence of id/name pairs for users with locked documents."""
     if not hasattr(self, "_lockers"):
         fields = "COUNT(*) AS n", "u.id", "u.name", "u.fullname"
         query = Query("usr u", *fields)
         query.join("checkout c", "c.usr = u.id")
         query.join("document d", "d.id = c.id")
         query.group(*fields[1:])
         query.where("c.dt_in IS NULL")
         users = []
         for row in query.execute(self.cursor):
             name = row.fullname or row.name
             display = f"{name} ({row.n} locks)"
             users.append((name.lower(), row.id, display))
         self._lockers = [(uid, label) for key, uid, label in sorted(users)]
     return self._lockers
Example #16
0
    def __find_pending_job(self):
        """
        See if we've already got a job of this type in progress

        Used to prevent having two or more of the same publishing job
        type in flight at the same time.

        Return:
          True if there's already another job of this type in progress
        """

        query = Query("pub_proc", "id").limit(1)
        query.where(query.Condition("pub_system", self.system.id))
        query.where(query.Condition("pub_subset", self.subsystem.name))
        query.where("status NOT IN ('Success', 'Failure')")
        row = query.execute(self.cursor).fetchone()
        return row.id if row else None
Example #17
0
    def completed(self):
        """
        Date/time when the job finished
        """

        if not hasattr(self, "_completed"):
            if self.id:
                query = Query("pub_proc", "completed")
                query.where(query.Condition("id", self.id))
                row = query.execute(self.cursor).fetchone()
                if not row:
                    raise Exception("Job {} not found".format(self.id))
                self._completed = row.completed
            else:
                self._completed = None
            if isinstance(self._completed, datetime.datetime):
                self._completed = self._completed.replace(microsecond=0)
        return self._completed
Example #18
0
    def no_output(self):
        """
        Flag indicating that document output won't be written to disk
        """

        if not hasattr(self, "_no_output"):
            if "no_output" in self.__opts:
                self._no_output = self.__opts["no_output"]
            elif self.id:
                query = Query("pub_proc", "no_output")
                query.where(query.Condition("id", self.id))
                row = query.execute(self.cursor).fetchone()
                if not row:
                    raise Exception("Job {} not found".format(self.id))
                self._no_output = row.no_output == "Y"
            else:
                self._no_output = False
        return self._no_output
Example #19
0
    def started(self):
        """
        When the job was created (so a bit of a misnomer)
        """

        if not hasattr(self, "_started"):
            if self.id:
                query = Query("pub_proc", "started")
                query.where(query.Condition("id", self.id))
                row = query.execute(self.cursor).fetchone()
                if not row:
                    raise Exception("Job {} not found".format(self.id))
                self._started = row.started
            else:
                self._started = datetime.datetime.now()
            if isinstance(self._started, datetime.datetime):
                self._started = self._started.replace(microsecond=0)
        return self._started
Example #20
0
    def rule_id(self):
        """
        Primary key of row in the `query_term_rule` table (or None)

        See notes above about the custom rule mechanism, which AFAIK
        has never been used.
        """

        if not hasattr(self, "_rule_id"):
            if not self.rule:
                self._rule_id = None
            else:
                query = Query("query_term_rule", "id")
                query.where(query.Condition("name", self.rule))
                row = query.execute(self.session.cursor).fetchone()
                if not row:
                    raise Exception(f"Unknown query term rule: {self.rule}")
                self._rule_id = row.id
        return self._rule_id
Example #21
0
    def get_rules(cls, session):
        """
        Find the available query term rules

        Used by the user interface for managing search path definitions.

        See notes above in the documentation for the `QueryTermDef` class
        on the custom rule mechanism, which AFAIK has never been used.

        Required positional argument:
          session - reference to object for current login

        Called by:
          cdr.listQueryTermRules()
          client XML wrapper command CdrListQueryTermRules
        """

        session.log("QueryTermDef.get_rules()")
        query = Query("query_term_rule", "name").order("name")
        return [row.name for row in query.execute(session.cursor).fetchall()]
Example #22
0
    def email(self):
        """
        String for the email address(es) to which processing notifications go
        """

        if not hasattr(self, "_email"):
            if "email" in self.__opts:
                self._email = self.__opts["email"]
                if isinstance(self._email, (list, tuple)):
                    self._email = ",".join(self._email)
            elif self.id:
                query = Query("pub_proc", "email")
                query.where(query.Condition("id", self.id))
                row = query.execute(self.cursor).fetchone()
                if not row:
                    raise Exception(f"Job {self.id} not found")
                self._email = row.email
            else:
                self._email = None
        return self._email
Example #23
0
    def get_definitions(cls, session):
        """
        Fetch the list of CDR query term definitions

        Required positional argument:
          session - reference to object for current login

        Called by:
          cdr.listQueryTermDefs()
          client XML wrapper command CdrListQueryTermDefs

        Return:
          sequence of `QueryTermDef` objects
        """

        session.log("QueryTermDef.get_definitions()")
        query = Query("query_term_def d", "d.path", "r.name")
        query.outer("query_term_rule r", "r.id = d.term_rule")
        query.order("d.path", "r.name")
        definitions = []
        for row in query.execute(session.cursor).fetchall():
            definitions.append(QueryTermDef(session, row.path, row.name))
        return definitions
Example #24
0
    def _term_sets(self):
        """
        Collect named sets of CDR terms

        Parameters:
          SetType - optional string restricting report to term sets
                    whose type name matches the pattern passed
                    (any wildcards in the pattern must be included,
                    the report code does not wrap the string in a
                    pair of wildcards); as of this writing, this
                    parameter is of limited use, as all of the
                    existing term sets have the set type of
                    "diagnosis macro"

        Return:
          XML document node with the following structure:
            ReportBody
              ReportName
              TermSet*
                Name
                Members [string of CDR term IDs]
        """

        query = Query("doc_version v", "v.id", "MAX(v.num) AS num")
        query.join("query_term_pub t", "t.doc_id = v.id")
        query.where("t.path = '/TermSet/TermSetType'")
        query.where("v.publishable = 'Y'")
        query.group("v.id")
        pattern = self.__opts.get("SetType")
        if pattern:
            query.where(query.Condition("t.value", pattern, "LIKE"))
        body = self.__start_body()
        for id, num in query.execute(self.cursor).fetchall():
            doc = Doc(self.session, id=id, version=num)
            result = doc.filter("name:Get TermSet Name and Members")
            body.append(result.result_tree.getroot())
        return body
Example #25
0
    def add(self):
        """
        Store the new query term definition

        Called by:
          cdr.addQueryTermDef()
          client XML wrapper command CdrAddQueryTermDef
        """

        self.session.log(f"QueryTermDef.add({self.path!r}, {self.rule!r})")
        if not self.session.can_do("ADD QUERY TERM DEF"):
            message = "User not authorized to add query term definitions"
            raise Exception(message)
        if not self.path:
            raise Exception("Missing required path")
        query = Query("query_term_def", "COUNT(*) AS n")
        query.where(query.Condition("path", self.path))
        if query.execute(self.session.cursor).fetchone().n > 0:
            raise Exception("Duplicate query term definition")
        names = "path, term_rule"
        values = self.path, self.rule_id
        insert = f"INSERT INTO query_term_def ({names}) VALUES (?, ?)"
        self.session.cursor.execute(insert, values)
        self.session.conn.commit()
Example #26
0
#!/usr/bin/env python
"""Get one XML document from pub_proc_cg to stdout.

If your document has non-ascii characters in it it may not work well
to try and pipe the output to a tool which expects ascii to be coming
from stdout. Consider instead using the --save option and loading the
encoded document directly into your tool.
"""

from argparse import ArgumentParser
from cdrapi.db import Query

parser = ArgumentParser()
parser.add_argument("id", type=int)
parser.add_argument("--tier")
parser.add_argument("--save", action="store_true")
opts = parser.parse_args()
query = Query("pub_proc_cg", "xml")
query.where(query.Condition("id", opts.id))
row = query.execute().fetchone()
if not row:
    raise Exception(f"CDR{opts.id:d} not found in pub_proc_cg")
if opts.save:
    with open(f"pub_proc_cg-{opts.id:d}.xml", "wb") as fp:
        fp.write(row.xml.encode("utf-8"))
else:
    print(row.xml)
Example #27
0
    def _menu_term_tree(self):
        """
        Report on the menu term hierarchy

        Parameters:
          MenuType - required string restricting report to a single menu type

        Return:
          XML document node with the following structure:
            ReportBody
              ReportName
              MenuItem*
                TermId
                TermName
                MenuType
                MenuStatus
                DisplayName?
                ParentId?
                SortString
        """

        menutype = self.__opts.get("MenuType", "%")
        operand = "LIKE" if "%" in menutype else "="
        fields = ("n.doc_id AS id", "n.value AS name", "t.value AS type",
                  "s.value AS status", "d.value AS display",
                  "p.int_val AS parent", "k.value AS sort_key")
        i_path = "/Term/MenuInformation/MenuItem"
        n_path = "/Term/PreferredName"
        t_path = "{}/MenuType".format(i_path)
        s_path = "{}/MenuStatus".format(i_path)
        d_path = "{}/DisplayName".format(i_path)
        p_path = "{}/MenuParent/@cdr:ref".format(i_path)
        k_path = "{}/@SortOrder".format(i_path)
        query = Query("query_term n", *fields).unique().order("n.doc_id")
        query.join("query_term t", "t.doc_id = n.doc_id")
        query.join("query_term s", "s.doc_id = t.doc_id",
                   "LEFT(s.node_loc, 8) = LEFT(t.node_loc, 8)")
        query.outer("query_term d", "d.doc_id = t.doc_id",
                    "d.path = '{}'".format(d_path),
                    "LEFT(d.node_loc, 8) = LEFT(t.node_loc, 8)")
        query.outer("query_term p", "p.doc_id = t.doc_id",
                    "p.path = '{}'".format(p_path),
                    "LEFT(p.node_loc, 8) = LEFT(t.node_loc, 8)")
        query.outer("query_term k", "k.doc_id = t.doc_id",
                    "k.path = '{}'".format(k_path),
                    "LEFT(k.node_loc, 8) = LEFT(t.node_loc, 8)")
        query.where("n.path = '{}'".format(n_path))
        query.where("t.path = '{}'".format(t_path))
        query.where("s.path = '{}'".format(s_path))
        query.where(query.Condition("t.value", menutype, operand))
        query.where(query.Condition("s.value", "Offline", "<>"))
        body = self.__start_body()
        for row in query.execute(self.cursor).fetchall():
            sort_key = row.sort_key if row.sort_key is not None else row.name
            wrapper = etree.SubElement(body, "MenuItem")
            etree.SubElement(wrapper, "TermId").text = str(row.id)
            etree.SubElement(wrapper, "TermName").text = row.name
            etree.SubElement(wrapper, "MenuType").text = row.type
            etree.SubElement(wrapper, "MenuStatus").text = row.status
            if row.display is not None:
                etree.SubElement(wrapper, "DisplayName").text = row.display
            if row.parent is not None:
                etree.SubElement(wrapper, "ParentId").text = str(row.parent)
            etree.SubElement(wrapper, "SortString").text = sort_key
        return body