Repository: biolab/orange3

Issue:
Edit Domain destroys datetime format
##### Orange version
<!-- From menu _Help→About→Version_ or code `Orange.version.full_version` -->
3.8.dev

##### Expected behavior
Edit Domain retains the set datetime iso format, e.g. 2017-11-14 12:12.


##### Actual behavior
Edit Domain converts datetime to floats, creating a mess downstream.


##### Steps to reproduce the behavior
Use on any datetime data set.

<img width="305" alt="screen shot 2017-11-14 at 13 40 05" src="https://user-images.githubusercontent.com/12524972/32780509-c71245ce-c941-11e7-83dc-1a4dfe2a3932.png">


##### Additional info (worksheets, data, screenshots, ...)

Original data
<img width="590" alt="screen shot 2017-11-14 at 13 39 49" src="https://user-images.githubusercontent.com/12524972/32780516-d35c4ef6-c941-11e7-8cb5-d29a5f586a09.png">

After Edit Domain
<img width="608" alt="screen shot 2017-11-14 at 13 39 57" src="https://user-images.githubusercontent.com/12524972/32780527-dd4341e0-c941-11e7-9f55-646e701440e9.png">





Discussion:
This requires creating a custom TimeVariableEditor that copies have_date and have_time to the new variable in `get_data`, as well as modifying `variable_description` and `variable_from_description` to be able to serialize and deserialize TimeVariables with their corresponding have_date/have_time options.

bonus points for being able to modify the format used to display date/time.

Resolving patch:
diff --git a/Orange/data/variable.py b/Orange/data/variable.py
--- a/Orange/data/variable.py
+++ b/Orange/data/variable.py
@@ -929,10 +929,10 @@ class TimeVariable(ContinuousVariable):
     utc_offset = None
     timezone = timezone.utc
 
-    def __init__(self, *args, **kwargs):
+    def __init__(self, *args, have_date=0, have_time=0, **kwargs):
         super().__init__(*args, **kwargs)
-        self.have_date = 0
-        self.have_time = 0
+        self.have_date = have_date
+        self.have_time = have_time
 
     def copy(self, compute_value=None):
         copy = super().copy(compute_value=compute_value)
diff --git a/Orange/widgets/data/oweditdomain.py b/Orange/widgets/data/oweditdomain.py
--- a/Orange/widgets/data/oweditdomain.py
+++ b/Orange/widgets/data/oweditdomain.py
@@ -55,6 +55,13 @@ def variable_description(var, skip_attributes=False):
                 var.name,
                 (("values", tuple(var.values)),),
                 attributes)
