class NewsContent(Base): __table__ = Table( 'news_content', Base.metadata, Column('id', Integer, primary_key=True, autoincrement=True), Column('WEBSITE_ID', Integer), Column('CRAWL_URL', VARCHAR(100)), Column('NEWS_NAME', VARCHAR(100)), Column('NEWS_URL', VARCHAR(100)), Column('NEWS_IMAGE', VARCHAR(100)), Column('NEWS_DESC', TEXT), Column('KEYWORDS', VARCHAR(100)), Column('PUBLISH_TIME', DATE), Column('NEWS_RESOURCE', VARCHAR(50)), Column('NEWS_AUTHOR', VARCHAR(50)), Column('COMMENT_NUM', Integer), Column('READ_NUM', Integer))
class ExportedTaskEmail(Base): """ Represents an individual email export. """ __tablename__ = "_exported_task_email" id = Column("id", BigInteger, primary_key=True, autoincrement=True, comment="Arbitrary primary key") exported_task_id = Column("exported_task_id", BigInteger, ForeignKey(ExportedTask.id), nullable=False, comment="FK to {}.{}".format( ExportedTask.__tablename__, ExportedTask.id.name)) email_id = Column("email_id", BigInteger, ForeignKey(Email.id), comment="FK to {}.{}".format(Email.__tablename__, Email.id.name)) exported_task = relationship(ExportedTask) email = relationship(Email) def __init__(self, exported_task: ExportedTask = None) -> None: """ Args: exported_task: :class:`ExportedTask` object """ self.exported_task = exported_task def export_task(self, req: "CamcopsRequest") -> None: """ Exports the task itself to an email. Args: req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` """ exported_task = self.exported_task task = exported_task.task recipient = exported_task.recipient task_format = recipient.task_format task_filename = os.path.basename(recipient.get_filename(req, task)) # ... we don't want a full path for e-mail! encoding = "utf8" # Export task attachments = [] # type: List[Tuple[str, bytes]] if task_format == FileType.PDF: binary = task.get_pdf(req) elif task_format == FileType.HTML: binary = task.get_html(req).encode(encoding) elif task_format == FileType.XML: binary = task.get_xml(req).encode(encoding) else: raise AssertionError("Unknown task_format") attachments.append((task_filename, binary)) self.email = Email( from_addr=recipient.email_from, # date: automatic sender=recipient.email_sender, reply_to=recipient.email_reply_to, to=recipient.email_to, cc=recipient.email_cc, bcc=recipient.email_bcc, subject=recipient.get_email_subject(req, task), body=recipient.get_email_body(req, task), content_type=(CONTENT_TYPE_HTML if recipient.email_body_as_html else CONTENT_TYPE_TEXT), charset=encoding, attachments_binary=attachments, save_msg_string=recipient.email_keep_message, ) self.email.send( host=recipient.email_host, username=recipient.email_host_username, password=recipient.email_host_password, port=recipient.email_port, use_tls=recipient.email_use_tls, ) if self.email.sent: exported_task.succeed() else: exported_task.abort("Failed to send e-mail")
class SimpleTask(Base, metaclass=SimpleTaskMetaclass): __tablename__ = "table_for_simple_task" some_pk = Column("some_pk", Integer, primary_key=True)
class CbiR(TaskHasPatientMixin, TaskHasRespondentMixin, Task, metaclass=CbiRMetaclass): """ Server implementation of the CBI-R task. """ __tablename__ = "cbir" shortname = "CBI-R" longname = "Cambridge Behavioural Inventory, Revised" confirm_blanks = CamcopsColumn( "confirm_blanks", Integer, permitted_value_checker=BIT_CHECKER, comment="Respondent confirmed that blanks are deliberate (N/A) " "(0/NULL no, 1 yes)") comments = Column("comments", UnicodeText, comment="Additional comments") MIN_SCORE = 0 MAX_SCORE = 4 QNUMS_MEMORY = (1, 8) # tuple: first, last QNUMS_EVERYDAY = (9, 13) QNUMS_SELF = (14, 17) QNUMS_BEHAVIOUR = (18, 23) QNUMS_MOOD = (24, 27) QNUMS_BELIEFS = (28, 30) QNUMS_EATING = (31, 34) QNUMS_SLEEP = (35, 36) QNUMS_STEREOTYPY = (37, 40) QNUMS_MOTIVATION = (41, 45) NQUESTIONS = 45 TASK_FIELDS = (strseq("frequency", 1, NQUESTIONS) + strseq("distress", 1, NQUESTIONS)) def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="memory_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_MEMORY), comment="Memory/orientation: frequency score (% of max)"), SummaryElement( name="memory_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_MEMORY), comment="Memory/orientation: distress score (% of max)"), SummaryElement( name="everyday_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_EVERYDAY), comment="Everyday skills: frequency score (% of max)"), SummaryElement( name="everyday_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_EVERYDAY), comment="Everyday skills: distress score (% of max)"), SummaryElement(name="selfcare_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_SELF), comment="Self-care: frequency score (% of max)"), SummaryElement(name="selfcare_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_SELF), comment="Self-care: distress score (% of max)"), SummaryElement( name="behaviour_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_BEHAVIOUR), comment="Abnormal behaviour: frequency score (% of max)"), SummaryElement( name="behaviour_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_BEHAVIOUR), comment="Abnormal behaviour: distress score (% of max)"), SummaryElement(name="mood_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_MOOD), comment="Mood: frequency score (% of max)"), SummaryElement(name="mood_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_MOOD), comment="Mood: distress score (% of max)"), SummaryElement(name="beliefs_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_BELIEFS), comment="Beliefs: frequency score (% of max)"), SummaryElement(name="beliefs_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_BELIEFS), comment="Beliefs: distress score (% of max)"), SummaryElement( name="eating_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_EATING), comment="Eating habits: frequency score (% of max)"), SummaryElement(name="eating_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_EATING), comment="Eating habits: distress score (% of max)"), SummaryElement(name="sleep_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_SLEEP), comment="Sleep: frequency score (% of max)"), SummaryElement(name="sleep_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_SLEEP), comment="Sleep: distress score (% of max)"), SummaryElement( name="stereotypic_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_STEREOTYPY), comment="Stereotypic and motor behaviours: frequency " "score (% of max)"), SummaryElement( name="stereotypic_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_STEREOTYPY), comment="Stereotypic and motor behaviours: distress " "score (% of max)"), SummaryElement( name="motivation_frequency_pct", coltype=Float(), value=self.frequency_subscore(*self.QNUMS_MOTIVATION), comment="Motivation: frequency score (% of max)"), SummaryElement( name="motivation_distress_pct", coltype=Float(), value=self.distress_subscore(*self.QNUMS_MOTIVATION), comment="Motivation: distress score (% of max)"), ] def subscore(self, first: int, last: int, fieldprefix: str) \ -> Optional[float]: score = 0 n = 0 for q in range(first, last + 1): value = getattr(self, fieldprefix + str(q)) if value is not None: score += value / self.MAX_SCORE n += 1 return 100 * score / n if n > 0 else None def frequency_subscore(self, first: int, last: int) -> Optional[float]: return self.subscore(first, last, "frequency") def distress_subscore(self, first: int, last: int) -> Optional[float]: return self.subscore(first, last, "distress") def is_complete(self) -> bool: if (not self.field_contents_valid() or not self.is_respondent_complete()): return False if self.confirm_blanks: return True return self.are_all_fields_complete(self.TASK_FIELDS) def get_task_html(self, req: CamcopsRequest) -> str: freq_dict = {None: None} distress_dict = {None: None} for a in range(self.MIN_SCORE, self.MAX_SCORE + 1): freq_dict[a] = self.wxstring(req, "f" + str(a)) distress_dict[a] = self.wxstring(req, "d" + str(a)) heading_memory = self.wxstring(req, "h_memory") heading_everyday = self.wxstring(req, "h_everyday") heading_selfcare = self.wxstring(req, "h_selfcare") heading_behaviour = self.wxstring(req, "h_abnormalbehaviour") heading_mood = self.wxstring(req, "h_mood") heading_beliefs = self.wxstring(req, "h_beliefs") heading_eating = self.wxstring(req, "h_eating") heading_sleep = self.wxstring(req, "h_sleep") heading_motor = self.wxstring(req, "h_stereotypy_motor") heading_motivation = self.wxstring(req, "h_motivation") def get_question_rows(first, last): html = "" for q in range(first, last + 1): f = getattr(self, "frequency" + str(q)) d = getattr(self, "distress" + str(q)) fa = ("{}: {}".format(f, get_from_dict(freq_dict, f)) if f is not None else None) da = ("{}: {}".format(d, get_from_dict(distress_dict, d)) if d is not None else None) html += tr( self.wxstring(req, "q" + str(q)), answer(fa), answer(da), ) return html h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {complete_tr} </table> <table class="{CssClass.SUMMARY}"> <tr> <th>Subscale</th> <th>Frequency (% of max)</th> <th>Distress (% of max)</th> </tr> <tr> <td>{heading_memory}</td> <td>{mem_f}</td> <td>{mem_d}</td> </tr> <tr> <td>{heading_everyday}</td> <td>{everyday_f}</td> <td>{everyday_d}</td> </tr> <tr> <td>{heading_selfcare}</td> <td>{self_f}</td> <td>{self_d}</td> </tr> <tr> <td>{heading_behaviour}</td> <td>{behav_f}</td> <td>{behav_d}</td> </tr> <tr> <td>{heading_mood}</td> <td>{mood_f}</td> <td>{mood_d}</td> </tr> <tr> <td>{heading_beliefs}</td> <td>{beliefs_f}</td> <td>{beliefs_d}</td> </tr> <tr> <td>{heading_eating}</td> <td>{eating_f}</td> <td>{eating_d}</td> </tr> <tr> <td>{heading_sleep}</td> <td>{sleep_f}</td> <td>{sleep_d}</td> </tr> <tr> <td>{heading_motor}</td> <td>{motor_f}</td> <td>{motor_d}</td> </tr> <tr> <td>{heading_motivation}</td> <td>{motivation_f}</td> <td>{motivation_d}</td> </tr> </table> </div> <table class="{CssClass.TASKDETAIL}"> {tr_blanks} {tr_comments} </table> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="50%">Question</th> <th width="25%">Frequency (0–4)</th> <th width="25%">Distress (0–4)</th> </tr> """.format( CssClass=CssClass, complete_tr=self.get_is_complete_tr(req), heading_memory=heading_memory, mem_f=answer(self.frequency_subscore(*self.QNUMS_MEMORY)), mem_d=answer(self.distress_subscore(*self.QNUMS_MEMORY)), heading_everyday=heading_everyday, everyday_f=answer(self.frequency_subscore(*self.QNUMS_EVERYDAY)), everyday_d=answer(self.distress_subscore(*self.QNUMS_EVERYDAY)), heading_selfcare=heading_selfcare, self_f=answer(self.frequency_subscore(*self.QNUMS_SELF)), self_d=answer(self.distress_subscore(*self.QNUMS_SELF)), heading_behaviour=heading_behaviour, behav_f=answer(self.frequency_subscore(*self.QNUMS_BEHAVIOUR)), behav_d=answer(self.distress_subscore(*self.QNUMS_BEHAVIOUR)), heading_mood=heading_mood, mood_f=answer(self.frequency_subscore(*self.QNUMS_MOOD)), mood_d=answer(self.distress_subscore(*self.QNUMS_MOOD)), heading_beliefs=heading_beliefs, beliefs_f=answer(self.frequency_subscore(*self.QNUMS_BELIEFS)), beliefs_d=answer(self.distress_subscore(*self.QNUMS_BELIEFS)), heading_eating=heading_eating, eating_f=answer(self.frequency_subscore(*self.QNUMS_EATING)), eating_d=answer(self.distress_subscore(*self.QNUMS_EATING)), heading_sleep=heading_sleep, sleep_f=answer(self.frequency_subscore(*self.QNUMS_SLEEP)), sleep_d=answer(self.distress_subscore(*self.QNUMS_SLEEP)), heading_motor=heading_motor, motor_f=answer(self.frequency_subscore(*self.QNUMS_STEREOTYPY)), motor_d=answer(self.distress_subscore(*self.QNUMS_STEREOTYPY)), heading_motivation=heading_motivation, motivation_f=answer( self.frequency_subscore(*self.QNUMS_MOTIVATION)), motivation_d=answer( self.distress_subscore(*self.QNUMS_MOTIVATION)), tr_blanks=tr( "Respondent confirmed that blanks are deliberate (N/A)", answer(get_yes_no(req, self.confirm_blanks))), tr_comments=tr("Comments", answer(self.comments, default="")), ) h += subheading_spanning_three_columns(heading_memory) h += get_question_rows(*self.QNUMS_MEMORY) h += subheading_spanning_three_columns(heading_everyday) h += get_question_rows(*self.QNUMS_EVERYDAY) h += subheading_spanning_three_columns(heading_selfcare) h += get_question_rows(*self.QNUMS_SELF) h += subheading_spanning_three_columns(heading_behaviour) h += get_question_rows(*self.QNUMS_BEHAVIOUR) h += subheading_spanning_three_columns(heading_mood) h += get_question_rows(*self.QNUMS_MOOD) h += subheading_spanning_three_columns(heading_beliefs) h += get_question_rows(*self.QNUMS_BELIEFS) h += subheading_spanning_three_columns(heading_eating) h += get_question_rows(*self.QNUMS_EATING) h += subheading_spanning_three_columns(heading_sleep) h += get_question_rows(*self.QNUMS_SLEEP) h += subheading_spanning_three_columns(heading_motor) h += get_question_rows(*self.QNUMS_STEREOTYPY) h += subheading_spanning_three_columns(heading_motivation) h += get_question_rows(*self.QNUMS_MOTIVATION) h += """ </table> """ return h
class ExportedTaskHL7Message(Base): """ Represents an individual HL7 message. """ __tablename__ = "_exported_task_hl7msg" id = Column("id", BigInteger, primary_key=True, autoincrement=True, comment="Arbitrary primary key") exported_task_id = Column("exported_task_id", BigInteger, ForeignKey(ExportedTask.id), nullable=False, comment="FK to {}.{}".format( ExportedTask.__tablename__, ExportedTask.id.name)) sent_at_utc = Column("sent_at_utc", DateTime, comment="Time message was sent at (UTC)") reply_at_utc = Column("reply_at_utc", DateTime, comment="Time message was replied to (UTC)") success = Column( "success", Boolean, comment="Message sent successfully and acknowledged by HL7 server") failure_reason = Column("failure_reason", Text, comment="Reason for failure") message = Column("message", LongText, comment="Message body, if kept") reply = Column("reply", Text, comment="Server's reply, if kept") exported_task = relationship(ExportedTask) def __init__(self, exported_task: ExportedTask = None, *args, **kwargs) -> None: """ Must support parameter-free construction, not least for :func:`merge_db`. """ super().__init__(*args, **kwargs) self.exported_task = exported_task self._hl7_msg = None # type: hl7.Message @reconstructor def init_on_load(self) -> None: """ Called when SQLAlchemy recreates an object; see https://docs.sqlalchemy.org/en/latest/orm/constructors.html. """ self._hl7_msg = None @staticmethod def task_acceptable_for_hl7(recipient: ExportRecipient, task: "Task") -> bool: """ Is the task valid for HL7 export. (For example, anonymous tasks and tasks missing key ID information may not be.) Args: recipient: an :class:`camcops_server.cc_modules.cc_exportrecipient.ExportRecipient` task: a :class:`camcops_server.cc_modules.cc_task.Task` object Returns: bool: valid? """ # noqa if not task: return False if task.is_anonymous: return False # Cannot send anonymous tasks via HL7 patient = task.patient if not patient: return False if not recipient.primary_idnum: return False # required for HL7 if not patient.has_idnum_type(recipient.primary_idnum): return False return True def valid(self) -> bool: """ Checks for internal validity; returns a bool. """ exported_task = self.exported_task task = exported_task.task recipient = exported_task.recipient return self.task_acceptable_for_hl7(recipient, task) def succeed(self, now: Pendulum = None) -> None: """ Record that we succeeded, and so did our associated task export. """ now = now or get_now_utc_datetime() self.success = True self.sent_at_utc = now self.exported_task.succeed() def abort(self, msg: str, diverted_not_sent: bool = False) -> None: """ Record that we failed, and so did our associated task export. (Called ``abort`` not ``fail`` because PyCharm has a bug relating to functions named ``fail``: https://stackoverflow.com/questions/21954959/pycharm-unreachable-code.) Args: msg: reason for failure diverted_not_sent: deliberately diverted (and not counted as sent) rather than a sending failure? """ self.success = False self.failure_reason = msg self.exported_task.abort( "HL7 message deliberately not sent; diverted to file" if diverted_not_sent else "HL7 sending failed") def export_task(self, req: "CamcopsRequest") -> None: """ Exports the task itself to an HL7 message. Args: req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` """ if not self.valid(): self.abort( "Unsuitable for HL7; should have been filtered out earlier") return self.make_hl7_message(req) recipient = self.exported_task.recipient if recipient.hl7_debug_divert_to_file: self.divert_to_file(req) else: # Proper HL7 message self.transmit_hl7() def divert_to_file(self, req: "CamcopsRequest") -> None: """ Write an HL7 message to a file. For debugging. Args: req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` """ exported_task = self.exported_task recipient = exported_task.recipient filename = recipient.get_filename(req, exported_task.task, override_task_format="hl7") now_utc = get_now_utc_pendulum() log.info("Diverting HL7 message to file {!r}", filename) written = exported_task.export_file(filename=filename, text=str(self._hl7_msg)) if not written: return if recipient.hl7_debug_treat_diverted_as_sent: self.sent_at_utc = now_utc self.succeed(now_utc) else: self.abort("Exported to file as requested but not sent via HL7", diverted_not_sent=True) def make_hl7_message(self, req: "CamcopsRequest") -> None: """ Makes an HL7 message and stores it in ``self._hl7_msg``. May also store it in ``self.message`` (which is saved to the database), if we're saving HL7 messages. See - http://python-hl7.readthedocs.org/en/latest/index.html """ task = self.exported_task.task recipient = self.exported_task.recipient # --------------------------------------------------------------------- # Parts # --------------------------------------------------------------------- msh_segment = make_msh_segment(message_datetime=req.now, message_control_id=str(self.id)) pid_segment = task.get_patient_hl7_pid_segment(req, recipient) other_segments = task.get_hl7_data_segments(req, recipient) # --------------------------------------------------------------------- # Whole message # --------------------------------------------------------------------- segments = [msh_segment, pid_segment] + other_segments self._hl7_msg = hl7.Message(SEGMENT_SEPARATOR, segments) if recipient.hl7_keep_message: self.message = str(self._hl7_msg) def transmit_hl7(self) -> None: """ Sends the HL7 message over TCP/IP. - Default MLLP/HL7 port is 2575 - MLLP = minimum lower layer protocol - http://www.cleo.com/support/byproduct/lexicom/usersguide/mllp_configuration.htm - http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=hl7 - Essentially just a TCP socket with a minimal wrapper: http://stackoverflow.com/questions/11126918 - http://python-hl7.readthedocs.org/en/latest/api.html; however, we've modified that """ # noqa recipient = self.exported_task.recipient if recipient.hl7_ping_first: pinged = self.ping_hl7_server(recipient) if not pinged: self.abort("Could not ping HL7 host") return try: log.info("Sending HL7 message to {}:{}", recipient.hl7_host, recipient.hl7_port) with MLLPTimeoutClient(recipient.hl7_host, recipient.hl7_port, recipient.hl7_network_timeout_ms) as client: server_replied, reply = client.send_message(self._hl7_msg) except socket.timeout: self.abort("Failed to send message via MLLP: timeout") return except Exception as e: self.abort("Failed to send message via MLLP: {}".format(e)) return if not server_replied: self.abort("No response from server") return self.reply_at_utc = get_now_utc_datetime() if recipient.hl7_keep_reply: self.reply = reply try: replymsg = hl7.parse(reply) except Exception as e: self.abort("Malformed reply: {}".format(e)) return success, failure_reason = msg_is_successful_ack(replymsg) if success: self.succeed() else: self.abort(failure_reason) @staticmethod def ping_hl7_server(recipient: ExportRecipient) -> bool: """ Performs a TCP/IP ping on our HL7 server; returns success. If we've already pinged successfully during this run, don't bother doing it again. (No HL7 PING method yet. Proposal is http://hl7tsc.org/wiki/index.php?title=FTSD-ConCalls-20081028 So use TCP/IP ping.) Args: recipient: an :class:`camcops_server.cc_modules.cc_exportrecipient.ExportRecipient` Returns: bool: success """ # noqa timeout_s = min(recipient.hl7_network_timeout_ms // 1000, 1) if ping(hostname=recipient.hl7_host, timeout_s=timeout_s): return True else: log.error("Failed to ping {!r}", recipient.hl7_host) return False
class AppliancesBrand(Base): __tablename__ = 'appliances_brand' id = Column(Integer, primary_key=True) brand_name = Column(String, unique=True)
class Address(Base): __tablename__ = 'addresses' id = Column(Integer, primary_key=True) name = Column(String, unique=True)
class HonosBase(TaskHasPatientMixin, TaskHasClinicianMixin, Task): __abstract__ = True provides_trackers = True period_rated = Column("period_rated", UnicodeText, comment="Period being rated") COPYRIGHT_DIV = f""" <div class="{CssClass.COPYRIGHT}"> Health of the Nation Outcome Scales: Copyright © Royal College of Psychiatrists. Used here with permission. </div> """ QFIELDS = None # type: List[str] # must be overridden MAX_SCORE = None # type: int # must be overridden def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo( value=self.total_score(), plot_label=f"{self.shortname} total score", axis_label=f"Total score (out of {self.MAX_SCORE})", axis_min=-0.5, axis_max=self.MAX_SCORE + 0.5, ) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo(content=(f"{self.shortname} total score " f"{self.total_score()}/{self.MAX_SCORE}")) ] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="total", coltype=Integer(), value=self.total_score(), comment=f"Total score (/{self.MAX_SCORE})", ) ] def _total_score_for_fields(self, fieldnames: List[str]) -> int: total = 0 for qname in fieldnames: value = getattr(self, qname) if value is not None and 0 <= value <= 4: # i.e. ignore null values and 9 (= not known) total += value return total def total_score(self) -> int: return self._total_score_for_fields(self.QFIELDS) def get_q(self, req: CamcopsRequest, q: int) -> str: return self.wxstring(req, "q" + str(q) + "_s") def get_answer(self, req: CamcopsRequest, q: int, a: int) -> Optional[str]: if a == 9: return self.wxstring(req, "option9") if a is None or a < 0 or a > 4: return None return self.wxstring(req, "q" + str(q) + "_option" + str(a))
class CPFTLPSDischarge(TaskHasPatientMixin, TaskHasClinicianMixin, Task): """ Server implementation of the CPFT_LPS_Discharge task. """ __tablename__ = "cpft_lps_discharge" shortname = "CPFT_LPS_Discharge" info_filename_stem = "clinical" discharge_date = Column("discharge_date", Date) discharge_reason_code = CamcopsColumn("discharge_reason_code", UnicodeText, exempt_from_anonymisation=True) leaflet_or_discharge_card_given = BoolColumn( "leaflet_or_discharge_card_given", constraint_name="ck_cpft_lps_discharge_lodcg", ) frequent_attender = BoolColumn("frequent_attender") patient_wanted_copy_of_letter = BoolColumn( # Was previously text! That wasn't right. "patient_wanted_copy_of_letter") gaf_at_first_assessment = CamcopsColumn( "gaf_at_first_assessment", Integer, permitted_value_checker=PermittedValueChecker(minimum=0, maximum=100), ) gaf_at_discharge = CamcopsColumn( "gaf_at_discharge", Integer, permitted_value_checker=PermittedValueChecker(minimum=0, maximum=100), ) referral_reason_self_harm_overdose = BoolColumn( "referral_reason_self_harm_overdose", constraint_name="ck_cpft_lps_discharge_rrshoverdose", ) referral_reason_self_harm_other = BoolColumn( "referral_reason_self_harm_other", constraint_name="ck_cpft_lps_discharge_rrshother", ) referral_reason_suicidal_ideas = BoolColumn( "referral_reason_suicidal_ideas", constraint_name="ck_cpft_lps_discharge_rrsuicidal", ) referral_reason_behavioural_disturbance = BoolColumn( "referral_reason_behavioural_disturbance", constraint_name="ck_cpft_lps_discharge_behavdisturb", ) referral_reason_low_mood = BoolColumn("referral_reason_low_mood") referral_reason_elevated_mood = BoolColumn("referral_reason_elevated_mood") referral_reason_psychosis = BoolColumn("referral_reason_psychosis") referral_reason_pre_transplant = BoolColumn( "referral_reason_pre_transplant", constraint_name="ck_cpft_lps_discharge_pretransplant", ) referral_reason_post_transplant = BoolColumn( "referral_reason_post_transplant", constraint_name="ck_cpft_lps_discharge_posttransplant", ) referral_reason_delirium = BoolColumn("referral_reason_delirium") referral_reason_anxiety = BoolColumn("referral_reason_anxiety") referral_reason_somatoform_mus = BoolColumn( "referral_reason_somatoform_mus", constraint_name="ck_cpft_lps_discharge_mus", ) referral_reason_motivation_adherence = BoolColumn( "referral_reason_motivation_adherence", constraint_name="ck_cpft_lps_discharge_motivadherence", ) referral_reason_capacity = BoolColumn("referral_reason_capacity") referral_reason_eating_disorder = BoolColumn( "referral_reason_eating_disorder", constraint_name="ck_cpft_lps_discharge_eatingdis", ) referral_reason_safeguarding = BoolColumn("referral_reason_safeguarding") referral_reason_discharge_placement = BoolColumn( "referral_reason_discharge_placement", constraint_name="ck_cpft_lps_discharge_dcplacement", ) referral_reason_cognitive_problem = BoolColumn( "referral_reason_cognitive_problem", constraint_name="ck_cpft_lps_discharge_cognitiveprob", ) referral_reason_substance_alcohol = BoolColumn( "referral_reason_substance_alcohol", constraint_name="ck_cpft_lps_discharge_alcohol", ) referral_reason_substance_other = BoolColumn( "referral_reason_substance_other", constraint_name="ck_cpft_lps_discharge_substanceother", ) referral_reason_other = BoolColumn("referral_reason_other") referral_reason_transplant_organ = CamcopsColumn( "referral_reason_transplant_organ", UnicodeText, exempt_from_anonymisation=True, ) referral_reason_other_detail = Column("referral_reason_other_detail", UnicodeText) diagnosis_no_active_mental_health_problem = BoolColumn( "diagnosis_no_active_mental_health_problem", constraint_name="ck_cpft_lps_discharge_nomhprob", ) diagnosis_psych_1_icd10code = Column("diagnosis_psych_1_icd10code", DiagnosticCodeColType) diagnosis_psych_1_description = CamcopsColumn( "diagnosis_psych_1_description", UnicodeText, exempt_from_anonymisation=True, ) diagnosis_psych_2_icd10code = Column("diagnosis_psych_2_icd10code", DiagnosticCodeColType) diagnosis_psych_2_description = CamcopsColumn( "diagnosis_psych_2_description", UnicodeText, exempt_from_anonymisation=True, ) diagnosis_psych_3_icd10code = Column("diagnosis_psych_3_icd10code", DiagnosticCodeColType) diagnosis_psych_3_description = CamcopsColumn( "diagnosis_psych_3_description", UnicodeText, exempt_from_anonymisation=True, ) diagnosis_psych_4_icd10code = Column("diagnosis_psych_4_icd10code", DiagnosticCodeColType) diagnosis_psych_4_description = CamcopsColumn( "diagnosis_psych_4_description", UnicodeText, exempt_from_anonymisation=True, ) diagnosis_medical_1 = Column("diagnosis_medical_1", UnicodeText) diagnosis_medical_2 = Column("diagnosis_medical_2", UnicodeText) diagnosis_medical_3 = Column("diagnosis_medical_3", UnicodeText) diagnosis_medical_4 = Column("diagnosis_medical_4", UnicodeText) management_assessment_diagnostic = BoolColumn( "management_assessment_diagnostic", constraint_name="ck_cpft_lps_discharge_mx_ass_diag", ) management_medication = BoolColumn("management_medication") management_specialling_behavioural_disturbance = BoolColumn( "management_specialling_behavioural_disturbance", # Constraint name too long for MySQL unless we do this: constraint_name="ck_cpft_lps_discharge_msbd", ) management_supportive_patient = BoolColumn("management_supportive_patient") management_supportive_carers = BoolColumn("management_supportive_carers") management_supportive_staff = BoolColumn("management_supportive_staff") management_nursing_management = BoolColumn("management_nursing_management") management_therapy_cbt = BoolColumn("management_therapy_cbt") management_therapy_cat = BoolColumn("management_therapy_cat") management_therapy_other = BoolColumn("management_therapy_other") management_treatment_adherence = BoolColumn( "management_treatment_adherence", constraint_name="ck_cpft_lps_discharge_mx_rx_adhere", ) management_capacity = BoolColumn("management_capacity") management_education_patient = BoolColumn("management_education_patient") management_education_carers = BoolColumn("management_education_carers") management_education_staff = BoolColumn("management_education_staff") management_accommodation_placement = BoolColumn( "management_accommodation_placement", constraint_name="ck_cpft_lps_discharge_accom", ) management_signposting_external_referral = BoolColumn( "management_signposting_external_referral", constraint_name="ck_cpft_lps_discharge_mx_signpostrefer", ) management_mha_s136 = BoolColumn("management_mha_s136") management_mha_s5_2 = BoolColumn("management_mha_s5_2") management_mha_s2 = BoolColumn("management_mha_s2") management_mha_s3 = BoolColumn("management_mha_s3") management_complex_case_conference = BoolColumn( "management_complex_case_conference", constraint_name="ck_cpft_lps_discharge_caseconf", ) management_other = BoolColumn("management_other") management_other_detail = Column("management_other_detail", UnicodeText) outcome = CamcopsColumn("outcome", UnicodeText, exempt_from_anonymisation=True) outcome_hospital_transfer_detail = Column( "outcome_hospital_transfer_detail", UnicodeText) outcome_other_detail = Column("outcome_other_detail", UnicodeText) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("CPFT LPS – discharge") def is_complete(self) -> bool: return bool(self.discharge_date and self.discharge_reason_code and # self.outcome and # v2.0.0 self.field_contents_valid()) def get_discharge_reason(self, req: CamcopsRequest) -> Optional[str]: if self.discharge_reason_code == "F": return self.wxstring(req, "reason_code_F") elif self.discharge_reason_code == "A": return self.wxstring(req, "reason_code_A") elif self.discharge_reason_code == "O": return self.wxstring(req, "reason_code_O") elif self.discharge_reason_code == "C": return self.wxstring(req, "reason_code_C") else: return None def get_referral_reasons(self, req: CamcopsRequest) -> List[str]: potential_referral_reasons = [ "referral_reason_self_harm_overdose", "referral_reason_self_harm_other", "referral_reason_suicidal_ideas", "referral_reason_behavioural_disturbance", "referral_reason_low_mood", "referral_reason_elevated_mood", "referral_reason_psychosis", "referral_reason_pre_transplant", "referral_reason_post_transplant", "referral_reason_delirium", "referral_reason_anxiety", "referral_reason_somatoform_mus", "referral_reason_motivation_adherence", "referral_reason_capacity", "referral_reason_eating_disorder", "referral_reason_safeguarding", "referral_reason_discharge_placement", "referral_reason_cognitive_problem", "referral_reason_substance_alcohol", "referral_reason_substance_other", "referral_reason_other", ] referral_reasons = [] for r in potential_referral_reasons: if getattr(self, r): referral_reasons.append(self.wxstring(req, "" + r)) return referral_reasons def get_managements(self, req: CamcopsRequest) -> List[str]: potential_managements = [ "management_assessment_diagnostic", "management_medication", "management_specialling_behavioural_disturbance", "management_supportive_patient", "management_supportive_carers", "management_supportive_staff", "management_nursing_management", "management_therapy_cbt", "management_therapy_cat", "management_therapy_other", "management_treatment_adherence", "management_capacity", "management_education_patient", "management_education_carers", "management_education_staff", "management_accommodation_placement", "management_signposting_external_referral", "management_mha_s136", "management_mha_s5_2", "management_mha_s2", "management_mha_s3", "management_complex_case_conference", "management_other", ] managements = [] for r in potential_managements: if getattr(self, r): managements.append(self.wxstring(req, "" + r)) return managements def get_psychiatric_diagnoses(self, req: CamcopsRequest) -> List[str]: psychiatric_diagnoses = ([ self.wxstring(req, "diagnosis_no_active_mental_health_problem") ] if self.diagnosis_no_active_mental_health_problem else []) for i in range(1, 4 + 1): # magic number if getattr(self, "diagnosis_psych_" + str(i) + "_icd10code"): psychiatric_diagnoses.append( ws.webify( getattr(self, "diagnosis_psych_" + str(i) + "_icd10code")) + " – " + ws.webify( getattr(self, "diagnosis_psych_" + str(i) + "_description"))) return psychiatric_diagnoses def get_medical_diagnoses(self) -> List[str]: medical_diagnoses = [] for i in range(1, 4 + 1): # magic number if getattr(self, "diagnosis_medical_" + str(i)): medical_diagnoses.append( ws.webify(getattr(self, "diagnosis_medical_" + str(i)))) return medical_diagnoses def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: diagnoses = (self.get_psychiatric_diagnoses(req) + self.get_medical_diagnoses()) return [ CtvInfo( heading=ws.webify(self.wxstring(req, "discharge_reason")), content=self.get_discharge_reason(req), ), CtvInfo( heading=ws.webify(self.wxstring(req, "referral_reason_t")), content=", ".join(self.get_referral_reasons(req)), ), CtvInfo( heading=ws.webify(self.wxstring(req, "diagnoses_t")), content=", ".join(diagnoses), ), CtvInfo( heading=ws.webify(self.wxstring(req, "management_t")), content=", ".join(self.get_managements(req)), ), CtvInfo( heading=ws.webify(self.wxstring(req, "outcome_t")), content=self.outcome, ), ] def get_task_html(self, req: CamcopsRequest) -> str: h = f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} </table> </div> <table class="{CssClass.TASKDETAIL}"> <col width="40%"> <col width="60%"> """ h += tr_qa( self.wxstring(req, "discharge_date"), format_datetime( self.discharge_date, DateFormat.LONG_DATE_WITH_DAY, default=None, ), "", ) h += tr_qa( self.wxstring(req, "discharge_reason"), self.get_discharge_reason(req), "", ) h += tr_qa( self.wxstring(req, "leaflet_or_discharge_card_given"), get_yes_no_none(req, self.leaflet_or_discharge_card_given), "", ) h += tr_qa( self.wxstring(req, "frequent_attender"), get_yes_no_none(req, self.frequent_attender), "", ) h += tr_qa( self.wxstring(req, "patient_wanted_copy_of_letter"), self.patient_wanted_copy_of_letter, "", ) h += tr_qa( self.wxstring(req, "gaf_at_first_assessment"), self.gaf_at_first_assessment, "", ) h += tr_qa(self.wxstring(req, "gaf_at_discharge"), self.gaf_at_discharge, "") h += subheading_spanning_two_columns( self.wxstring(req, "referral_reason_t")) h += tr_span_col(answer(", ".join(self.get_referral_reasons(req))), cols=2) h += tr_qa( self.wxstring(req, "referral_reason_transplant_organ"), self.referral_reason_transplant_organ, "", ) h += tr_qa( self.wxstring(req, "referral_reason_other_detail"), self.referral_reason_other_detail, "", ) h += subheading_spanning_two_columns(self.wxstring(req, "diagnoses_t")) h += tr_qa( self.wxstring(req, "psychiatric_t"), "\n".join(self.get_psychiatric_diagnoses(req)), "", ) h += tr_qa( self.wxstring(req, "medical_t"), "\n".join(self.get_medical_diagnoses()), "", ) h += subheading_spanning_two_columns(self.wxstring( req, "management_t")) h += tr_span_col(answer(", ".join(self.get_managements(req))), cols=2) h += tr_qa( self.wxstring(req, "management_other_detail"), self.management_other_detail, "", ) h += subheading_spanning_two_columns(self.wxstring(req, "outcome_t")) h += tr_qa(self.wxstring(req, "outcome_t"), self.outcome, "") h += tr_qa( self.wxstring(req, "outcome_hospital_transfer_detail"), self.outcome_hospital_transfer_detail, "", ) h += tr_qa( self.wxstring(req, "outcome_other_detail"), self.outcome_other_detail, "", ) h += """ </table> """ return h
of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ from sqlalchemy.sql.schema import Column, ForeignKey, Table from sqlalchemy.sql.sqltypes import Integer, String from heliotrope.database.orm.base import mapper_registry parody_table = Table( "parody", mapper_registry.metadata, Column("id", Integer, primary_key=True, autoincrement=True), Column("galleryinfo_id", Integer, ForeignKey("galleryinfo.id")), Column("parody", String, nullable=False), Column("url", String, nullable=False), )
class Honos65(HonosBase, metaclass=Honos65Metaclass): """ Server implementation of the HoNOS 65+ task. """ __tablename__ = "honos65" shortname = "HoNOS 65+" info_filename_stem = "honos" q8problemtype = CamcopsColumn( "q8problemtype", CharColType, permitted_value_checker=PermittedValueChecker( permitted_values=PV_PROBLEMTYPE), comment="Q8: type of problem (A phobic; B anxiety; " "C obsessive-compulsive; D stress; " # NB slight difference: D "E dissociative; F somatoform; G eating; H sleep; " "I sexual; J other, specify)", ) q8otherproblem = Column("q8otherproblem", UnicodeText, comment="Q8: other problem: specify") NQUESTIONS = 12 QFIELDS = strseq("q", 1, NQUESTIONS) MAX_SCORE = 48 @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Health of the Nation Outcome Scales, older adults") # noinspection PyUnresolvedReferences def is_complete(self) -> bool: if self.any_fields_none(self.QFIELDS): return False if not self.field_contents_valid(): return False if self.q8 != 0 and self.q8 != 9 and self.q8problemtype is None: return False if (self.q8 != 0 and self.q8 != 9 and self.q8problemtype == "J" and self.q8otherproblem is None): return False return self.period_rated is not None def get_task_html(self, req: CamcopsRequest) -> str: q8_problem_type_dict = { None: None, "A": self.wxstring(req, "q8problemtype_option_a"), "B": self.wxstring(req, "q8problemtype_option_b"), "C": self.wxstring(req, "q8problemtype_option_c"), "D": self.wxstring(req, "q8problemtype_option_d"), "E": self.wxstring(req, "q8problemtype_option_e"), "F": self.wxstring(req, "q8problemtype_option_f"), "G": self.wxstring(req, "q8problemtype_option_g"), "H": self.wxstring(req, "q8problemtype_option_h"), "I": self.wxstring(req, "q8problemtype_option_i"), "J": self.wxstring(req, "q8problemtype_option_j"), } one_to_eight = "" for i in range(1, 8 + 1): one_to_eight += tr_qa( self.get_q(req, i), self.get_answer(req, i, getattr(self, "q" + str(i))), ) nine_onwards = "" for i in range(9, Honos.NQUESTIONS + 1): nine_onwards += tr_qa( self.get_q(req, i), self.get_answer(req, i, getattr(self, "q" + str(i))), ) h = """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {total_score} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="50%">Question</th> <th width="50%">Answer <sup>[1]</sup></th> </tr> {period_rated} {one_to_eight} {q8problemtype} {q8otherproblem} {nine_onwards} </table> <div class="{CssClass.FOOTNOTES}"> {FOOTNOTE_SCORING} </div> {copyright_div} """.format( CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), total_score=tr( req.sstring(SS.TOTAL_SCORE), answer(self.total_score()) + f" / {self.MAX_SCORE}", ), period_rated=tr_qa(self.wxstring(req, "period_rated"), self.period_rated), one_to_eight=one_to_eight, q8problemtype=tr_qa( self.wxstring(req, "q8problemtype_s"), get_from_dict(q8_problem_type_dict, self.q8problemtype), ), q8otherproblem=tr_qa(self.wxstring(req, "q8otherproblem_s"), self.q8otherproblem), nine_onwards=nine_onwards, FOOTNOTE_SCORING=FOOTNOTE_SCORING, copyright_div=self.COPYRIGHT_DIV, ) return h def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression( req.snomed(SnomedLookup.HONOS65_PROCEDURE_ASSESSMENT)) ] if self.is_complete(): codes.append( SnomedExpression( req.snomed(SnomedLookup.HONOS65_SCALE), { req.snomed(SnomedLookup.HONOS65_SCORE): self.total_score() }, )) return codes
class Ace3(TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=Ace3Metaclass): """ Server implementation of the ACE-III task. """ __tablename__ = "ace3" shortname = "ACE-III" longname = "Addenbrooke’s Cognitive Examination III" provides_trackers = True age_at_leaving_full_time_education = Column( "age_at_leaving_full_time_education", Integer, comment="Age at leaving full time education") occupation = Column("occupation", UnicodeText, comment="Occupation") handedness = CamcopsColumn( "handedness", String(length=1), # was Text comment="Handedness (L or R)", permitted_value_checker=PermittedValueChecker( permitted_values=["L", "R"])) attn_num_registration_trials = Column( "attn_num_registration_trials", Integer, comment="Attention, repetition, number of trials (not scored)") fluency_letters_score = CamcopsColumn( "fluency_letters_score", Integer, comment="Fluency, words beginning with P, score 0-7", permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7)) fluency_animals_score = CamcopsColumn( "fluency_animals_score", Integer, comment="Fluency, animals, score 0-7", permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7)) lang_follow_command_practice = CamcopsColumn( "lang_follow_command_practice", Integer, comment="Language, command, practice trial (not scored)", permitted_value_checker=BIT_CHECKER) lang_read_words_aloud = CamcopsColumn( "lang_read_words_aloud", Integer, comment="Language, read five irregular words (0 or 1)", permitted_value_checker=BIT_CHECKER) vsp_copy_infinity = CamcopsColumn( "vsp_copy_infinity", Integer, comment="Visuospatial, copy infinity (0-1)", permitted_value_checker=BIT_CHECKER) vsp_copy_cube = CamcopsColumn( "vsp_copy_cube", Integer, comment="Visuospatial, copy cube (0-2)", permitted_value_checker=PermittedValueChecker(minimum=0, maximum=2)) vsp_draw_clock = CamcopsColumn( "vsp_draw_clock", Integer, comment="Visuospatial, draw clock (0-5)", permitted_value_checker=PermittedValueChecker(minimum=0, maximum=5)) picture1_blobid = CamcopsColumn("picture1_blobid", Integer, comment="Photo 1/2 PNG BLOB ID", is_blob_id_field=True, blob_relationship_attr_name="picture1") picture1_rotation = Column( # DEFUNCT as of v2.0.0 # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE "picture1_rotation", Integer, comment="Photo 1/2 rotation (degrees clockwise)") picture2_blobid = CamcopsColumn("picture2_blobid", Integer, comment="Photo 2/2 PNG BLOB ID", is_blob_id_field=True, blob_relationship_attr_name="picture2") picture2_rotation = Column( # DEFUNCT as of v2.0.0 # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE "picture2_rotation", Integer, comment="Photo 2/2 rotation (degrees clockwise)") comments = Column("comments", UnicodeText, comment="Clinician's comments") picture1 = blob_relationship( "Ace3", "picture1_blobid") # type: Optional[Blob] # noqa picture2 = blob_relationship( "Ace3", "picture2_blobid") # type: Optional[Blob] # noqa ATTN_SCORE_FIELDS = (strseq("attn_time", 1, 5) + strseq("attn_place", 1, 5) + strseq("attn_repeat_word", 1, 3) + strseq("attn_serial7_subtraction", 1, 5)) MEM_NON_RECOG_SCORE_FIELDS = (strseq("mem_recall_word", 1, 3) + strseq("mem_repeat_address_trial3_", 1, 7) + strseq("mem_famous", 1, 4) + strseq("mem_recall_address", 1, 7)) LANG_SIMPLE_SCORE_FIELDS = (strseq("lang_write_sentences_point", 1, 2) + strseq("lang_repeat_sentence", 1, 2) + strseq("lang_name_picture", 1, 12) + strseq("lang_identify_concept", 1, 4)) LANG_FOLLOW_CMD_FIELDS = strseq("lang_follow_command", 1, 3) LANG_REPEAT_WORD_FIELDS = strseq("lang_repeat_word", 1, 4) VSP_SIMPLE_SCORE_FIELDS = (strseq("vsp_count_dots", 1, 4) + strseq("vsp_identify_letter", 1, 4)) BASIC_COMPLETENESS_FIELDS = ( ATTN_SCORE_FIELDS + MEM_NON_RECOG_SCORE_FIELDS + ["fluency_letters_score", "fluency_animals_score"] + ["lang_follow_command_practice"] + LANG_SIMPLE_SCORE_FIELDS + LANG_REPEAT_WORD_FIELDS + [ "lang_read_words_aloud", "vsp_copy_infinity", "vsp_copy_cube", "vsp_draw_clock" ] + VSP_SIMPLE_SCORE_FIELDS + strseq("mem_recall_address", 1, 7)) def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: return [ TrackerInfo(value=self.total_score(), plot_label="ACE-III total score", axis_label="Total score (out of 100)", axis_min=-0.5, axis_max=100.5, horizontal_lines=[82.5, 88.5]) ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE a = self.attn_score() m = self.mem_score() f = self.fluency_score() lang = self.lang_score() v = self.vsp_score() t = a + m + f + lang + v text = ("ACE-III total: {t}/{tmax} " "(attention {a}/{amax}, memory {m}/{mmax}, " "fluency {f}/{fmax}, language {lang}/{lmax}, " "visuospatial {v}/{vmax})").format(t=t, a=a, m=m, f=f, lang=lang, v=v, tmax=TOTAL_MAX, amax=ATTN_MAX, mmax=MEMORY_MAX, fmax=FLUENCY_MAX, lmax=LANG_MAX, vmax=VSP_MAX) return [CtvInfo(content=text)] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement(name="total", coltype=Integer(), value=self.total_score(), comment="Total score (/{})".format(TOTAL_MAX)), SummaryElement(name="attn", coltype=Integer(), value=self.attn_score(), comment="Attention (/{})".format(ATTN_MAX)), SummaryElement(name="mem", coltype=Integer(), value=self.mem_score(), comment="Memory (/{})".format(MEMORY_MAX)), SummaryElement(name="fluency", coltype=Integer(), value=self.fluency_score(), comment="Fluency (/{})".format(FLUENCY_MAX)), SummaryElement(name="lang", coltype=Integer(), value=self.lang_score(), comment="Language (/{})".format(LANG_MAX)), SummaryElement(name="vsp", coltype=Integer(), value=self.vsp_score(), comment="Visuospatial (/{})".format(VSP_MAX)), ] def attn_score(self) -> int: return self.sum_fields(self.ATTN_SCORE_FIELDS) @staticmethod def get_recog_score(recalled: Optional[int], recognized: Optional[int]) -> int: if recalled == 1: return 1 return score_zero_for_absent(recognized) @staticmethod def get_recog_text(recalled: Optional[int], recognized: Optional[int]) -> str: if recalled: return "<i>1 (already recalled)</i>" return answer(recognized) # noinspection PyUnresolvedReferences def get_mem_recognition_score(self) -> int: score = 0 score += self.get_recog_score( (self.mem_recall_address1 == 1 and self.mem_recall_address2 == 1), self.mem_recognize_address1) score += self.get_recog_score((self.mem_recall_address3 == 1), self.mem_recognize_address2) score += self.get_recog_score( (self.mem_recall_address4 == 1 and self.mem_recall_address5 == 1), self.mem_recognize_address3) score += self.get_recog_score((self.mem_recall_address6 == 1), self.mem_recognize_address4) score += self.get_recog_score((self.mem_recall_address7 == 1), self.mem_recognize_address5) return score def mem_score(self) -> int: return (self.sum_fields(self.MEM_NON_RECOG_SCORE_FIELDS) + self.get_mem_recognition_score()) def fluency_score(self) -> int: return (score_zero_for_absent(self.fluency_letters_score) + score_zero_for_absent(self.fluency_animals_score)) def get_follow_command_score(self) -> int: if self.lang_follow_command_practice != 1: return 0 return self.sum_fields(self.LANG_FOLLOW_CMD_FIELDS) def get_repeat_word_score(self) -> int: n = self.sum_fields(self.LANG_REPEAT_WORD_FIELDS) return 2 if n >= 4 else (1 if n == 3 else 0) def lang_score(self) -> int: return (self.sum_fields(self.LANG_SIMPLE_SCORE_FIELDS) + self.get_follow_command_score() + self.get_repeat_word_score() + score_zero_for_absent(self.lang_read_words_aloud)) def vsp_score(self) -> int: return (self.sum_fields(self.VSP_SIMPLE_SCORE_FIELDS) + score_zero_for_absent(self.vsp_copy_infinity) + score_zero_for_absent(self.vsp_copy_cube) + score_zero_for_absent(self.vsp_draw_clock)) def total_score(self) -> int: return (self.attn_score() + self.mem_score() + self.fluency_score() + self.lang_score() + self.vsp_score()) # noinspection PyUnresolvedReferences def is_recognition_complete(self) -> bool: return ( ((self.mem_recall_address1 == 1 and self.mem_recall_address2 == 1) or self.mem_recognize_address1 is not None) and (self.mem_recall_address3 == 1 or self.mem_recognize_address2 is not None) and ((self.mem_recall_address4 == 1 and self.mem_recall_address5 == 1) or self.mem_recognize_address3 is not None) and (self.mem_recall_address6 == 1 or self.mem_recognize_address4 is not None) and (self.mem_recall_address7 == 1 or self.mem_recognize_address5 is not None)) def is_complete(self) -> bool: if not self.are_all_fields_complete(self.BASIC_COMPLETENESS_FIELDS): return False if not self.field_contents_valid(): return False if (self.lang_follow_command_practice == 1 and not self.are_all_fields_complete(self.LANG_FOLLOW_CMD_FIELDS)): return False return self.is_recognition_complete() # noinspection PyUnresolvedReferences def get_task_html(self, req: CamcopsRequest) -> str: def percent(score: int, maximum: int) -> str: return ws.number_to_dp(100 * score / maximum, PERCENT_DP) a = self.attn_score() m = self.mem_score() f = self.fluency_score() lang = self.lang_score() v = self.vsp_score() t = a + m + f + lang + v if self.is_complete(): figsize = (FULLWIDTH_PLOT_WIDTH / 3, FULLWIDTH_PLOT_WIDTH / 4) width = 0.9 fig = req.create_figure(figsize=figsize) ax = fig.add_subplot(1, 1, 1) scores = numpy.array([a, m, f, lang, v]) maxima = numpy.array( [ATTN_MAX, MEMORY_MAX, FLUENCY_MAX, LANG_MAX, VSP_MAX]) y = 100 * scores / maxima x_labels = ["Attn", "Mem", "Flu", "Lang", "VSp"] # noinspection PyTypeChecker n = len(y) xvar = numpy.arange(n) ax.bar(xvar, y, width, color="b") ax.set_ylabel("%", fontdict=req.fontdict) ax.set_xticks(xvar) x_offset = -0.5 ax.set_xlim(0 + x_offset, len(scores) + x_offset) ax.set_xticklabels(x_labels, fontdict=req.fontdict) fig.tight_layout() # or the ylabel drops off the figure # fig.autofmt_xdate() req.set_figure_font_sizes(ax) figurehtml = req.get_html_from_pyplot_figure(fig) else: figurehtml = "<i>Incomplete; not plotted</i>" return ( self.get_standard_clinician_comments_block(req, self.comments) + """ <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> <tr> {is_complete} <td class="{CssClass.FIGURE}" rowspan="7">{figurehtml}</td> </tr> """.format( # noqa CssClass=CssClass, is_complete=self.get_is_complete_td_pair(req), figurehtml=figurehtml) + tr("Total ACE-III score <sup>[1]</sup>", answer(t) + " / 100") + tr( "Attention", answer(a) + " / {} ({}%)".format(ATTN_MAX, percent(a, ATTN_MAX))) + tr( "Memory", answer(m) + " / {} ({}%)".format(MEMORY_MAX, percent(m, MEMORY_MAX))) + tr( "Fluency", answer(f) + " / {} ({}%)".format(FLUENCY_MAX, percent(f, FLUENCY_MAX))) + tr( "Language", answer(lang) + " / {} ({}%)".format(LANG_MAX, percent(lang, LANG_MAX))) + tr("Visuospatial", answer(v) + " / {} ({}%)".format(VSP_MAX, percent(v, VSP_MAX))) + """ </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="75%">Question</th> <th width="25%">Answer/score</td> </tr> """.format(CssClass=CssClass) + tr_qa("Age on leaving full-time education", self.age_at_leaving_full_time_education) + tr_qa("Occupation", ws.webify(self.occupation)) + tr_qa("Handedness", ws.webify(self.handedness)) + subheading_spanning_two_columns("Attention") + tr( "Day? Date? Month? Year? Season?", ", ".join([ answer(x) for x in [ self.attn_time1, self.attn_time2, self.attn_time3, self.attn_time4, self.attn_time5 ] ])) + tr( "House number/floor? Street/hospital? Town? County? Country?", ", ".join([ answer(x) for x in [ self.attn_place1, self.attn_place2, self.attn_place3, self.attn_place4, self.attn_place5 ] ])) + tr( "Repeat: Lemon? Key? Ball?", ", ".join([ answer(x) for x in [ self.attn_repeat_word1, self.attn_repeat_word2, self.attn_repeat_word3 ] ])) + tr( "Repetition: number of trials <i>(not scored)</i>", answer(self.attn_num_registration_trials, formatter_answer=italic)) + tr( "Serial subtractions: First correct? Second? Third? Fourth? " "Fifth?", ", ".join([ answer(x) for x in [ self.attn_serial7_subtraction1, self.attn_serial7_subtraction2, self.attn_serial7_subtraction3, self.attn_serial7_subtraction4, self.attn_serial7_subtraction5 ] ])) + subheading_spanning_two_columns("Memory (1)") + tr( "Recall: Lemon? Key? Ball?", ", ".join([ answer(x) for x in [ self.mem_recall_word1, self.mem_recall_word2, self.mem_recall_word3 ] ])) + subheading_spanning_two_columns("Fluency") + tr( "Score for words beginning with ‘P’ <i>(≥18 scores 7, 14–17 " "scores 6, 11–13 scores 5, 8–10 scores 4, 6–7 scores 3, " "4–5 scores 2, 2–3 scores 1, 0–1 scores 0)</i>", answer(self.fluency_letters_score) + " / 7") + tr( "Score for animals <i>(≥22 scores 7, 17–21 scores 6, " "14–16 scores 5, 11–13 scores 4, 9–10 scores 3, " "7–8 scores 2, 5–6 scores 1, <5 scores 0)</i>", answer(self.fluency_animals_score) + " / 7") + subheading_spanning_two_columns("Memory (2)") + tr( "Third trial of address registration: Harry? Barnes? 73? " "Orchard? Close? Kingsbridge? Devon?", ", ".join([ answer(x) for x in [ self.mem_repeat_address_trial3_1, self.mem_repeat_address_trial3_2, self.mem_repeat_address_trial3_3, self.mem_repeat_address_trial3_4, self.mem_repeat_address_trial3_5, self.mem_repeat_address_trial3_6, self.mem_repeat_address_trial3_7 ] ])) + tr( "Current PM? Woman who was PM? USA president? USA president " "assassinated in 1960s?", ", ".join([ answer(x) for x in [ self.mem_famous1, self.mem_famous2, self.mem_famous3, self.mem_famous4 ] ])) + subheading_spanning_two_columns("Language") + tr( "<i>Practice trial (“Pick up the pencil and then the " "paper”)</i>", answer(self.lang_follow_command_practice, formatter_answer=italic)) + tr_qa("“Place the paper on top of the pencil”", self.lang_follow_command1) + tr_qa("“Pick up the pencil but not the paper”", self.lang_follow_command2) + tr_qa("“Pass me the pencil after touching the paper”", self.lang_follow_command3) + tr( "Sentence-writing: point for ≥2 complete sentences about " "the one topic? Point for correct grammar and spelling?", ", ".join([ answer(x) for x in [ self.lang_write_sentences_point1, self.lang_write_sentences_point2 ] ])) + tr( "Repeat: caterpillar? eccentricity? unintelligible? " "statistician? <i>(score 2 if all correct, 1 if 3 correct, " "0 if ≤2 correct)</i>", "<i>{}, {}, {}, {}</i> (score <b>{}</b> / 2)".format( answer(self.lang_repeat_word1, formatter_answer=italic), answer(self.lang_repeat_word2, formatter_answer=italic), answer(self.lang_repeat_word3, formatter_answer=italic), answer(self.lang_repeat_word4, formatter_answer=italic), self.get_repeat_word_score(), )) + tr_qa("Repeat: “All that glitters is not gold”?", self.lang_repeat_sentence1) + tr_qa("Repeat: “A stitch in time saves nine”?", self.lang_repeat_sentence2) + tr( "Name pictures: spoon, book, kangaroo/wallaby", ", ".join([ answer(x) for x in [ self.lang_name_picture1, self.lang_name_picture2, self.lang_name_picture3 ] ])) + tr( "Name pictures: penguin, anchor, camel/dromedary", ", ".join([ answer(x) for x in [ self.lang_name_picture4, self.lang_name_picture5, self.lang_name_picture6 ] ])) + tr( "Name pictures: harp, rhinoceros/rhino, barrel/keg/tub", ", ".join([ answer(x) for x in [ self.lang_name_picture7, self.lang_name_picture8, self.lang_name_picture9 ] ])) + tr( "Name pictures: crown, alligator/crocodile, " "accordion/piano accordion/squeeze box", ", ".join([ answer(x) for x in [ self.lang_name_picture10, self. lang_name_picture11, self.lang_name_picture12 ] ])) + tr( "Identify pictures: monarchy? marsupial? Antarctic? nautical?", ", ".join([ answer(x) for x in [ self.lang_identify_concept1, self. lang_identify_concept2, self.lang_identify_concept3, self.lang_identify_concept4 ] ])) + tr_qa("Read all successfully: sew, pint, soot, dough, height", self.lang_read_words_aloud) + subheading_spanning_two_columns("Visuospatial") + tr("Copy infinity", answer(self.vsp_copy_infinity) + " / 1") + tr("Copy cube", answer(self.vsp_copy_cube) + " / 2") + tr("Draw clock with numbers and hands at 5:10", answer(self.vsp_draw_clock) + " / 5") + tr( "Count dots: 8, 10, 7, 9", ", ".join([ answer(x) for x in [ self.vsp_count_dots1, self.vsp_count_dots2, self.vsp_count_dots3, self.vsp_count_dots4 ] ])) + tr( "Identify letters: K, M, A, T", ", ".join([ answer(x) for x in [ self.vsp_identify_letter1, self.vsp_identify_letter2, self.vsp_identify_letter3, self.vsp_identify_letter4 ] ])) + subheading_spanning_two_columns("Memory (3)") + tr( "Recall address: Harry? Barnes? 73? Orchard? Close? " "Kingsbridge? Devon?", ", ".join([ answer(x) for x in [ self.mem_recall_address1, self.mem_recall_address2, self.mem_recall_address3, self.mem_recall_address4, self.mem_recall_address5, self.mem_recall_address6, self.mem_recall_address7 ] ])) + tr( "Recognize address: Jerry Barnes/Harry Barnes/Harry Bradford?", self.get_recog_text( (self.mem_recall_address1 == 1 and self.mem_recall_address2 == 1), self.mem_recognize_address1)) + tr( "Recognize address: 37/73/76?", self.get_recog_text((self.mem_recall_address3 == 1), self.mem_recognize_address2)) + tr( "Recognize address: Orchard Place/Oak Close/Orchard " "Close?", self.get_recog_text( (self.mem_recall_address4 == 1 and self.mem_recall_address5 == 1), self.mem_recognize_address3)) + tr( "Recognize address: Oakhampton/Kingsbridge/Dartington?", self.get_recog_text( (self.mem_recall_address6 == 1), self.mem_recognize_address4)) + tr( "Recognize address: Devon/Dorset/Somerset?", self.get_recog_text((self.mem_recall_address7 == 1), self.mem_recognize_address5)) + subheading_spanning_two_columns("Photos of test sheet") + tr_span_col(get_blob_img_html(self.picture1), td_class=CssClass.PHOTO) + tr_span_col(get_blob_img_html(self.picture2), td_class=CssClass.PHOTO) + """ </table> <div class="{CssClass.FOOTNOTES}"> [1] In the ACE-R (the predecessor of the ACE-III), scores ≤82 had sensitivity 0.84 and specificity 1.0 for dementia, and scores ≤88 had sensitivity 0.94 and specificity 0.89 for dementia, in a context of patients with AlzD, FTD, LBD, MCI, and controls (Mioshi et al., 2006, PMID 16977673). </div> <div class="{CssClass.COPYRIGHT}"> ACE-III: Copyright © 2012, John Hodges. “The ACE-III is available for free. The copyright is held by Professor John Hodges who is happy for the test to be used in clinical practice and research projects. There is no need to contact us if you wish to use the ACE-III in clinical practice.” (ACE-III FAQ, 7 July 2013, www.neura.edu.au). </div> """.format(CssClass=CssClass)) def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: codes = [ SnomedExpression( req.snomed(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT)) ] # noqa # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_ATTENTION_ORIENTATION) # noqa # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_MEMORY) # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_FLUENCY) # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_LANGUAGE) # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_VISUOSPATIAL) if self.is_complete(): # could refine: is each subscale complete? a = self.attn_score() m = self.mem_score() f = self.fluency_score() lang = self.lang_score() v = self.vsp_score() t = a + m + f + lang + v codes.append( SnomedExpression( req.snomed(SnomedLookup.ACE_R_SCALE), { req.snomed(SnomedLookup.ACE_R_SCORE): t, req.snomed(SnomedLookup.ACE_R_SUBSCORE_ATTENTION_ORIENTATION): a, # noqa req.snomed(SnomedLookup.ACE_R_SUBSCORE_MEMORY): m, req.snomed(SnomedLookup.ACE_R_SUBSCORE_FLUENCY): f, req.snomed(SnomedLookup.ACE_R_SUBSCORE_LANGUAGE): lang, req.snomed(SnomedLookup.ACE_R_SUBSCORE_VISUOSPATIAL): v, })) return codes
class Icd10Schizotypal(TaskHasClinicianMixin, TaskHasPatientMixin, Task, metaclass=Icd10SchizotypalMetaclass): """ Server implementation of the ICD10-SZTYP task. """ __tablename__ = "icd10schizotypal" shortname = "ICD10-SZTYP" longname = "ICD-10 criteria for schizotypal disorder (F21)" date_pertains_to = Column("date_pertains_to", Date, comment="Date the assessment pertains to") comments = Column("comments", UnicodeText, comment="Clinician's comments") b = CamcopsColumn( "b", Boolean, permitted_value_checker=BIT_CHECKER, comment="Criterion (B). True if: the subject has never met " "the criteria for any disorder in F20 (Schizophrenia).") N_A = 9 A_FIELDS = strseq("a", 1, N_A) def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE c = self.meets_criteria() if c is None: category = "Unknown if met or not met" elif c: category = "Met" else: category = "Not met" infolist = [ CtvInfo( content=("Pertains to: {}. Criteria for schizotypal " "disorder: {}.".format( format_datetime(self.date_pertains_to, DateFormat.LONG_DATE), category))) ] if self.comments: infolist.append(CtvInfo(content=ws.webify(self.comments))) return infolist def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement(name="meets_criteria", coltype=Boolean(), value=self.meets_criteria(), comment="Meets criteria for schizotypal disorder?"), ] # Meets criteria? These also return null for unknown. def meets_criteria(self) -> Optional[bool]: if not self.is_complete(): return None return self.count_booleans(self.A_FIELDS) >= 4 and self.b def is_complete(self) -> bool: return (self.date_pertains_to is not None and self.are_all_fields_complete(self.A_FIELDS) and self.b is not None and self.field_contents_valid()) def text_row(self, req: CamcopsRequest, wstringname: str) -> str: return tr(td(self.wxstring(req, wstringname)), td("", td_class=CssClass.SUBHEADING), literal=True) def get_task_html(self, req: CamcopsRequest) -> str: q_a = self.text_row(req, "a") for i in range(1, self.N_A + 1): q_a += self.get_twocol_bool_row_true_false( req, "a" + str(i), self.wxstring(req, "a" + str(i))) q_a += self.get_twocol_bool_row_true_false(req, "b", self.wxstring(req, "b")) h = """ {clinician_comments} <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {date_pertains_to} {meets_criteria} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="80%">Question</th> <th width="20%">Answer</th> </tr> {q_a} </table> {ICD10_COPYRIGHT_DIV} """.format( clinician_comments=self.get_standard_clinician_comments_block( req, self.comments), CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), date_pertains_to=tr_qa( req.wappstring("date_pertains_to"), format_datetime(self.date_pertains_to, DateFormat.LONG_DATE, default=None)), meets_criteria=tr_qa(req.wappstring("meets_criteria"), get_yes_no_none(req, self.meets_criteria())), q_a=q_a, ICD10_COPYRIGHT_DIV=ICD10_COPYRIGHT_DIV, ) return h
class User(BaseModel, DatedModel): """Holds users' data""" __tablename__ = "users" username = Column(String, nullable=False, unique=True, comment="User's identifier") active = Column( "is_active", BOOLEAN(), nullable=False, server_default=cast(1, BOOLEAN), comment="Denotes active users", ) _password = Column("password", String, nullable=False, comment="Password hash") # User identifiers email = Column( String, nullable=True, unique=True, comment="User's personal unique email" ) # meta data _photo = Column("photo", String, nullable=True, comment="User's avatar url") phone = Column(String, nullable=True, comment="Contact number") # User information first_name = Column(String, nullable=False, comment="First Name") last_name = Column(String, nullable=False, server_default="", comment="Last Name") manager_id = Column(Integer, ForeignKey("users.id"), nullable=True) manager: "User" = relationship( "User", foreign_keys=[manager_id], lazy=True, uselist=False ) # Relationships # Define the relationship to Role via UserRoles roles = relationship("Role", secondary="user_roles") # user sessions sessions = relationship( "Session", order_by="Session.created_at.asc()", uselist=True ) affiliation: "UserAffiliation" = relationship("UserAffiliation", uselist=False) token = None def set_password(self, val: str): regx = re.compile(current_app.config["PASSWORD_RULE"]) if not regx.match(val): raise UserExceptions.password_check_invalid() self._password = generate_password_hash(val) def get_password(self): return PasswordHelper(self._password) password = property(get_password, set_password) def get_photo(self): return ( self.__photo_handler if getattr(self, "__photo_handler", None) is not None else FileHandler(url=self._photo) if self._photo else None ) def set_photo(self, val: FileHandler): self.__photo_handler = val self._photo = getattr(val, "url", None) photo = property(get_photo, set_photo) def __init__( self, username: str, password: str, password_check: str, active: bool = True, email: str = None, photo: "FileHandler" = None, phone: str = None, first_name: str = "", last_name: str = "", **kwargs, ) -> None: if password != password_check: raise UserExceptions.password_check_invalid() self.username = username self.password = password self.active = active self.email = email self.photo = photo self.phone = phone self.first_name = first_name self.last_name = last_name @hybrid_property def name(self) -> str: """concatenates user's name""" return f"{self.first_name} {self.last_name}" def add_roles(self, roles: Union[List["Role"], "Role"]): """add roles to user Args: roles: A list of or a single role instances """ from ._UserRoles import UserRoles new_roles = [ UserRoles(user=self, role=role) for role in (roles if isinstance(roles, list) else [roles]) ] db.session.add_all(new_roles) def delete(self, persist=False): """Delete user's record""" if self.photo: self.photo.delete() super().delete(persist=persist) def add_entity(self, entity: "Entity", create: bool = False, edit: bool = False): from ._UserEntityPermission import UserEntityPermission permission = UserEntityPermission( entity=entity, user=self, create=create, edit=edit ) db.session.add(permission) db.session.commit() @hybrid_property def employees(self) -> List["User"]: return User.query.filter(User.manager_id == User.id).all() @hybrid_property def assets(self) -> List[AssetStorage]: return AssetStorage.query.filter(AssetStorage.added_by_id == self.id).all()
from sqlalchemy import Integer, String from sqlalchemy.orm import relationship from sqlalchemy.sql.schema import Column, Table, ForeignKey from .database import Base admin_phones = Table('admin_phones', Base.metadata, Column('admin_id', Integer, ForeignKey('admins.id')), Column('phone_id', Integer, ForeignKey('phones.id'))) operator_phones = Table( 'operator_phones', Base.metadata, Column('operator_id', Integer, ForeignKey('operators.id')), Column('phone_id', Integer, ForeignKey('phones.id'))) customer_phones = Table( 'customer_phones', Base.metadata, Column('customer_id', Integer, ForeignKey('customers.id')), Column('phone_id', Integer, ForeignKey('phones.id'))) worker_phones = Table('worker_phones', Base.metadata, Column('worker_id', Integer, ForeignKey('workers.id')), Column('phone_id', Integer, ForeignKey('phones.id'))) worker_brands = Table( 'worker_brands', Base.metadata, Column('worker_id', Integer, ForeignKey('workers.id')), Column('brand_id', Integer, ForeignKey('appliances_brand.id'))) worker_appliances_types = Table( 'worker_appliances_types', Base.metadata, Column('worker_id', Integer, ForeignKey('workers.id')),
class CPFTLPSReferral(TaskHasPatientMixin, Task): """ Server implementation of the CPFT_LPS_Referral task. """ __tablename__ = "cpft_lps_referral" shortname = "CPFT_LPS_Referral" info_filename_stem = "clinical" referral_date_time = Column("referral_date_time", PendulumDateTimeAsIsoTextColType) lps_division = CamcopsColumn("lps_division", UnicodeText, exempt_from_anonymisation=True) referral_priority = CamcopsColumn("referral_priority", UnicodeText, exempt_from_anonymisation=True) referral_method = CamcopsColumn("referral_method", UnicodeText, exempt_from_anonymisation=True) referrer_name = Column("referrer_name", UnicodeText) referrer_contact_details = Column("referrer_contact_details", UnicodeText) referring_consultant = Column("referring_consultant", UnicodeText) referring_specialty = CamcopsColumn("referring_specialty", UnicodeText, exempt_from_anonymisation=True) referring_specialty_other = Column("referring_specialty_other", UnicodeText) patient_location = Column("patient_location", UnicodeText) admission_date = Column("admission_date", Date) estimated_discharge_date = Column("estimated_discharge_date", Date) patient_aware_of_referral = BoolColumn("patient_aware_of_referral") interpreter_required = BoolColumn("interpreter_required") sensory_impairment = BoolColumn("sensory_impairment") marital_status_code = CamcopsColumn( "marital_status_code", CharColType, permitted_value_checker=PermittedValueChecker( permitted_values=PV_NHS_MARITAL_STATUS), ) ethnic_category_code = CamcopsColumn( "ethnic_category_code", CharColType, permitted_value_checker=PermittedValueChecker( permitted_values=PV_NHS_ETHNIC_CATEGORY), ) admission_reason_overdose = BoolColumn("admission_reason_overdose") admission_reason_self_harm_not_overdose = BoolColumn( "admission_reason_self_harm_not_overdose", constraint_name="ck_cpft_lps_referral_arshno", ) admission_reason_confusion = BoolColumn("admission_reason_confusion") admission_reason_trauma = BoolColumn("admission_reason_trauma") admission_reason_falls = BoolColumn("admission_reason_falls") admission_reason_infection = BoolColumn("admission_reason_infection") admission_reason_poor_adherence = BoolColumn( "admission_reason_poor_adherence", constraint_name="ck_cpft_lps_referral_adpa", ) admission_reason_other = BoolColumn("admission_reason_other") existing_psychiatric_teams = Column("existing_psychiatric_teams", UnicodeText) care_coordinator = Column("care_coordinator", UnicodeText) other_contact_details = Column("other_contact_details", UnicodeText) referral_reason = Column("referral_reason", UnicodeText) @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("CPFT LPS – referral") def is_complete(self) -> bool: return bool(self.patient_location and self.referral_reason and self.field_contents_valid()) def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: return [ CtvInfo( heading=ws.webify(self.wxstring(req, "f_referral_reason_t")), content=self.referral_reason, ) ] @staticmethod def four_column_row(q1: str, a1: Any, q2: str, a2: Any, default: str = "") -> str: return f""" <tr> <td>{q1}</td><td>{answer(a1, default=default)}</td> <td>{q2}</td><td>{answer(a2, default=default)}</td> </tr> """ @staticmethod def tr_qa(q: str, a: Any, default: str = "") -> str: return f""" <tr> <td colspan="2">{q}</td> <td colspan="2"><b>{default if a is None else a}</b></td> </tr> """ def get_task_html(self, req: CamcopsRequest) -> str: person_marital_status = get_nhs_dd_person_marital_status(req) ethnic_category_code = get_nhs_dd_ethnic_category_code(req) if self.lps_division == "G": banner_class = CssClass.BANNER_REFERRAL_GENERAL_ADULT division_name = self.wxstring(req, "service_G") elif self.lps_division == "O": banner_class = CssClass.BANNER_REFERRAL_OLD_AGE division_name = self.wxstring(req, "service_O") elif self.lps_division == "S": banner_class = CssClass.BANNER_REFERRAL_SUBSTANCE_MISUSE division_name = self.wxstring(req, "service_S") else: banner_class = "" division_name = None if self.referral_priority == "R": priority_name = self.wxstring(req, "priority_R") elif self.referral_priority == "U": priority_name = self.wxstring(req, "priority_U") elif self.referral_priority == "E": priority_name = self.wxstring(req, "priority_E") else: priority_name = None potential_admission_reasons = [ "admission_reason_overdose", "admission_reason_self_harm_not_overdose", "admission_reason_confusion", "admission_reason_trauma", "admission_reason_falls", "admission_reason_infection", "admission_reason_poor_adherence", "admission_reason_other", ] admission_reasons = [] for r in potential_admission_reasons: if getattr(self, r): admission_reasons.append(self.wxstring(req, "f_" + r)) h = f""" <div class="{CssClass.BANNER} {banner_class}"> {answer(division_name, default_for_blank_strings=True)} referral at { answer(format_datetime( self.referral_date_time, DateFormat.SHORT_DATETIME_WITH_DAY_NO_TZ, default=None))} </div> <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} </table> </div> <table class="{CssClass.TASKDETAIL}"> <col width="25%"> <col width="25%"> <col width="25%"> <col width="25%"> """ h += subheading_spanning_four_columns( self.wxstring(req, "t_about_referral")) h += """ <tr> <td>{q_method}</td> <td>{a_method}</td> <td>{q_priority}</td> <td class="{CssClass.HIGHLIGHT}">{a_priority}</td> </tr> """.format( CssClass=CssClass, q_method=self.wxstring(req, "f_referral_method"), a_method=answer(self.referral_method), q_priority=self.wxstring(req, "f_referral_priority"), a_priority=( answer(self.referral_priority, default_for_blank_strings=True) + ": " # noqa + answer(priority_name)), ) h += self.four_column_row( self.wxstring(req, "f_referrer_name"), self.referrer_name, self.wxstring(req, "f_referring_specialty"), self.referring_specialty, ) h += self.four_column_row( self.wxstring(req, "f_referrer_contact_details"), self.referrer_contact_details, self.wxstring(req, "f_referring_specialty_other"), self.referring_specialty_other, ) h += self.four_column_row( self.wxstring(req, "f_referring_consultant"), self.referring_consultant, "", "", ) h += subheading_spanning_four_columns(self.wxstring(req, "t_patient")) h += """ <tr> <td>{q_when}</td> <td>{a_when}</td> <td>{q_where}</td> <td class="{CssClass.HIGHLIGHT}">{a_where}</td> </tr> """.format( CssClass=CssClass, q_when=self.wxstring(req, "f_admission_date"), a_when=answer( format_datetime(self.admission_date, DateFormat.LONG_DATE, default=None), "", ), q_where=self.wxstring(req, "f_patient_location"), a_where=answer(self.patient_location), ) h += self.four_column_row( self.wxstring(req, "f_estimated_discharge_date"), format_datetime(self.estimated_discharge_date, DateFormat.LONG_DATE, ""), self.wxstring(req, "f_patient_aware_of_referral"), get_yes_no_none(req, self.patient_aware_of_referral), ) h += self.four_column_row( self.wxstring(req, "f_marital_status"), person_marital_status.get(self.marital_status_code, INVALID_VALUE), self.wxstring(req, "f_interpreter_required"), get_yes_no_none(req, self.interpreter_required), ) h += self.four_column_row( self.wxstring(req, "f_ethnic_category"), ethnic_category_code.get(self.ethnic_category_code, INVALID_VALUE), self.wxstring(req, "f_sensory_impairment"), get_yes_no_none(req, self.sensory_impairment), ) h += subheading_spanning_four_columns( self.wxstring(req, "t_admission_reason")) h += tr_span_col(answer(", ".join(admission_reasons), ""), cols=4) h += subheading_spanning_four_columns( self.wxstring(req, "t_other_people")) h += self.tr_qa( self.wxstring(req, "f_existing_psychiatric_teams"), self.existing_psychiatric_teams, "", ) h += self.tr_qa(self.wxstring(req, "f_care_coordinator"), self.care_coordinator, "") h += self.tr_qa( self.wxstring(req, "f_other_contact_details"), self.other_contact_details, "", ) h += subheading_spanning_four_columns( self.wxstring(req, "t_referral_reason")) h += tr_span_col(answer(self.referral_reason, ""), cols=4) h += """ </table> """ return h
class Phone(Base): __tablename__ = 'phones' id = Column(Integer, primary_key=True) phone = Column(Integer, unique=True)
class Icd10Schizophrenia(TaskHasClinicianMixin, TaskHasPatientMixin, Task): """ Server implementation of the ICD10-SZ task. """ __tablename__ = "icd10schizophrenia" shortname = "ICD10-SZ" longname = "ICD-10 criteria for schizophrenia (F20)" passivity_bodily = CamcopsColumn( "passivity_bodily", Boolean, permitted_value_checker=BIT_CHECKER, comment="Passivity: delusions of control, influence, or " "passivity, clearly referred to body or limb movements...") passivity_mental = CamcopsColumn( "passivity_mental", Boolean, permitted_value_checker=BIT_CHECKER, comment="(passivity) ... or to specific thoughts, actions, or " "sensations.") hv_commentary = CamcopsColumn( "hv_commentary", Boolean, permitted_value_checker=BIT_CHECKER, comment="Hallucinatory voices giving a running commentary on the " "patient's behaviour") hv_discussing = CamcopsColumn( "hv_discussing", Boolean, permitted_value_checker=BIT_CHECKER, comment="Hallucinatory voices discussing the patient among " "themselves") hv_from_body = CamcopsColumn( "hv_from_body", Boolean, permitted_value_checker=BIT_CHECKER, comment="Other types of hallucinatory voices coming from some " "part of the body") delusions = CamcopsColumn( "delusions", Boolean, permitted_value_checker=BIT_CHECKER, comment="Delusions: persistent delusions of other kinds that are " "culturally inappropriate and completely impossible, such as " "religious or political identity, or superhuman powers and " "abilities (e.g. being able to control the weather, or being " "in communication with aliens from another world).") delusional_perception = CamcopsColumn( "delusional_perception", Boolean, permitted_value_checker=BIT_CHECKER, comment="Delusional perception [a normal perception, " "delusionally interpreted]") thought_echo = CamcopsColumn( "thought_echo", Boolean, permitted_value_checker=BIT_CHECKER, comment="Thought echo [hearing one's own thoughts aloud, just " "before, just after, or simultaneously with the thought]") thought_withdrawal = CamcopsColumn( "thought_withdrawal", Boolean, permitted_value_checker=BIT_CHECKER, comment="Thought withdrawal [the feeling that one's thoughts " "have been removed by an outside agency]") thought_insertion = CamcopsColumn( "thought_insertion", Boolean, permitted_value_checker=BIT_CHECKER, comment="Thought insertion [the feeling that one's thoughts have " "been placed there from outside]") thought_broadcasting = CamcopsColumn( "thought_broadcasting", Boolean, permitted_value_checker=BIT_CHECKER, comment="Thought broadcasting [the feeling that one's thoughts " "leave oneself and are diffused widely, or are audible to " "others, or that others think the same thoughts in unison]") hallucinations_other = CamcopsColumn( "hallucinations_other", Boolean, permitted_value_checker=BIT_CHECKER, comment="Hallucinations: persistent hallucinations in any " "modality, when accompanied either by fleeting or half-formed " "delusions without clear affective content, or by persistent " "over-valued ideas, or when occurring every day for weeks or " "months on end.") thought_disorder = CamcopsColumn( "thought_disorder", Boolean, permitted_value_checker=BIT_CHECKER, comment="Thought disorder: breaks or interpolations in the train " "of thought, resulting in incoherence or irrelevant speech, " "or neologisms.") catatonia = CamcopsColumn( "catatonia", Boolean, permitted_value_checker=BIT_CHECKER, comment="Catatonia: catatonic behaviour, such as excitement, " "posturing, or waxy flexibility, negativism, mutism, and " "stupor.") negative = CamcopsColumn( "negative", Boolean, permitted_value_checker=BIT_CHECKER, comment="Negative symptoms: 'negative' symptoms such as marked " "apathy, paucity of speech, and blunting or incongruity of " "emotional responses, usually resulting in social withdrawal " "and lowering of social performance; it must be clear that " "these are not due to depression or to neuroleptic " "medication.") present_one_month = CamcopsColumn( "present_one_month", Boolean, permitted_value_checker=BIT_CHECKER, comment="Symptoms in groups A-C present for most of the time " "during an episode of psychotic illness lasting for at least " "one month (or at some time during most of the days).") also_manic = CamcopsColumn( "also_manic", Boolean, permitted_value_checker=BIT_CHECKER, comment="Also meets criteria for manic episode (F30)?") also_depressive = CamcopsColumn( "also_depressive", Boolean, permitted_value_checker=BIT_CHECKER, comment="Also meets criteria for depressive episode (F32)?") if_mood_psychosis_first = CamcopsColumn( "if_mood_psychosis_first", Boolean, permitted_value_checker=BIT_CHECKER, comment="If the patient also meets criteria for manic episode " "(F30) or depressive episode (F32), the criteria listed above " "must have been met before the disturbance of mood developed.") not_organic_or_substance = CamcopsColumn( "not_organic_or_substance", Boolean, permitted_value_checker=BIT_CHECKER, comment="The disorder is not attributable to organic brain " "disease (in the sense of F0), or to alcohol- or drug-related " "intoxication, dependence or withdrawal.") behaviour_change = CamcopsColumn( "behaviour_change", Boolean, permitted_value_checker=BIT_CHECKER, comment="A significant and consistent change in the overall " "quality of some aspects of personal behaviour, manifest as " "loss of interest, aimlessness, idleness, a self-absorbed " "attitude, and social withdrawal.") performance_decline = CamcopsColumn( "performance_decline", Boolean, permitted_value_checker=BIT_CHECKER, comment="Marked decline in social, scholastic, or occupational " "performance.") subtype_paranoid = CamcopsColumn( "subtype_paranoid", Boolean, permitted_value_checker=BIT_CHECKER, comment="PARANOID (F20.0): dominated by delusions or hallucinations.") subtype_hebephrenic = CamcopsColumn( "subtype_hebephrenic", Boolean, permitted_value_checker=BIT_CHECKER, comment="HEBEPHRENIC (F20.1): dominated by affective changes " "(shallow, flat, incongruous, or inappropriate affect) and " "either pronounced thought disorder or aimless, disjointed " "behaviour is present.") subtype_catatonic = CamcopsColumn( "subtype_catatonic", Boolean, permitted_value_checker=BIT_CHECKER, comment="CATATONIC (F20.2): psychomotor disturbances dominate " "(such as stupor, mutism, excitement, posturing, negativism, " "rigidity, waxy flexibility, command automatisms, or verbal " "perseveration).") subtype_undifferentiated = CamcopsColumn( "subtype_undifferentiated", Boolean, permitted_value_checker=BIT_CHECKER, comment="UNDIFFERENTIATED (F20.3): schizophrenia with active " "psychosis fitting none or more than one of the above three " "types.") subtype_postschizophrenic_depression = CamcopsColumn( "subtype_postschizophrenic_depression", Boolean, permitted_value_checker=BIT_CHECKER, comment="POST-SCHIZOPHRENIC DEPRESSION (F20.4): in which a depressive " "episode has developed for at least 2 weeks following a " "schizophrenic episode within the last 12 months and in which " "schizophrenic symptoms persist but are not as prominent as " "the depression.") subtype_residual = CamcopsColumn( "subtype_residual", Boolean, permitted_value_checker=BIT_CHECKER, comment="RESIDUAL (F20.5): in which previous psychotic episodes " "of schizophrenia have given way to a chronic condition with " "'negative' symptoms of schizophrenia for at least 1 year.") subtype_simple = CamcopsColumn( "subtype_simple", Boolean, permitted_value_checker=BIT_CHECKER, comment="SIMPLE SCHIZOPHRENIA (F20.6), in which 'negative' " "symptoms (C) with a change in personal behaviour (D) develop " "for at least one year without any psychotic episodes (no " "symptoms from groups A or B or other hallucinations or " "well-formed delusions), and with a marked decline in social, " "scholastic, or occupational performance.") subtype_cenesthopathic = CamcopsColumn( "subtype_cenesthopathic", Boolean, permitted_value_checker=BIT_CHECKER, comment="CENESTHOPATHIC (within OTHER F20.8): body image " "aberration (e.g. desomatization, loss of bodily boundaries, " "feelings of body size change) or abnormal bodily sensations " "(e.g. numbness, stiffness, feeling strange, " "depersonalization, or sensations of pain, temperature, " "electricity, heaviness, lightness, or discomfort when " "touched) dominate.") date_pertains_to = Column("date_pertains_to", Date, comment="Date the assessment pertains to") comments = Column("comments", UnicodeText, comment="Clinician's comments") A_NAMES = [ "passivity_bodily", "passivity_mental", "hv_commentary", "hv_discussing", "hv_from_body", "delusions", "delusional_perception", "thought_echo", "thought_withdrawal", "thought_insertion", "thought_broadcasting" ] B_NAMES = ["hallucinations_other", "thought_disorder", "catatonia"] C_NAMES = ["negative"] D_NAMES = ["present_one_month"] E_NAMES = ["also_manic", "also_depressive", "if_mood_psychosis_first"] F_NAMES = ["not_organic_or_substance"] G_NAMES = ["behaviour_change", "performance_decline"] H_NAMES = [ "subtype_paranoid", "subtype_hebephrenic", "subtype_catatonic", "subtype_undifferentiated", "subtype_postschizophrenic_depression", "subtype_residual", "subtype_simple", "subtype_cenesthopathic" ] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE c = self.meets_general_criteria() if c is None: category = "Unknown if met or not met" elif c: category = "Met" else: category = "Not met" infolist = [ CtvInfo( content=("Pertains to: {}. General criteria for " "schizophrenia: {}.".format( format_datetime(self.date_pertains_to, DateFormat.LONG_DATE), category))) ] if self.comments: infolist.append(CtvInfo(content=ws.webify(self.comments))) return infolist def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="meets_general_criteria", coltype=Boolean(), value=self.meets_general_criteria(), comment="Meets general criteria for paranoid/hebephrenic/" "catatonic/undifferentiated schizophrenia " "(F20.0-F20.3)?"), ] # Meets criteria? These also return null for unknown. def meets_general_criteria(self) -> Optional[bool]: t_a = self.count_booleans(Icd10Schizophrenia.A_NAMES) u_a = self.n_incomplete(Icd10Schizophrenia.A_NAMES) t_b = self.count_booleans(Icd10Schizophrenia.B_NAMES) + \ self.count_booleans(Icd10Schizophrenia.C_NAMES) u_b = self.n_incomplete(Icd10Schizophrenia.B_NAMES) + \ self.n_incomplete(Icd10Schizophrenia.C_NAMES) if t_a + u_a < 1 and t_b + u_b < 2: return False if self.present_one_month is not None and not self.present_one_month: return False if ((self.also_manic or self.also_depressive) and is_false(self.if_mood_psychosis_first)): return False if is_false(self.not_organic_or_substance): return False if ((t_a >= 1 or t_b >= 2) and self.present_one_month and ((is_false(self.also_manic) and is_false(self.also_depressive)) or self.if_mood_psychosis_first) and self.not_organic_or_substance): return True return None def is_complete(self) -> bool: return (self.date_pertains_to is not None and self.meets_general_criteria() is not None and self.field_contents_valid()) def heading_row(self, req: CamcopsRequest, wstringname: str, extra: str = None) -> str: return heading_spanning_two_columns( self.wxstring(req, wstringname) + (extra or "")) def text_row(self, req: CamcopsRequest, wstringname: str) -> str: return subheading_spanning_two_columns(self.wxstring(req, wstringname)) def row_true_false(self, req: CamcopsRequest, fieldname: str) -> str: return self.get_twocol_bool_row_true_false( req, fieldname, self.wxstring(req, fieldname)) def row_present_absent(self, req: CamcopsRequest, fieldname: str) -> str: return self.get_twocol_bool_row_present_absent( req, fieldname, self.wxstring(req, fieldname)) def get_task_html(self, req: CamcopsRequest) -> str: h = """ {clinician_comments} <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {tr_is_complete} {date_pertains_to} {meets_general_criteria} </table> </div> <div class="{CssClass.EXPLANATION}"> {comments} </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="80%">Question</th> <th width="20%">Answer</th> </tr> """.format( clinician_comments=self.get_standard_clinician_comments_block( req, self.comments), CssClass=CssClass, tr_is_complete=self.get_is_complete_tr(req), date_pertains_to=tr_qa( req.wappstring("date_pertains_to"), format_datetime(self.date_pertains_to, DateFormat.LONG_DATE, default=None)), meets_general_criteria=tr_qa( self.wxstring(req, "meets_general_criteria") + " <sup>[1]</sup>", # noqa get_true_false_none(req, self.meets_general_criteria())), comments=self.wxstring(req, "comments"), ) h += self.heading_row(req, "core", " <sup>[2]</sup>") for x in Icd10Schizophrenia.A_NAMES: h += self.row_present_absent(req, x) h += self.heading_row(req, "other_positive") for x in Icd10Schizophrenia.B_NAMES: h += self.row_present_absent(req, x) h += self.heading_row(req, "negative_title") for x in Icd10Schizophrenia.C_NAMES: h += self.row_present_absent(req, x) h += self.heading_row(req, "other_criteria") for x in Icd10Schizophrenia.D_NAMES: h += self.row_true_false(req, x) h += self.text_row(req, "duration_comment") for x in Icd10Schizophrenia.E_NAMES: h += self.row_true_false(req, x) h += self.text_row(req, "affective_comment") for x in Icd10Schizophrenia.F_NAMES: h += self.row_true_false(req, x) h += self.heading_row(req, "simple_title") for x in Icd10Schizophrenia.G_NAMES: h += self.row_present_absent(req, x) h += self.heading_row(req, "subtypes") for x in Icd10Schizophrenia.H_NAMES: h += self.row_present_absent(req, x) h += """ </table> <div class="{CssClass.FOOTNOTES}"> [1] All of: (a) at least one core symptom, or at least two of the other positive or negative symptoms; (b) present for a month (etc.); (c) if also manic/depressed, schizophreniform psychosis came first; (d) not attributable to organic brain disease or psychoactive substances. [2] Symptom definitions from: (a) Oyebode F (2008). Sims’ Symptoms in the Mind: An Introduction to Descriptive Psychopathology. Fourth edition, Saunders, Elsevier, Edinburgh. (b) Pawar AV & Spence SA (2003), PMID 14519605. </div> """.format(CssClass=CssClass) + ICD10_COPYRIGHT_DIV return h
class AppliancesType(Base): __tablename__ = 'appliances_type' id = Column(Integer, primary_key=True) type_name = Column(String, unique=True)
class InventoryItems(Base): __tablename__ = "inventory_item_tbl" __table_args__ = {'schema': 'apps'} item_id = Column('item_id', Integer, Sequence('item_id_sequence', schema='apps'), primary_key=True) item_number = Column('item_number', String) organization_id = Column('organization_id', Integer) enabled_flag = Column('enabled_flag', String) description = Column('description', String) buyer_id = Column('buyer_id', Integer) item_type = Column('item_type', String) long_description = Column('long_description', String) asset_flag = Column('asset_flag', String) asset_id = Column('asset_id', Integer) purchasing_enabled_flag = Column('purchasing_enabled_flag', String) customer_order_enabled_flag = Column('customer_order_enabled_flag', String) returnable_flag = Column('returnable_flag', String) inspection_required_flag = Column('inspection_required_flag', String) list_price_per_unit = Column('list_price_per_unit', String) shelf_life_days = Column('shelf_life_days', String) item_control_code = Column('item_control_code', String) min_order_quantity = Column('min_order_quantity', String) max_order_quantity = Column('max_order_quantity', String) planner_code = Column('planner_code', String) transaction_uom = Column('transaction_uom', String) conversion = Column('conversion', Float) base_uom = Column('base_uom', String) created_by = Column('created_by', Integer) creation_date = Column('creation_date', DateTime, default=datetime.datetime.utcnow) last_update_date = Column('last_update_date', DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow) last_updated_by = Column('last_updated_by', Integer)
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ from sqlalchemy.sql.schema import Column, ForeignKey, Table from sqlalchemy.sql.sqltypes import Integer, String from heliotrope.database.orm.base import mapper_registry tag_table = Table( "tag", mapper_registry.metadata, Column("id", Integer, primary_key=True, autoincrement=True), Column("galleyinfo_id", Integer, ForeignKey("galleryinfo.id")), Column("male", String(1)), Column("female", String(1)), Column("tag", String, nullable=False), Column("url", String, nullable=False), )
class IdNumDefinition(Base): """ Represents an ID number definition. """ __tablename__ = "_idnum_definitions" which_idnum = Column( "which_idnum", Integer, primary_key=True, index=True, comment="Which of the server's ID numbers is this?", ) description = Column( "description", IdDescriptorColType, comment="Full description of the ID number", ) short_description = Column( "short_description", IdDescriptorColType, comment="Short description of the ID number", ) hl7_id_type = Column( "hl7_id_type", HL7IdTypeType, comment="HL7: Identifier Type code: 'a code corresponding to the type " "of identifier. In some cases, this code may be used as a " 'qualifier to the "Assigning Authority" component.\'', ) hl7_assigning_authority = Column( "hl7_assigning_authority", HL7AssigningAuthorityType, comment="HL7: Assigning Authority for ID number (unique name of the " "system/organization/agency/department that creates the data).", ) validation_method = Column( "validation_method", String(length=ID_NUM_VALIDATION_METHOD_MAX_LEN), comment="Optional validation method", ) fhir_id_system = Column( "fhir_id_system", UrlColType, comment="FHIR external ID 'system' URL" ) def __init__( self, which_idnum: int = None, description: str = "", short_description: str = "", hl7_id_type: str = "", hl7_assigning_authority: str = "", validation_method: str = "", fhir_id_system: str = "", ): # We permit a "blank" constructor for automatic copying, e.g. merge_db. self.which_idnum = which_idnum self.description = description self.short_description = short_description self.hl7_id_type = hl7_id_type self.hl7_assigning_authority = hl7_assigning_authority self.validation_method = validation_method self.fhir_id_system = fhir_id_system def __repr__(self) -> str: return simple_repr( self, ["which_idnum", "description", "short_description"], with_addr=False, ) def _camcops_default_fhir_id_system(self, req: "CamcopsRequest") -> str: """ The built-in FHIR ID system URL that we'll use if the user hasn't specified one for the selected ID number type. """ return req.route_url( Routes.FHIR_PATIENT_ID_SYSTEM, which_idnum=self.which_idnum ) # path will be e.g. /fhir_patient_id_system/3 def effective_fhir_id_system(self, req: "CamcopsRequest") -> str: """ If the user has set a FHIR ID system, return that. Otherwise, return a CamCOPS default. """ return self.fhir_id_system or self._camcops_default_fhir_id_system(req) def verbose_fhir_id_system(self, req: "CamcopsRequest") -> str: """ Returns a human-readable description of the FHIR ID system in effect, in HTML form. """ _ = req.gettext if self.fhir_id_system: prefix = "" url = self.fhir_id_system else: prefix = _("Default:") + " " url = self._camcops_default_fhir_id_system(req) return f'{prefix} <a href="{url}">{url}</a>'
class ExportedTask(Base): """ Class representing an attempt to exported a task (as part of a :class:`ExportRun` to a specific :class:`camcops_server.cc_modules.cc_exportrecipient.ExportRecipient`. """ __tablename__ = "_exported_tasks" id = Column("id", BigInteger, primary_key=True, autoincrement=True, comment="Arbitrary primary key") recipient_id = Column("recipient_id", BigInteger, ForeignKey(ExportRecipient.id), nullable=False, comment="FK to {}.{}".format( ExportRecipient.__tablename__, ExportRecipient.id.name)) basetable = Column("basetable", TableNameColType, nullable=False, index=True, comment="Base table of task concerned") task_server_pk = Column( "task_server_pk", Integer, nullable=False, index=True, comment="Server PK of task in basetable (_pk field)") start_at_utc = Column("start_at_utc", DateTime, nullable=False, index=True, comment="Time export was started (UTC)") finish_at_utc = Column("finish_at_utc", DateTime, comment="Time export was finished (UTC)") success = Column("success", Boolean, default=False, nullable=False, comment="Task exported successfully?") failure_reasons = Column("failure_reasons", StringListType, comment="Reasons for failure") cancelled = Column( "cancelled", Boolean, default=False, nullable=False, comment= "Export subsequently cancelled/invalidated (may trigger resend)" # noqa ) cancelled_at_utc = Column("cancelled_at_utc", DateTime, comment="Time export was cancelled at (UTC)") recipient = relationship(ExportRecipient) hl7_messages = relationship("ExportedTaskHL7Message") filegroups = relationship("ExportedTaskFileGroup") emails = relationship("ExportedTaskEmail") def __init__(self, recipient: ExportRecipient = None, task: "Task" = None, basetable: str = None, task_server_pk: int = None, *args, **kwargs) -> None: """ Can initialize with a task, or a basetable/task_server_pk combination. Args: recipient: an :class:`camcops_server.cc_modules.cc_exportrecipient.ExportRecipient` task: a :class:`camcops_server.cc_modules.cc_task.Task` object basetable: base table name of the task task_server_pk: server PK of the task (However, we must also support a no-parameter constructor, not least for our :func:`merge_db` function.) """ # noqa super().__init__(*args, **kwargs) self.recipient = recipient self.start_at_utc = get_now_utc_datetime() if task: assert (not basetable) and task_server_pk is None, ( "Task specified; mustn't specify basetable/task_server_pk") self.basetable = task.tablename self.task_server_pk = task.get_pk() self._task = task else: self.basetable = basetable self.task_server_pk = task_server_pk self._task = None # type: Task @reconstructor def init_on_load(self) -> None: """ Called when SQLAlchemy recreates an object; see https://docs.sqlalchemy.org/en/latest/orm/constructors.html. """ self._task = None # type: Task @property def task(self) -> "Task": """ Returns the associated task. """ if self._task is None: dbsession = SqlASession.object_session(self) try: self._task = task_factory_no_security_checks( dbsession, self.basetable, self.task_server_pk) except KeyError: log.warning( "Failed to retrieve task for basetable={!r}, PK={!r}", self.basetable, self.task_server_pk) self._task = None return self._task def succeed(self) -> None: """ Register success. """ self.success = True self.finish() def abort(self, msg: str) -> None: """ Record failure, and why. (Called ``abort`` not ``fail`` because PyCharm has a bug relating to functions named ``fail``: https://stackoverflow.com/questions/21954959/pycharm-unreachable-code.) Args: msg: why """ self.success = False log.error("Task export failed: {}", msg) self._add_failure_reason(msg) self.finish() def _add_failure_reason(self, msg: str) -> None: """ Writes to our ``failure_reasons`` list in a way that (a) obviates the need to create an empty list via ``__init__()``, and (b) will definitely mark it as dirty, so it gets saved to the database. See :class:`cardinal_pythonlib.sqlalchemy.list_types.StringListType`. Args: msg: the message """ if self.failure_reasons is None: self.failure_reasons = [msg] else: # Do not use .append(); that won't mark the record as dirty. # Don't use "+="; similarly, that calls list.__iadd__(), not # InstrumentedAttribute.__set__(). # noinspection PyAugmentAssignment self.failure_reasons = self.failure_reasons + [msg] def finish(self) -> None: """ Records the finish time. """ self.finish_at_utc = get_now_utc_datetime() def export(self, req: "CamcopsRequest") -> None: """ Performs an export of the specific task. Args: req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` """ dbsession = req.dbsession recipient = self.recipient transmission_method = recipient.transmission_method log.info("Exporting task {!r} to recipient {}", self.task, recipient) if transmission_method == ExportTransmissionMethod.EMAIL: email = ExportedTaskEmail(self) dbsession.add(email) email.export_task(req) elif transmission_method == ExportTransmissionMethod.FILE: efg = ExportedTaskFileGroup(self) dbsession.add(efg) efg.export_task(req) elif transmission_method == ExportTransmissionMethod.HL7: ehl7 = ExportedTaskHL7Message(self) if ehl7.valid(): dbsession.add(ehl7) ehl7.export_task(req) else: self.abort("Task not valid for HL7 export") else: raise AssertionError("Bug: bad transmission_method") @property def filegroup(self) -> "ExportedTaskFileGroup": """ Returns a :class:`ExportedTaskFileGroup`, creating it if necessary. """ if self.filegroups: # noinspection PyUnresolvedReferences filegroup = self.filegroups[0] # type: ExportedTaskFileGroup else: filegroup = ExportedTaskFileGroup(self) # noinspection PyUnresolvedReferences self.filegroups.append(filegroup) return filegroup def export_file(self, filename: str, text: str = None, binary: bytes = None, text_encoding: str = UTF8) -> bool: """ Exports a file. Args: filename: text: text contents (specify this XOR ``binary``) binary: binary contents (specify this XOR ``text``) text_encoding: encoding to use when writing text Returns: was it exported? """ filegroup = self.filegroup return filegroup.export_file(filename=filename, text=text, binary=binary, text_encoding=text_encoding) def cancel(self) -> None: """ Marks the task export as cancelled/invalidated. May trigger a resend (which is the point). """ self.cancelled = True self.cancelled_at_utc = get_now_utc_datetime() @classmethod def task_already_exported(cls, dbsession: SqlASession, recipient_name: str, basetable: str, task_pk: int) -> bool: """ Has the specified task already been successfully exported? Args: dbsession: a :class:`sqlalchemy.orm.session.Session` recipient_name: basetable: name of the task's base table task_pk: server PK of the task Returns: does a successful export record exist for this task? """ exists_q = ( dbsession.query(cls).join(cls.recipient).filter( ExportRecipient.recipient_name == recipient_name).filter( cls.basetable == basetable).filter( cls.task_server_pk == task_pk).filter( cls.success == True) # nopep8 .filter(cls.cancelled == False) # nopep8 .exists()) return bool_from_exists_clause(dbsession, exists_q)
class URLDBRecord(DBBase): __tablename__ = 'urls' id = Column(Integer, primary_key=True, autoincrement=True) url_str_id = Column(Integer, ForeignKey('url_strings.id'), nullable=False, index=True) url_str_record = relationship('URLStrDBRecord', uselist=False, foreign_keys=[url_str_id]) url = association_proxy('url_str_record', 'url') status = Column( Enum( Status.done, Status.error, Status.in_progress, Status.skipped, Status.todo, ), index=True, default=Status.todo, nullable=False, ) try_count = Column(Integer, nullable=False, default=0) level = Column(Integer, nullable=False, default=0) top_url_str_id = Column(Integer, ForeignKey('url_strings.id')) top_url_record = relationship('URLStrDBRecord', uselist=False, foreign_keys=[top_url_str_id]) top_url = association_proxy('top_url_record', 'url') status_code = Column(Integer) referrer_id = Column(Integer, ForeignKey('url_strings.id')) referrer_record = relationship('URLStrDBRecord', uselist=False, foreign_keys=[referrer_id]) referrer = association_proxy('referrer_record', 'url') inline = Column(Boolean) link_type = Column(String) url_encoding = Column(String) post_data = Column(String) def to_plain(self): return URLRecord( self.url, self.status, self.try_count, self.level, self.top_url, self.status_code, self.referrer, self.inline, self.link_type, self.url_encoding, self.post_data, )
class ExportedTaskFileGroup(Base): """ Represents a small set of files exported in relation to a single task. """ __tablename__ = "_exported_task_filegroup" id = Column("id", BigInteger, primary_key=True, autoincrement=True, comment="Arbitrary primary key") exported_task_id = Column("exported_task_id", BigInteger, ForeignKey(ExportedTask.id), nullable=False, comment="FK to {}.{}".format( ExportedTask.__tablename__, ExportedTask.id.name)) filenames = Column("filenames", StringListType, comment="List of filenames exported") script_called = Column( "script_called", Boolean, default=False, nullable=False, comment="Was the {} script called?".format( ConfigParamExportRecipient.FILE_SCRIPT_AFTER_EXPORT)) script_retcode = Column( "script_retcode", Integer, comment="Return code from the {} script".format( ConfigParamExportRecipient.FILE_SCRIPT_AFTER_EXPORT)) script_stdout = Column( "script_stdout", UnicodeText, comment="stdout from the {} script".format( ConfigParamExportRecipient.FILE_SCRIPT_AFTER_EXPORT)) script_stderr = Column( "script_stderr", UnicodeText, comment="stderr from the {} script".format( ConfigParamExportRecipient.FILE_SCRIPT_AFTER_EXPORT)) exported_task = relationship(ExportedTask) def __init__(self, exported_task: ExportedTask = None) -> None: """ Args: exported_task: :class:`ExportedTask` object """ self.exported_task = exported_task def export_file(self, filename: str, text: str = None, binary: bytes = None, text_encoding: str = UTF8) -> False: """ Exports the file. Args: filename: text: text contents (specify this XOR ``binary``) binary: binary contents (specify this XOR ``text``) text_encoding: encoding to use when writing text Returns: bool: was it exported? """ assert bool(text) != bool(binary), "Specify text XOR binary" exported_task = self.exported_task filename = os.path.abspath(filename) directory = os.path.dirname(filename) recipient = exported_task.recipient if not recipient.file_overwrite_files and os.path.isfile(filename): self.abort("File already exists: {!r}".format(filename)) return False if recipient.file_make_directory: try: mkdir_p(directory) except Exception as e: self.abort("Couldn't make directory {!r}: {}".format( directory, e)) return False try: log.debug("Writing to {!r}", filename) if text: with open(filename, mode="w", encoding=text_encoding) as f: f.write(text) else: with open(filename, mode="wb") as f: f.write(binary) except Exception as e: self.abort("Failed to open or write file {!r}: {}".format( filename, e)) return False self.note_exported_file(filename) return True def note_exported_file(self, *filenames: str) -> None: """ Records a filename that has been exported, or several. Args: *filenames: filenames """ if self.filenames is None: self.filenames = list(filenames) else: # See ExportedTask._add_failure_reason() above: # noinspection PyAugmentAssignment,PyTypeChecker self.filenames = self.filenames + list(filenames) def export_task(self, req: "CamcopsRequest") -> None: """ Exports the task itself to a file. Args: req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest` """ exported_task = self.exported_task task = exported_task.task recipient = exported_task.recipient task_format = recipient.task_format task_filename = recipient.get_filename(req, task) rio_metadata_filename = change_filename_ext(task_filename, ".metadata").replace( " ", "") # ... in case we use it. No spaces in its filename. # Before we calculate the PDF, etc., we can pre-check for existing # files. if not recipient.file_overwrite_files: target_filenames = [task_filename] if recipient.file_export_rio_metadata: target_filenames.append(rio_metadata_filename) for fname in target_filenames: if os.path.isfile(os.path.abspath(fname)): self.abort("File already exists: {!r}".format(fname)) return # Export task if task_format == FileType.PDF: binary = task.get_pdf(req) text = None elif task_format == FileType.HTML: binary = None text = task.get_html(req) elif task_format == FileType.XML: binary = None text = task.get_xml(req) else: raise AssertionError("Unknown task_format") written = self.export_file(task_filename, text=text, binary=binary, text_encoding=UTF8) if not written: return # RiO metadata too? if recipient.file_export_rio_metadata: metadata = task.get_rio_metadata(recipient.rio_idnum, recipient.rio_uploading_user, recipient.rio_document_type) # We're going to write in binary mode, to get the newlines right. # One way is: # with codecs.open(filename, mode="w", encoding="ascii") as f: # f.write(metadata.replace("\n", DOS_NEWLINE)) # Here's another. metadata = metadata.replace("\n", DOS_NEWLINE) # ... Servelec say CR = "\r", but DOS is \r\n. metadata_binary = metadata.encode("ascii") # UTF-8 is NOT supported by RiO for metadata. written_metadata = self.export_file(rio_metadata_filename, binary=metadata_binary) if not written_metadata: return self.finish_run_script_if_necessary() def succeed(self) -> None: """ Register success. """ self.exported_task.succeed() def abort(self, msg: str) -> None: """ Record failure, and why. (Called ``abort`` not ``fail`` because PyCharm has a bug relating to functions named ``fail``: https://stackoverflow.com/questions/21954959/pycharm-unreachable-code.) Args: msg: why """ self.exported_task.abort(msg) def finish_run_script_if_necessary(self) -> None: """ Completes the file export by running the external script, if required. """ recipient = self.exported_task.recipient if self.filenames and recipient.file_script_after_export: args = [recipient.file_script_after_export] + self.filenames try: encoding = sys.getdefaultencoding() p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() self.script_called = True self.script_stdout = out.decode(encoding) self.script_stderr = err.decode(encoding) self.script_retcode = p.returncode except Exception as e: self.script_called = False self.script_stdout = "" self.script_stderr = str(e) self.abort("Failed to run script") return self.succeed()
class BzAttachment(GenericTable): __tablename__ = "bzattachments" __lobs__ = {"content": 1 << 24} id = Column(Integer, primary_key=True) bug_id = Column(Integer, ForeignKey("{0}.id".format(BzBug.__tablename__)), nullable=False, index=True) user_id = Column(Integer, ForeignKey("{0}.id".format(BzUser.__tablename__)), nullable=False, index=True) mimetype = Column(String(256), nullable=False) description = Column(String(256), nullable=False) creation_time = Column(DateTime, nullable=False) last_change_time = Column(DateTime, nullable=False) is_private = Column(Boolean, nullable=False) is_patch = Column(Boolean, nullable=False) is_obsolete = Column(Boolean, nullable=False) filename = Column(String(256), nullable=False) bug = relationship(BzBug, backref="attachments") user = relationship(BzUser)
ID_SHORT_DESCRIPTION_PREFIX = ( "idShortDescription" # text; apply suffixes 1-8 ) StoredVarNameColTypeDefunct = String(length=255) StoredVarTypeColTypeDefunct = String(length=255) _ssv_metadata = MetaData() server_stored_var_table_defunct = Table( "_server_storedvars", # table name _ssv_metadata, # metadata separate from everything else Column( "name", StoredVarNameColTypeDefunct, primary_key=True, index=True, comment="Variable name", ), Column( "type", StoredVarTypeColTypeDefunct, nullable=False, comment="Variable type ('integer', 'real', 'text')", ), Column("valueInteger", Integer, comment="Value of an integer variable"), Column("valueText", UnicodeText, comment="Value of a text variable"), Column("valueReal", Float, comment="Value of a real (floating-point) variable"), )
class BzBug(GenericTable): __tablename__ = "bzbugs" id = Column(Integer, primary_key=True) summary = Column(String(256), nullable=False) status = Column(Enum(*BUG_STATES, name="bzbug_status"), nullable=False) resolution = Column(Enum(*BUG_RESOLUTIONS, name="bzbug_resolution"), nullable=True) duplicate = Column(Integer, ForeignKey("{0}.id".format(__tablename__)), nullable=True, index=True) creation_time = Column(DateTime, nullable=False) last_change_time = Column(DateTime, nullable=False) private = Column(Boolean, nullable=False) tracker_id = Column(Integer, ForeignKey("{0}.id".format(Bugtracker.__tablename__)), nullable=False, index=True) opsysrelease_id = Column(Integer, ForeignKey("{0}.id".format(OpSysRelease.__tablename__)), nullable=False, index=True) component_id = Column(Integer, ForeignKey("{0}.id".format(OpSysComponent.__tablename__)), nullable=False, index=True) whiteboard = Column(String(256), nullable=False) creator_id = Column(Integer, ForeignKey("{0}.id".format(BzUser.__tablename__)), nullable=False, index=True) tracker = relationship(Bugtracker, backref="bugs") opsysrelease = relationship(OpSysRelease) component = relationship(OpSysComponent) creator = relationship(BzUser) def __str__(self) -> str: return 'BZ#{0}'.format(self.id) def order(self) -> int: return BUG_STATES.index(self.status) @property def url(self) -> str: return "{0}{1}".format(self.tracker.web_url, self.id) @property def serialize(self) -> Dict[str, Any]: return { 'id': self.id, 'summary': self.summary, 'status': self.status, 'resolution': self.resolution, 'duplicate': self.duplicate, 'creation_time': self.creation_time, 'last_change_time': self.last_change_time, 'private': self.private, 'tracker_id': self.tracker_id, 'opsysrelease_id': self.opsysrelease_id, 'component_id': self.component_id, 'whiteboard': self.whiteboard, 'creator_id': self.creator_id, 'type': 'BUGZILLA' }
class ExtraMixin(object): has_extra_bits = True x = Column("x", Integer) y = Column("y", Integer) z = Column("z", Integer)
class Character(BaseContent, Base): __tablename__ = 'charactersheet' id = Column(Integer, primary_key=True) title = Column(String(256)) body = Column(JSON) timestamp = Column(DateTime, index=True, default=datetime.utcnow, onupdate=datetime.utcnow) user_id = Column(Integer, ForeignKey('user_profile.id')) player = relationship('UserProfile', backref=backref( 'characters', lazy='dynamic')) folder = relationship('Folder', backref='characters') def __repr__(self): return '<Character {}>'.format(self.title) def __init__(self, mechanics: Type[CharacterMechanics] = CharacterMechanics, *args, **kwargs): super(Character, self).__init__(*args, **kwargs) self._data = None # Add a subclass or something that # has the mechanics of the character. self.mechanics = mechanics(self) @reconstructor def init_on_load(self): system = self.data.get('system', '') logger.debug(f"Loading character of type {system}") system = self.system self.mechanics = MECHANICS.get(system, CharacterMechanics)(self) @property def data(self) -> Dict[str, Any]: if isinstance(self.body, dict): return self.body raise TypeError("Body is not a dictionary") @property def system(self) -> str: s = self.data.get('system', None) if s is not None: return s logger.warning("Deprecation: Outdated character data") default = "Call of Cthulhu TM" if self.data.get('meta', {}).get('GameName') == default: logger.warning("Trying old CoC stuff.") return "coc7e" return "Unknown" @property def version(self): v = self.data.get('version', None) return v @property def game(self): return self.mechanics.game() def validate(self): return self.mechanics.validate() def to_dict(self): return { 'id': self.id, 'title': self.title, 'body': self.data, 'timestamp': self.timestamp, 'user_id': self.user_id } def get_sheet(self): return self.data @property def name(self): return self.mechanics.name @property def age(self): return self.mechanics.age @property def portrait(self): return self.mechanics.portrait() @property def description(self): return self.mechanics.description def attribute(self, *args): path = args[0] val = reduce(lambda x, y: x.get(y, None) if x is not None else None, path.split("."), self.data) return val def set_attribute(self, attribute: Dict): """Set a specific attribute.""" if attribute.get('category', None) == 'skill': logger.debug("Set a skill") datatype = attribute.get('type', 'string') skill = attribute['field'] subfield = attribute.get('subfield', None) value = attribute.get('value') if datatype == 'number' and not isinstance(value, int): value = None skill = self.skill(skill, subfield) skill['value'] = value elif attribute.get('type', None) == 'skillcheck': logger.debug("Check a skill") skill = attribute['field'] subfield = attribute.get('subfield', None) check = attribute.get('value', False) skill = self.skill(skill, subfield) skill['checked'] = check elif attribute.get('type', None) == 'occupationcheck': logger.debug("Mark occupation skill") skill = attribute['field'] subfield = attribute.get('subfield', None) check = attribute.get('value', False) skill = self.skill(skill, subfield) skill['occupation'] = check elif attribute.get('type', None) == 'portrait': logger.debug("Set portrait") data = attribute.get('value', None) if data is not None: self.mechanics.set_portrait(fix_image(data)) else: logger.debug( f"Set '{attribute['field']}' to '{attribute['value']}'") s = reduce(lambda x, y: x[y], attribute['field'].split(".")[:-1], self.data) s[attribute['field'].split(".")[-1]] = attribute['value'] def store_data(self): """Mark data as modified.""" flag_modified(self, "body") def skill(self, *args, **kwargs): return self.mechanics.skill(*args, **kwargs) def skills(self, *args): """Return a list of skills.""" return self.data['skills'] def add_skill(self, skillname: str, value: int = 1): if self.skill(skillname) is not None: raise ValueError(f"Skill {skillname} already exists.") self.data['skills'].append({"name": skillname, "value": value, "start_value": value}) if isinstance(self.data['skills'], list): self.data['skills'].sort(key=lambda x: x['name']) def add_subskill(self, name: str, parent: str): value = self.skill(parent)['value'] start_value = self.skill(parent)['start_value'] logger.debug("Try to add subskill") logger.debug(f"Name: {name}, parent {parent}, value {value}") if self.skill(parent, name) is not None: raise ValueError(f"Subskill {name} in {parent} already exists.") skill = self.skill(parent) if 'subskills' not in skill: skill['subskills'] = [] skill['subskills'].append({ 'name': name, 'value': value, 'start_value': start_value }) @property def schema_version(self): return self.data['meta']['Version']