import collections
import functools
from copy import deepcopy
from datetime import datetime
from enum import Enum
from operator import attrgetter
from typing import List
from schrodinger import structure
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.ui.qt import appframework as af1
from schrodinger.ui.qt import delegates
from schrodinger.ui.qt import pop_up_widgets
from schrodinger.ui.qt import table_helper
from schrodinger.ui.qt import utils as qtutils
from schrodinger.ui.qt import widgetmixins
from schrodinger.ui.qt.standard.icons import icons
from . import assay_selection_pop_up_ui
from . import data_classes
from . import export_models
from . import icons_rc  # noqa:F401 # pylint: disable=unused-import
from . import ld_folder_tree as ldt
from .constants import LD_PROP_NAME
get_long_family_name = data_classes.get_long_family_name
LDData = data_classes.LDData
COLOR_MISSING = QtGui.QColor(225, 0, 0, 120)
NO_FOLDER_NAME = 'Project Home'
SELECT_TEXT = 'Select LiveReport...'
NEW_TEXT = 'New LiveReport'
CLICK = 'Click to select...'
FOLDER_CONTENTS = '__FOLDER_CONTENTS__'
TYPE_TO_CREATE = 'Type here to create new'
NEW_NAME = 'New name'
ID = 'ID'
STRUCTURE = 'Compound Structure'
CustomRole = table_helper.UserRolesEnum(
    "CustomRole", ("AssayData", "AssayFolderPathData", "EndpointData"))
ENDPOINT_MISSING = 'Endpoint Missing'
MODEL_OR_ASSAY_MISSING = 'Model or Assay Missing'
MAESTRO_ASSAY = 'Maestro'
MAESTRO_FAMILY_NAME = get_long_family_name('m')
ENDPOINT_3D = '3D'
LD_DATA_3D = export_models.LD_DATA_3D
LRSort = Enum('LRSort', ('Owner', 'Folder'))
DEFAULT_LR_SORT = LRSort.Folder
# =============================================================================
# StyleSheet
# =============================================================================
# Icon prefix
ICON_PREFIX = ':/maestro_ld_gui_dir/icons/'
SEARCH_BOX = """
QLineEdit{
    background-image: url('""" + ICON_PREFIX + """search_icon.png');
    background-repeat: no-repeat;
    background-position: left;
    padding-left: 17px;
    border: 1px solid #D3D3D3;
    padding-top: 1px;
    border-radius: 7px;
    margin-top: 1px;
    height: 20px;
}
"""
# =============================================================================
# Status Bar
# =============================================================================
[docs]class StatusBarDialog(QtWidgets.QDialog):
    """
    Helper class to setup the status bar for the panel. This class acts as
    the parent to avoid thread issues.
    """
    def _help(self):
        """
        Display the help dialog.
        """
        af1.help_dialog(self.help_topic, parent=self) 
[docs]class LRSortCombo(QtWidgets.QComboBox):
    """
    Combo box used to specify the method used to sort and organize live reports.
    Emits a custom signal with an enum designating the sort method.
    :cvar LRSortMethodChanged: signal indicating that a new live report sort
        method has been chosen; emitted with an `LRSort` value
    :vartype LRSortMethodChanged: QtCore.pyqtSignal
    """
    LRSortMethodChanged = QtCore.pyqtSignal(LRSort)
[docs]    def __init__(self, parent=None):
        super().__init__(parent=parent)
        # Populate the combo box and set up internal signal/slot connections
        for sort_type in LRSort:
            self.addItem(sort_type.name, sort_type)
        self.currentIndexChanged.connect(self.onCurrentIndexChanged)
        # Select the default sort method
        idx = self.findData(DEFAULT_LR_SORT)
        self.setCurrentIndex(idx) 
[docs]    def onCurrentIndexChanged(self):
        """
        When the user makes a selection in this combo box, emit the enum value
        associated with their selection (rather than the less-useful index of
        the selection).
        """
        self.LRSortMethodChanged.emit(self.currentData())  
# =============================================================================
# Base Widgets
# =============================================================================
# =============================================================================
# LD Data Tree Model
# =============================================================================
[docs]class CascadingCheckboxItem(QtGui.QStandardItem):
    """
    A subclass of QStandardItem that implements checkboxes that automatically
    respond to changes in child/parent check state. Checking or unchecking an
    item will cause all of its children to become checked/unchecked accordingly
    and will update its parent to be either checked, unchecked, or partially
    checked, depending on the state of all of the other children.
    """
[docs]    def __init__(self, *args, **kwargs):
        super(CascadingCheckboxItem, self).__init__(*args, **kwargs)
        self.setCheckable(True)
        self.setTristate(True)
        self.update_in_progress = False 
[docs]    def getChildItems(self, column=0):
        """
        Returns a list of all the child items. A column may be optionally
        specified if desired, otherwise the first column's item will be
        returned from each descendent row.
        :param column: the column of the item to be returned from each row
        :type column: int
        """
        return [self.child(row, column) for row in range(self.rowCount())] 
[docs]    def updateCheckState(self):
        """
        Updates the item's check state depending on the check state of all the
        child items. If all the child items are checked or unchecked, this item
        will be checked/unchecked accordingly. If only some of the children are
        checked, this item will be partially checked.
        """
        if self.update_in_progress:
            return
        states = [item.checkState() for item in self.getChildItems()]
        if all([state == Qt.Unchecked for state in states]):
            self.setCheckState(Qt.Unchecked)
        elif all([state == Qt.Checked for state in states]):
            self.setCheckState(Qt.Checked)
        else:
            self.setCheckState(Qt.PartiallyChecked) 
[docs]    def updateEnabled(self):
        """
        If this item has children and they are all disabled, disable this item.
        If any such children are enabled, enable this item.
        """
        child_items = self.getChildItems()
        if not child_items:
            return
        enabled = any(item.isEnabled() for item in child_items)
        if self.isEnabled() != enabled:
            self.setEnabled(enabled) 
[docs]    def setData(self, value, role):
        """
        Overrides setData() to trigger an update of the parent item's check
        state and enabled state, and propagate check state to all the child
        items (i.e. checking this item will cause all its child items to become
        checked).
        See parent class for documentation on parameters.
        """
        self.update_in_progress = True
        super().setData(value, role)
        parent = self.parent()
        if parent:
            parent.updateCheckState()
            parent.updateEnabled()
        if value in (Qt.Unchecked, Qt.Checked):
            for item in self.getChildItems():
                if item.isEnabled():
                    item.setCheckState(value)
        self.update_in_progress = False 
[docs]    def setEnabled(self, enabled):
        super().setEnabled(enabled)
        parent = self.parent()
        if parent:
            parent.updateEnabled()  
[docs]class LDDataCheckboxItem(CascadingCheckboxItem):
    """
    A `CascadingCheckboxItem` that stores and knows how to display a
    `data_classes.LDData` instance.
    """
[docs]    def __init__(self, ld_data):
        """
        :param ld_data: LD data instance
        :type ld_data: data_classes.LDData
        """
        super(LDDataCheckboxItem, self).__init__(ld_data.user_name)
        self._ld_data = ld_data 
    @property
    def ld_data(self):
        return self._ld_data 
[docs]class LDDataSelectionModel(QtGui.QStandardItemModel):
    """
    A tree structured model for storing LD data by family name.
    :cvar item_dict: a dictionary mapping LD data to items from this model
    :vartype item_dict: dict(data_classes.LDData, QtGui.QStandardItem)
    """
[docs]    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.item_dict = {} 
[docs]    def clear(self):
        super().clear()
        self.item_dict = {} 
[docs]    def loadData(self, ld_data_list):
        """
        Replaces the data in the model with the specified list of export data.
        :param ld_data_list: a list of LD data to export
        :type ld_data_list: list(data_classes.LDData)
        """
        self.clear()
        # Organize the data by family name
        ld_data_map = organize_ld_data_tree(ld_data_list)
        for family_name, ld_data_list in ld_data_map.items():
            product_row_item = CascadingCheckboxItem(family_name)
            sort_func = lambda ld_data: ld_data.user_name.lower()
            for ld_data in sorted(ld_data_list, key=sort_func):
                ld_data_item = LDDataCheckboxItem(ld_data)
                self.item_dict[ld_data] = ld_data_item
                product_row_item.appendRow(ld_data_item)
            self.appendRow([product_row_item]) 
[docs]    def getCheckedData(self):
        """
        Recursively traverses the entire tree and returns LD data instances
        checked by the user.
        :return: LD data specified for export by the user
        :rtype: list(data_classes.LDData)
        """
        ld_data_list = []
        for row_idx in range(self.rowCount()):
            item = self.item(row_idx, 0)
            ld_data_list += self._recurseGetCheckedData(item)
        return ld_data_list 
    def _recurseGetCheckedData(self, item):
        """
        Recursively search item and all children, returning data assocated with
        checked items.
        :param item: an item from this model
        :type item: QtGui.QStandardItem
        :return: a list of `LDData` associated with this item or its children
            if they are checked
        :rtype: list(data_classes.LDData)
        """
        ld_data_list = []
        child_items = item.getChildItems()
        for child_item in child_items:
            ld_data_list += self._recurseGetCheckedData(child_item)
        if not child_items and item.checkState() == Qt.Checked:
            ld_data_list = [item.ld_data]
        return ld_data_list
[docs]    def setItemsChecked(self, ld_data_list, checked):
        """
        Set the checkstate of the items associated with the supplied data, if
        they are enabled.
        :param ld_data_list: a list of `LDData` instances corresponding to items
            in the model
        :type ld_data_list: List[data_classes.LDData]
        :param checked: whether to check or uncheck the specified item
        :type checked: bool
        """
        self.beginResetModel()
        for ld_data in ld_data_list:
            item = self.item_dict.get(ld_data)
            if item is None or not item.isEnabled():
                continue
            if checked:
                item.setCheckState(Qt.Checked)
            else:
                item.setCheckState(Qt.Unchecked)
        self.endResetModel() 
