#!/usr/bin/env python
# camcops_server/cc_modules/cc_recipdef.py
"""
===============================================================================
Copyright (C) 2012-2018 Rudolf Cardinal (rudolf@pobox.com).
This file is part of CamCOPS.
CamCOPS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CamCOPS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CamCOPS. If not, see <http://www.gnu.org/licenses/>.
===============================================================================
"""
import configparser
import logging
from typing import Dict, List, TYPE_CHECKING
from cardinal_pythonlib.configfiles import (
get_config_parameter,
get_config_parameter_boolean,
)
from cardinal_pythonlib.datetimefunc import coerce_to_pendulum_date
from cardinal_pythonlib.logs import BraceStyleAdapter
from cardinal_pythonlib.reprfunc import simple_repr
from pendulum import DateTime as Pendulum
from .cc_filename import (
filename_spec_is_valid,
FileType,
get_export_filename,
patient_spec_for_filename_is_valid,
)
from .cc_group import Group
if TYPE_CHECKING:
from .cc_patientidnum import PatientIdNum
from .cc_request import CamcopsRequest
log = BraceStyleAdapter(logging.getLogger(__name__))
# =============================================================================
# Constants
# =============================================================================
DEFAULT_HL7_PORT = 2575
RIO_MAX_USER_LEN = 10
class Hl7RecipientType(object):
HL7 = "hl7"
FILE = "file"
ALL_RECIPIENT_TYPES = [
Hl7RecipientType.HL7,
Hl7RecipientType.FILE,
]
class ConfigParamRecipient(object):
DIVERT_TO_FILE = "DIVERT_TO_FILE"
END_DATE = "END_DATE"
FILENAME_SPEC = "FILENAME_SPEC"
FINALIZED_ONLY = "FINALIZED_ONLY"
GROUP_ID = "GROUP_ID"
HOST = "HOST"
IDNUM_AA_PREFIX = "IDNUM_AA_" # unusual
IDNUM_TYPE_PREFIX = "IDNUM_TYPE_" # unusual
INCLUDE_ANONYMOUS = "INCLUDE_ANONYMOUS"
KEEP_MESSAGE = "KEEP_MESSAGE"
KEEP_REPLY = "KEEP_REPLY"
MAKE_DIRECTORY = "MAKE_DIRECTORY"
NETWORK_TIMEOUT_MS = "NETWORK_TIMEOUT_MS"
OVERWRITE_FILES = "OVERWRITE_FILES"
PATIENT_SPEC = "PATIENT_SPEC"
PATIENT_SPEC_IF_ANONYMOUS = "PATIENT_SPEC_IF_ANONYMOUS"
PING_FIRST = "PING_FIRST"
PORT = "PORT"
PRIMARY_IDNUM = "PRIMARY_IDNUM"
REQUIRE_PRIMARY_IDNUM_MANDATORY_IN_POLICY = "REQUIRE_PRIMARY_IDNUM_MANDATORY_IN_POLICY" # noqa
RIO_DOCUMENT_TYPE = "RIO_DOCUMENT_TYPE"
RIO_IDNUM = "RIO_IDNUM"
RIO_METADATA = "RIO_METADATA"
RIO_UPLOADING_USER = "RIO_UPLOADING_USER"
SCRIPT_AFTER_FILE_EXPORT = "SCRIPT_AFTER_FILE_EXPORT"
START_DATE = "START_DATE"
TASK_FORMAT = "TASK_FORMAT"
TREAT_DIVERTED_AS_SENT = "TREAT_DIVERTED_AS_SENT"
TYPE = "TYPE"
XML_FIELD_COMMENTS = "XML_FIELD_COMMENTS"
# =============================================================================
# RecipientDefinition class
# =============================================================================
[docs]class RecipientDefinition(object):
"""
Class representing HL7/file recipient.
Full details of parameters are in the demonstration config file.
"""
def __init__(self,
config: configparser.ConfigParser = None,
section: str = None) -> None:
"""
Initialize. Possible methods:
RecipientDefinition() # FOR TESTING ONLY
RecipientDefinition(config, section)
Args:
config: configparser INI file object
section: name of recipient and of INI file section
"""
# Some attributes will be copied to HL7Run:
# ... common
self.recipient = None # type: str
self.type = None # type: str
self.group_id = None # type: int
self.primary_idnum = None # type: int
self.require_idnum_mandatory = True
self.start_date = None # type: Pendulum
self.end_date = None # type: Pendulum
self.finalized_only = True
self.task_format = None # type: str
self.xml_field_comments = True
# ... HL7
self.host = ''
self.port = None # type: int
self.divert_to_file = None # type: str
self.treat_diverted_as_sent = False
# ... File
self.include_anonymous = False
self.overwrite_files = False
self.rio_metadata = True
self.rio_idnum = None # type: int
self.rio_uploading_user = None # type: str
self.rio_document_type = None # type: str
self.script_after_file_export = None # type: str
# HL7 fields not copied to database
self.ping_first = True
self.network_timeout_ms = None # type: int
self.idnum_type_list = {} # type: Dict[int, str]
self.idnum_aa_list = {} # type: Dict[int, str]
self.keep_message = True
self.keep_reply = True
# File fields not copied to database (because actual filename stored):
self.patient_spec_if_anonymous = None # type: str
self.patient_spec = None # type: str
self.filename_spec = None # type: str
self.make_directory = True
# Some default values we never want to be None
self.include_anonymous = False
# Variable constructor...
if config is None and section is None:
# dummy one
self.type = Hl7RecipientType.FILE
self.group_id = 1
self.primary_idnum = 1
self.require_idnum_mandatory = False
self.finalized_only = False
self.task_format = FileType.XML
# File
self.include_anonymous = True
self.patient_spec_if_anonymous = "anonymous"
self.patient_spec = "{surname}_{forename}_{idshortdesc1}{idnum1}"
self.filename_spec = (
"/tmp/camcops_debug_testing/"
"TestCamCOPS_{patient}_{created}_{tasktype}-{serverpk}"
".{filetype}"
)
self.overwrite_files = False
self.make_directory = True
return
assert config and section, "RecipientDefinition: bad __init__ call"
# Standard constructor
self.recipient = section
cpr = ConfigParamRecipient
try:
self.type = get_config_parameter(
config, section, cpr.TYPE, str, "hl7")
self.type = str(self.type).lower()
self.group_id = get_config_parameter(
config, section, cpr.GROUP_ID, int, None)
self.primary_idnum = get_config_parameter(
config, section, cpr.PRIMARY_IDNUM, int, None)
self.require_idnum_mandatory = get_config_parameter_boolean(
config, section, cpr.REQUIRE_PRIMARY_IDNUM_MANDATORY_IN_POLICY,
True)
sd = get_config_parameter(
config, section, cpr.START_DATE, str, None)
self.start_date = coerce_to_pendulum_date(sd)
ed = get_config_parameter(
config, section, cpr.END_DATE, str, None)
self.end_date = coerce_to_pendulum_date(ed)
self.finalized_only = get_config_parameter_boolean(
config, section, cpr.FINALIZED_ONLY, True)
self.task_format = get_config_parameter(
config, section, cpr.TASK_FORMAT, str, FileType.PDF)
self.xml_field_comments = get_config_parameter_boolean(
config, section, cpr.XML_FIELD_COMMENTS, True)
# HL7
if self.using_hl7():
self.host = get_config_parameter(
config, section, cpr.HOST, str, None)
self.port = get_config_parameter(
config, section, cpr.PORT, int, DEFAULT_HL7_PORT)
self.ping_first = get_config_parameter_boolean(
config, section, cpr.PING_FIRST, True)
self.network_timeout_ms = get_config_parameter(
config, section, cpr.NETWORK_TIMEOUT_MS, int, 10000)
self.keep_message = get_config_parameter_boolean(
config, section, cpr.KEEP_MESSAGE, False)
self.keep_reply = get_config_parameter_boolean(
config, section, cpr.KEEP_REPLY, False)
self.divert_to_file = get_config_parameter(
# a filename:
config, section, cpr.DIVERT_TO_FILE, str, None)
self.treat_diverted_as_sent = get_config_parameter_boolean(
config, section, cpr.TREAT_DIVERTED_AS_SENT, False)
if self.divert_to_file:
self.host = None
self.port = None
self.ping_first = None
self.network_timeout_ms = None
self.keep_reply = None
self.include_anonymous = False
# File
if self.using_file():
self.include_anonymous = get_config_parameter_boolean(
config, section, cpr.INCLUDE_ANONYMOUS, False)
self.patient_spec_if_anonymous = get_config_parameter(
config, section, cpr.PATIENT_SPEC_IF_ANONYMOUS, str,
"anonymous")
self.patient_spec = get_config_parameter(
config, section, cpr.PATIENT_SPEC, str, None)
self.filename_spec = get_config_parameter(
config, section, cpr.FILENAME_SPEC, str, None)
self.overwrite_files = get_config_parameter_boolean(
config, section, cpr.OVERWRITE_FILES, False)
self.make_directory = get_config_parameter_boolean(
config, section, cpr.MAKE_DIRECTORY, False)
self.rio_metadata = get_config_parameter_boolean(
config, section, cpr.RIO_METADATA, False)
self.rio_idnum = get_config_parameter(
config, section, cpr.RIO_IDNUM, int, None)
self.rio_uploading_user = get_config_parameter(
config, section, cpr.RIO_UPLOADING_USER, str, None)
self.rio_document_type = get_config_parameter(
config, section, cpr.RIO_DOCUMENT_TYPE, str, None)
self.script_after_file_export = get_config_parameter(
config, section, cpr.SCRIPT_AFTER_FILE_EXPORT, str, None)
except configparser.NoSectionError:
log.warning("Config file section missing: [{}]", section)
@staticmethod
def report_error(msg) -> None:
log.error("RecipientDefinition: {}", msg)
[docs] def valid(self, req: "CamcopsRequest") -> bool:
"""Is this definition valid?"""
if self.type not in ALL_RECIPIENT_TYPES:
self.report_error("missing/invalid type: {}".format(self.type))
return False
if not self.group_id:
self.report_error("missing group_id")
return False
group = Group.get_group_by_id(req.dbsession, self.group_id)
if not group:
self.report_error("invalid group_id: {}".format(self.group_id))
return False
if not self.primary_idnum and self.using_hl7():
self.report_error("missing primary_idnum")
return False
valid_which_idnums = req.valid_which_idnums
if self.primary_idnum not in valid_which_idnums:
self.report_error("invalid primary_idnum: {}".format(
self.primary_idnum))
return False
if self.primary_idnum and self.require_idnum_mandatory:
# (a) ID number must be mandatory in finalized records
finalize_policy = group.tokenized_finalize_policy()
if not finalize_policy.is_idnum_mandatory_in_policy(
which_idnum=self.primary_idnum,
valid_which_idnums=valid_which_idnums):
self.report_error(
"primary_idnum ({}) not mandatory in finalizing policy, "
"but needs to be".format(self.primary_idnum))
return False
if not self.finalized_only:
# (b) ID number must also be mandatory in uploaded,
# non-finalized records
upload_policy = group.tokenized_upload_policy()
if not upload_policy.is_idnum_mandatory_in_policy(
which_idnum=self.primary_idnum,
valid_which_idnums=valid_which_idnums):
self.report_error(
"primary_idnum ({}) not mandatory in upload policy, "
"but needs to be".format(self.primary_idnum))
return False
if not self.task_format or self.task_format not in [FileType.HTML,
FileType.PDF,
FileType.XML]:
self.report_error(
"missing/invalid task_format: {}".format(self.task_format))
return False
if not self.task_format == FileType.XML:
self.xml_field_comments = None
# HL7
if self.type == Hl7RecipientType.HL7:
if not self.divert_to_file:
if not self.host:
self.report_error("missing host")
return False
if not self.port or self.port <= 0:
self.report_error(
"missing/invalid port: {}".format(self.port))
return False
if not self.idnum_type_list.get(self.primary_idnum, None):
self.report_error(
"missing IDNUM_TYPE_{} (for primary ID)".format(
self.primary_idnum))
return False
# File
if self.type == Hl7RecipientType.FILE:
if not self.patient_spec_if_anonymous:
self.report_error("missing patient_spec_if_anonymous")
return False
if not self.patient_spec:
self.report_error("missing patient_spec")
return False
if not patient_spec_for_filename_is_valid(
patient_spec=self.patient_spec,
valid_which_idnums=valid_which_idnums):
self.report_error(
"invalid patient_spec: {}".format(self.patient_spec))
return False
if not self.filename_spec:
self.report_error("missing filename_spec")
return False
if not filename_spec_is_valid(
filename_spec=self.filename_spec,
valid_which_idnums=valid_which_idnums):
self.report_error(
"invalid filename_spec: {}".format(self.filename_spec))
return False
# RiO metadata
if self.rio_metadata:
if self.rio_idnum not in valid_which_idnums:
self.report_error(
"invalid rio_idnum: {}".format(self.rio_idnum))
return False
if (not self.rio_uploading_user or
" " in self.rio_uploading_user or
len(self.rio_uploading_user) > RIO_MAX_USER_LEN):
self.report_error(
"missing/invalid rio_uploading_user: {} (must be "
"present, contain no spaces, and max length "
"{})".format(
self.rio_uploading_user,
RIO_MAX_USER_LEN))
return False
if not self.rio_document_type:
self.report_error("missing rio_document_type")
return False
# This section would be BETTER with a try/raise/except block, rather
# than a bunch of return statements.
# Done
return True
[docs] def using_hl7(self) -> bool:
"""Is the recipient an HL7 recipient?"""
return self.type == Hl7RecipientType.HL7
[docs] def using_file(self) -> bool:
"""Is the recipient a filestore?"""
return self.type == Hl7RecipientType.FILE
[docs] @staticmethod
def get_id_type(req: "CamcopsRequest", which_idnum: int) -> str:
"""Get HL7 ID type for a specific ID number."""
iddef = req.get_idnum_definition(which_idnum)
return (iddef.hl7_id_type or '') if iddef else ''
[docs] @staticmethod
def get_id_aa(req: "CamcopsRequest", which_idnum: int) -> str:
"""Get HL7 ID type for a specific ID number."""
iddef = req.get_idnum_definition(which_idnum)
return (iddef.hl7_assigning_authority or '') if iddef else ''
[docs] def get_filename(self,
req: "CamcopsRequest",
is_anonymous: bool = False,
surname: str = None,
forename: str = None,
dob: Pendulum = None,
sex: str = None,
idnum_objects: List['PatientIdNum'] = None,
creation_datetime: Pendulum = None,
basetable: str = None,
serverpk: int = None) -> str:
"""Get filename, for file transfers."""
return get_export_filename(
req=req,
patient_spec_if_anonymous=self.patient_spec_if_anonymous,
patient_spec=self.patient_spec,
filename_spec=self.filename_spec,
task_format=self.task_format,
is_anonymous=is_anonymous,
surname=surname,
forename=forename,
dob=dob,
sex=sex,
idnum_objects=idnum_objects,
creation_datetime=creation_datetime,
basetable=basetable,
serverpk=serverpk
)
def __str__(self):
"""String representation."""
attrnames = [key for key in self.__dict__ if not key.startswith('_')]
return simple_repr(self, attrnames)