Repository: biolab/orange3

Issue:
Rank Widget should show attributes of attributes on hover
It is one of the few widgets that don't.


Discussion:

Resolving patch:
diff --git a/Orange/widgets/data/owrank.py b/Orange/widgets/data/owrank.py
--- a/Orange/widgets/data/owrank.py
+++ b/Orange/widgets/data/owrank.py
@@ -31,6 +31,7 @@
 from Orange.widgets.unsupervised.owdistances import InterruptException
 from Orange.widgets.utils.concurrent import ConcurrentWidgetMixin, TaskState
 from Orange.widgets.utils.sql import check_sql_input
+from Orange.widgets.utils.itemmodels import VariableListModel
 from Orange.widgets.utils.widgetpreview import WidgetPreview
 from Orange.widgets.widget import AttributeList, Input, MultiInput, Output, Msg, \
     OWWidget
@@ -77,6 +78,29 @@ def from_variable(cls, variable):
 VARNAME_COL, NVAL_COL = range(2)
 
 
+class RankTableModel(BarRatioTableModel):
+    """
+    BarRatioTableModel that passes the first column (variables) into
+    VariableListModel to get the tooltips and decoration
+    """
+    def __init__(self, *args, **kwargs):
+        self._variable_model = VariableListModel()
+        super().__init__(*args, **kwargs)
+
+    def wrap(self, table):
+        super().wrap(table)
+        self._variable_model[:] = [var for var, *_ in table]
+
+    def data(self, index, role=Qt.DisplayRole):
+        column = index.column()
+        if column == 0:
+            row = self.mapToSourceRows(index.row())
+            index = self._variable_model.index(row, column)
+            return self._variable_model.data(index, role)
+        else:
+            return super().data(index, role)
+
+
 class TableView(QTableView):
     manualSelection = Signal()
 
@@ -240,7 +264,7 @@ def __init__(self):
                                      if method.is_default}
 
         # GUI
-        self.ranksModel = model = BarRatioTableModel(parent=self)  # type:
+        self.ranksModel = model = RankTableModel(parent=self)  # type:
         # BarRatioTableModel
         self.ranksView = view = TableView(self)            # type: TableView
         self.mainArea.layout().addWidget(view)
@@ -538,7 +562,7 @@ def headerClick(self, index):
 
         # Store the header states
         sort_order = self.ranksModel.sortOrder()
-        sort_column = self.ranksModel.sortColumn() - 2  # -2 for '#' (discrete count) column
+        sort_column = self.ranksModel.sortColumn() - 2  # -2 for name and '#' columns
         self.sorting = (sort_column, sort_order)
 
     def methodSelectionChanged(self, state: int, method_name):


Test patch:
diff --git a/Orange/widgets/data/tests/test_owrank.py b/Orange/widgets/data/tests/test_owrank.py
--- a/Orange/widgets/data/tests/test_owrank.py
+++ b/Orange/widgets/data/tests/test_owrank.py
@@ -2,6 +2,7 @@
 import time
 import warnings
 import unittest
+from itertools import count
 from unittest.mock import patch
 
 import numpy as np
@@ -9,9 +10,11 @@
 
 from AnyQt.QtCore import Qt, QItemSelection, QItemSelectionModel, \
     QT_VERSION_INFO
+from AnyQt.QtGui import QIcon
 from AnyQt.QtWidgets import QCheckBox, QApplication
 
 from orangewidget.settings import Context, IncompatibleContext
+from orangewidget.tests.base import GuiTest
 
 from Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable
 from Orange.modelling import RandomForestLearner, SGDLearner
@@ -19,8 +22,9 @@
 from Orange.classification import LogisticRegressionLearner
 from Orange.regression import LinearRegressionLearner
 from Orange.projection import PCA
+from Orange.widgets import gui
 from Orange.widgets.data.owrank import OWRank, ProblemType, CLS_SCORES, \
-    REG_SCORES
+    REG_SCORES, RankTableModel
 from Orange.widgets.tests.base import WidgetTest, datasets
 from Orange.widgets.widget import AttributeList
 
@@ -33,6 +37,62 @@ def score_data(self, data, feature=None):
         return np.ones((1, len(data.domain.attributes)))
 
 
+class TestRankModel(GuiTest):
+    def setUp(self):
+        attributes = [DiscreteVariable("ann", values=tuple("ab")),
+                      DiscreteVariable("great", values=tuple("defg"))] \
+            + [ContinuousVariable(x) for x in "def"]
+        self.attributes = attributes
+        attributes[0].attributes["foo"] = "bar"
+        data = [[var, nvals] + [10 * i + j for j in range(3)]
+                for i, var, nvals in zip(count(), attributes,
+                                         (2, 4, np.nan, np.nan, np.nan))]
+        self.model = RankTableModel()
+        self.model.wrap(data)
+
+    def _get(self, row, column, role=Qt.DisplayRole):
+        return self.model.index(row, column).data(role)
+
+    def test_data(self):
+        # scores
+        self.assertEqual(self._get(0, 4), 2)
+        self.assertEqual(self._get(1, 2), 10)
+
+        # n values
+        self.assertEqual(self._get(0, 1), 2)
+        self.assertEqual(self._get(1, 1), 4)
+        self.assertEqual(self._get(2, 1), "")
+
+        # variables
+        self.assertEqual(self._get(0, 0), "ann")
+        self.assertEqual(self._get(1, 0), "great")
+        self.assertEqual(self._get(3, 0), "e")
+
+        self.assertIsInstance(self._get(0, 0, Qt.DecorationRole), QIcon)
+        self.assertIsInstance(self._get(2, 0, Qt.DecorationRole), QIcon)
+
+        self.assertIn("foo", self._get(0, 0, Qt.ToolTipRole))
+        self.assertIn("bar", self._get(0, 0, Qt.ToolTipRole))
+        self.assertNotIn("bar", self._get(1, 0, Qt.ToolTipRole))
+
+        self.assertIs(self._get(0, 0, gui.TableVariable), self.attributes[0])
+        self.assertIs(self._get(3, 0, gui.TableVariable), self.attributes[3])
+
+    def test_sorting(self):
+        self.model.sort(0, Qt.AscendingOrder)
+        self.assertIs(self._get(0, 0, gui.TableVariable), self.attributes[0])
+        self.assertIs(self._get(1, 0, gui.TableVariable), self.attributes[2])
+        self.assertIs(self._get(0, 2 + 1), 1)
+        self.assertIs(self._get(1, 2 + 0), 20)
+
+        self.model.sort(2, Qt.DescendingOrder)
+        self.assertIs(self._get(0, 0, gui.TableVariable), self.attributes[4])
+        self.assertIs(self._get(4, 0, gui.TableVariable), self.attributes[0])
+        self.assertIs(self._get(4, 2 + 1), 1)
+        self.assertIs(self._get(3, 2 + 0), 10)
+        self.assertIs(self._get(0, 2 + 0), 40)
+
+
 class TestOWRank(WidgetTest):
     def setUp(self):
         self.widget = self.create_widget(OWRank)  # type: OWRank