[docs]    def uncheckAll(self):
        """
        Un-check all items in tree.
        """
        for row in range(self.rowCount()):
            item = self.item(row, 0)
            item.setData(Qt.Unchecked, Qt.CheckStateRole) 
[docs]    def setItemEnabled(self, ld_data, enable):
        """
        Set an item to be enabled or disabled.
        :param ld_data: data object associated with the item to be enabled or
            disabled
        :type ld_data: data_classes.LDData
        :param enable: whether to enable (`True`) or disable (`False`) the
            specified item
        :type enable: bool
        """
        item = self.item_dict.get(ld_data)
        if item is None:
            return
        item.setEnabled(enable)
        if not enable:
            item.setCheckState(Qt.Unchecked) 
 
# =============================================================================
# LD Data Tree View
# =============================================================================
[docs]class LDDataSelectionTreeView(QtWidgets.QTreeView):
    """
    A class for displaying LD data selection.
    """
[docs]    def __init__(self):
        super().__init__()
        self.setMinimumWidth(200)
        self.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
                           QtWidgets.QSizePolicy.Expanding)
        self.setHeaderHidden(True)  
# =============================================================================
# LD Data Tree Widget
# =============================================================================
[docs]class LDDataTree(widgetmixins.InitMixin, BaseSearchTreeWidgetHelper,
                 QtWidgets.QWidget):
    """
    A QWidget with a initialization mixin to group together a search bar
    LineEdit and the LD Data Selection QTreeView.
    """
    dataChanged = QtCore.pyqtSignal()
[docs]    def initSetUp(self):
        """
        Sets up the model, view, proxy and search box.
        See BaseSearchTreeWidgetHelper.setUpWidgets for more info.
        """
        model = LDDataSelectionModel()
        view = LDDataSelectionTreeView()
        self.setUpWidgets(model, view)
        # Signals
        self.proxy_model.dataChanged.connect(self.dataChanged.emit) 
[docs]    def initLayOut(self):
        super(LDDataTree, self).initLayOut()
        # Add subwidgets
        self.main_layout.addWidget(self.search_le)
        self.main_layout.addWidget(self.view)
        # Aesthetics
        self.main_layout.setContentsMargins(0, 0, 0, 0) 
[docs]    def loadData(self, ld_data_list):
        """
        See `LDDataSelectionModel.loadData()` for more information.
        """
        checked_ldd_items = self.getCheckedData()
        self.model.loadData(ld_data_list)
        self.setCheckedData(checked_ldd_items) 
[docs]    def getCheckedData(self):
        """
        See `LDDataSelectionModel.getCheckedData()` for more information.
        """
        return self.model.getCheckedData() 
[docs]    def setCheckedData(self, ld_data_list):
        """
        Check the items corresponding to the supplied LD data (if found), and
        uncheck all other items.
        :param ld_data_list: a list of LD data instances that should be checked
            in the tree
        :type ld_data_list: list(data_classes.LDData)
        """
        with qtutils.suppress_signals(self):
            self.uncheckAll()
            self.setItemsChecked(ld_data_list, True)
        self.dataChanged.emit() 
[docs]    def setItemChecked(self, ld_data, checked):
        """
        Convenience method to check or uncheck a single item.
        :param ld_data: a LD data object corresponding to an item in the model
        :type ld_data: data_classes.LDData
        :param checked: whether to check or uncheck the specified item
        :type checked: bool
        """
        self.setItemsChecked([ld_data], checked) 
[docs]    def setItemsChecked(self, ld_data_list, checked):
        """
        Set the checkstate of the items associated with the supplied data, if
        they are enabled.
        :param ld_data_list: a list of `LDData` instances corresponding to items
            in the model
        :type ld_data_list: List[data_classes.LDData]
        :param checked: whether to check or uncheck the specified items
        :type checked: bool
        """
        self.model.setItemsChecked(ld_data_list, checked)
        self.dataChanged.emit() 
[docs]    def isItemChecked(self, ld_data):
        """
        Return whether the item associated with the specified LiveDesign data
        object is checked.
        :param ld_data: a `LDData` instance corresponding to an item in the
            model
        :type ld_data: data_classes.LDData
        :return: whether the specified item is checked, if possible; if the item
            cannot be found, return `None`
        :rtype: bool or NoneType
        """
        item = self.model.item_dict.get(ld_data)
        if item:
            return item.checkState() == Qt.Checked 
[docs]    def isItemEnabled(self, ld_data):
        """
        Return whether the specified item is enabled.
        :param ld_data: a `LDData` instance corresponding to an item in the
            model
        :type ld_data: data_classes.LDData
        :return: if possible, whether the specified item is enabled; if no such
            item exists, return `None`
        :rtype: bool or NoneType
        """
        item = self.model.item_dict.get(ld_data)
        if item is not None:
            return item.isEnabled() 
[docs]    def uncheckAll(self):
        """
        See `LDDataSelectionModel.uncheckAll()` for more information.
        """
        with qtutils.suppress_signals(self):
            self.model.uncheckAll()
        self.dataChanged.emit() 
[docs]    def initSetDefaults(self):
        self.resetWidgets() 
[docs]    def setItemsEnabled(self, ld_data_list, enabled):
        """
        Enable or disable the items associated with the specified data list.
        :param ld_data_list: a list of LD Data objects that correspond to items
            that should be enabled or disabled
        :type ld_data_list: list(data_classes.LDData)
        :param enabled: enable (`True`) or disable (`False`) specified items
        :type enabled: bool
        """
        for ld_data in ld_data_list:
            self.model.setItemEnabled(ld_data, enabled) 
[docs]    def expandFamily(self, family_name):
        """
        Expand the item associated with the supplied family name if possible.
        :param family_name: the family name of the item to expand
        :type family_name: str
        """
        items = self.model.findItems(family_name)
        if not items:
            return
        # There should only be one item per LD data family name
        assert len(items) == 1
        index = self.proxy_model.mapFromSource(items[0].index())
        self.view.setExpanded(index, True)  
# =============================================================================
# Column List Model
# =============================================================================
[docs]class ColumnSelectionModel(QtGui.QStandardItemModel):
    """
    A tree structured model for storing data by family.
    """
[docs]    def __init__(self, *args, **kwargs):
        super(ColumnSelectionModel, self).__init__(*args, **kwargs)
        self.showing_unavailable = False 
[docs]    def loadData(self, columns, unavailable_ids=None):
        """
        Populates the model using a list of ldclient.models.Column objects.
        This clears out all previous data in the model.
        :param columns: the list of columns to add
        :type columns: list of ldclient.models.Column
        :param unavailable_ids: list of column IDs that should always be
                unavailable for import
        :type unavailable_ids: `list` of `str`, or `None`
        """
        self.clear()
        if not columns:
            return
        unavailable_ids = unavailable_ids or []
        sorted_cols = sorted(columns, key=attrgetter(LD_PROP_NAME))
        # pull out ID and Comp Structure and insert at beginning
        for i, col in enumerate(sorted_cols[:]):
            if col.name in [STRUCTURE, ID]:
                sorted_cols.insert(0, sorted_cols.pop(i))
        for col in sorted_cols:
            unavailable = col.id in unavailable_ids
            product_row_item = ColumnCheckboxItem(col,
                                                  force_unavailable=unavailable)
            # ID always required
            if col.name in [STRUCTURE, ID]:
                product_row_item.setCheckState(Qt.Checked)
                product_row_item.setEnabled(False)
            self.appendRow([product_row_item]) 
[docs]    def getCheckedColumns(self, all_checked: bool = False):
        """
        Traverse all the columns and return all the checked
        properties.
        :param all_checked: if the all columns checked property is enabled
        :return: the checked properties
        :rtype: list of structure.PropertyName
        """
        checked_props = []
        for row in range(self.rowCount()):
            item = self.item(row, 0)
            if (all_checked and item.isEnabled() or
                    item.checkState() == Qt.Checked):
                checked_props.append(item.column())
        return checked_props 
[docs]    def getVisibleColCount(self) -> int:
        """
        Calculate the total amount of columns that are visible.
        """
        return len([
            row for row in range(self.rowCount())
            if self.item(row, 0).isEnabled() or
            self.item(row, 0).checkState() == Qt.Checked
        ]) 
[docs]    def getHiddenColCount(self):
        """
        Calculate the total amount of unsupported columns.
        """
        return len([
            row for row in range(self.rowCount())
            if not self.item(row, 0).isEnabled() and
            self.item(row, 0).checkState() != Qt.Checked
        ])  
[docs]class ColumnSelectionProxyModel(QtCore.QSortFilterProxyModel):
    """
    A proxy model to filter an assay model based on the availability
    """
[docs]    def __init__(self, parent=None):
        super(ColumnSelectionProxyModel, self).__init__(parent)
        # maintain Qt4 dynamicSortFilter default
        self.setDynamicSortFilter(False)
        self.showing_unavailable = False 
[docs]    def filterAcceptsRow(self, source_row, source_parent):
        """
        See Qt Documentation for more information on parameters.
        This filter accepts a particular row if any of the following are true:
        1. The proxy model is currently set to show unavailable items
        2. The item is marked as available
        Note that the conditions specified above are searched in that order.
        """
        index = self.sourceModel().index(source_row, 0, source_parent)
        item = self.sourceModel().itemFromIndex(index)
        return self.showing_unavailable or item.available 
[docs]    def showUnavailable(self, show=True):
        self.showing_unavailable = show
        self.invalidate()  
[docs]class StructSelectionModel(QtGui.QStandardItemModel):
    """
    A selection model for storing structures to be selected.
    """
