Repository: coddingtonbear/django-measurement

Issue:
Cannot deserialize measurement fields
## Steps
1. Having a model with measurement data
1. Exporting the database data using `./manage.py dumpdata [app[.model]] -o output.json`
1. Loading the fixture data back into the database using `./manage.py loaddata output.json`

## Error Stack
```
  File "python3.7/site-packages/django/core/management/commands/loaddata.py", line 72, in handle
    self.loaddata(fixture_labels)
  File "python3.7/site-packages/django/core/management/commands/loaddata.py", line 114, in loaddata
    self.load_label(fixture_label)
  File "python3.7/site-packages/django/core/management/commands/loaddata.py", line 172, in load_label
    for obj in objects:
  File "python3.7/site-packages/django/core/serializers/json.py", line 69, in Deserializer
    yield from PythonDeserializer(objects, **options)
  File "python3.7/site-packages/django/core/serializers/python.py", line 145, in Deserializer
    raise base.DeserializationError.WithData(e, d['model'], d.get('pk'), field_value)
django.core.serializers.base.DeserializationError: Problem installing fixture 'dump.json': ["'1.0 kg' value must be a float."]: (myapp.mymodel:pk=7) field_value was '1.0 kg'
```

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/coddingtonbear/django-measurement/pull/92?src=pr&el=h1) Report
> Merging [#92](https://codecov.io/gh/coddingtonbear/django-measurement/pull/92?src=pr&el=desc) into [master](https://codecov.io/gh/coddingtonbear/django-measurement/commit/85351334087e078c5693aaa458201bee755f65f6?src=pr&el=desc) will **increase** coverage by `0.52%`.
> The diff coverage is `87.5%`.

[![Impacted file tree graph](https://codecov.io/gh/coddingtonbear/django-measurement/pull/92/graphs/tree.svg?width=650&token=4IrpBMui0H&height=150&src=pr)](https://codecov.io/gh/coddingtonbear/django-measurement/pull/92?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #92      +/-   ##
==========================================
+ Coverage    88.8%   89.33%   +0.52%     
==========================================
  Files           4        4              
  Lines         134      150      +16     
==========================================
+ Hits          119      134      +15     
- Misses         15       16       +1
```


| [Impacted Files](https://codecov.io/gh/coddingtonbear/django-measurement/pull/92?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [django\_measurement/models.py](https://codecov.io/gh/coddingtonbear/django-measurement/pull/92/diff?src=pr&el=tree#diff-ZGphbmdvX21lYXN1cmVtZW50L21vZGVscy5weQ==) | `94.66% <87.5%> (-0.25%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/coddingtonbear/django-measurement/pull/92?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/coddingtonbear/django-measurement/pull/92?src=pr&el=footer). Last update [8535133...eb174b7](https://codecov.io/gh/coddingtonbear/django-measurement/pull/92?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.travis.yml b/.travis.yml
index 8f1698c..2ea067c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,16 +1,21 @@
+dist: xenial
 language: python
 sudo: false
 cache: pip
-python: 3.6
+python:
+  - "3.6"
+  - "3.7"
+  - "3.8"
 env:
   matrix:
   - DJANGO=111
-  - DJANGO=20
+  - DJANGO=22
   - DJANGO=master
-  - TOXENV=qa
-  - TOXENV=docs
 matrix:
   fast_finish: true
+  include:
+    - env: TOXENV=qa
+    - env: TOXENV=docs
   allow_failures:
   - env: DJANGO=master
 install:
diff --git a/django_measurement/models.py b/django_measurement/models.py
index 7e7b2c7..29fec45 100644
--- a/django_measurement/models.py
+++ b/django_measurement/models.py
@@ -90,11 +90,30 @@ class MeasurementField(FloatField):
             original_unit=self.get_default_unit(),
         )
 
+    def value_to_string(self, obj):
+        value = self.value_from_object(obj)
+        if not isinstance(value, self.MEASURE_BASES):
+            return value
+        return '%s:%s' % (value.value, value.unit)
+
+    def deserialize_value_from_string(self, s: str):
+        parts = s.split(':', 1)
+        if len(parts) != 2:
+            return None
+        value, unit = float(parts[0]), parts[1]
+        measure = get_measurement(self.measurement, value=value, unit=unit)
+        return measure
+
     def to_python(self, value):
+
         if value is None:
             return value
         elif isinstance(value, self.MEASURE_BASES):
             return value
+        elif isinstance(value, str):
+            parsed = self.deserialize_value_from_string(value)
+            if parsed is not None:
+                return parsed
         value = super(MeasurementField, self).to_python(value)
 
         return_unit = self.get_default_unit()
diff --git a/tox.ini b/tox.ini
index 825362b..dd3d75b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,10 +1,10 @@
 [tox]
-envlist = py{36}-dj{111,20,master},qa,docs
+envlist = py{36,37,38}-dj{111,22,master},qa,docs
 [testenv]
 deps=
     -rrequirements-dev.txt
     dj111: https://github.com/django/django/archive/stable/1.11.x.tar.gz#egg=django
-    dj20: https://github.com/django/django/archive/stable/2.0.x.tar.gz#egg=django
+    dj22: https://github.com/django/django/archive/stable/2.2.x.tar.gz#egg=django
     djmaster: https://github.com/django/django/archive/master.tar.gz#egg=django
 commands=
     coverage run --source=django_measurement -m 'pytest' \


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 746593f..c1986f6 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1,4 +1,5 @@
 import pytest
+from django.core import serializers
 from django.core.exceptions import ValidationError
 from django.utils import module_loading
 from measurement import measures
@@ -160,6 +161,26 @@ class TestDeconstruct:
         )
 
 
+@pytest.mark.parametrize('fieldname, measure, expected_serialized_value', [
+    ('measurement_weight', measures.Weight(kg=4.0), "4.0:kg"),
+    ('measurement_speed', measures.Speed(mi__hr=2.0), "2.0:mi__hr"),
+])
+class TestSerialization:
+    def test_deconstruct(self, fieldname, measure, expected_serialized_value):
+        instance = MeasurementTestModel(pk=0)
+        setattr(instance, fieldname, measure)
+        serialized_object = serializers.serialize("python", [instance])[0]
+        serialized_value = serialized_object["fields"][fieldname]
+
+        assert isinstance(serialized_value, str)
+        assert serialized_value == expected_serialized_value
+
+        deserialized_obj = next(serializers.deserialize("python", [serialized_object]))
+        deserialized_value = getattr(deserialized_obj.object, fieldname)
+
+        assert deserialized_value == measure
+
+
 class TestMeasurementFormField:
     def test_max_value(self):
         valid_form = MeasurementTestForm({