##
# File:  MessagingDepict.py
# Date:  02-Feb-2012
# Updates:
#    2012-04-30    RPS    Created based on EditorDepict
#    2013-09-03    RPS    Updated to use cif-based backend storage.
#    2013-10-30    RPS    Support for "Notes" UI and for UI feature for classifying messages (e.g. "action required" or "unread")
#                            Also, now supporting canned starter template for composing messages.
#    2013-11-19    RPS    Added support for obtaining content from "correspondence-to-depositor" files to integrate within starter
#                            template for message body. Also added PDB ID field to starter message template.
#    2013-12-03    RPS    Introduced support for different message templates.
#    2013-12-16    RPS    Introduced support for rendering view as embedded view (iframe)
#    2014-01-14    RPS    Message templates updated.
#    2014-02-05    RPS    Value of "wwpdb annotator" value enforced as sender when login account is "dep" user
#    2014-03-04    RPS    Introduced support for message templates related to validation.
#    2014-06-09    RPS    Updated handling for citation related info so that these data points are rendered in related message tempalte
#                            only when the given data items are valid/populated.
#    2014-08-12    RPS    Moved message templates into separate MessagingTemplates module
#    2014-09-23    RPS    Integrated use of PdbxPersist/datastore to minimize reliance on parsing model file which will improve
#                            response times on front-end when sending messages in cases of very large model files (e.g. ribosomes)
#    2014-10-10    RPS    msgTmplt_vldtnMinorIssues and msgTmplt_vldtnMajorIssues abbreviated to eliminate boilerplate that is already contained in correspondence content.
#    2014-12-04    RPS    msgTmplt_correspondence, msgTmplt_vldtnMinorIssues, and msgTmplt_vldtnMajorIssues removed.
#                            Updates to support new "Complete Correspondence History" view and new msgTmplt_systemUnlocked template.
#    2014-12-05    RPS    getDataTableTemplate updated to provide inventory of Notes message IDs
#    2014-12-09    RPS    Updates to support new strategy of rendering message content in separate browser window
#    2015-03-02    RPS    Updates per introduction of sanity check safeguards on writes to messaging cif data files.
#    2015-05-08    RPS    Needed to correct getDataTableTemplate() to create proper target HTML table as per upgrade to use of DataTables 1.10.5
#    2015-07-28    RPS    Updates to support handling for cases of EM experimental method. Introduced support for "Unlock Dep UI" button.
#    2015-10-27    RPS    Introduction support for EM specific message templates.
#    2015-10-28    RPS    More updates to support EM specific message templates.
#    2015-12-02    RPS    Updates to optimize response time to user by running template processing in background.
#                            Removed obsolete validation letter content (validation content no longer generated by Msgmodule)
#    2016-01-24    RPS    Updated for new template for EM map-only, post-annotation letter
#    2016-02-29    RPS    Removing use of "COLUMN_DISPLAY_NAMES" as this hadn't found a use on front-end
#    2016-05-06    RPS    Setting annotator to "wwPDB annotator" if received as "unknown" from calling code.
#    2016-08-09    RPS    Changes to support site/annotator specific footers in message templates.
#                            Introducing support for standalone correspondence viewer.
#    2016-09-14    ZF     In doRender() function, introducing support for group deposition
#    2022-02-28    CS     Add processs to deal with map_only withdrawn template
##
##
"""
Base class for HTML depictions containing common HTML constructs.

"""
__docformat__ = "restructuredtext en"
__author__ = "Raul Sala"
__email__ = "rsala@rcsb.rutgers.edu"
__license__ = "Creative Commons Attribution 3.0 Unported"
__version__ = "V0.02"

import os
import sys
import time
from wwpdb.apps.msgmodule.io.MessagingIo import MessagingIo
from wwpdb.apps.msgmodule.io.MessagingDataImport import MessagingDataImport
from wwpdb.apps.msgmodule.depict.MessagingTemplates import MessagingTemplates
from wwpdb.utils.config.ConfigInfo import ConfigInfo

import logging

logger = logging.getLogger(__name__)