[docs]    def loadData(self, structs: List[structure.Structure]):
        """
        Populate the model using a list of structures. This clears
        all previous data in the model.
        :param structs: structures to be included in the table
        """
        self.clear()
        if not structs:
            return
        for struct in sorted(structs, key=lambda st: st.title):
            product_row_item = StructCheckboxItem(struct)
            self.appendRow([product_row_item]) 
[docs]    def getCheckedStructs(self,
                          all_checked: bool = False
                         ) -> List[structure.Structure]:
        """
        Obtain a list of all checked structures.
        :param all_checked: if all structures should be included
        :return: all structures that are checked
        """
        checked_structs = []
        for row in range(self.rowCount()):
            item = self.item(row, 0)
            if all_checked or item.checkState() == Qt.Checked:
                checked_structs.append(item.structure)
        return checked_structs  
[docs]class ColumnCheckboxItem(QtGui.QStandardItem):
    """
    A CascadingCheckboxItem that stores and knows how to display a Column
    object.
    """
    # TODO these will eventually be supported - PANEL-9680
    UNAVAIL_VALUE_TYPES = ['attachment']
[docs]    def __init__(self, col, force_unavailable=False):
        """
        :param col: the object describing a live report column.
        :type col: ldclient.models.Column
        :param force_unavailable: if `True`, make this column unavailable for
                import by disabling it in the list view
        :type force_unavailable: `bool`
        """
        super().__init__(col.name)
        self.col = col
        if force_unavailable:
            # Caller has specified that this column should be unavailable
            self.available = False
        else:
            self.available = (col.value_type not in self.UNAVAIL_VALUE_TYPES)
        self.setEnabled(self.available)
        self.setCheckable(True) 
[docs]    def column(self):
        return self.col  
[docs]class StructCheckboxItem(QtGui.QStandardItem):
    """
    A CascadingCheckboxItem that stores and displays a Structure.
    """
[docs]    def __init__(self, struct):
        """
        :param struct: the structure to be filtered on
        :type  struct: structure.Structure
        """
        super().__init__(struct.title)
        self._struct = struct
        self.setCheckable(True) 
    @property
    def structure(self):
        return self._struct 
# =============================================================================
# Generic List View
# =============================================================================
[docs]class SelectionListView(QtWidgets.QListView):
    """
    A class for displaying a column selection
    """
[docs]    def __init__(self):
        super().__init__()
        self.setMinimumWidth(200)
        self.setMinimumHeight(200)
        self.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
                           QtWidgets.QSizePolicy.Expanding)  
# =============================================================================
# Export Table Model
# =============================================================================
class ExportTableColumns(table_helper.TableColumns):
    Data = table_helper.Column('Maestro Property')
    Assay = table_helper.Column('LiveDesign Folder', editable=True)
    Endpoint = table_helper.Column('LiveDesign Property', editable=True)
    Units = table_helper.Column('Units')
    Decimal = table_helper.Column('Decimal Places')
    Options = table_helper.Column('Options')
[docs]class ExportRow:
    """
    An object for data in each row in the table.
    """
[docs]    def __init__(self,
                 ld_data=None,
                 assay=None,
                 endpoint=None,
                 units=None,
                 decimal=None,
                 option=None,
                 assay_folder_path=None):
        self.ld_data = ld_data
        self.assay = assay
        self.endpoint = endpoint
        self.units = units
        self.decimal = decimal
        self.option = option
        self.assay_folder_path = assay_folder_path 
    def __eq__(self, other):
        """
        Compare this ExportRow to other instance of ExportRow to see if they
        are equivalent.
        :param other: instance to compare
        :type other: ExportRow
        :return: whether the two instances are equal
        :rtype: bool
        """
        return self.__dict__ == other.__dict__
    def __ne__(self, other):
        """
        Compare this ExportRow to other instance of ExportRow to see if they
        are NOT equivalent.
        :param other: instance to compare
        :type other: ExportRow
        :return: whether the two instances are not equal
        :rtype: bool
        """
        return not self.__eq__(other) 
[docs]class ExportTableModel(table_helper.RowBasedTableModel):
    Column = ExportTableColumns
    ROW_CLASS = ExportRow
[docs]    def __init__(self):
        super().__init__()
        self.assay_data = None
        self.endpoint_dict = {}
        self._disable_lr_columns = True
        self.highlight_missing_fields = False
        self._3d_data_spec_map = {}
        self._ffc_data_spec_map = {} 
[docs]    def set3DDataSpecMap(self, spec_map):
        self._3d_data_spec_map = spec_map 
[docs]    def setFFCDataSpecMap(self, spec_map):
        self._ffc_data_spec_map = spec_map 
    @property
    def disable_lr_columns(self):
        """
        :return: whether to disable certain live report-dependent export columns
        :rtype: bool
        """
        return self._disable_lr_columns
    @disable_lr_columns.setter
    def disable_lr_columns(self, disable):
        """
        Set certain live report-dependent table columns to be disabled or not.
        If the state of this value is changed, emit signals notifying the table
        view that it should be updated.
        :param disable: whether certain live report-dependent columns should be
            disabled
        :type disable: bool
        """
        if self.disable_lr_columns == disable:
            return
        self._disable_lr_columns = disable
        for column in self.getLRDependentColumns():
            self.columnChanged(column)
[docs]    def getLRDependentColumns(self):
        """
        Return a list of columns that should be disabled if a live report is not
        selected in the panel.
        :return: a list of columns that depend on the live report selection
        :rtype: `list` of `table_helper.TableColumns` enum values
        """
        return [self.Column.Endpoint, self.Column.Assay] 
[docs]    @table_helper.model_reset_method
    def reset(self):
        super().reset()
        self.disable_lr_columns = True 
[docs]    @table_helper.model_reset_method
    def loadData(self, ld_data_list):
        """
        Load in the data values to be shown as rows with default information
        for Assays and Endpoints. Note, this method resets the table.
        :param ld_data_list: list of data values
        :type ld_data_list: list(data_classes.LDData)
        """
        rows = []
        primary_3d_row = self._getPrimary3DRow()
        assay_3d = primary_3d_row.assay if primary_3d_row else None
        for ld_data in ld_data_list:
            row_idx = self._getRowIndexByLDData(ld_data)
            if row_idx >= 0:
                # LD data is already loaded to the table
                rows.append(self._rows[row_idx])
                continue
            # By default, new properties have no assay/endpoint
            assay, endpoint = CLICK, CLICK
            if ld_data.requires_3d:
                assay = LD_DATA_3D.family_name
                endpoint = ENDPOINT_3D
                if assay_3d:
                    assay = assay_3d
            elif ld_data.family_name == MAESTRO_FAMILY_NAME:
                # Maestro properties assigned assays/endpoints automatically
                assay = MAESTRO_ASSAY
                endpoint = ld_data.user_name
            rows.append(
                self.ROW_CLASS(ld_data=ld_data, assay=assay, endpoint=endpoint))
        super().loadData(rows) 
    def _getPrimary3DRow(self):
        """
        :return: the row corresponding to the `LD_DATA_3D` LD data instance
        :rtype: ExportRow or None
        """
        for row in self.rows:
            if row.ld_data == LD_DATA_3D:
                return row
[docs]    def loadMappings(self, map_rows):
        """
        Load in the mapping data. The properties in the mapping data are
        assumed to already exist in the table. Note, that this method does not
        reset the table.
        :param map_rows: mapped row data to set
        :type map_rows: List of ExportRow
        :return: whether the loading of the mapping data was successful
        :rtype: bool
        """
        success = True
        for map_row in map_rows:
            # ignore custom Maestro rows with default values
            if map_row.assay == MAESTRO_ASSAY and not map_row.assay_folder_path:
                continue
            # get row index
            row_idx = self._getRowIndexByLDData(map_row.ld_data)
            if row_idx == -1:
                success = False
                continue
            self._applyRowMapping(row_idx, map_row)
        return success 
    def _applyRowMapping(self, row_idx, map_row):
        """
        Apply mapping data to row at specified index.
        :param row_idx: the row index to apply mapping to
        :type row_idx: int
        :param map_row: mapped row data to set
        :type map_row: ExportData
        """
        # Set assay-related data
        assay_names_to_path = {
            a_wrapper.name: a_wrapper.folder_path
            for a_wrapper in self.assay_data
        }
        map_assay = map_row.assay
        map_assay_folder_path = map_row.assay_folder_path
        assay_index = self.index(row_idx, self.Column.Assay)
        if map_assay != CLICK and (
                map_assay not in assay_names_to_path or
                assay_names_to_path[map_assay] != map_assay_folder_path):
            # since this assay doesn't exist in the host's assay data,
            # this assay will be added as a new user created assay.
            if map_assay_folder_path is not None:
                map_assay_folder_path = 'User Created'
            new_assay = BaseLDTreeItemWrapper(ld_name=map_assay,
                                              path=map_assay_folder_path)
            self.setData(assay_index, new_assay, role=CustomRole.AssayData)
        self.setData(assay_index, map_assay)
        self.setData(assay_index,
                     map_assay_folder_path,
                     role=CustomRole.AssayFolderPathData)
        # Set endpoint, units, decimal, and options
        endpoint_index = self.index(row_idx, self.Column.Endpoint)
        self.setData(endpoint_index, map_row.endpoint)
        units_index = self.index(row_idx, self.Column.Units)
        self.setData(units_index, map_row.units)
        decimal_index = self.index(row_idx, self.Column.Decimal)
        self.setData(decimal_index, map_row.decimal)
        options_index = self.index(row_idx, self.Column.Options)
        self.setData(options_index, map_row.option)
