From fd57afd87bcf19f0bcdcd6271d9a9ffd95c09ac7 Mon Sep 17 00:00:00 2001
From: Alan <alan@aptivate.org>
Date: Wed, 1 Apr 2020 14:26:34 +0100
Subject: [PATCH] Key value

---
 Pipfile                                       |   3 +-
 Pipfile.lock                                  | 409 ++++++++++++------
 internewshid/chn_spreadsheet/importer.py      |  32 +-
 .../chn_spreadsheet/tests/importer_tests.py   |   2 +
 .../chn_spreadsheet/tests/key_values_tests.py |  42 ++
 .../test_files/sample_kobo_keyValues.xlsx     | Bin 0 -> 6235 bytes
 .../data_layer/migrations/0027_key_value.py   |  30 ++
 internewshid/data_layer/models.py             |  39 ++
 .../hid/tests/add_edit_item_view_tests.py     |   2 +-
 internewshid/rest_api/serializers.py          |   4 +
 internewshid/rest_api/views.py                |  17 +-
 internewshid/transport/items.py               |  28 +-
 12 files changed, 468 insertions(+), 140 deletions(-)
 create mode 100644 internewshid/chn_spreadsheet/tests/key_values_tests.py
 create mode 100644 internewshid/chn_spreadsheet/tests/test_files/sample_kobo_keyValues.xlsx
 create mode 100644 internewshid/data_layer/migrations/0027_key_value.py

diff --git a/Pipfile b/Pipfile
index a06a11cc..edc6a86b 100644
--- a/Pipfile
+++ b/Pipfile
@@ -10,7 +10,7 @@ python_version = "3.6"
 asgiref = "*"
 "django-bootstrap3" = "*"
 "django-tables2" = "*"
-"django.js" = {ref = "ca328a94b00023bd64f4fc1c908675edaaf2ac19",git = "https://git@github.com/aptivate/django.js.git",editable = true}
+"django.js" = {editable = true,git = "https://git@github.com/aptivate/django.js.git",ref = "ca328a94b00023bd64f4fc1c908675edaaf2ac19"}
 "linecache2" = "*"
 Django = ">2.2.8,<3.0"
 Pillow = "*"
@@ -32,6 +32,7 @@ pytz = "*"
 rest-pandas = "*"
 mysqlclient = "*"
 django-debug-toolbar-template-timings = "*"
+ipdb = "*"
 
 [dev-packages]
 django-debug-toolbar = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index 7e26ea66..bad64889 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "8a5bc8f5d57faebea7b5e6fc4f117b54ae9dbc2410c85feca8332dc7831aa7dd"
