Repository: biolab/orange3

Issue:
Concatenate widget: autoapply problems
##### Orange version
3.3.8+

##### Expected behavior
Changes in the concatenate widget should be applied automatically.

##### Actual behavior
Changed input data is applied automatically, other changes are not.
Concatenate widget: append source ID as class
##### Orange version
master

##### Expected behavior
Should append source ID as class.

##### Actual behavior
Raises exception.

##### Steps to reproduce the behavior
Connect Iris and Titanic to Concatenate, set appending source ID as class.


Discussion:

Resolving patch:
diff --git a/Orange/widgets/data/owconcatenate.py b/Orange/widgets/data/owconcatenate.py
--- a/Orange/widgets/data/owconcatenate.py
+++ b/Orange/widgets/data/owconcatenate.py
@@ -28,9 +28,9 @@ class OWConcatenate(widget.OWWidget):
     icon = "icons/Concatenate.svg"
 
     inputs = [("Primary Data", Orange.data.Table,
-               "set_primary_data", widget.Default),
+               "set_primary_data"),
               ("Additional Data", Orange.data.Table,
-               "set_more_data", widget.Multiple)]
+               "set_more_data", widget.Multiple | widget.Default)]
     outputs = [("Data", Orange.data.Table)]
 
     #: Domain merging operations
@@ -64,9 +64,9 @@ def __init__(self):
         self.primary_data = None
         self.more_data = OrderedDict()
 
-        mergebox = gui.vBox(self.controlArea, "Domain Merging")
+        self.mergebox = gui.vBox(self.controlArea, "Domain Merging")
         box = gui.radioButtons(
-            mergebox, self, "merge_type",
+            self.mergebox, self, "merge_type",
             callback=self._merge_type_changed)
 
         gui.widgetLabel(
@@ -91,7 +91,8 @@ def __init__(self):
 
         cb = gui.checkBox(
             box, self, "append_source_column",
-            self.tr("Append data source IDs"))
+            self.tr("Append data source IDs"),
+            callback=self._source_changed)
 
         ibox = gui.indentedBox(box, sep=gui.checkButtonOffsetHint(cb))
 
@@ -104,12 +105,13 @@ def __init__(self):
 
         form.addRow(
             self.tr("Feature name:"),
-            gui.lineEdit(ibox, self, "source_attr_name", valueType=str))
+            gui.lineEdit(ibox, self, "source_attr_name", valueType=str,
+                         callback=self._source_changed))
 
         form.addRow(
             self.tr("Place:"),
-            gui.comboBox(ibox, self, "source_column_role", items=self.id_roles)
-        )
+            gui.comboBox(ibox, self, "source_column_role", items=self.id_roles,
+                         callback=self._source_changed))
 
         ibox.layout().addLayout(form)
         mleft, mtop, mright, _ = ibox.layout().getContentsMargins()
@@ -135,6 +137,7 @@ def set_more_data(self, data=None, id=None):
             del self.more_data[id]
 
     def handleNewSignals(self):
+        self.mergebox.setDisabled(self.primary_data is not None)
         self.apply()
 
     def apply(self):
@@ -180,6 +183,9 @@ def _merge_type_changed(self, ):
         if self.primary_data is None and self.more_data:
             self.apply()
 
+    def _source_changed(self):
+        self.apply()
+
     def send_report(self):
         items = OrderedDict()
         if self.primary_data is not None:
@@ -261,7 +267,7 @@ def ascolumn(array):
     metas = [ascolumn(col) for _, col in metas]
 
     X = numpy.hstack((data.X,) + tuple(attr_cols))
-    Y = numpy.hstack((data.Y,) + tuple(class_cols))
+    Y = numpy.hstack((data._Y,) + tuple(class_cols))
     metas = numpy.hstack((data.metas,) + tuple(metas))
 
     new_data = Orange.data.Table.from_numpy(new_domain, X, Y, metas)