[docs]    def getMappableRows(self):
        """
        Get rows that can be saved to a mapping state.
        :return: mapped row data
        :rtype: List of ExportRow
        """
        return [deepcopy(row) for row in self.rows] 
    def _getRowIndexByLDData(self, ld_data):
        """
        Returns the row index that matches the supplied LD Data.
        :param ld_data: object identifying one piece of data for export
        :type ld_data: data_classes.LDData
        :return: row index, or -1 if does not exist
        :rtype: int
        """
        for r, row_obj in enumerate(self.rows):
            if row_obj.ld_data == ld_data:
                return r
        return -1
[docs]    def loadAssayData(self, assay_paths, favorite_assay_paths):
        """
        Load in the complete Assay data - full path name - wrapped as
        BaseLDTreeItemWrapper
        :param assay_paths: Assay data to store.
        :type assay_paths: List of paths
        :param favorite_assay_paths: Favorite Assay data to store.
        :type favorite_assay_paths: List of (assay names, folder path) tuples
        """
        assay_data = []
        pathsplit = lambda path: path.rsplit(ldt.SEP, 1)
        for a_path, a_name in map(pathsplit, assay_paths):
            assay_data.append(
                BaseLDTreeItemWrapper(ld_name=a_name, path=str(a_path)))
        # keep track of duplicate favorites so we can add their original path
        # to distinguish them
        favorite_names = collections.defaultdict(int)
        for a_path, a_name in map(pathsplit, favorite_assay_paths):
            favorite_names[a_name] += 1
        for a_path, a_name in map(pathsplit, favorite_assay_paths):
            is_duplicate = favorite_names[a_name] > 1
            # TODO should path be 'Project Favorites/subfolders/assay_name' or
            # just 'Project Favorites/assay_name'?
            assay_data.append(
                BaseLDTreeItemWrapper(ld_name=a_name,
                                      path='Project Favorites',
                                      linked_path=str(a_path),
                                      show_path=is_duplicate))
        self.assay_data = assay_data 
[docs]    def loadEndpointData(self, endpoint_map):
        """
        Set the assay path to endpoint dictionary.
        :param endpoint_map: a dictionary that maps assay folder paths to
            endpoints
        :type endpoint_map: dict(str, set(str))
        """
        self.endpoint_dict = endpoint_map 
    @table_helper.data_method(CustomRole.AssayData)
    def _assayData(self, col, row_data, role):
        """
        Get the Assay data. Note, all rows in the Model / Assay column hold
        the same Assay Data.
        See superclass for argument docstring.
        :return: list of Assays
        :rtype: List of BaseLDTreeItemWrapper.
        """
        if col == self.Column.Assay:
            return self.assay_data
    @table_helper.data_method(CustomRole.AssayFolderPathData)
    def _assayFolderPathData(self, col, row_data, role):
        """
        Get the Assay folder path data for this row
        See superclass for argument docstring.
        :return: the assay folder path for this cell
        :rtype: str
        """
        if col == self.Column.Assay:
            return row_data.assay_folder_path
    @table_helper.data_method(CustomRole.EndpointData)
    def _endpointData(self, col, row_data, role):
        """
        Get the Endpoint Data for this row given an Assay has already been
        selected. Filter out the correct existing endpoints for the given
        Assay / Model name.
        See superclass for argument docstring.
        :return: list of endpoints
        :rtype: List of str
        """
        if col == self.Column.Endpoint:
            assay_name = row_data.assay
            assay_folder_path = row_data.assay_folder_path
            endpoint_folder_path = None
            if assay_folder_path:
                # existing assay
                endpoint_folder_path = ldt.SEP.join(
                    [assay_folder_path, assay_name])
                # FIXME: Finalize whether only Assay Types should be shown here
                # or Endpoints in the form of Model (Assay Type).
                # FIXME figure out how to use favorite_endpoints for selections
                # from project favorites
            return self.endpoint_dict.get(endpoint_folder_path, set())
    @table_helper.data_method(Qt.BackgroundRole)
    def _backgroundData(self, col, row, role):
        if not self.highlight_missing_fields:
            return
        if col == self.Column.Assay:
            if not row.assay or row.assay == CLICK:
                return QtGui.QBrush(COLOR_MISSING)
        if col == self.Column.Endpoint:
            if not row.endpoint or row.endpoint == CLICK:
                return QtGui.QBrush(COLOR_MISSING)
    @table_helper.data_method(Qt.DisplayRole, Qt.EditRole)
    def _data(self, col, row, role):
        if col == self.Column.Data:
            return row.ld_data.user_name
        elif col == self.Column.Assay:
            return row.assay
        elif col == self.Column.Endpoint:
            return row.endpoint
        elif col == self.Column.Units:
            return row.units
    def _setData(self, col, row_data, value, role, row_num):
        allowed_roles = (Qt.EditRole, Qt.DisplayRole,
                         CustomRole.AssayFolderPathData, CustomRole.AssayData)
        if role not in allowed_roles:
            return False
        if col == self.Column.Assay and role == CustomRole.AssayFolderPathData:
            row_data.assay_folder_path = value
        elif col == self.Column.Assay and role == CustomRole.AssayData:
            self.assay_data.append(value)
        elif col == self.Column.Endpoint and role == CustomRole.EndpointData:
            pass  # TODO - add to the endpoint_dict with the title in the form
        #  of Model (Assay Type) depending on what Noeris says.
        elif col == self.Column.Assay:
            row_data.assay = value
        elif col == self.Column.Endpoint:
            # user editing already locked, but stop programmatic changes
            # to the 3D data columns
            if not row_data.ld_data.requires_3d:
                row_data.endpoint = value
        elif col == self.Column.Options:
            row_data.option = value
        elif col == self.Column.Units:
            row_data.units = value
        else:
            return False
        return True
[docs]    def flags(self, index):
        """
        See Qt documentation for an method documentation. Overriding
        table_helper.RowBasedTableModel.
        """
        flag = super().flags(index)
        col_num = index.column()
        try:
            column = self.Column(col_num)
        except (ValueError, TypeError):
            # If the request is for a column that isn't defined in self.Column,
            # then no further processing is necessary or possible
            return flag
        if column in self.getLRDependentColumns() and self.disable_lr_columns:
            return Qt.NoItemFlags
        if column == self.Column.Endpoint:
            row = self._getRowFromIndex(index)
            ldd = row.ld_data
            if row.assay == CLICK or ldd.requires_3d or ldd.requires_ffc:
                # Do not allow the user to edit 3D and FFC data item endpoints
                return Qt.NoItemFlags
        return flag 
[docs]    def getPropertyExportSpecMap(self):
        """
        Return a dictionary mapping LDData to corresponding property export
        specs.
        Property export specs are export specs that represent data meant to be
        stored as structure properties for export to LiveDesign.
        :return: a dictionary mapping an `LDData` instance to the corresponding
            export spec for property rows in this table
        :rtype: dict[data_classes.LDData, export_models.PropertyExportSpec]
        """
        prop_rows = [
            row for row in self.rows
            if not row.ld_data.requires_3d and not row.ld_data.requires_ffc
        ]
        return self.getExportSpecMap(prop_rows) 
[docs]    def get3DExportSpecMap(self):
        """
        Return dictionary mapping LDData to corresponding 3D export specs in
        this table.
        :return: a dictionary mapping an `LDData` instance to the corresponding
            export spec for 3D data rows in this table
        :rtype: dict[data_classes.LDData, export_models.Base3DExportSpec]
        """
        three_d_rows = [row for row in self.rows if row.ld_data.requires_3d]
        return self.getExportSpecMap(three_d_rows) 
[docs]    def getFFCExportSpecMap(self):
        """
        Return dictionary mapping LDData to corresponding freeform column
        export specs in this table.
        :return: a dictionary mapping an `LDData` instance to the corresponding
            export spec for FFC data rows in this table
        :rtype: dict[data_classes.LDData, export_models.Base3DExportSpec]
        """
        ffc_rows = [row for row in self.rows if row.ld_data.requires_ffc]
        return self.getExportSpecMap(ffc_rows) 
[docs]    def getExportSpecMap(self, rows=None):
        """
        Return a list of specs corresponding to the supplied list of rows.
        :param rows: optionally, a list of rows to include in the map; all rows
            are used by default
        :type rows: list[ExportRow] or NoneType
        :return: a dictionary mapping `LDData` to corresponding export specs for
            each of `rows`
        :rtype: dict[data_classes.LDData, export_models.DataExportSpec]
        """
        ld_data_spec_map = {}
        rows = self.rows if rows is None else rows
        for row in rows:
            model = filter_missing_entry(row.assay, MODEL_OR_ASSAY_MISSING)
            endpoint = filter_missing_entry(row.endpoint, ENDPOINT_MISSING)
            if row.ld_data.data_name is not None:
                spec = export_models.PropertyExportSpec()
            elif row.ld_data in self._3d_data_spec_map:
                spec = self._3d_data_spec_map[row.ld_data]()
            elif row.ld_data in self._ffc_data_spec_map:
                spec = self._ffc_data_spec_map[row.ld_data]()
            else:
                msg = f'No export spec exists for data row "{row}".'
                raise ValueError(msg)
            spec.data_name = row.ld_data.data_name
            spec.ld_model = model
            spec.ld_endpoint = endpoint
            spec.units = row.units
            spec.option = row.option
            ld_data_spec_map[row.ld_data] = spec
        return ld_data_spec_map 
[docs]    def isCustomMaestroAssay(self, assay_index):
        """
        Checks if the given assay index is a custom Maestro assay, which is the
        initial custom assay of 'Maestro' selected for Maestro properties,
        along with the endpoint.
        :param assay_index: the index of the assay cell
        :type assay_index: `QtCore.QModelIndex`
        :return: whether the assay holds the initial 'Maestro' assay set for
            maestro properties.
        :rtype: bool
        """
        assay = assay_index.data()
        assay_folder_path = assay_index.data(
            role=CustomRole.AssayFolderPathData)
        return assay == MAESTRO_ASSAY and not assay_folder_path  