+            "sha256": "853bc2ef3d6225526ec3254e2e004b9d724e7d198064ad801c0b47c0bd8e4943"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -18,11 +18,18 @@
     "default": {
         "asgiref": {
             "hashes": [
-                "sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0",
-                "sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5"
+                "sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5",
+                "sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"
             ],
             "index": "pypi",
-            "version": "==3.2.3"
+            "version": "==3.2.7"
+        },
+        "backcall": {
+            "hashes": [
+                "sha256:38ecd85be2c1e78f77fd91700c76e14667dc21e2713b63876c0eb901196e01e4",
+                "sha256:bbbf4b1e5cd2bdb08f915895b51081c041bac22394fdfcfdfbe9f14b77c08bf2"
+            ],
+            "version": "==0.1.0"
         },
         "cssmin": {
             "hashes": [
@@ -31,6 +38,13 @@
             "index": "pypi",
             "version": "==0.2.0"
         },
+        "decorator": {
+            "hashes": [
+                "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760",
+                "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"
+            ],
+            "version": "==4.4.2"
+        },
         "django": {
             "hashes": [
                 "sha256:65e2387e6bde531d3bb803244a2b74e0253550a9612c64a60c8c5be267b30f50",
@@ -133,11 +147,11 @@
         },
         "django-widget-tweaks": {
             "hashes": [
-                "sha256:65c960f3d75008a285e4b10f4d21f9eae4160fd77a0f6097ad545185f8648bd6",
-                "sha256:f2e2c9c9be1ccc59061e248dcc2144f4906d594abe1a563902f4bdf6aa14e432"
+                "sha256:9f91ca4217199b7671971d3c1f323a2bec71a0c27dec6260b3c006fa541bc489",
+                "sha256:f80bff4a8a59b278bb277a405a76a8b9a884e4bae7a6c70e78a39c626cd1c836"
             ],
             "index": "pypi",
-            "version": "==1.4.5"
+            "version": "==1.4.8"
         },
         "django.js": {
             "editable": true,
@@ -165,6 +179,27 @@
             ],
             "version": "==1.0.1"
         },
+        "ipdb": {
+            "hashes": [
+                "sha256:77fb1c2a6fccdfee0136078c9ed6fe547ab00db00bebff181f1e8c9e13418d49"
+            ],
+            "index": "pypi",
+            "version": "==0.13.2"
+        },
+        "ipython": {
+            "hashes": [
+                "sha256:ca478e52ae1f88da0102360e57e528b92f3ae4316aabac80a2cd7f7ab2efb48a",
+                "sha256:eb8d075de37f678424527b5ef6ea23f7b80240ca031c2dd6de5879d687a65333"
+            ],
+            "version": "==7.13.0"
+        },
+        "ipython-genutils": {
+            "hashes": [
+                "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8",
+                "sha256:eb2e116e75ecef9d4d228fdc66af54269afa26ab4463042e33785b887c628ba8"
+            ],
+            "version": "==0.2.0"
+        },
         "jdcal": {
             "hashes": [
                 "sha256:1abf1305fce18b4e8aa248cf8fe0c56ce2032392bc64bbd61b5dff2a19ec8bba",
@@ -172,6 +207,13 @@
             ],
             "version": "==1.4.1"
         },
+        "jedi": {
+            "hashes": [
+                "sha256:b4f4052551025c6b0b0b193b29a6ff7bdb74c52450631206c262aef9f7159ad2",
+                "sha256:d5c871cb9360b414f981e7072c52c33258d598305280fef91c6cae34739d65d5"
+            ],
+            "version": "==0.16.0"
+        },
         "jsmin": {
             "hashes": [
                 "sha256:b6df99b2cd1c75d9d342e4335b535789b8da9107ec748212706ef7bbe5c2553b"
@@ -207,29 +249,29 @@
         },
         "numpy": {
             "hashes": [
-                "sha256:1786a08236f2c92ae0e70423c45e1e62788ed33028f94ca99c4df03f5be6b3c6",
-                "sha256:17aa7a81fe7599a10f2b7d95856dc5cf84a4eefa45bc96123cbbc3ebc568994e",
-                "sha256:20b26aaa5b3da029942cdcce719b363dbe58696ad182aff0e5dcb1687ec946dc",
-                "sha256:2d75908ab3ced4223ccba595b48e538afa5ecc37405923d1fea6906d7c3a50bc",
-                "sha256:39d2c685af15d3ce682c99ce5925cc66efc824652e10990d2462dfe9b8918c6a",
-                "sha256:56bc8ded6fcd9adea90f65377438f9fea8c05fcf7c5ba766bef258d0da1554aa",
-                "sha256:590355aeade1a2eaba17617c19edccb7db8d78760175256e3cf94590a1a964f3",
-                "sha256:70a840a26f4e61defa7bdf811d7498a284ced303dfbc35acb7be12a39b2aa121",
-                "sha256:77c3bfe65d8560487052ad55c6998a04b654c2fbc36d546aef2b2e511e760971",
-                "sha256:9537eecf179f566fd1c160a2e912ca0b8e02d773af0a7a1120ad4f7507cd0d26",
-                "sha256:9acdf933c1fd263c513a2df3dceecea6f3ff4419d80bf238510976bf9bcb26cd",
-                "sha256:ae0975f42ab1f28364dcda3dde3cf6c1ddab3e1d4b2909da0cb0191fa9ca0480",
-                "sha256:b3af02ecc999c8003e538e60c89a2b37646b39b688d4e44d7373e11c2debabec",
-                "sha256:b6ff59cee96b454516e47e7721098e6ceebef435e3e21ac2d6c3b8b02628eb77",
-                "sha256:b765ed3930b92812aa698a455847141869ef755a87e099fddd4ccf9d81fffb57",
-                "sha256:c98c5ffd7d41611407a1103ae11c8b634ad6a43606eca3e2a5a269e5d6e8eb07",
-                "sha256:cf7eb6b1025d3e169989416b1adcd676624c2dbed9e3bcb7137f51bfc8cc2572",
-                "sha256:d92350c22b150c1cae7ebb0ee8b5670cc84848f6359cf6b5d8f86617098a9b73",
-                "sha256:e422c3152921cece8b6a2fb6b0b4d73b6579bd20ae075e7d15143e711f3ca2ca",
-                "sha256:e840f552a509e3380b0f0ec977e8124d0dc34dc0e68289ca28f4d7c1d0d79474",
-                "sha256:f3d0a94ad151870978fb93538e95411c83899c9dc63e6fb65542f769568ecfa5"
-            ],
-            "version": "==1.18.1"
+                "sha256:1598a6de323508cfeed6b7cd6c4efb43324f4692e20d1f76e1feec7f59013448",
+                "sha256:1b0ece94018ae21163d1f651b527156e1f03943b986188dd81bc7e066eae9d1c",
+                "sha256:2e40be731ad618cb4974d5ba60d373cdf4f1b8dcbf1dcf4d9dff5e212baf69c5",
+                "sha256:4ba59db1fcc27ea31368af524dcf874d9277f21fd2e1f7f1e2e0c75ee61419ed",
+                "sha256:59ca9c6592da581a03d42cc4e270732552243dc45e87248aa8d636d53812f6a5",
+                "sha256:5e0feb76849ca3e83dd396254e47c7dba65b3fa9ed3df67c2556293ae3e16de3",
+                "sha256:6d205249a0293e62bbb3898c4c2e1ff8a22f98375a34775a259a0523111a8f6c",
+                "sha256:6fcc5a3990e269f86d388f165a089259893851437b904f422d301cdce4ff25c8",
+                "sha256:82847f2765835c8e5308f136bc34018d09b49037ec23ecc42b246424c767056b",
+                "sha256:87902e5c03355335fc5992a74ba0247a70d937f326d852fc613b7f53516c0963",
+                "sha256:9ab21d1cb156a620d3999dd92f7d1c86824c622873841d6b080ca5495fa10fef",
+                "sha256:a1baa1dc8ecd88fb2d2a651671a84b9938461e8a8eed13e2f0a812a94084d1fa",
+                "sha256:a244f7af80dacf21054386539699ce29bcc64796ed9850c99a34b41305630286",
+                "sha256:a35af656a7ba1d3decdd4fae5322b87277de8ac98b7d9da657d9e212ece76a61",
+                "sha256:b1fe1a6f3a6f355f6c29789b5927f8bd4f134a4bd9a781099a7c4f66af8850f5",
+                "sha256:b5ad0adb51b2dee7d0ee75a69e9871e2ddfb061c73ea8bc439376298141f77f5",
+                "sha256:ba3c7a2814ec8a176bb71f91478293d633c08582119e713a0c5351c0f77698da",
+                "sha256:cd77d58fb2acf57c1d1ee2835567cd70e6f1835e32090538f17f8a3a99e5e34b",
+                "sha256:cdb3a70285e8220875e4d2bc394e49b4988bdb1298ffa4e0bd81b2f613be397c",
+                "sha256:deb529c40c3f1e38d53d5ae6cd077c21f1d49e13afc7936f7f868455e16b64a0",
+                "sha256:e7894793e6e8540dbeac77c87b489e331947813511108ae097f1715c018b8f3d"
+            ],
+            "version": "==1.18.2"
         },
         "openpyxl": {
             "hashes": [
@@ -240,22 +282,46 @@
         },
         "pandas": {
             "hashes": [
-                "sha256:23e177d43e4bf68950b0f8788b6a2fef2f478f4ec94883acb627b9264522a98a",
-                "sha256:2530aea4fe46e8df7829c3f05e0a0f821c893885d53cb8ac9b89cc67c143448c",
-                "sha256:303827f0bb40ff610fbada5b12d50014811efcc37aaf6ef03202dc3054bfdda1",
-                "sha256:3b019e3ea9f5d0cfee0efabae2cfd3976874e90bcc3e97b29600e5a9b345ae3d",
-                "sha256:3c07765308f091d81b6735d4f2242bb43c332cc3461cae60543df6b10967fe27",
-                "sha256:5036d4009012a44aa3e50173e482b664c1fae36decd277c49e453463798eca4e",
-                "sha256:6f38969e2325056f9959efbe06c27aa2e94dd35382265ad0703681d993036052",
-                "sha256:74a470d349d52b9d00a2ba192ae1ee22155bb0a300fd1ccb2961006c3fa98ed3",
-                "sha256:7d77034e402165b947f43050a8a415aa3205abfed38d127ea66e57a2b7b5a9e0",
-                "sha256:7f9a509f6f11fa8b9313002ebdf6f690a7aa1dd91efd95d90185371a0d68220e",
-                "sha256:942b5d04762feb0e55b2ad97ce2b254a0ffdd344b56493b04a627266e24f2d82",
-                "sha256:a9fbe41663416bb70ed05f4e16c5f377519c0dc292ba9aa45f5356e37df03a38",
-                "sha256:d10e83866b48c0cdb83281f786564e2a2b51a7ae7b8a950c3442ad3c9e36b48c",
-                "sha256:e2140e1bbf9c46db9936ee70f4be6584d15ff8dc3dfff1da022d71227d53bad3"
+                "sha256:07c1b58936b80eafdfe694ce964ac21567b80a48d972879a359b3ebb2ea76835",
+                "sha256:0ebe327fb088df4d06145227a4aa0998e4f80a9e6aed4b61c1f303bdfdf7c722",
+                "sha256:11c7cb654cd3a0e9c54d81761b5920cdc86b373510d829461d8f2ed6d5905266",
+                "sha256:12f492dd840e9db1688126216706aa2d1fcd3f4df68a195f9479272d50054645",
+                "sha256:167a1315367cea6ec6a5e11e791d9604f8e03f95b57ad227409de35cf850c9c5",
+                "sha256:1a7c56f1df8d5ad8571fa251b864231f26b47b59cbe41aa5c0983d17dbb7a8e4",
+                "sha256:1fa4bae1a6784aa550a1c9e168422798104a85bf9c77a1063ea77ee6f8452e3a",
+                "sha256:32f42e322fb903d0e189a4c10b75ba70d90958cc4f66a1781ed027f1a1d14586",
+                "sha256:387dc7b3c0424327fe3218f81e05fc27832772a5dffbed385013161be58df90b",
+                "sha256:6597df07ea361231e60c00692d8a8099b519ed741c04e65821e632bc9ccb924c",
+                "sha256:743bba36e99d4440403beb45a6f4f3a667c090c00394c176092b0b910666189b",
+                "sha256:858a0d890d957ae62338624e4aeaf1de436dba2c2c0772570a686eaca8b4fc85",
+                "sha256:863c3e4b7ae550749a0bb77fa22e601a36df9d2905afef34a6965bed092ba9e5",
+                "sha256:a210c91a02ec5ff05617a298ad6f137b9f6f5771bf31f2d6b6367d7f71486639",
+                "sha256:ca84a44cf727f211752e91eab2d1c6c1ab0f0540d5636a8382a3af428542826e",
+                "sha256:d234bcf669e8b4d6cbcd99e3ce7a8918414520aeb113e2a81aeb02d0a533d7f7"
             ],
-            "version": "==1.0.1"
+            "version": "==1.0.3"
+        },
+        "parso": {
+            "hashes": [
+                "sha256:0c5659e0c6eba20636f99a04f469798dca8da279645ce5c387315b2c23912157",
+                "sha256:8515fc12cfca6ee3aa59138741fc5624d62340c97e401c74875769948d4f2995"
+            ],
+            "version": "==0.6.2"
+        },
+        "pexpect": {
+            "hashes": [
+                "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937",
+                "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"
+            ],
+            "markers": "sys_platform != 'win32'",
+            "version": "==4.8.0"
+        },
+        "pickleshare": {
+            "hashes": [
+                "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca",
+                "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"
+            ],
+            "version": "==0.7.5"
         },
         "pillow": {
             "hashes": [
@@ -285,6 +351,27 @@
             "index": "pypi",
             "version": "==7.0.0"
         },
+        "prompt-toolkit": {
+            "hashes": [
+                "sha256:563d1a4140b63ff9dd587bda9557cffb2fe73650205ab6f4383092fb882e7dc8",
+                "sha256:df7e9e63aea609b1da3a65641ceaf5bc7d05e0a04de5bd45d05dbeffbabf9e04"
+            ],
+            "version": "==3.0.5"
+        },
+        "ptyprocess": {
+            "hashes": [
+                "sha256:923f299cc5ad920c68f2bc0bc98b75b9f838b93b599941a6b63ddbc2476394c0",
+                "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"
+            ],
+            "version": "==0.6.0"
+        },
+        "pygments": {
+            "hashes": [
+                "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44",
+                "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"
+            ],
+            "version": "==2.6.1"
+        },
         "python-dateutil": {
             "hashes": [
                 "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
@@ -323,6 +410,20 @@
             ],
             "version": "==0.3.1"
         },
+        "traitlets": {
+            "hashes": [
+                "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
+                "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
+            ],
+            "version": "==4.3.3"
+        },
+        "wcwidth": {
+            "hashes": [
+                "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1",
+                "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"
+            ],
+            "version": "==0.1.9"
+        },
         "webassets": {
             "hashes": [
                 "sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd",
@@ -332,13 +433,20 @@
         }
     },
     "develop": {
+        "appdirs": {
+            "hashes": [
+                "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
+                "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
+            ],
+            "version": "==1.4.3"
+        },
         "asgiref": {
             "hashes": [
-                "sha256:7e06d934a7718bf3975acbf87780ba678957b87c7adc056f13b6215d610695a0",
-                "sha256:ea448f92fc35a0ef4b1508f53a04c4670255a3f33d22a81c8fc9c872036adbe5"
+                "sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5",
+                "sha256:9ca8b952a0a9afa61d30aa6d3d9b570bb3fd6bafcf7ec9e6bed43b936133db1c"
             ],
             "index": "pypi",
-            "version": "==3.2.3"
+            "version": "==3.2.7"
         },
         "attrs": {
             "hashes": [
@@ -363,46 +471,52 @@
         },
         "click": {
             "hashes": [
-                "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
-                "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
+                "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
+                "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
             ],
-            "version": "==7.0"
+            "version": "==7.1.1"
         },
         "coverage": {
             "hashes": [
-                "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3",
-                "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c",
-                "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0",
-                "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477",
-                "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a",
-                "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf",
-                "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691",
-                "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73",
-                "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987",
-                "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894",
-                "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e",
-                "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef",
-                "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf",
-                "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68",
-                "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8",
-                "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954",
-                "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2",
-                "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40",
-                "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc",
-                "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc",
-                "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e",
-                "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d",
-                "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f",
-                "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc",
-                "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301",
-                "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea",
-                "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb",
-                "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af",
-                "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52",
-                "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37",
-                "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"
-            ],
-            "version": "==5.0.3"
+                "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0",
+                "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30",
+                "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b",
+                "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0",
+                "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823",
+                "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe",
+                "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037",
+                "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6",
+                "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31",
+                "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd",
+                "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892",
+                "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1",
+                "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78",
+                "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac",
+                "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006",
+                "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014",
+                "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2",
+                "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7",
+                "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8",
+                "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7",
+                "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9",
+                "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1",
+                "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307",
+                "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a",
+                "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435",
+                "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0",
+                "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5",
+                "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441",
+                "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732",
+                "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de",
+                "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1"
+            ],
+            "version": "==5.0.4"
+        },
+        "distlib": {
+            "hashes": [
+                "sha256:2e166e231a26b36d6dfe35a48c4464346620f8645ed0ace01ee31822b288de21"
+            ],
+            "version": "==0.3.0"
         },
         "django": {
             "hashes": [
@@ -421,25 +535,25 @@
         },
         "django-dynamic-fixture": {
             "hashes": [
-                "sha256:cb6fac74b60ced901ce1f8f9e4dd8c438ab6e9021301a5ce2550fb424add4c33"
+                "sha256:e772102ba40f70c2e66470cae85f3874aa992e6b22a0d0c360450f2949b0728d"
             ],
             "index": "pypi",
-            "version": "==3.0.3"
+            "version": "==3.1.0"
         },
         "django-extensions": {
             "hashes": [
-                "sha256:1a03c4e8bade575f8c2be6c76456f8a2be3f9b02ab9f47d3535afa9562dc0493",
-                "sha256:2699cc1d6fb4bd393c0b5832fea4bc685f2ace5800b3c9ff222b2080f161ac04"
+                "sha256:2f81b618ba4d1b0e58603e25012e5c74f88a4b706e0022a3b21f24f0322a6ce6",
+                "sha256:b19182d101a441fe001c5753553a901e2ef3ff60e8fbbe38881eb4a61fdd17c4"
             ],
             "index": "pypi",
-            "version": "==2.2.8"
+            "version": "==2.2.9"
         },
         "dparse": {
             "hashes": [
-                "sha256:00a5fdfa900629e5159bf3600d44905b333f4059a3366f28e0dbd13eeab17b19",
-                "sha256:cef95156fa0adedaf042cd42f9990974bec76f25dfeca4dc01f381a243d5aa5b"
+                "sha256:14fed5efc5e98c0a81dfe100c4c2ea0a4c189104e9a9d18b5cfd342a163f97be",
+                "sha256:db349e53f6d03c8ee80606c49b35f515ed2ab287a8e1579e2b4bdf52b12b1530"
             ],
-            "version": "==0.4.1"
+            "version": "==0.5.0"
         },
         "factory-boy": {
             "hashes": [
@@ -451,10 +565,17 @@
         },
         "faker": {
             "hashes": [
-                "sha256:440d68fe0e46c1658b1975b2497abe0c24a7f772e3892253f31e713ffcc48965",
-                "sha256:ee24608768549c2c69e593e9d7a3b53c9498ae735534243ec8390cae5d529f8b"
+                "sha256:2d3f866ef25e1a5af80e7b0ceeacc3c92dec5d0fdbad3e2cb6adf6e60b22188f",
+                "sha256:b89aa33837498498e15c709eb40c31386408a901a53c7a5e12a425737a767976"
+            ],
+            "version": "==4.0.2"
+        },
+        "filelock": {
+            "hashes": [
+                "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59",
+                "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"
             ],
-            "version": "==4.0.1"
+            "version": "==3.0.12"
         },
         "idna": {
             "hashes": [
@@ -465,11 +586,19 @@
         },
         "importlib-metadata": {
             "hashes": [
-                "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302",
-                "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"
+                "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
+                "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
             ],
             "markers": "python_version < '3.8'",
-            "version": "==1.5.0"
+            "version": "==1.6.0"
+        },
+        "importlib-resources": {
+            "hashes": [
+                "sha256:4019b6a9082d8ada9def02bece4a76b131518866790d58fdda0b5f8c603b36c2",
+                "sha256:dd98ceeef3f5ad2ef4cc287b8586da4ebad15877f351e9688987ad663a0a29b8"
+            ],
+            "markers": "python_version < '3.7'",
+            "version": "==1.4.0"
         },
         "isort": {
             "hashes": [
@@ -488,11 +617,11 @@
         },
         "mock": {
             "hashes": [
-                "sha256:2a572b715f09dd2f0a583d8aeb5bb67d7ed7a8fd31d193cf1227a99c16a67bc3",
-                "sha256:5e48d216809f6f393987ed56920305d8f3c647e6ed35407c1ff2ecb88a9e1151"
+                "sha256:3f9b2c0196c60d21838f307f5825a7b86b678cedc58ab9e50a8988187b4d81e0",
+                "sha256:dd33eb70232b6118298d516bbcecd26704689c386594f0f3c4f13867b2c56f72"
             ],
             "index": "pypi",
-            "version": "==4.0.1"
+            "version": "==4.0.2"
         },
         "more-itertools": {
             "hashes": [
@@ -508,6 +637,14 @@
             ],
             "version": "==20.3"
         },
+        "pipenv": {
+            "hashes": [
+                "sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330",
+                "sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e",
+                "sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846"
+            ],
+            "version": "==2018.11.26"
+        },
         "pluggy": {
             "hashes": [
                 "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
@@ -563,10 +700,10 @@
         },
         "pytest": {
             "hashes": [
-                "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d",
-                "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6"
+                "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172",
+                "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"
             ],
-            "version": "==5.3.5"
+            "version": "==5.4.1"
         },
         "pytest-cov": {
             "hashes": [
@@ -578,11 +715,11 @@
         },
         "pytest-django": {
             "hashes": [
-                "sha256:456fa6854d04ee625d6bbb8b38ca2259e7040a6f93333bfe8bc8159b7e987203",
-                "sha256:489b904f695f9fb880ce591cf5a4979880afb467763b1f180c07574554bdfd26"
+                "sha256:64f99d565dd9497af412fcab2989fe40982c1282d4118ff422b407f3f7275ca5",
+                "sha256:664e5f42242e5e182519388f01b9f25d824a9feb7cd17d8f863c8d776f38baf9"
             ],
             "index": "pypi",
-            "version": "==3.8.0"
+            "version": "==3.9.0"
         },
         "pytest-env": {
             "hashes": [
@@ -616,19 +753,19 @@
         },
         "pyyaml": {
             "hashes": [
-                "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
-                "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
-                "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
-                "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
-                "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
-                "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
-                "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
-                "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
-                "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
-                "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
-                "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
+                "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
+                "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
+                "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
+                "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
+                "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
+                "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
+                "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
+                "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
+                "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
+                "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
+                "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
             ],
-            "version": "==5.3"
+            "version": "==5.3.1"
         },
         "requests": {
             "hashes": [
@@ -639,11 +776,11 @@
         },
         "safety": {
             "hashes": [
-                "sha256:0a3a8a178a9c96242b224f033ee8d1d130c0448b0e6622d12deaf37f6c3b4e59",
-                "sha256:5059f3ffab3648330548ea9c7403405bbfaf085b11235770825d14c58f24cb78"
+                "sha256:05f77773bbab834502328b29ed013677aa53ed0c22b6e330aef7d2a7e1dfd838",
+                "sha256:3016631e0dd17193d6cf12e8ed1af92df399585e8ee0e4b1300d9e7e32b54903"
             ],
             "index": "pypi",
-            "version": "==1.8.5"
+            "version": "==1.8.7"
         },
         "six": {
             "hashes": [
@@ -673,6 +810,13 @@
             ],
             "version": "==1.3"
         },
+        "toml": {
+            "hashes": [
+                "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
+                "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
+            ],
+            "version": "==0.10.0"
+        },
         "urllib3": {
             "hashes": [
                 "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
@@ -680,12 +824,26 @@
             ],
             "version": "==1.25.8"
         },
+        "virtualenv": {
+            "hashes": [
+                "sha256:4e399f48c6b71228bf79f5febd27e3bbb753d9d5905776a86667bc61ab628a25",
+                "sha256:9e81279f4a9d16d1c0654a127c2c86e5bca2073585341691882c1e66e31ef8a5"
+            ],
+            "version": "==20.0.15"
+        },
+        "virtualenv-clone": {
+            "hashes": [
+                "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27",
+                "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29"
+            ],
+            "version": "==0.5.4"
+        },
         "wcwidth": {
             "hashes": [
-                "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603",
-                "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"
+                "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1",
+                "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"
             ],
-            "version": "==0.1.8"
+            "version": "==0.1.9"
         },
         "werkzeug": {
             "hashes": [
@@ -700,6 +858,7 @@
                 "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
                 "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
             ],
+            "markers": "python_version < '3.8'",
             "version": "==3.1.0"
         }
     }
diff --git a/internewshid/chn_spreadsheet/importer.py b/internewshid/chn_spreadsheet/importer.py
index 88cdb3b1..365fd8b5 100644
--- a/internewshid/chn_spreadsheet/importer.py
+++ b/internewshid/chn_spreadsheet/importer.py
@@ -1,4 +1,5 @@
 import datetime
+from collections import OrderedDict
 from decimal import Decimal
 
 from django.utils import six
@@ -61,14 +62,21 @@ class Importer(object):
         if first_row:
             col_map = self.get_columns_map()
 
-            for label in first_row[:len(col_map)]:
+            for label in first_row:
                 if label is not None:
+                    stripped_label = ''
                     try:
                         stripped_label = label.strip()
                         columns.append(col_map[stripped_label])
-                    except Exception:
-                        error_msg = _('Unknown column: {0}').format(label)
-                        raise SheetImportException(error_msg)
+                    except KeyError:
+                        # If the column isn't in the importer specification then save the data as a keyvalue.
+                        if stripped_label:
+                            col = OrderedDict([('field', 'values'), ('type', 'keyvalue'), ('name', stripped_label)])
+                            columns.append(col)
+
+                    except Exception as exception:
+                        # error_msg = _('Unknown column: {0}').format(label)
+                        raise SheetImportException(exception)
         else:
             columns = [d.copy() for d in profile_columns]
 
@@ -108,7 +116,6 @@ class Importer(object):
 
             except SheetImportException as e:
                 raise type(e)(str(e) + 'in row {0} '.format(i)) from e
-
         return objects
 
     def process_row(self, values, columns):
@@ -127,6 +134,11 @@ class Importer(object):
                 if value:
                     self._append_term_to_item(
                         item, 'tags', value.strip())
+            elif col['type'] == 'keyvalue':
+                key, value = converter.convert_value()
+                if value:
+                    self._append_keyvalue_to_item(
+                        item, key, value)
             else:
                 converter.add_to(item)
 
@@ -136,6 +148,10 @@ class Importer(object):
         term = self._get_term_dict(taxonomy, name)
         item.setdefault('terms', []).append(term)
 
+    def _append_keyvalue_to_item(self, item, key, value):
+        keyvalue = {'key': key, 'value': value}
+        item.setdefault('keyvalues', []).append(keyvalue)
+
     def _get_term_dict(self, taxonomy, name):
         return {'taxonomy': taxonomy, 'name': name}
 
@@ -145,11 +161,15 @@ class Importer(object):
         for obj in objects:
             row = obj.pop('_row_number', '')
             terms = obj.pop('terms', [])
+            keyvalues = obj.pop('keyvalues', [])
             try:
                 item = transport.items.create(obj)
                 for term in terms:
                     transport.items.add_terms(
                         item['id'], term['taxonomy'], term['name'])
+                for keyvalue in keyvalues:
+                    transport.items.add_keyvalue(
+                        item['id'], keyvalue['key'], keyvalue['value'])
             except ItemNotUniqueException:
                 pass
 
@@ -244,6 +264,7 @@ class CellConverter(object):
         self.value = value
         self.type = col_spec['type']
         self.field = col_spec['field']
+        self.name = col_spec.get('name')
         self.date_format = col_spec.get('date_format', None)
 
     def add_to(self, object_dict):
@@ -258,6 +279,7 @@ class CellConverter(object):
             'integer': lambda x: int(x),
             'number': lambda x: Decimal(x),
             'taxonomy': lambda x: x if x else '',
+            'keyvalue': lambda x: (self.name, x) if x else '',
             'protection_concern': lambda x: 'Protection Concern' if x.lower() == 'yes' else ''
         }
         if self.type not in converters:
diff --git a/internewshid/chn_spreadsheet/tests/importer_tests.py b/internewshid/chn_spreadsheet/tests/importer_tests.py
index f891e6a0..8707b8b7 100644
--- a/internewshid/chn_spreadsheet/tests/importer_tests.py
+++ b/internewshid/chn_spreadsheet/tests/importer_tests.py
@@ -139,6 +139,7 @@ def test_order_columns_with_first_row_return_first_row_order(importer):
     assert ordered == [cleaned[2], cleaned[0], cleaned[1]]
 
 
+@pytest.mark.skip("importing all columns")  # Now importing all missing columns as key value pairs
 def test_order_columns_ignores_extra_columns_in_first_row(importer):
     cleaned = _make_columns_row(COLUMN_LIST)
     first_row = ['Message', 'Province', 'Sub-Province', 'None', 'None', 'None']
@@ -148,6 +149,7 @@ def test_order_columns_ignores_extra_columns_in_first_row(importer):
     assert ordered == [cleaned[2], cleaned[0], cleaned[1]]
 
 
+@pytest.mark.skip("importing all columns")  # Now importing all missing columns as key value pairs
 def test_order_columns_ignores_none_and_missing_columns_in_first_row(importer):
     first_row = ['Province', None]
 
diff --git a/internewshid/chn_spreadsheet/tests/key_values_tests.py b/internewshid/chn_spreadsheet/tests/key_values_tests.py
new file mode 100644
index 00000000..c65aa90f
--- /dev/null
+++ b/internewshid/chn_spreadsheet/tests/key_values_tests.py
@@ -0,0 +1,42 @@
+import datetime
+from os import path
+
+from django.core.management import call_command
+
+import pytest
+
+import transport
+from chn_spreadsheet.tests.conftest import taxonomies  # noqa
+
+TEST_BASE_DIR = path.abspath(path.dirname(__file__))
+TEST_DIR = path.join(TEST_BASE_DIR, 'test_files')
+
+
+@pytest.fixture
+def django_db_setup(django_db_setup, django_db_blocker):
+    with django_db_blocker.unblock():
+        call_command('loaddata', 'spreadsheet-profiles.json')
+
+
+@pytest.mark.django_db  # noqa
+def test_kobo_keyvalue_imported(importer, django_db_setup, taxonomies):  # noqa
+    assert len(transport.items.list_items()['results']) == 0
+
+    file_path = path.join(TEST_DIR, 'sample_kobo_keyValues.xlsx')
+    (num_saved, _) = importer.store_spreadsheet('kobo', open(file_path, 'rb'))
+
+    assert num_saved > 0
+
+    items = transport.items.list_items()['results']
+    assert len(items) == num_saved
+
+    # default ordering is by timestamp desc
+    assert items[2]['body'] == 'the community members want more food.'
+    assert items[2]['translation'] == ''
+    assert items[2]['location'] == 'Camp 4'
+    assert items[2]['language'] == 'English'
+    assert items[2]['risk'] == '1'
+    assert items[2]['values']['KV1'] == 'one'
+    assert items[1]['values']['KV1'] == '2020-12-12 00:00:00'
+    assert items[0]['values']['KV1'] == '123'
+    assert isinstance(items[2]['timestamp'], datetime.datetime)
diff --git a/internewshid/chn_spreadsheet/tests/test_files/sample_kobo_keyValues.xlsx b/internewshid/chn_spreadsheet/tests/test_files/sample_kobo_keyValues.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..15e8d2d321f6b57ac5edb851c829673fa65fbe7f
GIT binary patch
literal 6235
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
Mo_pZC1p>nV03sB5w*UYD

literal 0
HcmV?d00001

diff --git a/internewshid/data_layer/migrations/0027_key_value.py b/internewshid/data_layer/migrations/0027_key_value.py
new file mode 100644
index 00000000..f737a852
--- /dev/null
+++ b/internewshid/data_layer/migrations/0027_key_value.py
@@ -0,0 +1,30 @@
+# Generated by Django 2.2.11 on 2020-03-26 16:14
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('data_layer', '0026_message_risk'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Key',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('key', models.CharField(db_index=True, help_text='Key', max_length=190, unique=True, verbose_name='Key')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Value',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('value', models.CharField(help_text='Value', max_length=190, verbose_name='Value')),
+                ('key', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='data_layer.Key', verbose_name='Key')),
+                ('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='values', to='data_layer.Message', verbose_name='Message')),
+            ],
+        ),
+    ]
diff --git a/internewshid/data_layer/models.py b/internewshid/data_layer/models.py
index 7d21935f..2edfb50a 100644
--- a/internewshid/data_layer/models.py
+++ b/internewshid/data_layer/models.py
@@ -14,6 +14,45 @@ except ImportError:
     raise ImproperlyConfigured('Could not import django-picklefield.')
 
 
+class Key(models.Model):
+
+    key = models.CharField(
+        verbose_name=_('Key'),
+        max_length=190,
+        help_text=_('Key'),
+        unique=True,
+        db_index=True,
+    )
+
+    def __str__(self):
+        return self.key
+
+
+class Value(models.Model):
+
+    value = models.CharField(
+        verbose_name=_('Value'),
+        max_length=190,
+        help_text=_('Value'),
+    )
+
+    key = models.ForeignKey(
+        Key,
+        verbose_name=_('Key'),
+        on_delete=models.CASCADE
+    )
+
+    message = models.ForeignKey(
+        "Message",
+        related_name='values',
+        verbose_name=_('Message'),
+        on_delete=models.CASCADE
+    )
+
+    def __str__(self):
+        return self.value
+
+
 class DataLayerModel(models.Model):
     created = models.DateTimeField(auto_now_add=True)
     last_modified = models.DateTimeField(auto_now=True)
diff --git a/internewshid/hid/tests/add_edit_item_view_tests.py b/internewshid/hid/tests/add_edit_item_view_tests.py
index ed1114eb..d5eb246f 100644
--- a/internewshid/hid/tests/add_edit_item_view_tests.py
+++ b/internewshid/hid/tests/add_edit_item_view_tests.py
@@ -662,7 +662,7 @@ def test_item_can_be_deleted_with_post_request(item):
 @pytest.mark.django_db
 def test_item_can_be_updated(view, update_form):
     new_text = "What is the cause of Ebola?"
-    update_form.cleaned_data['body'] = new_text,
+    update_form.cleaned_data['body'] = new_text
 
     view.form_valid(update_form)
     item = transport.items.get(view.item['id'])
diff --git a/internewshid/rest_api/serializers.py b/internewshid/rest_api/serializers.py
index 8109d7f9..3cfacc16 100644
--- a/internewshid/rest_api/serializers.py
+++ b/internewshid/rest_api/serializers.py
@@ -79,6 +79,10 @@ class ItemSerializer(serializers.ModelSerializer):
 
     timestamp = IgnoreMicrosecondsDateTimeField()
     terms = TermSerializer(many=True, required=False)
+    values = serializers.SerializerMethodField()
+
+    def get_values(self, item):
+        return {kv.key.key: kv.value for kv in item.values.all()}
 
     def create(self, validated_data):
         """ Create an item with nested metadata terms."""
diff --git a/internewshid/rest_api/views.py b/internewshid/rest_api/views.py
index bb17428b..3d380834 100644
--- a/internewshid/rest_api/views.py
+++ b/internewshid/rest_api/views.py
@@ -12,7 +12,7 @@ from rest_framework.response import Response
 from rest_framework_bulk.mixins import BulkDestroyModelMixin
 from rest_pandas import PandasView
 
-from data_layer.models import Item
+from data_layer.models import Item, Key, Value
 from taxonomies.models import Taxonomy, Term
 
 from .serializers import (
@@ -183,6 +183,21 @@ class ItemViewSet(viewsets.ModelViewSet, BulkDestroyModelMixin):
         serializer = ItemSerializer(item)
         return Response(serializer.data, status=status.HTTP_200_OK)
 
+    @action(methods=['post'], detail=True)
+    def add_keyvalue(self, request, item_pk):
+        try:
+            item = Item.objects.get(pk=item_pk)
+        except Item.DoesNotExist as e:
+            data = {'detail': str(e)}
+            return Response(data, status=status.HTTP_404_NOT_FOUND)
+
+        keyvalue = request.data
+        key, _ = Key.objects.get_or_create(key=keyvalue['key'])
+        value = Value.objects.create(key=key, value=keyvalue['value'], message=item)
+
+        serializer = ItemSerializer(item)
+        return Response(serializer.data, status=status.HTTP_200_OK)
+
     @action(methods=['post'], detail=True)
     def delete_all_terms(self, request, item_pk):
         taxonomy_slug = request.data['taxonomy']
diff --git a/internewshid/transport/items.py b/internewshid/transport/items.py
index dfc2845f..d45cb1d2 100644
--- a/internewshid/transport/items.py
+++ b/internewshid/transport/items.py
@@ -111,7 +111,7 @@ def create(item):
 def update(id, item):
     """ Update an Item from the given dict """
     view = get_view({'put': 'update'})
-    request = request_factory.put("", item)
+    request = request_factory.put("", item, format='json')
     response = view(request, pk=id)
     if status.is_success(response.status_code):
         return response.data
@@ -180,13 +180,27 @@ def add_terms(item_id, taxonomy_slug, names):
 
     if status.is_success(response.status_code):
         return response.data
-    else:
-        response.data['status_code'] = response.status_code
-        response.data['terms'] = terms
-        response.data['item_id'] = item_id
-        raise TransportException(response.data)
 
-    return response.data
+    response.data['status_code'] = response.status_code
+    response.data['terms'] = terms
+    response.data['item_id'] = item_id
+    raise TransportException(response.data)
+
+
+def add_keyvalue(item_id, key, value):
+    view = get_view({'post': 'add_keyvalue'})
+
+    keyvalue = {'key': key, 'value': value}
+    request = request_factory.post('', keyvalue)
+    response = view(request, item_pk=item_id)
+
+    if status.is_success(response.status_code):
+        return response.data
+
+    response.data['status_code'] = response.status_code
+    response.data['value'] = keyvalue
+    response.data['item_id'] = item_id
+    raise TransportException(response.data)
 
 
 def delete_all_terms(item_id, taxonomy_slug):
-- 
GitLab