+    elif var.is_time:
+        return (var_type.__module__,
+                var_type.__name__,
+                var.name,
+                (("have_date", var.have_date),
+                 ("have_time", var.have_time)),
+                attributes)
     else:
         return (var_type.__module__,
                 var_type.__name__,
@@ -68,14 +75,14 @@ def variable_from_description(description, compute_value=None):
     :func:`variable_description`).
 
     """
-    module, type_name, name, kwargs, attrs = description
+    module, type_name, name, args, attrs = description
     try:
         constructor = get_qualified(module, type_name)
     except (ImportError, AttributeError):
         raise ValueError("Invalid descriptor type '{}.{}"
                          "".format(module, type_name))
 
-    var = constructor(name, compute_value=compute_value, **dict(list(kwargs)))
+    var = constructor(name, compute_value=compute_value, **dict(list(args)))
     var.attributes.update(attrs)
     return var
 
@@ -364,6 +371,31 @@ class ContinuousVariableEditor(VariableEditor):
     pass
 
 
+class TimeVariableEditor(VariableEditor):
+    """An editor widget for editing a Time variable.
+
+       Extends the :class:`VariableEditor` to enable editing of
+       time variables.
+
+    """
+    def get_data(self):
+        """Retrieve the modified variable"""
+        name = str(self.name_edit.text()).strip()
+        labels = self.labels_model.get_dict()
+
+        # Is the variable actually changed.
+        if self.var is not None and not self.is_same():
+            var = type(self.var)(name)
+            var.attributes.update(labels)
+            var.have_date = self.var.have_date
+            var.have_time = self.var.have_time
+            self.var = var
+        else:
+            var = self.var
+
+        return var
+
+
 class OWEditDomain(widget.OWWidget):
     name = "Edit Domain"
     description = "Rename features and their values."
@@ -413,6 +445,7 @@ def __init__(self):
 
         self.editor_stack.addWidget(DiscreteVariableEditor())
         self.editor_stack.addWidget(ContinuousVariableEditor())
+        self.editor_stack.addWidget(TimeVariableEditor())
         self.editor_stack.addWidget(VariableEditor())
 
         box.layout().addWidget(self.editor_stack)
@@ -508,9 +541,12 @@ def open_editor(self, index):
 
         var = self.domain_model[index]
 
-        editor_index = 2
+        editor_index = 3
         if var.is_discrete:
             editor_index = 0
+        # TimeVariable is also continuous, first check if time
+        elif var.is_time:
+            editor_index = 2
         elif var.is_continuous:
             editor_index = 1
         editor = self.editor_stack.widget(editor_index)


Test patch:
diff --git a/Orange/data/tests/test_variable.py b/Orange/data/tests/test_variable.py
--- a/Orange/data/tests/test_variable.py
+++ b/Orange/data/tests/test_variable.py
@@ -417,6 +417,12 @@ def test_repr_value(self):
         var = TimeVariable('time')
         self.assertEqual(var.repr_val(Value(var, 416.3)), '416.3')
 
+    def test_have_date_have_time_in_construct(self):
+        """Test if have_time and have_date is correctly set"""
+        var = TimeVariable('time', have_date=1)
+        self.assertTrue(var.have_date)
+        self.assertFalse(var.have_time)
+
 
 PickleContinuousVariable = create_pickling_tests(
     "PickleContinuousVariable",
diff --git a/Orange/widgets/data/tests/test_oweditdomain.py b/Orange/widgets/data/tests/test_oweditdomain.py
--- a/Orange/widgets/data/tests/test_oweditdomain.py
+++ b/Orange/widgets/data/tests/test_oweditdomain.py
@@ -7,9 +7,10 @@
 from AnyQt.QtCore import QModelIndex, Qt
 
 from Orange.data import ContinuousVariable, DiscreteVariable, \
-    StringVariable, Table, Domain
+    StringVariable, TimeVariable, Table, Domain
 from Orange.widgets.data.oweditdomain import EditDomainReport, OWEditDomain, \
-    ContinuousVariableEditor, DiscreteVariableEditor, VariableEditor
+    ContinuousVariableEditor, DiscreteVariableEditor, VariableEditor, \
+    TimeVariableEditor
 from Orange.widgets.data.owcolor import OWColor, ColorRole
 from Orange.widgets.tests.base import WidgetTest, GuiTest
 
@@ -169,6 +170,20 @@ def test_duplicate_names(self):
         output = self.get_output(self.widget.Outputs.data)
         self.assertIsInstance(output, Table)
 
+    def test_time_variable_preservation(self):
+        """Test if time variables preserve format specific attributes"""
+        table = Table("cyber-security-breaches")
+        self.send_signal(self.widget.Inputs.data, table)
+        output = self.get_output(self.widget.Outputs.data)
+        self.assertEqual(str(table[0, 4]), str(output[0, 4]))
+
+        editor = self.widget.editor_stack.findChild(TimeVariableEditor)
+        editor.name_edit.setText("Date")
+        editor.commit()
+        self.widget.commit()
+        output = self.get_output(self.widget.Outputs.data)
+        self.assertEqual(str(table[0, 4]), str(output[0, 4]))
+
 
 class TestEditors(GuiTest):
     def test_variable_editor(self):
@@ -221,3 +236,24 @@ def test_discrete_editor(self):
         self.assertEqual(w.name_edit.text(), "")
         self.assertEqual(w.labels_model.get_dict(), {})
         self.assertIs(w.get_data(), None)
+
+    def test_time_editor(self):
+        w = TimeVariableEditor()
+        self.assertIs(w.get_data(), None)
+
+        v = TimeVariable("T", have_date=1)
+        v.attributes.update({"A": 1, "B": "b"})
+        w.set_data(v)
+
+        self.assertEqual(w.name_edit.text(), v.name)
+        self.assertEqual(w.labels_model.get_dict(), v.attributes)
+        self.assertTrue(w.is_same())
+
+        var = w.get_data()
+        self.assertTrue(var.have_date)
+        self.assertFalse(var.have_time)
+
+        w.set_data(None)
+        self.assertEqual(w.name_edit.text(), "")
+        self.assertEqual(w.labels_model.get_dict(), {})
+        self.assertIs(w.get_data(), None)