# =============================================================================
# Export Table View
# =============================================================================
[docs]class ExportTableView(QtWidgets.QTableView):
    """
    The table view showing all the Properties and Assay / Endpoints.
    Assay and Endpoint columns are set with different PopUp
    Delegates to show when the respective column cells are edited.
    """
[docs]    def __init__(self, parent):
        super().__init__(parent)
        self.setMinimumWidth(700)
        self.horizontalHeader().setSectionResizeMode(
            QtWidgets.QHeaderView.Stretch)
        self.horizontalHeader().setDefaultAlignment(Qt.AlignLeft)
        self.endpoint_delegate = None
        self.setAlternatingRowColors(True)
        self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        self.setEditTriggers(QtWidgets.QAbstractItemView.CurrentChanged |
                             QtWidgets.QAbstractItemView.SelectedClicked |
                             QtWidgets.QAbstractItemView.DoubleClicked) 
[docs]    def setModel(self, model):
        super().setModel(model)
        self.setExtraColumnsVisible(False)
        self.setColumnHidden(self.Column.Options, True)
        self._setupDelegates()
        self.selectionModel().selectionChanged.connect(self._onSelectionChanged) 
    def _onSelectionChanged(self):
        num_selected_rows = len(self.selectionModel().selectedRows())
        self.assay_delegate.numSelectedRowsChanged.emit(num_selected_rows)
    @property
    def Column(self):
        return self.model().Column
    def _setupDelegates(self):
        parent = self.parent()
        self.assay_delegate = AssaySelectionPopUpDelegate(parent)
        self.setItemDelegateForColumn(self.Column.Assay, self.assay_delegate)
        self.endpoint_delegate = EndpointSelectionPopUpDelegate(parent)
        self.setItemDelegateForColumn(self.Column.Endpoint,
                                      self.endpoint_delegate)
        self.assay_delegate.commitDataToSelected.connect(
            self.onCommitDataToSelected)
        self.assay_delegate.newAssaySelected.connect(self._setDefaultEndpoint)
        self.assay_delegate.assaySelected.connect(self._setDefaultEndpoint)
    def _setDefaultEndpoint(self, index=None):
        """
        When the model/assay value is set, update the endpoint value to an
        approprate default, according to the following hierarchy:
            1. If there is no assay name (the cell shows `CLICK`), show `CLICK`
            2. If the property name is among the known list of endpoints, use
               that
            3. If the current endpoint value is among the known list of
               endpoints, use that
            4. Finally, default to using the property name.
        :param index: Index of the current assay/model cell in the table.
            If index is None, the current index of the table model is used.
        :type index: QModelIndex or None
        """
        model = self.model()
        if index is None:
            index = self.currentIndex()
        cur_row = index.row()
        assay_name = model.index(cur_row, self.Column.Assay).data()
        endpoint_index = model.index(cur_row, self.Column.Endpoint)
        if assay_name == CLICK:
            endpoint = CLICK
        else:
            prop_name = model.index(cur_row, self.Column.Data).data()
            endpoints = endpoint_index.data(role=CustomRole.EndpointData)
            current_endpoint = endpoint_index.data()
            if prop_name in endpoints:
                endpoint = prop_name
            elif current_endpoint in endpoints:
                endpoint = current_endpoint
            else:
                endpoint = prop_name
        model.setData(endpoint_index, endpoint)
[docs]    def onCommitDataToSelected(self, editor, index, delegate):
        """
        Called when "Apply to Selected Rows" is clicked in Assay popup.
        See parent class for args documentations.
        """
        indices = self.selectionModel().selectedIndexes()
        if index not in indices:
            indices.append(index)  # possible using keyboard navigation
        model = self.model()
        for cur_index in indices:
            if cur_index.column() != self.Column.Assay:
                continue  # ignore all columns that are not the assay column
            delegate.setModelData(editor, model, cur_index) 
[docs]    def setExtraColumnsVisible(self, visible):
        """
        Show or hide units and decimal places columns from table.
        :param hide: whether to show or hide columns.
        :type hide: bool
        """
        hidden = not visible
        self.setColumnHidden(self.Column.Units, hidden)
        self.setColumnHidden(self.Column.Decimal, hidden)
        self.resizeColumnsToContents()  
# =============================================================================
# Assay Tree Model
# =============================================================================
[docs]class PathTreeDict(collections.defaultdict):
[docs]    def __init__(self, paths=None):
        super().__init__(PathTreeDict)
        if paths is None:
            return
        for path in paths:
            self.addPath(path) 
[docs]    def addPath(self, path):
        split_path = path.split(ldt.SEP, 1)
        root_path = split_path[0]
        child_dict = self[root_path]
        if len(split_path) == 1:
            child_dict[FOLDER_CONTENTS] = []
            return
        child_path = split_path[1]
        child_dict.addPath(child_path) 
[docs]    def findPath(self, path):
        split_path = path.split(ldt.SEP, 1)
        root_path = split_path[0]
        if root_path not in self:
            raise KeyError('%s not in tree.' % root_path)
        sub_tree = self[root_path]
        if len(split_path) == 1:
            return sub_tree
        child_path = split_path[1]
        return sub_tree.findPath(child_path)  
[docs]def make_ld_item_tree(ld_items):
    """
    Makes LD folder/assay(model) tree.
    :param ld_items: List of LD items.
    :type ld_items: list of BaseLDTreeItemWrapper
    """
    path_set = set()
    for ld_item in ld_items:
        path_set.add(ld_item.folder_path)
    path_tree = PathTreeDict(path_set)
    for ld_item in ld_items:
        # all ld_items are leaf nodes, so we don't need to worry about
        # skipping subfolders
        folder_contents = path_tree.findPath(
            ld_item.folder_path)[FOLDER_CONTENTS]
        folder_contents.append(ld_item)
    return path_tree 
[docs]class LDTreeItem(QtGui.QStandardItem):
    """
    A custom Tree item.
    """
[docs]    def __init__(self, ld_item):
        """
        :param ld_item: an object that holds a name and folder_path attributes.
        :type ld_item: BaseLDTreeItemWrapper
        """
        super(LDTreeItem, self).__init__(ld_item.display_name)
        self.ld_item = ld_item
        self.setEditable(False)  
[docs]class LDSelectionModel(QtGui.QStandardItemModel):
    """
    Tree model class which stores BaseLDTreeItemWrappers.
    """
[docs]    def loadData(self, ld_items):
        """
        Load in the generic LiveDesign item and store in Tree form.
        Note: All previous rows in model are removed.
        :param ld_items: an object that holds a name and folder_path attributes.
        :type ld_items: List of BaseLDTreeItemWrapper
        """
        self.clear()
        ld_tree = make_ld_item_tree(ld_items)
        self._generateItemTree(self, ld_tree) 
[docs]    def loadRows(self, row_data):
        """
        Load in data and append each data item as a row in Tree.
        Note: All previous rows in model are removed.
        :param row_data: text to set for each row.
        :type row_data: list of str
        """
        self.clear()
        if row_data is None:
            return
        for data in row_data:
            self.loadRow(data) 
[docs]    def loadRow(self, row_data):
        """
        Append a single row to the tree model. This method does not clear the
        model.
        :param row_data: text to set for row.
        :type row_data: str
        """
        self.appendRow([QtGui.QStandardItem(row_data)]) 
    def _generateItemTree(self, root, tree):
        # The tree is sorted (but not grouped) in a case-insensitive manner
        item_key = lambda x: x.display_name.lower()
        for branch, sub_tree in _sort_live_report_branches(tree):
            if isinstance(sub_tree, PathTreeDict):
                folder_item = QtGui.QStandardItem(branch)
                root.appendRow([folder_item])
                self._generateItemTree(folder_item, sub_tree)
            else:
                for ld_item in sorted(sub_tree, key=item_key):
                    ld_tree_item = LDTreeItem(ld_item)
                    root.appendRow([ld_tree_item])
[docs]    def flags(self, index):
        """
        Only leaf nodes in the tree model are selectable.
        """
        parent_item_flags = Qt.ItemIsEnabled
        leave_item_flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
        if self.hasChildren(index):
            return parent_item_flags
        else:
            return leave_item_flags 
[docs]    def findItem(self, item_id, item=None):
        """
        Recursively finds the livereport item under a given QStandardItem that
        matches the lr_id. If item is None, the search will start from the root
        of the model
        :param item_id: Id of the item to be found in the model
        :type item_id: str
        :param item: A model item under which the item_id has to be searched. If
                    None, the serach will start from the root item of the model
        :type item: QStandardItem
        :return: Returns the matched livereport item or None
        :rtype: BaseLDTreeItemWrapper
        """
        if not item:
            return self.findItem(item_id, self.invisibleRootItem())
        if isinstance(item, QtGui.QStandardItem) and item.hasChildren():
            # item is a QStandardItem which has children
            for row in range(item.rowCount()):
                child_item = item.child(row, 0)
                if not child_item:
                    continue
                ld_item = self.findItem(item_id, child_item)
                if ld_item:
                    return ld_item
        elif isinstance(item, LDTreeItem) and item.ld_item.id == item_id:
            # item is a LDTreeItem.
            return item.ld_item  
[docs]class EndpointSelectionModel(LDSelectionModel):
[docs]    def flags(self, index):
        """
        Prevent user from selecting endpoints with the `ENDPOINT_UNAVAILABLE`
        text. It indicates that the correct endpoint could not be parsed from
        LiveDesign.
        """
        if index.data() == ldt.ENDPOINT_UNAVAILABLE:
            return Qt.NoItemFlags
        return super(EndpointSelectionModel, self).flags(index) 