class MessagingDepict(object):
    """Base class for HTML depictions contain definitions of common constructs."""

    def __init__(self, verbose=False, log=sys.stderr):
        """

        :param `verbose`:  boolean flag to activate verbose logging.
        :param `log`:      stream for logging.

        """
        self.__verbose = verbose
        self.__lfh = log
        self.__debug = False
        #
        self.absltSessionPath = None
        self.absltEdtrSessionPath = None
        self.rltvSessionPath = None
        self.rltvEdtrSessionPath = None
        #
        self.__expMethodList = []

    def doRender(self, p_reqObj, p_bIsWorkflow):
        """Render HTML used as starter page/container for the wwPDB Messaging interface
        Once this content is in the browser, AJAX calls are made to populate the page
        with content when needed.

        :Params:

            + ``p_reqObj``: Web Request object
            + ``p_bIsWorkflow``: boolean indicating whether or not operating in Workflow Manager environment

        :Returns:
            ``oL``: output list consisting of HTML markup
        """
        oL = []
        #
        sessionId = p_reqObj.getSessionId()
        wfInstId = str(p_reqObj.getValue("instance")).upper()
        depId = str(p_reqObj.getValue("identifier"))
        classId = str(p_reqObj.getValue("classID")).lower()
        fileSource = str(p_reqObj.getValue("filesource")).lower()
        contentType = str(p_reqObj.getValue("content_type")).lower()
        annotator = str(p_reqObj.getValue("annotator"))
        # expMethodList = (p_reqObj.getValue("expmethod").replace('"', '')).split(",") if (len(p_reqObj.getValue("expmethod").replace('"', '')) > 1) else []
        initContentType = str(p_reqObj.getValue("init_content_type")).lower()
        tmpltPath = p_reqObj.getValue("TemplatePath")
        embeddedVw = str(p_reqObj.getValue("embed"))
        autoLaunchCompose = str(p_reqObj.getValue("auto_launch_compose"))
        allowUnlockDepUI = str(p_reqObj.getValue("allowunlock"))
        siteId = str(p_reqObj.getValue("WWPDB_SITE_ID"))
        cI = ConfigInfo(siteId)
        fileFormatExtDict = cI.get("FILE_FORMAT_EXTENSION_DICTIONARY")
        #
        contentType = (
            initContentType if (contentType is None or len(contentType) < 1) else contentType
        )  # if content type not specified then fall back on initial content type requested
        #
        if contentType is None or len(contentType) < 1:  # if we don't have a requested content type at this point, use "msgs" as default
            contentType = "msgs"
        #
        # if depId is not None:
        #    depId = self.__formatDepositionDataId(depId, p_bIsWorkflow)
        #
        self.__expMethodList = (p_reqObj.getValue("expmethod").replace('"', "")).split(",") if (len(p_reqObj.getValue("expmethod").replace('"', "")) > 1) else []
        # bEmDeposition = True if ("ELECTRON MICROSCOPY" in self.__expMethodList or "ELECTRON CRYSTALLOGRAPHY" in self.__expMethodList) else False
        #
        if self.__verbose:
            logger.info("--------------------------------------------")
            logger.info("Starting %s", time.strftime("%Y %m %d %H:%M:%S", time.localtime()))
            logger.info("identifier   %s", depId)
            logger.info("instance     %s", wfInstId)
            logger.info("file source  %s", fileSource)
            logger.info("sessionId    %s", sessionId)
        #

        # Determine if submitted

        deposited = False
        msgDI = MessagingDataImport(p_reqObj, verbose=self.__verbose, log=self.__lfh)
        modelFilePath = msgDI.getFilePath(contentType="model", format="pdbx")
        # parse info from model file
        if modelFilePath is not None and os.access(modelFilePath, os.R_OK):
            deposited = True

        annotator = "wwPDB annotator" if (annotator.lower() in ["dep", "unknown"]) else annotator

        ############################################################################
        # create dictionary of content that will be used to populate HTML template
        ############################################################################
        myD = {}
        myD["sessionid"] = sessionId
        myD["instance"] = wfInstId
        myD["classid"] = classId
        myD["filesource"] = fileSource
        myD["content_type"] = contentType
        myD["annotator"] = annotator if (annotator is not None and len(annotator) > 1) else "wwPDB annotator"
        myD["content_type_hdr"] = "Annotator Processing Notes" if contentType == "notes" else "Communications with Depositor"
        myD["cntnt_type_btnlbl"] = "Notes" if contentType == "notes" else "Messages"
        myD["new_cntnt_type_btnlbl"] = "Note" if contentType == "notes" else "Message"
        myD["auto_launch_compose"] = autoLaunchCompose if (autoLaunchCompose is not None and len(autoLaunchCompose) > 1 and autoLaunchCompose.lower() == "false") else "true"
        myD["expmethod"] = p_reqObj.getValue("expmethod")
        myD["display_unlock"] = "" if (allowUnlockDepUI and allowUnlockDepUI == "yes") else "hidden"
        myD["display_rel_status"] = "" if deposited else "hidden"
        #
        listKnownFileExtensions = list(set(fileFormatExtDict.values()))
        listKnownFileExtensions.sort()
        myD["known_file_extensions"] = ", ".join(listKnownFileExtensions)

        # following params only for rcsb stand-alone version
        myD["caller"] = p_reqObj.getValue("caller")
        myD["filepath"] = p_reqObj.getValue("filePath")
        myD["filetype"] = p_reqObj.getValue("fileType")
        #
        if p_bIsWorkflow:
            myD["identifier"] = depId
        else:
            if not depId or len(depId) < 1:
                myD["identifier"] = "D_000000"
            else:
                myD["identifier"] = depId
        #
        myD["session_url_prefix"] = self.rltvEdtrSessionPath
        #
        # Added by ZF to handle group deposition
        #
        myD["display_title"] = "DEPOSITION ID"
        myD["display_identifier"] = myD["identifier"]
        groupId = str(p_reqObj.getValue("groupid"))
        if groupId:
            if groupId == depId:
                myD["display_title"] = "GROUP ID"
                myD["display_identifier"] = groupId
            else:
                myD["display_title"] = "GROUP/DEPOSITION IDS"
                myD["display_identifier"] = groupId + "/" + myD["identifier"]
            #
        #
        tmpltFile = "msging_launch_embed_tmplt.html" if embeddedVw.lower() == "true" else "msging_launch_tmplt.html"

        oL.append(self.processTemplate(tmpltPth=tmpltPath, fn=tmpltFile, parameterDict=myD))
        #
        return oL

    def getMsgTmplts(self, p_reqObj, p_bIsWorkflow):
        """Get

        :Helpers:

        :Returns:

        """
        #
        oL = []
        #
        strParamDict = {}
        #
        if self.__verbose:
            logging.info("-- Starting.")
        #
        depId = str(p_reqObj.getValue("identifier"))
        tmpltPath = p_reqObj.getValue("TemplatePath")
        self.__expMethodList = (p_reqObj.getValue("expmethod").replace('"', "")).split(",") if (len(p_reqObj.getValue("expmethod").replace('"', "")) > 1) else []
        bEmDeposition = True if ("ELECTRON MICROSCOPY" in self.__expMethodList or "ELECTRON CRYSTALLOGRAPHY" in self.__expMethodList) else False
        #
        if p_bIsWorkflow:
            strParamDict["identifier"] = depId
        else:
            if not depId or len(depId) < 1:
                strParamDict["identifier"] = "D_000000"
            else:
                strParamDict["identifier"] = depId
        #
        if self.__verbose:
            logger.info("\n -- dep_id is:%s", depId)
        #
        msgingIo = MessagingIo(p_reqObj, self.__verbose, self.__lfh)
        msgingIo.initializeDataStore()  # THIS CALL MUST BE MADE HERE TO SUPPORT ALL DOWNSTREAM PROCESSING IN NEED OF DATA PARSED FROM THE COORDINATE FILE
        msgingIo.getMsgTmpltDataItems(strParamDict)
        starterMsgBody = msgingIo.getStarterMsgBody()
        #
        strParamDict["starter_msg_body"] = starterMsgBody if starterMsgBody is not None else ""
        strParamDict["msg_tmplt_default"] = (MessagingTemplates.msgTmplt_default_em % strParamDict) if bEmDeposition else (MessagingTemplates.msgTmplt_default % strParamDict)
        strParamDict["msg_tmplt_approval-expl"] = (
            (MessagingTemplates.msgTmplt_approvalExplicit_em % strParamDict) if bEmDeposition else (MessagingTemplates.msgTmplt_approvalExplicit % strParamDict)
        )  # noqa: E501
        strParamDict["msg_tmplt_approval-impl"] = (
            (MessagingTemplates.msgTmplt_approvalImplicit_em % strParamDict) if bEmDeposition else (MessagingTemplates.msgTmplt_approvalImplicit % strParamDict)
        )  # noqa: E501
        strParamDict["msg_tmplt_reminder"] = (MessagingTemplates.msgTmplt_reminder_em % strParamDict) if bEmDeposition else (MessagingTemplates.msgTmplt_reminder % strParamDict)
        strParamDict["msg_tmplt_release-publ"] = (
            (MessagingTemplates.msgTmplt_releaseWthPblctn_em % strParamDict) if bEmDeposition else (MessagingTemplates.msgTmplt_releaseWthPblctn % strParamDict)
        )  # noqa: E501
        strParamDict["msg_tmplt_release-nopubl"] = (
            (MessagingTemplates.msgTmplt_releaseWthOutPblctn_em % strParamDict) if bEmDeposition else (MessagingTemplates.msgTmplt_releaseWthOutPblctn % strParamDict)
        )  # noqa: E501

        #  CS 2022-02-27 Handle EM map_only withdrawn template
        if bEmDeposition:
            if strParamDict.get("pdb_id", "") == "[PDBID NOT AVAIL]":  # map_only
                strParamDict["msg_tmplt_withdrawn"] = MessagingTemplates.msgTmplt_withdrawn_em_map_only % strParamDict
            else:
                strParamDict["msg_tmplt_withdrawn"] = MessagingTemplates.msgTmplt_withdrawn_em % strParamDict
        else:
            strParamDict["msg_tmplt_withdrawn"] = MessagingTemplates.msgTmplt_withdrawn % strParamDict

        strParamDict["msg_tmplt_vldtn"] = MessagingTemplates.msgTmplt_vldtn % strParamDict

        statusCode = strParamDict.get("status_code", "???")
        if statusCode == "REL" and strParamDict.get("pdb_id", "") != "[PDBID NOT AVAIL]":
            # Post Release Unlock
            strParamDict["msg_tmplt_system-unlocked"] = MessagingTemplates.msgTmplt_systemUnlockedPostRel % strParamDict
        else:
            strParamDict["msg_tmplt_system-unlocked"] = (
                (MessagingTemplates.msgTmplt_systemUnlocked_em % strParamDict) if bEmDeposition else (MessagingTemplates.msgTmplt_systemUnlocked % strParamDict)
            )  # noqa: E501
        strParamDict["msg_tmplt_maponly-authstatus-em"] = (MessagingTemplates.msgTmplt_mapOnly_authStatus_em % strParamDict) if bEmDeposition else ""

        oL.append(self.processTemplate(tmpltPth=tmpltPath, fn="msg_tmplts.html", parameterDict=strParamDict))
        #
        return oL

    def doRenderAllCorrespondence(self, p_reqObj):
        """Render HTML used as starter page/container for the wwPDB Messaging interface
        Once this content is in the browser, AJAX calls are made to populate the page
        with content when needed.

        :Params:

            + ``p_reqObj``: Web Request object

        :Returns:
            ``oL``: output list consisting of HTML markup
        """
        oL = []
        #
        sessionId = p_reqObj.getSessionId()
        depId = str(p_reqObj.getValue("identifier")).upper()
        fileSource = str(p_reqObj.getValue("filesource")).lower()
        contentType = "commhstry"

        # initContentType = str(p_reqObj.getValue("init_content_type")).lower()
        tmpltPath = p_reqObj.getValue("TemplatePath")
        # siteId = str(p_reqObj.getValue("WWPDB_SITE_ID"))
        # cI = ConfigInfo(siteId)
        #
        if self.__verbose:
            logger.info("--------------------------------------------")
            logger.info("Starting at %s", time.strftime("%Y %m %d %H:%M:%S", time.localtime()))
            logger.info("identifier   %s", depId)
            logger.info("file source  %s", fileSource)
            logger.info("sessionId    %s", sessionId)
        #

        ############################################################################
        # create dictionary of content that will be used to populate HTML template
        ############################################################################
        myD = {}
        myD["sessionid"] = sessionId
        myD["filesource"] = fileSource
        myD["content_type"] = contentType
        #

        myD["identifier"] = depId
        myD["session_url_prefix"] = self.rltvEdtrSessionPath

        tmpltFile = "msging_launch_commhstry_tmplt.html"
        # tmpltFile = "msging_launch_embed_tmplt.previewMsg.html" if embeddedVw.lower() == "true" else "msging_launch_tmplt.previewMsg.20150622.html"

        oL.append(self.processTemplate(tmpltPth=tmpltPath, fn=tmpltFile, parameterDict=myD))
        #
        return oL

    def doRenderDisplayMsg(self, p_reqObj, p_bIsWorkflow, p_msgDict):
        """Render HTML used to display individual message

        :Params:

            + ``p_reqObj``: Web Request object
            + ``p_bIsWorkflow``: boolean indicating whether or not operating in Workflow Manager environment
            + ``p_msgDict``: dictionary representing individual message and associated data

        :Returns:
            ``oL``: output list consisting of HTML markup
        """
        oL = []
        #
        sessionId = p_reqObj.getSessionId()
        wfInstId = str(p_reqObj.getValue("instance")).upper()
        depId = str(p_reqObj.getValue("identifier"))
        classId = str(p_reqObj.getValue("classID")).lower()
        fileSource = str(p_reqObj.getValue("filesource")).lower()
        contentType = str(p_reqObj.getValue("content_type")).lower()
        annotator = str(p_reqObj.getValue("annotator"))
        initContentType = str(p_reqObj.getValue("init_content_type")).lower()
        tmpltPath = p_reqObj.getValue("TemplatePath")
        # embeddedVw = str(p_reqObj.getValue("embed"))
        msgsHighWatermark = str(p_reqObj.getValue("msgs_high_watermark"))
        notesHighWatermark = str(p_reqObj.getValue("notes_high_watermark"))
        #
        contentType = (
            initContentType if (contentType is None or len(contentType) < 1) else contentType
        )  # if content type not specified then fall back on initial content type requested
        #
        if contentType is None or len(contentType) < 1:  # if we don't have a requested content type at this point, use "msgs" as default
            contentType = "msgs"
        #
        # if depId is not None:
        #    depId = self.__formatDepositionDataId(depId, p_bIsWorkflow)
        #
        if self.__verbose:
            logger.info("--------------------------------------------")
            logger.info("Starting %s", time.strftime("%Y %m %d %H:%M:%S", time.localtime()))
            logger.info("identifier   %s", depId)
            logger.info("instance     %s", wfInstId)
            logger.info("file source  %s", fileSource)
            logger.info("sessionId    %s", sessionId)
        #

        annotator = "wwPDB annotator" if annotator.lower() == "dep" else annotator

        ############################################################################
        # create dictionary of content that will be used to populate HTML template
        ############################################################################
        myD = {}
        myD["sessionid"] = sessionId
        myD["instance"] = wfInstId
        myD["classid"] = classId
        myD["filesource"] = fileSource
        myD["content_type"] = contentType
        myD["annotator"] = annotator if (annotator is not None and len(annotator) > 1) else "wwPDB annotator"
        myD["cntnt_type_btnlbl"] = "Notes" if contentType == "notes" else "Messages"
        myD["new_cntnt_type_btnlbl"] = "Note" if contentType == "notes" else "Message"
        myD["msgs_high_watermark"] = msgsHighWatermark
        myD["notes_high_watermark"] = notesHighWatermark

        if contentType == "notes":
            myD["content_type_hdr"] = "Annotator Processing Notes"
        elif contentType == "msgs":
            myD["content_type_hdr"] = "Communications with Depositor"
        else:
            myD["content_type_hdr"] = "Complete Correspondence History"

        ############################################################################
        # get message template data from MessagingIo
        ############################################################################
        msgingIo = MessagingIo(p_reqObj, self.__verbose, self.__lfh)
        msgingIo.getMsgTmpltDataItems(myD)

        if len(p_msgDict["files_referenced"]) > 0:
            htmlStr = ""
            for fileRef in p_msgDict["files_referenced"]:
                filePathSplitArr = fileRef["relative_file_url"].split("/")
                fileName = filePathSplitArr[len(filePathSplitArr) - 1]
                displayUploadFlName = " (" + fileRef["upload_file_name"] + ")" if len(fileRef["upload_file_name"]) > 1 else ""
                htmlStr += '<p><span class=""><a href="' + fileRef["relative_file_url"] + '" target="_blank">' + fileName + displayUploadFlName + "</a></span></p>"

            myD["files_rfrncd_dsply"] = ""
            myD["files_rfrncd"] = htmlStr
        else:
            myD["files_rfrncd_dsply"] = "displaynone"
            myD["files_rfrncd"] = ""

        if len(myD["pdb_id"]) > 1:
            myD["display_pdbid"] = ""
        else:
            myD["display_pdbid"] = "displaynone"

        # following params only for rcsb stand-alone version
        myD["caller"] = p_reqObj.getValue("caller")
        myD["filepath"] = p_reqObj.getValue("filePath")
        myD["filetype"] = p_reqObj.getValue("fileType")
        #
        if p_bIsWorkflow:
            myD["identifier"] = depId
        else:
            if not depId or len(depId) < 1:
                myD["identifier"] = "D_000000"
            else:
                myD["identifier"] = depId
        #
        myD["session_url_prefix"] = self.rltvEdtrSessionPath
        myD.update(p_msgDict)

        logger.info("p_msgDict is:  %r", p_msgDict)
        logger.info("myD is:  %r", myD)

        tmpltFile = "display_msg_tmplt.html"
        oL.append(self.processTemplate(tmpltPth=tmpltPath, fn=tmpltFile, parameterDict=myD))
        #
        return oL

    # ####### BEGIN -- Specific to DataTable Implementation ##################
    # NOTE: consider encapsulating DataTable functionality as separate class

    def getJsonDataTable(self, p_msgRcrdList, p_msgColList, p_iDisplayStart=0):
        """Generate contents of json object expected by DataTables for populating display
        table with data.

        :Params:

            + ``p_msgColList``: list of column names, sequence of which corresponds to
                                sequence of fields in each record in p_recordList
            + ``p_msgRcrdList``: list of records corresponding to messages obtained from database for given deposition dataset
            + ``p_iDisplayStart``: DataTable parameter used by the plugin as index of first record in set actually being displayed on screen


        :Returns:
            ``rtrnDict``: dictionary of records for display on screen as complies with DataTables requirements for JSON object it expects from server
        """
        rtrnDict = {}

        sColumns = ""
        sComma = ""
        for idx, colName in enumerate(p_msgColList):
            if idx > 0:
                sComma = ","
            sColumns += sComma + colName
        #
        rtrnDict["sColumns"] = sColumns

        aaDataList = self.__createDataTableAaDataList(p_msgColList, p_msgRcrdList, p_iDisplayStart)
        rtrnDict["aaData"] = aaDataList

        return rtrnDict

    def getDataTableTemplate(self, p_reqObj):
        """
        For given deposition data set ID, obtain "staging" components to be used in
        preparation for loading webpage with DataTable for display of text messages:
                +  html skeleton template which will be populated by jQuery DataTables plugin

        :Params:

            + ``p_reqObj``: Web Request object

        :Returns:
            ``mrkpList``: output list consisting of HTML markup serving as skeleton starter template for DataTable
            ``dtblConfigDict``: dictionary of display, config settings for DataTable

        """
        dtblConfigDict = {}
        depId = p_reqObj.getValue("identifier")
        sContentType = p_reqObj.getValue("content_type")
        bCommHstryRqstd = True if sContentType == "commhstry" else False
        #
        msgingIo = MessagingIo(p_reqObj, self.__verbose, self.__lfh)
        bOk, msgColList = msgingIo.getMsgColList(bCommHstryRqstd)
        #
        if bOk:
            ###############################################################################################
            # FIRST: obtain any config info needed by DataTables plugin
            ###############################################################################################
            # provide DataTables "aoColumns" property to be used in initializing the DataTable
            # with set of known column names so that it can use for keeping track of column ordering
            sNameList = []
            for colName in msgColList:
                newSnameDict = {}
                newSnameDict["mDataProp"] = colName
                sNameList.append(newSnameDict)
            #
            dtblConfigDict["DTBL_AOCOLUMNS"] = sNameList

            ###############################################################################################
            # SECOND: generating HTML that serves as markup starter skeleton to be populated by DataTables
            ###############################################################################################
            mrkpList = []
            srchHdrList = []  # separate list of markup representing <th> elements accommodating individual column search filtering

            # Create dictionary of user-friendly labels for columns where desired.
            # The actual cif attribute name serves as the key and corresponding value is the friendlier label
            dictColDisplNames = {}
            dictColDisplNames["message_subject"] = "Subject"
            dictColDisplNames["sender"] = "Sender"
            dictColDisplNames["timestamp"] = "Date/Time"
            dictColDisplNames["orig_timestamp"] = "Orig Date/Time"
            dictColDisplNames["orig_sender"] = "Orig Sender"
            dictColDisplNames["orig_recipient"] = "Orig Recipient"
            dictColDisplNames["orig_message_subject"] = "Orig Subject"
            dictColDisplNames["orig_attachments"] = "Orig Attachments"

            # mrkpList.append('<table id="'+depId+'_tbl" width="100%"><thead><tr>')  #needed for targeted update to Bootstrap based UI?
            mrkpList.append('<table id="' + depId + '_tbl"><thead><tr>')

            for idx, cifAttribNm in enumerate(msgColList):
                # here we impose more user-friendly labels for column headers
                # as we iterate through each cif attrib name in the list of msging columns
                # we check dictionary--> if cif attrib name is a key that has corresponding value for user friendly label,
                # then that user-friendly label is used as the colName, otherwise the cifAttribName itself is used as the colName by default
                colName = dictColDisplNames.get(cifAttribNm, cifAttribNm)

                mrkpList.append('<th class="cifitemhdr">' + colName + "</th>")

                srchHdrList.append('<th><input type="text" name="search_' + str(idx) + '" placeholder="Search ' + colName + '" class="search_init" /></th>')

            mrkpList.append('</tr><tr class="srch_hdrs displaynone">')
            mrkpList.append("".join(srchHdrList))

            mrkpList.append("</tr></thead><tbody></tbody></table>")  # 2015-04-16, RPS removed <tr></tr> as this was causing DataTables 1.10.5 to choke

            dtblConfigDict["ORDERED_COLUMN_LIST"] = msgColList
            dtblConfigDict["MSGS_ALREADY_READ"] = msgingIo.getMsgReadList(depId)
            dtblConfigDict["MSGS_NO_ACTION_REQD"] = msgingIo.getMsgNoActionReqdList(depId)
            dtblConfigDict["MSGS_FOR_RELEASE"] = msgingIo.getMsgForReleaseList(depId)
            dtblConfigDict["NOTES_MSG_IDS"] = msgingIo.getNotesList()

        else:
            mrkpList = ["<h3>Problem getting messages for this deposition dataset.</h3>"]
        # RETURNing both markup and config dictionary
        return mrkpList, dtblConfigDict

    # ####### END -- Specific to DataTable Implementation ##################

    # #####################################   HELPER FUNCTIONS   #################################################

    # ####### BEGIN -- Specific to DataTable Implementation ##################
    def __createDataTableAaDataList(self, p_colList, p_recordList, p_iDisplayStart):
        """Generate contents of "aaData" json object expected by DataTables for populating display
        table with data.

        :Params:

            + ``p_colList``: list of column names corresponding sequence of which corresponds to
                                sequence of fields in each record in p_recordList
            + ``p_recordList``: list of records corresponding to messages obtained from database for given deposition dataset
            + ``p_iDisplayStart``: DataTable parameter used by the plugin as index of first record in set actually being displayed on screen


        :Returns:
            ``rtrnLst``: list of records for display on screen as complies with DataTables requirements for "aaData" object
        """
        if self.__verbose:
            logger.info("--------------------------------------------")
            logger.info("Starting at %s", time.strftime("%Y %m %d %H:%M:%S", time.localtime()))
        rtrnLst = []

        for indx, row in enumerate(p_recordList):
            if self.__verbose and self.__debug:
                logger.debug(" -- row[%s] is: %s", indx, row)
            #
            newRecordJsonObj = {}
            #
            if type(row) is list or type(row) is tuple:
                newRecordJsonObj["DT_RowId"] = "row_" + str(indx + p_iDisplayStart)
                newRecordJsonObj["DT_RowClass"] = "dt_row"
            elif type(row) is dict:
                # if the row is itself a dictionary, indicates that search filtering was done
                # and so dict here will consist of single entry where the "key" represents true
                # row index of the cif record as it sits in persistent store and "value" is the cif record itself
                key, value = row.popitem()
                newRecordJsonObj["DT_RowId"] = "row_" + str(key + p_iDisplayStart)
                newRecordJsonObj["DT_RowClass"] = "dt_row"
                row = value
            #
            for colName, rcrdValue in zip(p_colList, row):
                newRecordJsonObj[colName] = str(rcrdValue)
            #
            rtrnLst.append(newRecordJsonObj)

        return rtrnLst

    # ####### END -- Specific to DataTable Implementation ##################

    def setSessionPaths(self, p_reqObj):
        """Establish absolute/relative paths to be used for storing/accessing session-related data

        :Params:
            ``p_reqObj``: Web Request object
        """
        sessionMgr = p_reqObj.getSessionObj()

        # ### absolute paths ####
        absSessPth = sessionMgr.getPath()
        # absolute path used for referencing session directory content from front end
        self.absltSessionPath = absSessPth

        # ### relative paths #####
        rltvSessPth = sessionMgr.getRelativePath()
        # relative path used for referencing session directory content from front end
        self.rltvSessionPath = rltvSessPth

    def isWorkflow(self, p_reqObj):
        """Determine if currently operating in Workflow Managed environment"""
        #
        fileSource = str(p_reqObj.getValue("filesource")).lower()
        #
        if fileSource in ["archive", "wf-archive", "wf_archive", "wf-instance", "wf_instance"]:
            # if the file source is any of the above then we are in the workflow manager environment
            return True
        else:
            # else we are in the standalone dev environment
            return False

    def processTemplate(self, tmpltPth, fn, parameterDict=None):
        """Read the input HTML template data file and perform the key/value substitutions in the
        input parameter dictionary.
        """
        if parameterDict is None:
            parameterDict = {}

        fPath = os.path.join(tmpltPth, fn)
        ifh = open(fPath, "r")
        sIn = ifh.read()
        ifh.close()
        return sIn % parameterDict

    def truncateForDisplay(self, content, maxlength=20, suffix="..."):
        """Obtain truncated version of long identifiers for display purposes (e.g. in comparison panel)"""
        if content is not None:
            if len(content) <= maxlength:
                return content
            else:
                return content[: maxlength + 1] + suffix
        else:
            return ""

    # def __formatDepositionDataId(self, p_depid, p_bIsWorkflow):
    #     if p_bIsWorkflow or (p_depid.upper() == "TMP_ID"):
    #         depId = p_depid.upper()
    #     else:
    #         depId = p_depid.lower()
    #     return depId
