def columns(self): """ Create a sequence of column definitions for the output report. Number and types of columns depend on config parms. Return: Sequence of column definitions to add to object. """ return (Reporter.Column("Doc ID", width="70px"), Reporter.Column("Doc Title", width="200px"), Reporter.Column("Match", width="100px"), Reporter.Column("Context", width="200px"), Reporter.Column("Standard Wording?", width="50px"))
def excel_cols(self): """Column names wrapped in `Reporter.Cell` objects. This lets us have some control over wrapping and column width. """ if not hasattr(self, "_excel_cols"): self._excel_cols = [] for col in self.cols: self._excel_cols.append(Reporter.Column(col, width="250px")) return self._excel_cols
def build_tables(self): parms = {SESSION: self.session, REQUEST: "View", "full": "full"} ids = [] rows = [] for doc in FilterSet.get_filters(self.session): parms[DOCID] = doc.cdr_id url = f"EditFilter.py?{urlencode(parms)}" id_cell = Reporter.Cell(doc.cdr_id, href=url) ids.append((doc.id, id_cell, doc.title)) rows.append((id_cell, doc.title)) columns = ( Reporter.Column("CDR ID", classes="id-col"), Reporter.Column("Filter Title", classes="title-col"), ) caption = f"{len(rows):d} CDR Filters (Sorted By Title)" opts = dict(caption=caption, columns=columns, id="titlesort") opts["logger"] = self.logger tables = [Reporter.Table(rows, **opts)] rows = [(deepcopy(row[1]), row[2]) for row in sorted(ids)] opts["caption"] = f"{len(rows):d} CDR Filters (Sorted By CDR ID)" opts["id"] = "idsort" tables.append(Reporter.Table(rows, **opts)) return tables
def cols(self): """Column headers selected to match the report options.""" if not hasattr(self, "_cols"): if self.include_headers: self._cols = [] if self.include_id: self._cols = ["CDR ID"] self._cols.append("Title") self._cols.append("Drug Type") if self.include_fda_approval: self._cols.append("Accelerated") self._cols.append("Approved in Children") if self.include_blank_column: self._cols.append(Reporter.Column("", width="300px")) else: self._cols = None return self._cols
class Control(Controller): SUBTITLE = "Checked Out Documents" COLUMNS = ( Reporter.Column("Checked Out", width="140px"), Reporter.Column("Type", width="150px"), Reporter.Column("CDR ID", width="70px"), Reporter.Column("Document Title", width="700px"), ) def populate_form(self, page): """Show the form fields iff there are any locked documents.""" if self.lockers: fieldset = page.fieldset("Select User") page.form.append(fieldset) fieldset.append(page.select("User", options=self.lockers)) page.add_output_options("html") else: page.form.append(page.B.P("No CDR documents are locked.")) submit = page.body.xpath("//input[@value='Submit']") submit.getparent().remove(submit) def run(self): """Bypass the Submit button if we already have a user.""" if not self.request and self.user: self.show_report() else: Controller.run(self) @property 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 @property def user(self): """Get the subject of the report. This report can be invoked from the web admin menus, where the form has the user ID, and from XMetaL, which passes the user name instead. We have to be able to handle both. """ if not hasattr(self, "_user"): self._user = None value = self.fields.getvalue("User") if value: if value.isdigit(): opts = dict(id=int(value)) else: opts = dict(name=value) self._user = self.session.User(self.session, **opts) return self._user 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)
class Doctype: """Collection of newly published documents of a specific type.""" COLUMNS = ( Reporter.Column("CDR ID", width="80px"), Reporter.Column("Document Title", width="500px"), Reporter.Column("Created By", width="150px"), Reporter.Column("Creation Date", width="150px"), Reporter.Column("Latest Version Date", width="150px"), Reporter.Column("Latest Version By", width="150px"), Reporter.Column("Pub?", width="50px"), Reporter.Column("Earlier Pub Ver?", width="50px"), ) def __init__(self, control, name): """Capture the caller's values. Pass: control - access to the database name - string for this document type's name """ self.__control = control self.__name = name @property def control(self): """Access to the database.""" return self.__control @property def docs(self): """New documents of this type.""" if not hasattr(self, "_docs"): query = self.control.Query("docs_with_pub_status", "*") query.order("pv", "cre_date", "ver_date") query.where(query.Condition("doc_type", self.name)) start, end = self.control.start, self.control.end if start: query.where(query.Condition("cre_date", start, ">=")) if end: end = f"{end} 23:59:59" query.where(query.Condition("cre_date", end, "<=")) rows = query.execute(self.control.cursor).fetchall() self._docs = [Doc(self.control, row) for row in rows] return self._docs @property def name(self): """String for this document type's name.""" return self.__name @property def table(self): """Table of new documents (or None if there aren't any).""" if not hasattr(self, "_table"): self._table = None if self.docs: opts = dict(caption=self.name, columns=self.COLUMNS) rows = [doc.row for doc in self.docs] self._table = Reporter.Table(rows, **opts) return self._table
class Control(Controller): """Report logic.""" SUBTITLE = "Glossary Term Links QC Report" NAME_PATH = "/GlossaryTermName/TermName/TermNameString" SOURCE_PATH = "/GlossaryTermName/TermName/TermNameSource" COLUMNS = ( Reporter.Column("Doc ID", classes="doc-id"), Reporter.Column("Doc Title", classes="doc-title"), Reporter.Column("Element Name", classes="element-name"), Reporter.Column("Fragment ID", classes="frag-id"), ) def populate_form(self, page): """Add the two fields to the form for selecting a term name.""" fieldset = page.fieldset("Select a Glossary Term by Name or ID") fieldset.append(page.text_field("id", label="CDR ID")) fieldset.append(page.text_field("name", label="Term Name")) page.form.append(fieldset) def build_tables(self): """Assemble the report tables, one for each linking document type.""" tables = [] for doctype in sorted(self.types): rows = [] for linker in self.types[doctype]: rows += linker.rows opts = dict(cols=self.COLUMNS, caption=doctype, classes="linkers") tables.append(self.Reporter.Table(rows, **opts)) return tables def show_report(self): """Override the base class version to add a top table and css.""" css = ( "#name-and-source tr * { padding: 2px 5px; }", "#name-and-source { width: auto; }", "#name-and-source th { text-align: right; }", ".doc-id { width: 125px; }", ".doc-title { width: auto; }", ".element-name {width: 150px; }", ".frag-id { width: 100px; }", "table { width: auto; margin-top: 50px; }", "table.linkers { width: 90%; }", ) page = self.report.page h2 = page.B.H2("Documents Linked To Glossary Term Names Report") h2.set("class", "center") page.body.insert(1, h2) table = page.B.TABLE( page.B.CAPTION("Glossary Term"), page.B.TR(page.B.TH("Name"), page.B.TD(self.name)), page.B.TR(page.B.TH("Source"), page.B.TD(self.source))) table.set("id", "name-and-source") page.body.insert(2, table) h4 = page.B.H4("Documents Linked to Term Name") h4.set("class", "center emphasis") page.body.insert(3, h4) self.report.page.add_css("\n".join(css)) self.report.send() @property def types(self): """Dictionary of linking document lists, indexed by document type.""" if not hasattr(self, "_types"): fields = "d.id", "t.name" query = self.Query("document d", *fields).unique().order("d.id") query.join("doc_type t", "t.id = d.doc_type") query.join("query_term l", "l.doc_id = d.id") query.where(query.Condition("l.int_val", self.id)) self._types = {} for row in query.execute(self.cursor).fetchall(): doc = LinkingDoc(self, row.id) if row.name not in self._types: self._types[row.name] = [doc] else: self._types[row.name].append(doc) return self._types @property def cdr_id(self): """String version if the glossary term's CDR ID.""" if not hasattr(self, "_cdr_id"): self._cdr_id = f"CDR{self.id:010d}" return self._cdr_id @property def id(self): """Integer for the glossary term name document ID.""" if not hasattr(self, "_id"): id = self.fields.getvalue("id") if id: try: self._id = Doc.extract_id(id) except: self.bail(f"Invalid id: {id!r}") else: name = self.fields.getvalue("name") if not name: self.show_form() query = self.Query("query_term", "doc_id").unique() query.where(f"path = '{self.NAME_PATH}'") query.where(query.Condition("value", name)) rows = query.execute(self.cursor).fetchall() if len(rows) > 1: self.bail(f"Ambiguous term name: {name!r}") elif not rows: self.bail(f"Unknown term {name!r}") self._id = rows[0].doc_id return self._id @property def name(self): """String for the glossary term name.""" if not hasattr(self, "_name"): query = self.Query("query_term", "value") query.where(f"path = '{self.NAME_PATH}'") query.where(query.Condition("doc_id", self.id)) rows = query.execute(self.cursor).fetchall() self._name = rows[0].value return self._name @property def source(self): """Source for the term name, if any.""" if not hasattr(self, "_source"): query = self.Query("query_term", "value") query.where(f"path = '{self.SOURCE_PATH}'") query.where(query.Condition("doc_id", self.id)) rows = query.execute(self.cursor).fetchall() self._source = rows[0].value if rows else None return self._source @property def cdr_id(self): """Display version of the term name document ID.""" if not hasattr(self, "_cdr_id"): self._cdr_id = f"CDR{self.id:010d}" return self._cdr_id @property def xpath(self): """Search path for finding links to this document.""" if not hasattr(self, "_xpath"): tests = ( f"@cdr:ref='{self.cdr_id}'", f"@cdr:href='{self.cdr_id}'", ) self._xpath = f"//*[{' or '.join(tests)}]" return self._xpath
class Control(Controller): "Report logic." "" SUBTITLE = ("Report on Links From One Section of a Summary " "to Another Section") COLUMNS = ( Reporter.Column("FragID", width="75px"), Reporter.Column("Target Section/Subsection", width="500px"), Reporter.Column("Linking Section/Subsection", width="500px"), Reporter.Column("Text in Linking Node", width="500px"), Reporter.Column("In Table?", width="75px"), Reporter.Column("In List?", width="75px"), ) def populate_form(self, page): """Ask the user for a document ID. Pass: page - HTMLPage object to which the ID field is attached """ fieldset = page.fieldset("Specify a Summary Document") fieldset.append(page.text_field("id", label="Summary ID")) page.form.append(fieldset) def build_tables(self): """Return the single table for this report.""" return self.table @property def doc(self): """The `Doc` object for the report's Summary document.""" if not hasattr(self, "_doc"): self._doc = Doc(self.session, id=self.id) return self._doc @property def format(self): """Generate this report as an Excel workbook.""" return "excel" @property def id(self): """CDR ID of the summary for this report.""" return self.fields.getvalue("id") @property def rows(self): """Rows for the report table.""" if not hasattr(self, "_rows"): self._rows = [] for target in sorted(self.targets.values()): args = target.id, len(target.links) self.logger.debug("target %s has %d links", *args) opts = {} rowspan = len(target.links) if rowspan > 1: opts = dict(rowspan=rowspan) link = target.links[0] row = [ self.Reporter.Cell(target.id, right=True, **opts), self.Reporter.Cell(target.section, **opts), link.section, link.text, self.Reporter.Cell(link.in_table, center=True), self.Reporter.Cell(link.in_list, center=True), ] self._rows.append(row) for link in target.links[1:]: row = [ link.section, link.text, self.Reporter.Cell(link.in_table, center=True), self.Reporter.Cell(link.in_list, center=True), ] self._rows.append(row) args = len(self._rows), self.doc.cdr_id self.logger.info("%d internal links found in %s", *args) return self._rows @property def table(self): """Create the table for the document's internal links.""" if not hasattr(self, "_table"): self._table = None if self.rows: caption = f"Links for CDR{self.doc.id} ({self.doc.title})" opts = dict(columns=self.COLUMNS, caption=caption) self._table = Reporter.Table(self.rows, **opts) return self._table @property def targets(self): """Collect the targets by following the links.""" if not hasattr(self, "_targets"): self._targets = {} dead_links = set() root = self.doc.root opts = dict(namespaces=Doc.NSMAP) linking_nodes = root.xpath(self.xpath, **opts) for linking_node in linking_nodes: link = Link(linking_node) if link.id and link.id not in dead_links: if link.id not in self._targets: xpath = f"//*[@cdr:id = '{link.id}']" nodes = root.xpath(xpath, **opts) if len(nodes) > 1: args = len(nodes), self.id self.logger.warning("%d nodes have id %s", *args) if nodes: args = link.id, nodes[0] self._targets[link.id] = Target(*args) else: self.logger.warning("cdr:id %s not found", link.id) dead.add(link.id) if link.id in self._targets: self._targets[link.id].add(link) return self._targets @property def xpath(self): """String for finding the linking nodes.""" return f"//*[starts-with(@cdr:href, '{self.doc.cdr_id}#')]"
class DoctypeCounts: """Counts by status of new documents of a specific type.""" COLUMNS = ( Reporter.Column("Status", width="250px"), Reporter.Column("Count", width="75px"), ) def __init__(self, control, name): """Save the caller's values. Pass: control - access to the database name - string for this document type's name """ self.__control = control self.__name = name @property def control(self): """Access to the database.""" return self.__control @property def counts(self): """Dictionary of counts per status (None if we have no docs).""" if not hasattr(self, "_counts"): self._counts = {} for doc in self.docs: self._counts[doc.status] = self._counts.get(doc.status, 0) + 1 return self._counts @property def docs(self): """Documents of this type created during the report's date range.""" if not hasattr(self, "_docs"): query = self.control.Query("document d", "d.id") query.join("doc_created c", "c.doc_id = d.id") query.where(query.Condition("d.doc_type", self.id)) start, end = self.control.start, self.control.end if start: query.where(query.Condition("c.created", start, ">=")) if end: end = f"{end} 23:59:59" query.where(query.Condition("c.created", end, "<=")) rows = query.execute(self.control.cursor).fetchall() self._docs = [NewDocument(self.control, row.id) for row in rows] return self._docs @property def id(self): """Integer unique ID for the document type.""" if not hasattr(self, "_id"): self._id = Doctype(self.control.session, name=self.name).id return self._id @property def name(self): """String for this document type's name.""" return self.__name @property def rows(self): """Sequence of table rows (empty if we have no matching docs).""" if not hasattr(self, "_rows"): self._rows = [] if self.counts: for name in NewDocument.STATUSES: cell = Reporter.Cell(self.counts.get(name, 0), right=True) self._rows.append((name, cell)) return self._rows @property def table(self): """Status counts table (or None if we have no matching documents).""" if not hasattr(self, "_table"): self._table = None if self.rows: opts = dict(caption=self.name, columns=self.COLUMNS) self._table = Reporter.Table(self.rows, **opts) return self._table