[docs]    def data(self, index, role=Qt.DisplayRole):
        if role != Qt.ToolTipRole:
            return super(EndpointSelectionModel, self).data(index, role)
        if index.data() == ldt.ENDPOINT_UNAVAILABLE:
            return (
                'The LiveDesign column that describes the endpoint of this'
                ' assay has been aliased in such a way that the endpoint string'
                ' cannot be recovered. Please select another endpoint.')
        return super(EndpointSelectionModel, self).data(index, role)  
# =============================================================================
# Live Report Wrapper
# =============================================================================
[docs]class BaseLDTreeItemWrapper(object):
    """
    Simple wrapper for storing either a ld_entities.LiveReport.name or
    ld_entities.Assay.name, and the folder path used to determine its
    position in the tree. By building a common wrapper for both items,
    much of the popup tree item code is simplified.
    """
[docs]    def __init__(self,
                 ld_name,
                 ld_id=None,
                 path='',
                 linked_path=None,
                 show_path=False):
        """
        :param ld_name: ld_entities.LiveReport.name or ld_entities.Assay.name
        :type ld_name: str
        :param ld_id: LiveReport.id as a unique identifier
        :type ld_id: str or None
        :param path: the folder path to determine position in tree.
        :type path: str
        :param linked_path: for items duplicated in favorites, the original
                folder path
        :type linked_path: str
        :param show_path: whether to show the linked_path in the display name
        :type show_path: bool
        :raise ValueError if no name is given.
        """
        if not ld_name:
            raise ValueError('No name was given.')
        self.name = ld_name
        self.id = ld_id
        self.folder_path = path
        self.linked_path = linked_path
        self.display_name = ld_name
        # for favorite items, multiple assays with the same name can appear
        # in a single folder, this helps to distinguish them by showing their
        # origin in the tree
        if show_path and linked_path:
            # replace separator with more user friendly /, even if some of the
            # names will also have / characters.
            linked_path_display = linked_path.replace(ldt.SEP, '/')
            self.display_name += ' ({})'.format(linked_path_display)  
# =============================================================================
# Assay / Live Report Tree Views
# =============================================================================
[docs]class LDSelectionTreeView(QtWidgets.QTreeView):
    """
    Base class for Selecting an item from a Tree.
    """
    itemSelected = QtCore.pyqtSignal(BaseLDTreeItemWrapper)
[docs]    def __init__(self):
        super(LDSelectionTreeView, self).__init__()
        self.setMinimumWidth(200)
        self.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
                           QtWidgets.QSizePolicy.Expanding)
        self.setHeaderHidden(True)
        self.setSelectionMode(self.SingleSelection) 
[docs]    def selectionChanged(self, selected, deselected):
        """
        See Qt QTreeView for more information.
        """
        super(LDSelectionTreeView, self).selectionChanged(selected, deselected)
        if not selected.indexes():
            return
        index = selected.indexes()[0]
        model = self.model()
        source_model = model.sourceModel()
        item = source_model.itemFromIndex(model.mapToSource(index))
        # FIXME: Set top items as global constants
        top_items = [
            'Project Favorites', 'Computational Models', 'Experimental Assays'
        ]
        if item.text() in top_items:
            return
        if isinstance(item, LDTreeItem):
            self.itemSelected.emit(item.ld_item)
        else:
            folder_path = self._generatePath(item.parent())
            ld_item = BaseLDTreeItemWrapper(item.text(), folder_path)
            self.itemSelected.emit(ld_item) 
    def _generatePath(self, item):
        """
        Helper method to generate the folder path for a given tree item.
        :param item: starting item.
        :type item: QtGui.QStandardItem
        :return: the path to the root from this item in tree composed of the
            item's text and separated by `ld_folder_tree.SEP`.
        :rtype: str
        """
        folder_path = ''
        while item:
            text = item.text()
            folder_path = ldt.SEP.join([text, folder_path
                                       ]) if folder_path else text
            item = item.parent()
        return folder_path
 
# =============================================================================
# Custom Sort Filter Proxy Model
# =============================================================================
[docs]class StringSearchFilterProxyModel(QtCore.QSortFilterProxyModel):
    """
    A proxy model to filter a tree model to show both parents
    and children nodes if they match the regular expression string.
    """
[docs]    def __init__(self, parent=None):
        super(StringSearchFilterProxyModel, self).__init__(parent)
        # maintain Qt4 dynamicSortFilter default
        self.setDynamicSortFilter(False) 
[docs]    def filterAcceptsRow(self, source_row, source_parent):
        """
        See Qt Documentation for more information on parameters.
        This filter accepts a particular row if any of the following are true:
        1. The index's item's text matches the reg exp.
        2. Any of the index's children match the reg exp.
        3. Any of the index's parents (up to the root) match the reg exp.
        Note that the conditions specified above are searched in that order.
        """
        index = self.sourceModel().index(source_row, 0, source_parent)
        return self.filterAcceptsIndex(index) or self.filterAcceptsParent(
            index.parent()) 
[docs]    def filterAcceptsIndex(self, index):
        """
        Checks whether this index's item should be accepted by the filter.
        This DFS method checks if either this index's item's text or any of
        its children matches the filter reg exp.
        :param index: the index to filter in or out according to regular exp.
        :type index: QtCore.QModelIndex
        :return: whether the index should be accepted by the filter
        :rtype: bool
        """
        if self.filterRegExp().isEmpty():
            return True
        if self._matchText(index):
            return True
        source_model = self.sourceModel()
        rows = source_model.rowCount(index)
        if rows:
            for child_row_num in range(rows):
                child_index = source_model.index(child_row_num, 0, index)
                if not child_index.isValid():
                    break
                if self.filterAcceptsIndex(child_index):
                    return True
        return False 
[docs]    def filterAcceptsParent(self, index):
        """
        Checks whether this index's item's text or any of its ancestors
        matches the filter reg exp.
        :param index: the index to filter in or out according to regular exp.
        :type index: QtCore.QModelIndex
        :return: whether the index should be accepted by the filter
        :rtype: bool
        """
        if not index.isValid():
            return False
        if self._matchText(index):
            return True
        return self.filterAcceptsParent(index.parent()) 
    def _matchText(self, index):
        """
        Helper method to check if the given index's text matches the filter
        reg exp. The comparison is done in lower case to be case insensitive.
        :param index: the index to match to regular exp.
        :type index: QtCore.QModelIndex
        :return: whether the given index matches the regular exp.
        :rtype: bool
        """
        item = self.sourceModel().itemFromIndex(index)
        if item:
            text = item.text()
            if self.filterRegExp().pattern().lower() in text.lower():
                return True 
# =============================================================================
# Assay / Live Report / Endpoint Popups
# =============================================================================
[docs]class LiveReportSelectionComboBox(pop_up_widgets.ComboBoxWithPopUp):
    """
    A custom Combo Box to show a Popup (LiveReportSelectionPopup) when the
    user clicks on the menu arrow. Also provides a "refresh" button to update
    the list of live reports from the host.
    :cvar refreshRequested: signal indicating that a refresh was requested from
        the pop up.
    :cvar liveReportSelected: signal emitted when a live report has been chosen
        in the combo box, with an argument of the live report id.
    :vartype liveReportSelected: `QtCore.pyqtSignal`
    :cvar LRSortMethodChanged: signal indicating that a new live report sort
        method has been chosen; emitted with an `LRSort` value
    :vartype LRSortMethodChanged: QtCore.pyqtSignal
    """
    refreshRequested = QtCore.pyqtSignal()
    liveReportSelected = QtCore.pyqtSignal(str)
    LRSortMethodChanged = QtCore.pyqtSignal(LRSort)
[docs]    def __init__(self, parent, lr_widget):
        """
        Create an instance.
        :type parent: `PyQt5.QtWidgets.QWidget`
        :param parent: the Qt parent widget
        """
        self.lr_widget = lr_widget
        super().__init__(parent, pop_up_class=LiveReportSelectionPopup)
        self.addItem(SELECT_TEXT)
        # Signals
        self._pop_up.view.itemSelected.connect(self.onLiveReportSelected)
        self._pop_up.LRSortMethodChanged.connect(self.LRSortMethodChanged)
        self._pop_up.refreshRequested.connect(self.refreshRequested) 
[docs]    def reset(self):
        """
        Reset the combo box to initial state.
        """
        self._pop_up.reset()
        self.setItemText(0, SELECT_TEXT) 
[docs]    def setData(self, live_reports):
        """
        Load in the live reports to the Tree widget.
        :param live_reports: live reports to be added.
        :type live_reports: List of BaseLDTreeItemWrapper
        """
        self._pop_up.model.loadData(live_reports) 
[docs]    def addNewLiveReport(self):
        """
        Generates a new name for the live report depending on any reports
        selected in the tree and the current date.  Sets value on combo box
        for a new report.
        :return: Name of new live report
        :rtype: str
        """
        date = datetime.now().strftime("%m/%d/%y")
        name = f'{self.lr_widget.model.ld_destination.proj_name} {date}'
        self.setItemText(0, NEW_TEXT)
        self._pop_up.view.clearSelection()
        self._pop_up.close()
        return name 
[docs]    def onLiveReportSelected(self, item):
        """
        Slot connected to tree view's selection.
        :param item: selected live report item in the tree view.
        :type item: BaseLDTreeItemWrapper
        """
        self.setItemText(0, item.name)
        self.liveReportSelected.emit(item.id)
        self._pop_up.close() 
[docs]    def setCurrentLR(self, lr_id):
        """
        Sets the current livereport to the item pointed by lr_id
        :param lr_id: Livereport id
        :type lr_id: str
        :return: True if success else False
        :rtype: bool
        """
        ld_item = self._pop_up.model.findItem(lr_id)
        if ld_item:
            self._pop_up.view.itemSelected.emit(ld_item)
            return True
        else:
            return False 