Test patch:
diff --git a/Orange/widgets/data/tests/test_owconcatenate.py b/Orange/widgets/data/tests/test_owconcatenate.py
new file mode 100644
--- /dev/null
+++ b/Orange/widgets/data/tests/test_owconcatenate.py
@@ -0,0 +1,98 @@
+# Test methods with long descriptive names can omit docstrings
+# pylint: disable=missing-docstring
+
+import numpy as np
+
+from Orange.data import Table, Domain
+from Orange.widgets.data.owconcatenate import OWConcatenate
+from Orange.widgets.tests.base import WidgetTest
+
+
+class TestOWConcatenate(WidgetTest):
+
+    def setUp(self):
+        self.widget = self.create_widget(OWConcatenate)
+        self.iris = Table("iris")
+        self.titanic = Table("titanic")
+
+    def test_single_input(self):
+        self.assertIsNone(self.get_output("Data"))
+        self.send_signal("Primary Data", self.iris)
+        output = self.get_output("Data")
+        self.assertEqual(list(output), list(self.iris))
+        self.send_signal("Primary Data", None)
+        self.assertIsNone(self.get_output("Data"))
+        self.send_signal("Additional Data", self.iris)
+        output = self.get_output("Data")
+        self.assertEqual(list(output), list(self.iris))
+        self.send_signal("Additional Data", None)
+        self.assertIsNone(self.get_output("Data"))
+
+    def test_two_inputs_union(self):
+        self.send_signal("Additional Data", self.iris, 0)
+        self.send_signal("Additional Data", self.titanic, 1)
+        output = self.get_output("Data")
+        # needs to contain all instances
+        self.assertEqual(len(output), len(self.iris) + len(self.titanic))
+        # needs to contain all variables
+        outvars = output.domain.variables
+        self.assertLess(set(self.iris.domain.variables), set(outvars))
+        self.assertLess(set(self.titanic.domain.variables), set(outvars))
+        # the first part of the data set is iris, the second part is titanic
+        np.testing.assert_equal(self.iris.X, output.X[:len(self.iris), :-3])
+        self.assertTrue(np.isnan(output.X[:len(self.iris), -3:]).all())
+        np.testing.assert_equal(self.titanic.X, output.X[len(self.iris):, -3:])
+        self.assertTrue(np.isnan(output.X[len(self.iris):, :-3]).all())
+
+    def test_two_inputs_intersection(self):
+        self.send_signal("Additional Data", self.iris, 0)
+        self.send_signal("Additional Data", self.titanic, 1)
+        self.widget.controls.merge_type.buttons[1].click()
+        output = self.get_output("Data")
+        # needs to contain all instances
+        self.assertEqual(len(output), len(self.iris) + len(self.titanic))
+        # no common variables
+        outvars = output.domain.variables
+        self.assertEqual(0, len(outvars))
+
+    def test_source(self):
+        self.send_signal("Additional Data", self.iris, 0)
+        self.send_signal("Additional Data", self.titanic, 1)
+        outputb = self.get_output("Data")
+        outvarsb = outputb.domain.variables
+        def get_source():
+            output = self.get_output("Data")
+            outvars = output.domain.variables + output.domain.metas
+            return (set(outvars) - set(outvarsb)).pop()
+        # test adding source
+        self.widget.controls.append_source_column.toggle()
+        source = get_source()
+        self.assertEquals(source.name, "Source ID")
+        # test name changing
+        self.widget.controls.source_attr_name.setText("Source")
+        self.widget.controls.source_attr_name.callback()
+        source = get_source()
+        self.assertEquals(source.name, "Source")
+        # test source_column role
+        places = ["class_vars", "attributes", "metas"]
+        for i, place in enumerate(places):
+            self.widget.source_column_role = i
+            self.widget.apply()
+            source = get_source()
+            output = self.get_output("Data")
+            self.assertTrue(source in getattr(output.domain, place))
+            data = Table(Domain([source]), output)
+            np.testing.assert_equal(data[:len(self.iris)].X, 0)
+            np.testing.assert_equal(data[len(self.iris):].X, 1)
+
+    def test_singleclass_source_class(self):
+        self.send_signal("Primary Data", self.iris)
+        # add source into a class variable
+        self.widget.controls.append_source_column.toggle()
+
+    def test_disable_merging_on_primary(self):
+        self.assertTrue(self.widget.mergebox.isEnabled())
+        self.send_signal("Primary Data", self.iris)
+        self.assertFalse(self.widget.mergebox.isEnabled())
+        self.send_signal("Primary Data", None)
+        self.assertTrue(self.widget.mergebox.isEnabled())