Repository: biolab/orange3

Issue:
Rank widget uses DomainContextHandler, but stores indices
Rank widget stores selection as row indices instead of variables, so `DomainContextHandler` will match any data.


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
@@ -1,10 +1,3 @@
-"""
-Rank
-====
-
-Rank (score) features for prediction.
-
-"""
 import warnings
 from collections import namedtuple, OrderedDict
 import logging
@@ -23,6 +16,7 @@
     Qt, QItemSelection, QItemSelectionRange, QItemSelectionModel,
 )
 
+from orangewidget.settings import IncompatibleContext
 from Orange.data import (Table, Domain, ContinuousVariable, DiscreteVariable,
                          StringVariable)
 from Orange.misc.cache import memoize_method
@@ -198,9 +192,9 @@ class Outputs:
     sorting = Setting((0, Qt.DescendingOrder))
     selected_methods = Setting(set())
 
-    settings_version = 2
+    settings_version = 3
     settingsHandler = DomainContextHandler()
-    selected_rows = ContextSetting([])
+    selected_attrs = ContextSetting([], schema_only=True)
     selectionMethod = ContextSetting(SelectNBest)
 
     class Information(OWWidget.Information):
@@ -310,7 +304,7 @@ def switchProblemType(self, index):
     @check_sql_input
     def set_data(self, data):
         self.closeContext()
-        self.selected_rows = []
+        self.selected_attrs = []
         self.ranksModel.clear()
         self.ranksModel.resetSorting(True)
 
@@ -472,8 +466,10 @@ def updateScores(self):
 
     def on_select(self):
         # Save indices of attributes in the original, unsorted domain
-        self.selected_rows = list(self.ranksModel.mapToSourceRows([
-            i.row() for i in self.ranksView.selectionModel().selectedRows(0)]))
+        selected_rows = self.ranksView.selectionModel().selectedRows(0)
+        row_indices = [i.row() for i in selected_rows]
+        attr_indices = self.ranksModel.mapToSourceRows(row_indices)
+        self.selected_attrs = [self.data.domain[idx] for idx in attr_indices]
         self.commit()
 
     def setSelectionMethod(self, method):
@@ -502,8 +498,10 @@ def autoSelection(self):
             )
         else:
             selection = QItemSelection()
-            if self.selected_rows is not None:
-                for row in model.mapFromSourceRows(self.selected_rows):
+            if self.selected_attrs is not None:
+                attr_indices = [self.data.domain.attributes.index(var)
+                                for var in self.selected_attrs]
+                for row in model.mapFromSourceRows(attr_indices):
                     selection.append(QItemSelectionRange(
                         model.index(row, 0), model.index(row, columnCount - 1)))
 
@@ -536,21 +534,18 @@ def send_report(self):
             self.report_items("Output", self.out_domain_desc)
 
     def commit(self):
-        selected_attrs = []
-        if self.data is not None:
-            selected_attrs = [self.data.domain.attributes[i]
-                              for i in self.selected_rows]
-        if not selected_attrs:
+        if not self.selected_attrs:
             self.Outputs.reduced_data.send(None)
             self.Outputs.features.send(None)
             self.out_domain_desc = None
             self.info.set_output_summary(self.info.NoOutput)
         else:
             reduced_domain = Domain(
-                selected_attrs, self.data.domain.class_var, self.data.domain.metas)
+                self.selected_attrs, self.data.domain.class_var,
+                self.data.domain.metas)
             data = self.data.transform(reduced_domain)
             self.Outputs.reduced_data.send(data)
-            self.Outputs.features.send(AttributeList(selected_attrs))
+            self.Outputs.features.send(AttributeList(self.selected_attrs))
             self.out_domain_desc = report.describe_domain(data.domain)
             self.info.set_output_summary(len(data),
                                          format_summary_details(data))
@@ -595,11 +590,10 @@ def migrate_settings(cls, settings, version):
 
     @classmethod
     def migrate_context(cls, context, version):
-        if version is None or version < 2:
-            # Old selection was saved as sorted indices. New selection is original indices.
-            # Since we can't devise the latter without first computing the ranks,
-            # just reset the selection to avoid confusion.
-            context.values['selected_rows'] = []
+        if version is None or version < 3:
+            # Selections were stored as indices, so these contexts matched
+            # any domain. The only safe thing to do is to remove them.
+            raise IncompatibleContext
 
 
 if __name__ == "__main__":  # pragma: no cover


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
@@ -19,6 +19,7 @@
 from Orange.widgets.tests.base import WidgetTest, datasets
 from Orange.widgets.widget import AttributeList
 from Orange.widgets.utils.state_summary import format_summary_details
+from orangewidget.settings import Context, IncompatibleContext
 
 
 class TestOWRank(WidgetTest):
@@ -342,6 +343,29 @@ def test_setting_migration_fixes_header_state(self):
 
         self.assertEqual(w.sorting, (0, Qt.AscendingOrder))
 
+    def test_discard_settings_before_v3(self):
+        for version in (None, 1, 2):
+            self.assertRaises(IncompatibleContext, OWRank.migrate_context,
+                              Context(), version=version)
+
+    def test_auto_selection_manual(self):
+        w = self.widget
+
+        data = Table("heart_disease")
+        dom = data.domain
+        self.send_signal(w.Inputs.data, data)
+
+        # Sort by number of values and set selection to attributes with most
+        # values. This must select the top 4 rows.
+        self.widget.ranksView.horizontalHeader().setSortIndicator(0, Qt.DescendingOrder)
+        w.selectionMethod = w.SelectManual
+        w.selected_attrs = [dom["chest pain"], dom["rest ECG"],
+                            dom["slope peak exc ST"], dom["thal"]]
+        w.autoSelection()
+        self.assertEqual(
+            sorted({idx.row() for idx in w.ranksView.selectedIndexes()}),
+            [0, 1, 2, 3])
+
     def test_auto_send(self):
         widget = self.widget
         model = widget.ranksModel