[docs]    def onRefreshCompleted(self):
        self._pop_up.onRefreshCompleted()  
[docs]class LiveReportSelector(widgetmixins.InitMixin, QtWidgets.QWidget):
    """
    A widget containing a `LiveReportSelectionComboBox` and a create new LR button.
    :cvar refreshRequested: signals that the user has requested a refresh by
        clicking on the refresh button
    :vartype refreshRequested: QtCore.pyqtSignal
    :cvar liveReportSelected: signal emitted when a live report has been chosen
        in the combo box, with an argument of the live report id.
    :vartype liveReportSelected: `QtCore.pyqtSignal`
    :cvar newLiveReportSelected: signal emitted when a live report has been
        created, with an argument of the live report id.
    :vartype newLiveReportSelected: `QtCore.pyqtSignal`
    :cvar LRSortMethodChanged: signal indicating that a new live report sort
        method has been chosen; emitted with an `LRSort` value
    :vartype LRSortMethodChanged: QtCore.pyqtSignal
    """
    refreshRequested = QtCore.pyqtSignal()
    liveReportSelected = QtCore.pyqtSignal(str)
    newLiveReportSelected = QtCore.pyqtSignal(str)
    LRSortMethodChanged = QtCore.pyqtSignal(LRSort)
[docs]    def __init__(self, parent, lr_widget, allow_add=True):
        # Save this data only long enough to pass to combo box
        self.lr_widget = lr_widget
        self.allow_add = allow_add
        super().__init__(parent=parent) 
[docs]    def initSetUp(self):
        super().initSetUp()
        # Set up live report selection combo box
        self.lr_combo = LiveReportSelectionComboBox(self, self.lr_widget)
        self.add_new_btn = QtWidgets.QPushButton('Create New', parent=self)
        self.add_new_btn.clicked.connect(self.addNewLiveReport)
        self.add_new_btn.setVisible(self.allow_add)
        del self.lr_widget
        # Connect signals
        self.lr_combo.liveReportSelected.connect(self.onLiveReportSelected)
        self.lr_combo.LRSortMethodChanged.connect(self.LRSortMethodChanged)
        self.lr_combo.refreshRequested.connect(self.refreshRequested)
        # Set size policies
        policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
                                       QtWidgets.QSizePolicy.Fixed)
        self.setSizePolicy(policy)
        self.lr_combo.setSizePolicy(policy) 
[docs]    def initLayOut(self):
        super().initLayOut()
        h_layout = QtWidgets.QHBoxLayout()
        h_layout.addWidget(self.lr_combo)
        h_layout.addWidget(self.add_new_btn)
        self.layout().addLayout(h_layout) 
[docs]    def initSetDefaults(self):
        super().initSetDefaults()
        self.add_new_btn.setVisible(self.allow_add)
        self.lr_combo.reset() 
[docs]    def setData(self, live_reports):
        """
        Load in the live reports to the Tree widget on the combo box.
        :param live_reports: live reports to be added.
        :type live_reports: List of BaseLDTreeItemWrapper
        """
        self.lr_combo.setData(live_reports) 
[docs]    def setLiveReport(self, live_report_id):
        """
        Set the active live report, refreshing the available list if necessary.
        :param live_report_id: the live report ID of the desired live report
        :type live_report_id: str
        :return: True if success else False
        :rtype: bool
        """
        status = True
        if not self.lr_combo.setCurrentLR(live_report_id):
            self.refreshRequested.emit()
            # live_report_id could have been archived on LiveDesign meanwhile
            status = self.lr_combo.setCurrentLR(live_report_id)
        return status 
[docs]    def onLiveReportSelected(self, lr_id):
        self.add_new_btn.hide()
        self.liveReportSelected.emit(lr_id) 
[docs]    def onRefreshCompleted(self):
        self.lr_combo.onRefreshCompleted() 
[docs]    def addNewLiveReport(self):
        name = self.lr_combo.addNewLiveReport()
        self.newLiveReportSelected.emit(name) 
[docs]    def setComboToSelect(self):
        """
        Sets live report combo to Select mode
        """
        self.lr_combo.setItemText(0, SELECT_TEXT)
        self.add_new_btn.setVisible(self.allow_add)  
# =============================================================================
# Assay Delegate
# =============================================================================
[docs]class AddNewLineEdit(QtWidgets.QLineEdit):
    default_text = ''
[docs]    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs) 
[docs]    def focusInEvent(self, e):
        super(AddNewLineEdit, self).focusInEvent(e)
        if not self.isModified():
            self.setText(self.default_text)
            QtCore.QTimer.singleShot(0, self.selectAll)  
[docs]class AssaySelectionLineEdit(pop_up_widgets.LineEditWithPopUp):
    """
    Custom Line Edit to show a Popup (AssaySelectionPopup) when the
    user clicks on the table cell.
    """
    newAssaySelected = QtCore.pyqtSignal(str)
    assaySelected = QtCore.pyqtSignal(str)
    numSelectedRowsChanged = QtCore.pyqtSignal(int)
    mappingSaved = QtCore.pyqtSignal(list)
[docs]    def __init__(self, parent):
        """
        :type parent: `PyQt5.QtWidgets.QWidget`
        :param parent: the Qt parent widget
        """
        self._pop_up_class = AssaySelectionPopup
        super(AssaySelectionLineEdit,
              self).__init__(parent, pop_up_class=self._pop_up_class)
        self.setReadOnly(True)
        icon = QtGui.QIcon(icons.MENU_CARET_LB)
        self.addAction(icon, QtWidgets.QLineEdit.TrailingPosition)
        # Signals
        self._pop_up.newAssaySelected.connect(self.addNew)
        self._pop_up.ui.apply_to_rows_btn.clicked.connect(
            self.onApplyToRowsClicked)
        self._pop_up.view.itemSelected.connect(self.onAssaySelected)
        self.popUpClosing.connect(self.saveMapping)
        self.numSelectedRowsChanged.connect(self.setNumSelectedRows)
        self.assay_path_data = None
        self.assay_folder_path = None
        self.new_assay = None
        self.expanded_items = [] 
[docs]    def saveMapping(self):
        """
        Saves the state of collapsed and expanded items in the QTreeView
        """
        view = self._pop_up.view
        model = view.model()
        self.expanded_items = []
        rows = model.rowCount()
        for row_idx in range(rows):
            model_index = model.index(row_idx, 0)
            self.saveExpansionBFS(model_index)
        self.mappingSaved.emit(self.expanded_items) 
[docs]    def saveExpansionBFS(self, model_idx: QtCore.QModelIndex):
        """
        Searches for and saves the expanded items of an element
        and its children.  Uses breadth first searching
        """
        view = self._pop_up.view
        model = view.model()
        if view.isExpanded(model_idx):
            self.expanded_items.append(self.getTreePath(model_idx))
        for row_idx in range(model.rowCount(model_idx)):
            child = model_idx.child(row_idx, 0)
            self.saveExpansionBFS(child) 
[docs]    def getTreePath(self, model_idx: QtCore.QModelIndex) -> List[str]:
        """
        Gets full path of element at the model index. ::
            X
              Y
                Z
        tree path of element Z is ['X', 'Y', 'Z']
        """
        view = self._pop_up.view
        model = view.model()
        data = model_idx.data()
        tree_path = []
        while data is not None:
            tree_path.insert(0, data)
            parent = model.parent(model_idx)
            model_idx = parent
            data = parent.data()
        return tree_path 
[docs]    def applyMapping(self, mapping: List[List[str]]):
        """
        Applies the given expansion mapping to elements in the view
        """
        view = self._pop_up.view
        model = view.model()
        rows = model.rowCount()
        for row_idx in range(rows):
            model_index = model.index(row_idx, 0)
            self.setExpansionBFS(model_index, mapping) 
[docs]    def setExpansionBFS(self, model_idx: QtCore.QModelIndex,
                        mapping: List[List[str]]):
        """
        Applies the expansion states from mapping to the element at the
        given model index
        """
        view = self._pop_up.view
        model = view.model()
        idx_path = self.getTreePath(model_idx)
        if idx_path in mapping:
            view.expand(model_idx)
        for row_idx in range(model.rowCount(model_idx)):
            child = model_idx.child(row_idx, 0)
            self.setExpansionBFS(child, mapping) 
[docs]    def setNumSelectedRows(self, num_rows: int):
        self._pop_up.setNumSelectedRows(num_rows) 
[docs]    def onApplyToRowsClicked(self):
        """
        Triggers PopUpDelegate.commitDataToSelected signal
        """
        self.popUpClosing.emit(pop_up_widgets.ACCEPT_MULTI) 
[docs]    def addNew(self, name):
        """
        Adds the new Model / Assay name to the table cell and the popup tree.
        :param name: new Assay name
        :type name: str
        """
        self.setText(name)
        # Add the new assay entry to tree
        self.assay_folder_path = 'User Created'
        self.new_assay = BaseLDTreeItemWrapper(ld_name=self.text(),
                                               path=self.assay_folder_path)
        self.assay_path_data.append(self.new_assay)
        self.addData(self.assay_path_data)
        self.newAssaySelected.emit(name) 
[docs]    def addData(self, assay_path_data):
        """
        Add the assay data to populate the pop up tree model.
        :param assay_path_data: the list of tree item wrappers containing the
                name and path of the assays
        :type assay_path_data: List of BaseLDTreeItemWrapper
        """
        self.assay_path_data = assay_path_data
        self._pop_up.model.loadData(assay_path_data) 
[docs]    def onAssaySelected(self, item):
        """
        Slot connected to tree view's selection.
        :param item: selected Assay item in the tree view.
        :type item: BaseLDTreeItemWrapper
        """
        assay_name = item.name
        # the display objects in the tree use the folder path, but the ExportRow
        # should use the linked path if it exists in order to find the endpoint
        if item.linked_path is not None:
            self.assay_folder_path = item.linked_path
        else:
            self.assay_folder_path = item.folder_path
        self.setText(assay_name)
        self.assaySelected.emit(assay_name) 
[docs]    def getAssayFolderPath(self):
        """
        Return the currently selected assay's folder path. If no assay is
        selected, None is returned.
        :return: folder path of currently selected assay.
        :rtype: str or None
        """
        return self.assay_folder_path 
[docs]    def setAllAssayData(self, assays_list, assay_name, assay_folder_path):
        """
        Set all the assay data including the list of possible assays, the
        current (ie last selected) assay name value, and the folder path to
        the current assay (if exists). This will add the possible assays to the
        popup's selection view, set the line edit text, store the assay folder
        path, and scroll to and select the item corresponding to the current
        selection in the popup.
        Note that because the assay name and path does not currently store
        whether the original location was from its original path or the
        'Project Favorites' folder, the selection always scrolls to the
        original location rather than the 'Project Favorites' location.
        :param assays_list: the list of ld items specifying the assay names
                and paths
        :type assays_list: List of BaseLDTreeItemWrapper
        :param assay_name: the current value of the assay name
        :type assay_name: str
        :param assay_folder_path: the current value of the assay folder path
        :type assay_folder_path: str
        """
        self.addData(assays_list)
        self.setText(assay_name)
        self.assay_folder_path = assay_folder_path
        if assay_folder_path is not None:
            full_assay_path = ldt.SEP.join([assay_folder_path, assay_name])
            self._pop_up.view.scrollToItemWithPath(full_assay_path) 
[docs]    def getNewAssay(self):
        """
        Return the newly created assay if one exists.
        :return: the newly created assay
        :rtype: BaseLDTreeItemWrapper or None
        """
        return self.new_assay 
[docs]    def setText(self, text):
        if text != CLICK:
            self._pop_up.setDefaultText(text)
        super(AssaySelectionLineEdit, self).setText(text)  
# =============================================================================
# Endpoint Delegate
# =============================================================================
[docs]class EndpointSelectionLineEdit(pop_up_widgets.LineEditWithPopUp):
    """
    Custom Line Edit to show a Popup (EndpointSelectionPopup) when the
    user double clicks on the table cell.
    """
    newEndpointSelected = QtCore.pyqtSignal(str)
    endpointSelected = QtCore.pyqtSignal(str)
[docs]    def __init__(self, parent):
        """
        Create an instance.
        :type parent: `PyQt5.QtWidgets.QWidget`
        :param parent: the Qt parent widget
        """
        self._pop_up_class = EndpointSelectionPopup
        super(EndpointSelectionLineEdit,
              self).__init__(parent, pop_up_class=self._pop_up_class)
        self.setReadOnly(True)
        # Signals
        self._pop_up.newEndpointSelected.connect(self.addNew)
        self._pop_up.view.itemSelected.connect(self.onEndpointSelected) 
[docs]    def addNew(self, name):
        """
        Slot connected a new Endpoint creation. Add the new Endpoint to the
        lineedit and the tree.
        """
        self.setText(name)
        # Add a new endpoint entry to tree
        self._pop_up.model.loadRow(name)
        self.newEndpointSelected.emit(name) 
[docs]    def addData(self, endpoint_data):
        """
        Add the endpoint data to populate the pop up tree model.
        :param endpoint_data:
        :type endpoint_data: List of str
        """
        self._pop_up.model.loadRows(endpoint_data) 
[docs]    def onEndpointSelected(self, item):
        """
        Slot connected to tree view's selection.
        :param item: selected Endpoint item in the tree view.
        :type item: BaseLDTreeItemWrapper
        """
        endpoint_name = item.name
        self.setText(endpoint_name)
        self.endpointSelected.emit(endpoint_name)
        self._pop_up.close()  
# =============================================================================
# LD Projects ComboBox
# =============================================================================
[docs]class LiveDesignProjectsCombo(QtWidgets.QComboBox):
    """
    This is a standard QComboBox with a few helper methods.
    :cvar projectSelected: signal for when a any project in the combo box is
        selected. Emitted with the project name and ID.
    :vartype projectSelected: `QtCore.pyqtSignal`
    :cvar placeholderSelected: signal for when the placeholder in the combo box
        is selected. Emitted with no arguments.
    :vartype placeholderSelected: `QtCore.pyqtSignal`
    """
    projectSelected = QtCore.pyqtSignal(str, str)
    placeholderSelected = QtCore.pyqtSignal()
[docs]    def __init__(self, parent=None):
        """
        Custom QComboBox for selecting a LiveDesign Project
        """
        super().__init__(parent)
        self.setDefaults()
        self.currentIndexChanged.connect(self._checkIfProjectSelected) 
[docs]    def setDefaults(self):
        """
        Resets the combobox and sets the default placeholder entry.
        """
        self.clear()
        self.addItem('Select Project...', None) 
[docs]    def addProjects(self, projects):
        """
        Resets the combobox and adds the new LD projects in alphabetical order,
        along with the project id as the user data.
        :param projects: list of LD projects
        :type projects: [ldclient.models.Project]
        """
        self.setDefaults()
        # PANEL-8782 sort projects alphabetically by name
        proj_names = [p.name for p in projects]
        for proj_name, project in sorted(zip(proj_names, projects)):
            # must convert project ID to int since it's in unicode
            self.addItem(proj_name, str(project.id)) 
    def _checkIfProjectSelected(self, index):
        """
        Slot called when selection changes. Emits a projectChanged() signal if
        a project was indeed selected, otherwise the placeholderSelected()
        signal will be emitted.
        :param index: current index selected
        :type index: int
        """
        if index == -1:
            # Invalid selection when combobox is empty
            pass
        elif self.isPlaceholderItemSelected():
            self.placeholderSelected.emit()
        else:
            self.projectSelected.emit(self.currentProjectName(),
                                      self.currentProjectID())
[docs]    def currentProjectName(self):
        """
        Return the current selected project name or None if no project is
        selected.
        :return: project name if applicable
        :rtype: str or None
        """
        if not self.isPlaceholderItemSelected():
            return self.currentText() 
[docs]    def currentProjectID(self):
        """
        Return the current selected project's id. If placeholder item is
        currently selected, None will be returned.
        :return: project id if applicable
        :rtype: str or None
        """
        return self.currentData() 
[docs]    def isPlaceholderItemSelected(self):
        """
        Returns whether the placeholder text is currently selected.
        :return: whether the placeholder is selected.
        :rtype: bool
        """
        return self.currentIndex() == 0 
[docs]    def selectPlaceholderItem(self):
        """
        Simply set the current item to be the placeholder text.
        """
        self.setCurrentIndex(0)  
[docs]def organize_ld_data_tree(ld_data_list):
    """
    Given a list of LD data, organize it for display in the exportable data
    tree.
    :param ld_data_list: LD data to organize
    :type ld_data_list: list(data_classes.LDData)
    :return: a tuple representing the organized data: each
    :rtype: collections.OrderedDict(str, list(data_classes.LDData))
    """
    family_map = collections.defaultdict(list)
    for ld_data in ld_data_list:
        family_map[ld_data.family_name].append(ld_data)
    st_family_names = set(
        [ldd.family_name for ldd in ld_data_list if ldd.data_name is not None])
    other_family_names = set(
        [ldd.family_name for ldd in ld_data_list if ldd.data_name is None])
    # Non-structure property data should be first
    tree_map = _sort_ld_data_tree(family_map, other_family_names)
    # Structure property data should be last
    st_tree_map = _sort_ld_data_tree(family_map, st_family_names)
    tree_map.update(st_tree_map)
    return tree_map 
def _sort_ld_data_tree(family_map, family_names):
    """
    Return a dictionary mapping each of the family names in `family_names` to
    (sorted alphabetically) to the corresponding list of `LDData` objects from
    `family_map`, sorted alphabetically by user name.
    :param family_map: a dictionary mapping family names to a list of `LDData`
        objects that share the family name
    :type family_map: dict[str, list[data_classes.LDData]]
    :param family_names: a list of family names, which must be a subset of the
        keys of `family_map`
    :type family_names: list[str]
    :return: collections.OrderedDict[str, list[data_classes.LDData]]
    """
    sorted_map = collections.OrderedDict()
    for family_name in sorted(family_names, key=lambda x: x.lower()):
        ld_data_list = family_map[family_name]
        sorted_map[family_name] = sorted(
            ld_data_list, key=lambda ld_data: ld_data.user_name.lower())
    return sorted_map
[docs]def filter_missing_entry(entry, default_value):
    if entry in (CLICK, ''):
        return default_value
    else:
        return entry 
def _sort_live_report_branches(lr_tree):
    """
    Sort live reports by branch name. Live reports without a folder are always
    at the top of the list; otherwise, sort alphabetically
    :param lr_tree: dictionary containing tree information
    :type lr_tree: PathTreeDict
    :return: properly-ordered list of (branch, sub tree) pairs that can be used
        to populate the live report tree
    :rtype: list(tuple(str, list) or tuple(str, PathTreeDict))
    """
    tree_key = lambda x: (x[0].lower(), x[0])
    sorted_tree = sorted(lr_tree.items(), key=tree_key)
    project_home_item = None
    for branch, sub_tree in sorted_tree:
        if branch == NO_FOLDER_NAME:
            project_home_item = (branch, sub_tree)
            break
    if project_home_item is not None:
        sorted_tree.remove(project_home_item)
        sorted_tree.insert(0, project_home_item)
    return sorted_tree