diff --git a/.gitignore b/.gitignore
index e2717850b1e14a3e6bf870e52dbf753f02d810e0..5c7f6e2f05b1ec6593ece36bd402e0f5aa34bae9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ TAGS
 *.~lock.*#
 .python-version
 .ve
+.ignore
diff --git a/Pipfile b/Pipfile
index bf7e578b872016e2b1beb336ef1026a250028f30..7229b938d2a8c1b4f263b8b10a97c27ddfcd57f0 100644
--- a/Pipfile
+++ b/Pipfile
@@ -50,5 +50,4 @@ pytest-env = "*"
 pytest-pythonpath = "*"
 werkzeug = "*"
 pydocstyle = "*"
-django-dynamic-fixture = "*"
 safety = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index f9657a09e3483d086b3600a5b9e6b19b4b6b847b..c55eaec70fd461c3a0f85f8e3290677aa417225c 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "ca4eab6f51db983f5d27b4bd604989a90057dfd18a125558273c13e19b610f61"
+            "sha256": "9975a310b8c6f4b3229a1a36af6da58959cf2401628b5a671203204203094894"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -43,6 +43,7 @@
                 "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760",
                 "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"
             ],
+            "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'",
             "version": "==4.4.2"
         },
         "django": {
@@ -51,6 +52,7 @@
                 "sha256:6ecd229e1815d4fc5240fc98f1cca78c41e7a8cd3e3f2eefadc4735031077916"
             ],
             "index": "pypi",
+            "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.3.*'",
             "version": "==2.2.12"
         },
         "django-assets": {
@@ -63,11 +65,11 @@
         },
         "django-bootstrap3": {
             "hashes": [
-                "sha256:760f8664a15f9e1e6e0495265cebd3d4c65b1026e93e476e8322f10e1375f0b8",
-                "sha256:d161d23824fc2f43c18449285961b994fc3fe620d782b13240e382a7b233344a"
+                "sha256:0c5a3d7150069843ed0006d4d9cae16ea4624402aa3d569cddb4c3979642a055",
+                "sha256:4b495d2135ac3152367d2ddd0ce75dd861a9a0bd9a34cfdb1a33e612e17984e7"
             ],
             "index": "pypi",
-            "version": "==12.0.3"
+            "version": "==12.1.0"
         },
         "django-braces": {
             "hashes": [
@@ -78,13 +80,11 @@
             "version": "==1.14.0"
         },
         "django-constance": {
-            "extras": [
-                "database"
-            ],
             "hashes": [
                 "sha256:12d827f9d5552ee39884fb6fb356f231f32b1ab8958acc715e3d1a6ecf913653"
             ],
             "index": "pypi",
+            "markers": null,
             "version": "==2.6.0"
         },
         "django-debug-toolbar": {
@@ -112,11 +112,11 @@
         },
         "django-floppyforms": {
             "hashes": [
-                "sha256:5fc0151bf3000a831ce6d4931a5969993dc53ba198d7bbb9b8ac86ab4fe86dc4",
-                "sha256:8a9f0aa04f02c63a3cd33f4201295c7e938b378e2b9ba27f9e62db936d32e114"
+                "sha256:5b3f6891463149b83c44d2bd6109ce32de3d848e2ff7dbc02c41c65562ddf633",
+                "sha256:d620f1ed7ae3fc0377803e5ba2df4afb2d4cdda7b13fdf0a71840da0b5e5aae8"
             ],
             "index": "pypi",
-            "version": "==1.8.0"
+            "version": "==1.9.0"
         },
         "django-form-utils": {
             "hashes": [
@@ -125,11 +125,6 @@
             "index": "pypi",
             "version": "==1.0.3"
         },
-        "django-js": {
-            "editable": true,
-            "git": "https://git@github.com/aptivate/django.js.git",
-            "ref": "ca328a94b00023bd64f4fc1c908675edaaf2ac19"
-        },
         "django-jsoneditor": {
             "hashes": [
                 "sha256:574f33a60271f464cad5739b6f4718aca81a779188297faa5adab41754cac9ba",
@@ -196,10 +191,11 @@
         },
         "ipython": {
             "hashes": [
-                "sha256:ca478e52ae1f88da0102360e57e528b92f3ae4316aabac80a2cd7f7ab2efb48a",
-                "sha256:eb8d075de37f678424527b5ef6ea23f7b80240ca031c2dd6de5879d687a65333"
+                "sha256:5b241b84bbf0eb085d43ae9d46adf38a13b45929ca7774a740990c2c242534bb",
+                "sha256:f0126781d0f959da852fb3089e170ed807388e986a8dd4e6ac44855845b0fb1c"
             ],
-            "version": "==7.13.0"
+            "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.3.*'",
+            "version": "==7.14.0"
         },
         "ipython-genutils": {
             "hashes": [
@@ -217,10 +213,11 @@
         },
         "jedi": {
             "hashes": [
-                "sha256:b4f4052551025c6b0b0b193b29a6ff7bdb74c52450631206c262aef9f7159ad2",
-                "sha256:d5c871cb9360b414f981e7072c52c33258d598305280fef91c6cae34739d65d5"
+                "sha256:cd60c93b71944d628ccac47df9a60fec53150de53d42dc10a7fc4b5ba6aae798",
+                "sha256:df40c97641cb943661d2db4c33c2e1ff75d491189423249e989bcea4464f3030"
             ],
-            "version": "==0.16.0"
+            "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.3.*'",
+            "version": "==0.17.0"
         },
         "jsmin": {
             "hashes": [
@@ -257,29 +254,30 @@
         },
         "numpy": {
             "hashes": [
-                "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"
+                "sha256:00d7b54c025601e28f468953d065b9b121ddca7fff30bed7be082d3656dd798d",
+                "sha256:02ec9582808c4e48be4e93cd629c855e644882faf704bc2bd6bbf58c08a2a897",
+                "sha256:0e6f72f7bb08f2f350ed4408bb7acdc0daba637e73bce9f5ea2b207039f3af88",
+                "sha256:1be2e96314a66f5f1ce7764274327fd4fb9da58584eaff00b5a5221edefee7d6",
+                "sha256:2466fbcf23711ebc5daa61d28ced319a6159b260a18839993d871096d66b93f7",
+                "sha256:2b573fcf6f9863ce746e4ad00ac18a948978bb3781cffa4305134d31801f3e26",
+                "sha256:3f0dae97e1126f529ebb66f3c63514a0f72a177b90d56e4bce8a0b5def34627a",
+                "sha256:50fb72bcbc2cf11e066579cb53c4ca8ac0227abb512b6cbc1faa02d1595a2a5d",
+                "sha256:57aea170fb23b1fd54fa537359d90d383d9bf5937ee54ae8045a723caa5e0961",
+                "sha256:709c2999b6bd36cdaf85cf888d8512da7433529f14a3689d6e37ab5242e7add5",
+                "sha256:7d59f21e43bbfd9a10953a7e26b35b6849d888fc5a331fa84a2d9c37bd9fe2a2",
+                "sha256:904b513ab8fbcbdb062bed1ce2f794ab20208a1b01ce9bd90776c6c7e7257032",
+                "sha256:96dd36f5cdde152fd6977d1bbc0f0561bccffecfde63cd397c8e6033eb66baba",
+                "sha256:9933b81fecbe935e6a7dc89cbd2b99fea1bf362f2790daf9422a7bb1dc3c3085",
+                "sha256:bbcc85aaf4cd84ba057decaead058f43191cc0e30d6bc5d44fe336dc3d3f4509",
+                "sha256:dccd380d8e025c867ddcb2f84b439722cf1f23f3a319381eac45fd077dee7170",
+                "sha256:e22cd0f72fc931d6abc69dc7764484ee20c6a60b0d0fee9ce0426029b1c1bdae",
+                "sha256:ed722aefb0ebffd10b32e67f48e8ac4c5c4cf5d3a785024fdf0e9eb17529cd9d",
+                "sha256:efb7ac5572c9a57159cf92c508aad9f856f1cb8e8302d7fdb99061dbe52d712c",
+                "sha256:efdba339fffb0e80fcc19524e4fdbda2e2b5772ea46720c44eaac28096d60720",
+                "sha256:f22273dd6a403ed870207b853a856ff6327d5cbce7a835dfa0645b3fc00273ec"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==1.18.4"
         },
         "openpyxl": {
             "hashes": [
@@ -290,10 +288,11 @@
         },
         "packaging": {
             "hashes": [
-                "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
-                "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
+                "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
+                "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
             ],
-            "version": "==20.3"
+            "markers": "python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.2.*'",
+            "version": "==20.4"
         },
         "pandas": {
             "hashes": [
@@ -314,14 +313,16 @@
                 "sha256:ca84a44cf727f211752e91eab2d1c6c1ab0f0540d5636a8382a3af428542826e",
                 "sha256:d234bcf669e8b4d6cbcd99e3ce7a8918414520aeb113e2a81aeb02d0a533d7f7"
             ],
+            "markers": "python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.2.*' and python_version != '3.1.*'",
             "version": "==1.0.3"
         },
         "parso": {
             "hashes": [
-                "sha256:0c5659e0c6eba20636f99a04f469798dca8da279645ce5c387315b2c23912157",
-                "sha256:8515fc12cfca6ee3aa59138741fc5624d62340c97e401c74875769948d4f2995"
+                "sha256:158c140fc04112dc45bca311633ae5033c2c2a7b732fa33d0955bad8152a8dd0",
+                "sha256:908e9fae2144a076d72ae4e25539143d40b8e3eafbaeae03c1bfe226f4cdf12c"
             ],
-            "version": "==0.6.2"
+            "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.3.*'",
+            "version": "==0.7.0"
         },
         "pexpect": {
             "hashes": [
@@ -340,31 +341,32 @@
         },
         "pillow": {
             "hashes": [
-                "sha256:04a10558320eba9137d6a78ca6fc8f4a5801f1b971152938851dc4629d903579",
-                "sha256:0f89ddc77cf421b8cd34ae852309501458942bf370831b4a9b406156b599a14e",
-                "sha256:251e5618125ec12ac800265d7048f5857a8f8f1979db9ea3e11382e159d17f68",
-                "sha256:291bad7097b06d648222b769bbfcd61e40d0abdfe10df686d20ede36eb8162b6",
-                "sha256:2f0b52a08d175f10c8ea36685115681a484c55d24d0933f9fd911e4111c04144",
-                "sha256:3713386d1e9e79cea1c5e6aaac042841d7eef838cc577a3ca153c8bedf570287",
-                "sha256:433bbc2469a2351bea53666d97bb1eb30f0d56461735be02ea6b27654569f80f",
-                "sha256:4510c6b33277970b1af83c987277f9a08ec2b02cc20ac0f9234e4026136bb137",
-                "sha256:50a10b048f4dd81c092adad99fa5f7ba941edaf2f9590510109ac2a15e706695",
-                "sha256:670e58d3643971f4afd79191abd21623761c2ebe61db1c2cb4797d817c4ba1a7",
-                "sha256:6c1924ed7dbc6ad0636907693bbbdd3fdae1d73072963e71f5644b864bb10b4d",
-                "sha256:721c04d3c77c38086f1f95d1cd8df87f2f9a505a780acf8575912b3206479da1",
-                "sha256:8d5799243050c2833c2662b824dfb16aa98e408d2092805edea4300a408490e7",
-                "sha256:90cd441a1638ae176eab4d8b6b94ab4ec24b212ed4c3fbee2a6e74672481d4f8",
-                "sha256:a5dc9f28c0239ec2742d4273bd85b2aa84655be2564db7ad1eb8f64b1efcdc4c",
-                "sha256:b2f3e8cc52ecd259b94ca880fea0d15f4ebc6da2cd3db515389bb878d800270f",
-                "sha256:b7453750cf911785009423789d2e4e5393aae9cbb8b3f471dab854b85a26cb89",
-                "sha256:b99b2607b6cd58396f363b448cbe71d3c35e28f03e442ab00806463439629c2c",
-                "sha256:cd47793f7bc9285a88c2b5551d3f16a2ddd005789614a34c5f4a598c2a162383",
-                "sha256:d6bf085f6f9ec6a1724c187083b37b58a8048f86036d42d21802ed5d1fae4853",
-                "sha256:da737ab273f4d60ae552f82ad83f7cbd0e173ca30ca20b160f708c92742ee212",
-                "sha256:eb84e7e5b07ff3725ab05977ac56d5eeb0c510795aeb48e8b691491be3c5745b"
-            ],
-            "index": "pypi",
-            "version": "==7.1.1"
+                "sha256:04766c4930c174b46fd72d450674612ab44cca977ebbcc2dde722c6933290107",
+                "sha256:0e2a3bceb0fd4e0cb17192ae506d5f082b309ffe5fc370a5667959c9b2f85fa3",
+                "sha256:0f01e63c34f0e1e2580cc0b24e86a5ccbbfa8830909a52ee17624c4193224cd9",
+                "sha256:12e4bad6bddd8546a2f9771485c7e3d2b546b458ae8ff79621214119ac244523",
+                "sha256:1f694e28c169655c50bb89a3fa07f3b854d71eb47f50783621de813979ba87f3",
+                "sha256:3d25dd8d688f7318dca6d8cd4f962a360ee40346c15893ae3b95c061cdbc4079",
+                "sha256:4b02b9c27fad2054932e89f39703646d0c543f21d3cc5b8e05434215121c28cd",
+                "sha256:9744350687459234867cbebfe9df8f35ef9e1538f3e729adbd8fde0761adb705",
+                "sha256:a0b49960110bc6ff5fead46013bcb8825d101026d466f3a4de3476defe0fb0dd",
+                "sha256:ae2b270f9a0b8822b98655cb3a59cdb1bd54a34807c6c56b76dd2e786c3b7db3",
+                "sha256:b37bb3bd35edf53125b0ff257822afa6962649995cbdfde2791ddb62b239f891",
+                "sha256:b532bcc2f008e96fd9241177ec580829dee817b090532f43e54074ecffdcd97f",
+                "sha256:b67a6c47ed963c709ed24566daa3f95a18f07d3831334da570c71da53d97d088",
+                "sha256:b943e71c2065ade6fef223358e56c167fc6ce31c50bc7a02dd5c17ee4338e8ac",
+                "sha256:ccc9ad2460eb5bee5642eaf75a0438d7f8887d484490d5117b98edd7f33118b7",
+                "sha256:d23e2aa9b969cf9c26edfb4b56307792b8b374202810bd949effd1c6e11ebd6d",
+                "sha256:eaa83729eab9c60884f362ada982d3a06beaa6cc8b084cf9f76cae7739481dfa",
+                "sha256:ee94fce8d003ac9fd206496f2707efe9eadcb278d94c271f129ab36aa7181344",
+                "sha256:f455efb7a98557412dc6f8e463c1faf1f1911ec2432059fa3e582b6000fc90e2",
+                "sha256:f46e0e024346e1474083c729d50de909974237c72daca05393ee32389dabe457",
+                "sha256:f54be399340aa602066adb63a86a6a5d4f395adfdd9da2b9a0162ea808c7b276",
+                "sha256:f784aad988f12c80aacfa5b381ec21fd3f38f851720f652b9f33facc5101cf4d"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.5'",
+            "version": "==7.1.2"
         },
         "prompt-toolkit": {
             "hashes": [
@@ -385,6 +387,7 @@
                 "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44",
                 "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"
             ],
+            "markers": "python_version >= '3.5'",
             "version": "==2.6.1"
         },
         "pyparsing": {
@@ -392,6 +395,7 @@
                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
             ],
+            "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6' and python_version != '3.2.*'",
             "version": "==2.4.7"
         },
         "python-dateutil": {
@@ -404,11 +408,11 @@
         },
         "pytz": {
             "hashes": [
-                "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
-                "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
+                "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
+                "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
             ],
             "index": "pypi",
-            "version": "==2019.3"
+            "version": "==2020.1"
         },
         "rest-pandas": {
             "hashes": [
@@ -420,16 +424,18 @@
         },
         "six": {
             "hashes": [
-                "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
-                "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
+                "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
+                "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
             ],
-            "version": "==1.14.0"
+            "markers": "python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.2.*'",
+            "version": "==1.15.0"
         },
         "sqlparse": {
             "hashes": [
                 "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
                 "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
             ],
+            "markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version >= '2.7'",
             "version": "==0.3.1"
         },
         "traitlets": {
@@ -437,6 +443,7 @@
                 "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44",
                 "sha256:d023ee369ddd2763310e4c3eae1ff649689440d4ae59d7485eb4cfbbe3e359f7"
             ],
+            "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6'",
             "version": "==4.3.3"
         },
         "wcwidth": {
@@ -455,13 +462,6 @@
         }
     },
     "develop": {
-        "appdirs": {
-            "hashes": [
-                "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
-                "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
-            ],
-            "version": "==1.4.3"
-        },
         "asgiref": {
             "hashes": [
                 "sha256:8036f90603c54e93521e5777b2b9a39ba1bad05773fcf2d208f0299d1df58ce5",
@@ -475,6 +475,7 @@
                 "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
                 "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
             ],
+            "markers": "python_version != '3.2.*' and python_version != '3.3.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7'",
             "version": "==19.3.0"
         },
         "certifi": {
@@ -493,52 +494,48 @@
         },
         "click": {
             "hashes": [
-                "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
-                "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
+                "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
+                "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
             ],
-            "version": "==7.1.1"
+            "markers": "python_version != '3.0.*' and python_version != '3.4.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version >= '2.7'",
+            "version": "==7.1.2"
         },
         "coverage": {
             "hashes": [
-                "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"
+                "sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
+                "sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
+                "sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
+                "sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
+                "sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
+                "sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
+                "sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
+                "sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
+                "sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
+                "sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
+                "sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
+                "sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
+                "sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
+                "sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
+                "sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
+                "sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
+                "sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
+                "sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
+                "sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
+                "sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
+                "sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
+                "sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
+                "sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
+                "sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
+                "sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
+                "sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
+                "sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
+                "sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
+                "sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
+                "sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
+                "sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
+            ],
+            "markers": "python_version != '3.0.*' and python_version != '3.4.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version < '4' and python_version >= '2.7'",
+            "version": "==5.1"
         },
         "django": {
             "hashes": [
@@ -546,6 +543,7 @@
                 "sha256:6ecd229e1815d4fc5240fc98f1cca78c41e7a8cd3e3f2eefadc4735031077916"
             ],
             "index": "pypi",
+            "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7' and python_version != '3.2.*' and python_version != '3.3.*'",
             "version": "==2.2.12"
         },
         "django-debug-toolbar": {
@@ -555,13 +553,6 @@
             ],
             "version": "==2.2"
         },
-        "django-dynamic-fixture": {
-            "hashes": [
-                "sha256:e772102ba40f70c2e66470cae85f3874aa992e6b22a0d0c360450f2949b0728d"
-            ],
-            "index": "pypi",
-            "version": "==3.1.0"
-        },
         "django-extensions": {
             "hashes": [
                 "sha256:2f81b618ba4d1b0e58603e25012e5c74f88a4b706e0022a3b21f24f0322a6ce6",
@@ -572,38 +563,34 @@
         },
         "dparse": {
             "hashes": [
-                "sha256:14fed5efc5e98c0a81dfe100c4c2ea0a4c189104e9a9d18b5cfd342a163f97be",
-                "sha256:db349e53f6d03c8ee80606c49b35f515ed2ab287a8e1579e2b4bdf52b12b1530"
+                "sha256:a1b5f169102e1c894f9a7d5ccf6f9402a836a5d24be80a986c7ce9eaed78f367",
+                "sha256:e953a25e44ebb60a5c6efc2add4420c177f1d8404509da88da9729202f306994"
             ],
-            "version": "==0.5.0"
+            "markers": "python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.2.*'",
+            "version": "==0.5.1"
         },
         "factory-boy": {
             "hashes": [
                 "sha256:728df59b372c9588b83153facf26d3d28947fc750e8e3c95cefa9bed0e6394ee",
                 "sha256:faf48d608a1735f0d0a3c9cbf536d64f9132b547dae7ba452c4d99a79e84a370"
             ],
-            "index": "pypi",
+            "markers": "python_version != '3.2.*' and python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.7'",
             "version": "==2.12.0"
         },
         "faker": {
             "hashes": [
-                "sha256:2d3f866ef25e1a5af80e7b0ceeacc3c92dec5d0fdbad3e2cb6adf6e60b22188f",
-                "sha256:b89aa33837498498e15c709eb40c31386408a901a53c7a5e12a425737a767976"
-            ],
-            "version": "==4.0.2"
-        },
-        "filelock": {
-            "hashes": [
-                "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59",
-                "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"
+                "sha256:103c46b9701a151299c5bffe6fefcd4fb5fb04c3b5d06bee4952d36255d44ea2",
+                "sha256:34ae397aef03a0a17910452f1e8430d57fa59e2d67b20e9b637218e8f7dd22b3"
             ],
-            "version": "==3.0.12"
+            "markers": "python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.2.*'",
+            "version": "==4.1.0"
         },
         "idna": {
             "hashes": [
                 "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb",
                 "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"
             ],
+            "markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version >= '2.7'",
             "version": "==2.9"
         },
         "importlib-metadata": {
@@ -614,14 +601,6 @@
             "markers": "python_version < '3.8'",
             "version": "==1.6.0"
         },
-        "importlib-resources": {
-            "hashes": [
-                "sha256:4019b6a9082d8ada9def02bece4a76b131518866790d58fdda0b5f8c603b36c2",
-                "sha256:dd98ceeef3f5ad2ef4cc287b8586da4ebad15877f351e9688987ad663a0a29b8"
-            ],
-            "markers": "python_version < '3.7'",
-            "version": "==1.4.0"
-        },
         "isort": {
             "hashes": [
                 "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
@@ -647,31 +626,26 @@
         },
         "more-itertools": {
             "hashes": [
-                "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c",
-                "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"
+                "sha256:558bb897a2232f5e4f8e2399089e35aecb746e1f9191b6584a151647e89267be",
+                "sha256:7818f596b1e87be009031c7653d01acc46ed422e6656b394b0f765ce66ed4982"
             ],
-            "version": "==8.2.0"
+            "markers": "python_version >= '3.5'",
+            "version": "==8.3.0"
         },
         "packaging": {
             "hashes": [
-                "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
-                "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
-            ],
-            "version": "==20.3"
-        },
-        "pipenv": {
-            "hashes": [
-                "sha256:56ad5f5cb48f1e58878e14525a6e3129d4306049cb76d2f6a3e95df0d5fc6330",
-                "sha256:7df8e33a2387de6f537836f48ac6fcd94eda6ed9ba3d5e3fd52e35b5bc7ff49e",
-                "sha256:a673e606e8452185e9817a987572b55360f4d28b50831ef3b42ac3cab3fee846"
+                "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
+                "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
             ],
-            "version": "==2018.11.26"
+            "markers": "python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.2.*'",
+            "version": "==20.4"
         },
         "pluggy": {
             "hashes": [
                 "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
                 "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
             ],
+            "markers": "python_version >= '3.5'",
             "version": "==0.13.1"
         },
         "py": {
@@ -684,10 +658,11 @@
         },
         "pycodestyle": {
             "hashes": [
-                "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
-                "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
+                "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
+                "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
             ],
-            "version": "==2.5.0"
+            "markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version >= '2.7'",
+            "version": "==2.6.0"
         },
         "pydocstyle": {
             "hashes": [
@@ -699,10 +674,11 @@
         },
         "pyflakes": {
             "hashes": [
-                "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
-                "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
+                "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
+                "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
             ],
-            "version": "==2.1.1"
+            "markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version >= '2.7'",
+            "version": "==2.2.0"
         },
         "pylava": {
             "hashes": [
@@ -717,22 +693,24 @@
                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
             ],
+            "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version >= '2.6' and python_version != '3.2.*'",
             "version": "==2.4.7"
         },
         "pytest": {
             "hashes": [
-                "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172",
-                "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"
+                "sha256:95c710d0a72d91c13fae35dce195633c929c3792f54125919847fdcdf7caa0d3",
+                "sha256:eb2b5e935f6a019317e455b6da83dd8650ac9ffd2ee73a7b657a30873d67a698"
             ],
-            "version": "==5.4.1"
+            "markers": "python_version >= '3.6'",
+            "version": "==5.4.2"
         },
         "pytest-cov": {
             "hashes": [
-                "sha256:cc6742d8bac45070217169f5f72ceee1e0e55b0221f54bcf24845972d3a47f2b",
-                "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"
+                "sha256:b6a814b8ed6247bd81ff47f038511b57fe1ce7f4cc25b9106f1a4b106f1d9322",
+                "sha256:c87dfd8465d865655a8213859f1b4749b43448b5fae465cb981e16d52a811424"
             ],
             "index": "pypi",
-            "version": "==2.8.1"
+            "version": "==2.9.0"
         },
         "pytest-django": {
             "hashes": [
@@ -766,11 +744,11 @@
         },
         "pytz": {
             "hashes": [
-                "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
-                "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
+                "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
+                "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
             ],
             "index": "pypi",
-            "version": "==2019.3"
+            "version": "==2020.1"
         },
         "pyyaml": {
             "hashes": [
@@ -793,22 +771,24 @@
                 "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee",
                 "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"
             ],
+            "markers": "python_version != '3.0.*' and python_version != '3.4.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version < '4' and python_version >= '2.7'",
             "version": "==2.23.0"
         },
         "safety": {
             "hashes": [
-                "sha256:05f77773bbab834502328b29ed013677aa53ed0c22b6e330aef7d2a7e1dfd838",
-                "sha256:3016631e0dd17193d6cf12e8ed1af92df399585e8ee0e4b1300d9e7e32b54903"
+                "sha256:23bf20690d4400edc795836b0c983c2b4cbbb922233108ff925b7dd7750f00c9",
+                "sha256:86c1c4a031fe35bd624fce143fbe642a0234d29f7cbf7a9aa269f244a955b087"
             ],
             "index": "pypi",
-            "version": "==1.8.7"
+            "version": "==1.9.0"
         },
         "six": {
             "hashes": [
-                "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
-                "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
+                "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
+                "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
             ],
-            "version": "==1.14.0"
+            "markers": "python_version != '3.0.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.2.*'",
+            "version": "==1.15.0"
         },
         "snowballstemmer": {
             "hashes": [
@@ -822,6 +802,7 @@
                 "sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e",
                 "sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548"
             ],
+            "markers": "python_version != '3.0.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version >= '2.7'",
             "version": "==0.3.1"
         },
         "text-unidecode": {
@@ -833,31 +814,18 @@
         },
         "toml": {
             "hashes": [
-                "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
-                "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
+                "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
+                "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
             ],
-            "version": "==0.10.0"
+            "version": "==0.10.1"
         },
         "urllib3": {
             "hashes": [
-                "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
-                "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
-            ],
-            "version": "==1.25.8"
-        },
-        "virtualenv": {
-            "hashes": [
-                "sha256:6ea131d41c477f6c4b7863948a9a54f7fa196854dbef73efbdff32b509f4d8bf",
-                "sha256:94f647e12d1e6ced2541b93215e51752aecbd1bbb18eb1816e2867f7532b1fe1"
+                "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527",
+                "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"
             ],
-            "version": "==20.0.16"
-        },
-        "virtualenv-clone": {
-            "hashes": [
-                "sha256:07e74418b7cc64f4fda987bf5bc71ebd59af27a7bc9e8a8ee9fd54b1f2390a27",
-                "sha256:665e48dd54c84b98b71a657acb49104c54e7652bce9c1c4f6c6976ed4c827a29"
-            ],
-            "version": "==0.5.4"
+            "markers": "python_version != '3.0.*' and python_version != '3.4.*' and python_version != '3.3.*' and python_version != '3.2.*' and python_version != '3.1.*' and python_version < '4' and python_version >= '2.7'",
+            "version": "==1.25.9"
         },
         "wcwidth": {
             "hashes": [
@@ -879,7 +847,7 @@
                 "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
                 "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
             ],
-            "markers": "python_version < '3.8'",
+            "markers": "python_version >= '3.6'",
             "version": "==3.1.0"
         }
     }
diff --git a/README.md b/README.md
index 48848eff95df0f1e8cbe0dc9278ba6ab05924570..ec59890d6c1d15a9ef004230be5b585335286ff5 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
+# Internews Humanitarian Information Dashboard
+
 Master:
   * [![pipeline status](https://git.coop/aptivate/internewshid/badges/master/pipeline.svg)](https://git.coop/aptivate/internewshid/commits/master)
   * [![coverage report](https://git.coop/aptivate/internewshid/badges/master/coverage.svg)](https://git.coop/aptivate/internewshid/commits/master)
@@ -5,7 +7,3 @@ Master:
 Staging:
   * [![pipeline status](https://git.coop/aptivate/internewshid/badges/staging/pipeline.svg)](https://git.coop/aptivate/internewshid/commits/staging)
   * [![coverage report](https://git.coop/aptivate/internewshid/badges/staging/coverage.svg)](https://git.coop/aptivate/internewshid/commits/staging)
-
-# Internews Humanitarian Information Dashboard
-
-> https://projects.aptivate.org/projects/internewshid/wiki
diff --git a/docs/architecture.md b/docs/architecture.md
index 470c1f7a00e79ad4bd26370f9924464371bbdde8..f20f9fd3e18b5805162292a6bbd9f516e79f9e46 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,5 +1,54 @@
 # Humanitarian Information Dashboard Architecture
 
+## Overview
+
+The Internews Humanitarian Information Dashboard (HID) allow spreadsheets of message (eg. text messages, rumours etc) to be imported, edited, searched and visualised.
+
+It has a flexible database structure that allows arbitrary tagging and data to be imported with the message.
+
+## General Architecture
+
+The Internews HID application is written in Django.
+
+### API
+It uses the **DjangoREST framework** to provide an API directly to the data. All of the internal data access is done through the API rather than directly to the database. See the `transport` app.
+
+### Flexible Database
+The application has a number of features that are configured or stored in the database rather than coded in the structure which in principle allows them to be modified by an admin user in the django admin user interface.
+
+For instance the specification of spreadsheet data formats used by the spreadsheet importer can be specified in the admin interface.
+
+Also the data itself uses two mechanisms to allow flexibility in data storage. It has the concept of Tags and KeyValue pairs.
+
+The tags belong to taxonomies that are specified through the admin interface. A taxonomy can be closed (in which case all values that a tag of that taxonomy can take must be specified) or they can be open in which case the importer will create a new tag in the database if it comes across a new entry in the imported data. This means that taxonomies of tags that can be attached to message records are configurable and not hard-coded in the database.
+
+Similarly there are KeyValue pairs. Instead of importing data into a set database column, they allow and administrator to configure "virtual" columns by specifying a spreadsheet column as a KeyValue type. Additionally the importer can import any  spreadsheet columns that have not been specified in the importer specification into KeyValue pairs.   
+
+## CSS / LESS
+
+The CSS is auto-generated from .less files.
+There should be no need to manually run compass.
+
+### InterNews HID theme
+
+LESS files reside in internewshid/internewshid/media/less
+
+### Bootstrap 3.3.5
+
+We are using Bootstrap v3.3.5 (http://getbootstrap.com) as our base styling.
+See /media/bootstrap/less/bootstrap.less for the configuration, not all files are imported.
+We do not load glyph-icons for example.
+
+### Dashboard
+
+Has its own css file dashboard/dashboard.css, loaded in assets.py
+
+### Fontawesome icons
+
+Fontawesome 4.3.0 is loaded by assets.py
+Source files: /mediafonts/font-awesome-4.3.0/
+
+
 ## Adding a new spreadsheet format
 
 Create a new `SheetProfile` object with a `profile`, for example:
@@ -133,3 +182,11 @@ The `Settings` are specified in a blob of JSON with the following format:
 ## Importing a spreadsheet
 
 Importing a spreadsheet will create ``Item`` objects (see `data_layer/models.py` via the ``transport`` app).
+
+## Next steps for future development
+
+The spreadsheet importer was originally developed to handle the export of form based survey tools like KOBO (eg. for the Ebola crisis in Liberia and Bangladesh). Consequently it is deliberately unforgiving of any deviations in the specified import format.
+
+Later it was configured for use in DRC, South Sudan and for the COVID19 crisis, where data had been manually entered into spreadsheets. The importer could be improved for this scenario by being more tolerant. For instance it could force standard capitalisation and strip whitespace. It could also better report importing errors and allow for correction and re-importing workflow.
+
+The importer specification was designed to be flexible without having to change the code so it is set in the admin interface. It is exposed in the admin as an editable JSON structure. This isn't as user friendly as it could be and also it complicates the code significantly. The importer specification is a relatively technical bit of configuration and was not configured by admin users. Instead it was developers who edited it. It would be worth reviewing whether editing the importer specification in the admin interface is the best option or if editing it as source code would be better.
diff --git a/docs/deploy.md b/docs/deploy.md
index d38db2c2d85e30e45e1b21e9c99a6c06f0a64cc2..de9ea50665ecae2888da1e7929dbd2da3947fec6 100644
--- a/docs/deploy.md
+++ b/docs/deploy.md
@@ -1,15 +1,15 @@
-# How to set up the Internews Humanitarian Information Dashboard on a Ubuntu 14.04 environment
+# How to set up the Internews Humanitarian Information Dashboard on a Ubuntu 18.04 environment
 
 This document provides instructions on deploying the [Internews Humanitation
-Information Dashboard](https://github.com/aptivate/internewshid) on a [Ubuntu
-14.04 LTS](http://releases.ubuntu.com/14.04/) environment.
+Information Dashboard](https://git.coop/aptivate/client-projects/internewshid/) on a [Ubuntu
+18.04 LTS](http://releases.ubuntu.com/18.04/) environment.
 
 Familiarity with the command line is required.
 
 ## Development environment setup
 
 These instructions allow you to setup the HID on a local development machine,
-based on the desktop edition of Ubuntu 14.04. This setup does not require a web
+based on the desktop edition of Ubuntu 18.04. This setup does not require a web
 server, as it is for development only and instead uses Django's build in
 server.
 
@@ -24,7 +24,7 @@ other system tools:
 
 ```sh
     sudo apt-get update
-    sudo apt-get install -y mysql-server libmysqlclient-dev git python python-dev python-virtualenv python-pip node-less
+    sudo apt-get install -y default-libmysqlclient-dev python-pymysql python-mysqldb nodejs node-less
 ```
 
 You will need to know your MySql root password. If you set it up for the first
@@ -42,24 +42,46 @@ First, get a copy of internews hid:
 ```sh
     mkdir -p ~/projects
     cd ~/projects
-    git clone https://github.com/aptivate/internewshid.git
+    git clone git@git.coop:aptivate/client-projects/internewshid.git
 ```
 
 This will prompt you for your Github user account. Next you will want to
 download the application's dependencies and deploy the project locally:
 
 ```sh
-    cd ~/projects/internewshid/deploy
-    ./bootstrap.py
-    ./tasks.py deploy:dev
+    cd internewshid
+    ln -srf internewshid/local_settings.py.dev internewshid/local_settings.py
+    echo "SECRET_KEY = '$DJANGO_SECRET_KEY'" >> internewshid/private_settings.py
+    echo "DB_PASSWORD = 'internewshid'" >> internewshid/private_settings.py
+    sudo mysql -ve "CREATE DATABASE IF NOT EXISTS internewshid CHARACTER SET utf8 COLLATE utf8_general_ci;"
+    sudo mysql -ve "CREATE USER 'internewshid'@'localhost' IDENTIFIED BY 'internewshid'"
+    sudo mysql -ve "GRANT ALL ON internewshid.* TO 'internewshid'@'localhost'; FLUSH PRIVILEGES;"
+    pip install pipenv && pipenv sync --dev
 ```
 
-This will prompt you for your MySql root password. The final preparation step
-is to setup a super user who will have access to the administration interface:
+Note there are a number of different settings files (eg. `local_settings.py.covid19-dev`) and you will want to set your local settings to the appropriate settings file using a symbolic link, depending on what you are doing. For instance if you are doing development on the COVID19 version on your local machine you want to use `local_settings.py.covid19-dev`. The generic development settings are `local_settings.py.dev` which is what the following command uses (from the set of commands above). You can change this command to the appropriate file.
 
 ```sh
-    cd ~/projects/internewshid/django/website
-    ./manage.py createsuperuser
+    ln -srf internewshid/local_settings.py.dev internewshid/local_settings.py
+```
+
+When you deploy to your production server you will want to use the appropriate production settings file. 
+
+
+### Load the database fixtures
+
+The dashboard is configured through the database. This is most easily set up through
+fixture files.
+There are many fixture files depending on the context, for example:
+
+```sh
+    pipenv run python manage.py loaddata covid-19
+```
+
+The final preparation step is to setup a super user who will have access to the administration interface:
+
+```sh
+    pipenv run python manage.py createsuperuser
 ```
 
 ### Run the tests
@@ -75,8 +97,7 @@ additional dependencies:
 The automated tests can then be executing by running:
 
 ```sh
-    cd ~/projects/internhewshid/django/website
-    ./manage.py test
+    pipenv run pytest
 ```
 
 ### Run the Internews HID application
@@ -84,8 +105,7 @@ The automated tests can then be executing by running:
 You can start Django's internal web server by running the following command:
 
 ```sh
-    cd ~/projects/internhewshid/django/website
-    ./manage.py runserver
+    pipenv run python manage.py runserver
 ```
 
 Once this is started, you can point your web browser to `http://localhost:8000`
@@ -96,138 +116,3 @@ to see the Internews HID.
 In the development version, the javascript and CSS assets are not compressed,
 and not combined into a single file. This makes development easier - however it
 means the page size will be considerably larger than it would be in production.
-
-## Server environment setup
-
-These instructions allow you to setup the HID on a server machine, based on the
-server edition of Ubuntu 14.04.
-
-All instructions are to be run in a
-[terminal](https://help.ubuntu.com/community/UsingTheTerminal), and require a
-user who can [run sudo](https://help.ubuntu.com/community/RootSudo)
-
-### Pre-requisites
-
-You need to install an Apache server, a MySql server, git, a Python development
-environment and other system tools:
-
-```sh
-    sudo apt-get update
-    sudo apt-get install -y apache2 libapache2-mod-wsgi mysql-server libmysqlclient-dev git python python-dev python-virtualenv python-pip node-less
-```
-
-You will need to know your MySql root password. If you set it up for the first
-time you will be prompted for the password. If you have set it up previously
-and forgotten the password you will need to [reset your MySql root
-password](https://help.ubuntu.com/community/MysqlPasswordReset).
-
-You will also need to know the server hostname - the name that will be used to
-access the website, eg. `www.example.com`.
-
-### Fetch and prepare Internews HID application
-
-The configuration files expect the application to be under
-`/var/django/internewshid/current`.
-
-First, get a copy of internews hid:
-
-```sh
-    sudo mkdir -p /var/django/internewshid
-    cd /var/django/internewshid
-    sudo git clone https://github.com/aptivate/internewshid.git current
-```
-
-This will prompt you for your Github user account. Next you will want to
-download the application's dependencies and deploy the project locally:
-
-```sh
-    cd /var/django/internewshid/current/deploy
-    sudo ./bootstrap.py
-    sudo ./tasks.py deploy:production
-```
-
-This will prompt you for your MySql root password. Next you need to setup a
-super user who will have access to the administration interface:
-
-```sh
-    cd /var/django/internewshid/current/django/website
-    ./manage.py createsuperuser
-```
-
-The production version is stricter in terms of security, and you must
-explicitly allow the host on which you are installing hid by editing
-`/var/django/internewshid/current/django/website/settings.py` and adding the
-server host name to the `ALLOWED_HOSTS` configuration. For example you would
-replace:
-
-```python
-    ALLOWED_HOSTS = [
-        '.internewshid.aptivate.org',
-        'www.internewshid.aptivate.org'
-    ]
-```
-
-with
-
-```python
-    ALLOWED_HOSTS = ['www.example.com']
-```
-
-You need to ensure that the web server has write access to the `static` and `upload` folders:
-
-```sh
-    chown -R www-data:www-data /var/django/internewshid/current/django/website/static
-    chown -R www-data:www-data /var/django/internewshid/current/django/website/static
-```
-
-Finally you will need to set the Apache configuration file. First copy it in place:
-
-```sh
-    sudo cp /var/django/internewshid/current/apache/ubuntu/production.conf /etc/apache/sites-available/internewshid.conf
-```
-
-Then edit that file, to set the server name. For example replace the line
-
-```
-    ServerName lin-internewshid.aptivate.org
-```
-
-With
-
-```
-    ServerName www.example.com
-```
-
-And if you want to enable all subdomains to direct to this location, you add
-the following linebelow:
-
-```
-    ServerAlias *.example.com
-```
-
-
-Finally, enable the site and restart apache:
-
-```
-    sudo a2ensite internewshid
-    sudo service apache2 restart
-```
-
-The site will now be available in your browser at `http://www.example.com`
-
-### Run the tests
-
-To ensure long term maintainability the application contains a number of
-automated tests. If you want to run the tests you will need to install
-additional dependencies:
-
-```sh
-    sudo apt-get install -y phantomjs
-```
-
-The automated tests can then be executing by running:
-
-```sh
-    cd /var/django/internhewshid/current/django/website
-    sudo ./manage.py test
-```
diff --git a/internewshid/chn_spreadsheet/tests/importer_tests.py b/internewshid/chn_spreadsheet/tests/importer_tests.py
index 8707b8b7274d1a03bf125d077e51f8fa67233fd4..453106098cc69f15af61833aee53a28ccaa97cb2 100644
--- a/internewshid/chn_spreadsheet/tests/importer_tests.py
+++ b/internewshid/chn_spreadsheet/tests/importer_tests.py
@@ -436,19 +436,8 @@ def test_save_rows_handles_invalid_contributor(importer):
     with pytest.raises(SheetImportException) as excinfo:
         importer.save_rows(objects)
 
-    assert str(excinfo.value) == (
-        "There was a problem with row 29 of the spreadsheet:\n"
-        "Column: 'Ennumerator' (contributor)\n"
-        "Error (max_length): 'Ensure this field has no more "
-        "than 190 characters.'\n\n"
-        "Value: Yakub=Aara smart card no point in "
-        "Kialla hoi lay smart card hoday yan gor Sara Thor Sara ,hetalli "
-        "bolli aara loi bolla nosir ,zodi aara Thor Sara oi tum aara smart "
-        "card loi tum .Aara tum Thor asi day yan bishi manshe zani ar bishi "
-        "goba asi ,Bormar shorkari aarari zeyan hor yan oilday hetarar bolor "
-        "hota .kinto hetarar aarari forok gorid day ,zodi Burmar shor karotum "
-        "soyi ensaf takito aarari Thor Sara nohoito"
-    )
+    error_correct = str(excinfo.value).startswith("There was a problem with row 29 of the spreadsheet:")
+    assert error_correct
 
 
 @pytest.mark.django_db
diff --git a/internewshid/dashboard/tests/dashboard_view_tests.py b/internewshid/dashboard/tests/dashboard_view_tests.py
index b06061ec444b2aaf43af2c9cb2b38a6e1f3e237f..d85ef932769d6ccb10630cac1e77b24a036b946b 100644
--- a/internewshid/dashboard/tests/dashboard_view_tests.py
+++ b/internewshid/dashboard/tests/dashboard_view_tests.py
@@ -1,6 +1,6 @@
 from django.test import TestCase
 
-from mock import patch
+from mock import Mock, patch
 
 from dashboard.models import Dashboard
 from dashboard.views import DashboardView
@@ -63,7 +63,11 @@ class TestDashboardView(TestCase):
             it's get_context_data method
         """
         dashboard_view = DashboardView()
-        view_args = {'name': 'dashboard1'}
+        request = Mock()
+        user = Mock()
+        user.has_perm.return_value = True
+        request.user = user
+        view_args = {'name': 'dashboard1', 'request': request}
         dashboard_view.kwargs = view_args
         assets = [
             'file.js', 'app/file.js', 'file.css',
diff --git a/internewshid/dashboard/views.py b/internewshid/dashboard/views.py
index da26fdc09011e962d8088baba88a26294b5aba16..65b48e69ac8078170da7a2d7309c538668d52b37 100644
--- a/internewshid/dashboard/views.py
+++ b/internewshid/dashboard/views.py
@@ -15,6 +15,14 @@ class DashboardView(TemplateView):
         """
         context = super(DashboardView, self).get_context_data(**kwargs) or {}
 
+        form_disabled = True
+        if hasattr(self, 'request'):
+            if hasattr(self.request, 'user'):
+                if self.request.user.has_perm('data_layer.change_message'):
+                    form_disabled = False
+
+        context['form_disabled'] = form_disabled
+
         # Get dashboard
         if 'name' in self.kwargs and self.kwargs['name']:
             name = self.kwargs['name']
diff --git a/internewshid/data_layer/models.py b/internewshid/data_layer/models.py
index 2edfb50a70aa7bc1d54153f2ac61ad604d2d6309..1fb13573dff1aa313f2ee46cfdee75bfda78c878 100644
--- a/internewshid/data_layer/models.py
+++ b/internewshid/data_layer/models.py
@@ -3,6 +3,7 @@ from django.db import models
 from django.dispatch.dispatcher import receiver
 from django.utils.translation import ugettext_lazy as _
 
+from constance.backends import Backend
 from constance.backends.database import DatabaseBackend
 
 from taxonomies.exceptions import TermException
@@ -172,3 +173,21 @@ class CustomConstanceBackend(DatabaseBackend):
     def __init__(self, *args, **kwargs):
         super(CustomConstanceBackend, self).__init__(*args, **kwargs)
         self._model = CustomConstance
+
+
+class CustomConstanceTestBackend(Backend):
+    """
+    If we use the database backend, constants are initialised by pytest's test
+    discovery before the test database has been created, so tests can fail
+    depending on the contents of the actual non-test database.
+    """
+    def __init__(self, *args, **kwargs):
+        self.store = {}
+
+    def get(self, key):
+        return self.store.get(key, None)
+
+    def set(self, key, value):
+        self.store[key] = value
+
+    # Might need to implement mget at a future date
diff --git a/internewshid/hid/fixtures/common/group-permissions.json b/internewshid/hid/fixtures/common/group-permissions.json
new file mode 100644
index 0000000000000000000000000000000000000000..e639b4060eb735a33cbf55be449f787e23d96343
--- /dev/null
+++ b/internewshid/hid/fixtures/common/group-permissions.json
@@ -0,0 +1 @@
+[{"model": "auth.group", "fields": {"name": "read-only", "permissions": [["view_logentry", "admin", "logentry"], ["view_group", "auth", "group"], ["view_permission", "auth", "permission"], ["view_sheetprofile", "chn_spreadsheet", "sheetprofile"], ["view_contenttype", "contenttypes", "contenttype"], ["view_dashboard", "dashboard", "dashboard"], ["view_widgetinstance", "dashboard", "widgetinstance"], ["view_customconstance", "data_layer", "customconstance"], ["view_key", "data_layer", "key"], ["view_message", "data_layer", "message"], ["view_value", "data_layer", "value"], ["view_session", "sessions", "session"], ["view_site", "sites", "site"], ["view_tabbedpage", "tabbed_page", "tabbedpage"], ["view_tabinstance", "tabbed_page", "tabinstance"], ["view_taxonomy", "taxonomies", "taxonomy"], ["view_term", "taxonomies", "term"], ["view_user", "users", "user"]]}}]
\ No newline at end of file
diff --git a/internewshid/hid/forms/item.py b/internewshid/hid/forms/item.py
index 0b163c28b1a9087454b5083bf7f52864236dd4dd..3cdeb0cb44fb04858c766c307d9c11f96f74f987 100644
--- a/internewshid/hid/forms/item.py
+++ b/internewshid/hid/forms/item.py
@@ -57,15 +57,25 @@ class AddEditItemForm(forms.Form):
 
     def __init__(self, *args, **kwargs):
         feedback_disabled = kwargs.pop('feedback_disabled', False)
+        form_disabled = kwargs.pop('form_disabled', False)
+        keyvalues = kwargs.pop('keyvalues', False)
 
         super(AddEditItemForm, self).__init__(*args, **kwargs)
 
+        if form_disabled:
+            for key in self.fields:
+                if not isinstance(self.fields[key].widget, forms.widgets.HiddenInput):
+                    self.fields[key].disabled = True
+
         self.fields['body'].disabled = feedback_disabled
 
         self._maybe_add_category_field()
         self._maybe_add_feedback_type_field()
         self._maybe_add_age_range_field()
 
+        if keyvalues:
+            self._add_key_value_fields(keyvalues)
+
     def _maybe_add_category_field(self):
         # This used to be more flexible in that we had partial
         # support for per- item/feedback/message type categories
@@ -137,3 +147,15 @@ class AddEditItemForm(forms.Form):
             return choices
 
         return None
+
+    def _add_key_value_fields(self, keyvalues):
+        for key, value in keyvalues.items():
+            field_name = f'item-keyvalue-{key}'
+            self.fields[field_name] = forms.CharField(
+                required=False, label=key
+            )
+
+    def get_key_value_fields(self):
+        for field_name in self.fields:
+            if field_name.startswith('item-keyvalue-'):
+                yield self[field_name]
diff --git a/internewshid/hid/tables.py b/internewshid/hid/tables.py
index 6b01386a9a0c0dc8b8c691a9dd5d004e2e7f8599..fb8388ee67fb4f5455e7210f24657b8a3b9c7ba2 100644
--- a/internewshid/hid/tables.py
+++ b/internewshid/hid/tables.py
@@ -179,13 +179,26 @@ class ItemTable(tables.Table):
     def render_category(self, record, value):
         Template = loader.get_template('hid/categories_column.html')
         selected = []
+        selected_long_names = []
         for term in value:
             if term['taxonomy'] == ITEM_TYPE_CATEGORY['all']:
                 selected.append(term['name'])
+
+        for category in self.categories:
+            if category[0] in selected:
+                selected_long_names.append(category[1].title())
+
+        form_disabled = True
+
+        if hasattr(self, 'context'):
+            form_disabled = self.context.get('form_disabled', True)
+
         ctx = {
             'categories': self.categories,
+            'selected_long_names': selected_long_names,
             'selected': selected,
-            'record': record
+            'record': record,
+            'form_disabled': form_disabled
         }
 
         return Template.render(ctx)
diff --git a/internewshid/hid/tabs/view_and_edit_table.py b/internewshid/hid/tabs/view_and_edit_table.py
index 88db66d8ed153293325bb749fd497e7f1437343e..04d98f9db2ec983b3215038e7c48a78fe346a794 100644
--- a/internewshid/hid/tabs/view_and_edit_table.py
+++ b/internewshid/hid/tabs/view_and_edit_table.py
@@ -272,6 +272,11 @@ class ViewAndEditTableTab(object):
         response = self._get_items(request, **kwargs)
         items = PreSortedTableListData(response['results'])
 
+        form_disabled = True
+        if hasattr(request, 'user'):
+            if request.user.has_perm('data_layer.change_message'):
+                form_disabled = False
+
         category_options = self._get_category_options(**kwargs)
         location_options = self._get_location_options(items, **kwargs)
         sub_location_options = self._get_sub_location_options(items, **kwargs)
@@ -309,6 +314,7 @@ class ViewAndEditTableTab(object):
             'add_button_for': self._get_item_type_filter(kwargs),
             'type_label': kwargs.get('label', '?'),
             'table': table,
+            'form_disabled': form_disabled,
             'collection_type': kwargs.get('collection_type'),
             'actions': actions,
             'category_options': category_options,
@@ -448,8 +454,15 @@ def view_and_edit_table_form_process_items(request):
                     - select_action: List of items to apply
                       the action too.
     """
+
+    user_has_update_permission = False
+
+    if hasattr(request, 'user'):
+        if request.user.has_perm('data_layer.change_message'):
+            user_has_update_permission = True
+
     # Process the form
-    if request.method == "POST":
+    if request.method == "POST" and user_has_update_permission:
         params = _get_view_and_edit_form_request_parameters(request.POST)
         if params['action'] == 'batchupdate':
             selected = ItemTable.get_selected(params)
diff --git a/internewshid/hid/templates/hid/add_edit_item.html b/internewshid/hid/templates/hid/add_edit_item.html
index 53491bd66e499623a8f9c1458ae78934649dbbc0..ff5a81e215ef2f55dfd30de66916d3c60e9656cc 100644
--- a/internewshid/hid/templates/hid/add_edit_item.html
+++ b/internewshid/hid/templates/hid/add_edit_item.html
@@ -58,8 +58,6 @@
                         {% bootstrap_button _("Cancel") button_type="submit" name="action" value="cancel" button_class="btn btn-md btn-border" %}
                         {% if update %}
                         {% bootstrap_button _("Update") button_type="submit" name="action" value="update" button_class="btn btn-md btn-success" %}
-                        {% else %}
-                        {% bootstrap_button _("Create") button_type="submit" name="action" value="create" button_class="btn btn-md btn-success" %}
                         {% endif %}
                     </div>
 
@@ -209,16 +207,12 @@
                             </div>
                             {% endif %}
 
-                            {% if keyvalues %}
-
-                                {% for kv in keyvalues.items %}
-                                    <div class="item-keyvalue item-keyvalue-{{ kv.0 }}">
-                                        <label for="item-keyvalue-{{ kv.0 }}">{{ kv.0 }}</label>
-                                        <input id="item-keyvalue-{{ kv.0 }}" class="form-control item-keyvalue-value" value="{{ kv.1 }}" disabled/>
-                                    </div>
-                                {% endfor %}
-
-                            {% endif %}
+                            {% for kv_field in form.get_key_value_fields %}
+                                <div class="item-keyvalue {{ kv_field.name }}">
+                                  <label for="{{ kv_field.name }}">{{ kv_field.label }}</label>
+                                  {{ kv_field|add_class:'form-control item-keyvalue-value' }}
+                                </div>
+                            {% endfor %}
 
                         </div>
                 </div>
diff --git a/internewshid/hid/templates/hid/categories_column.html b/internewshid/hid/templates/hid/categories_column.html
index a1b5356f30ea5eb880974c0cf13ecb86f003ea9b..3a730bee98ae15d4efa4e58991ade8394fef7804 100644
--- a/internewshid/hid/templates/hid/categories_column.html
+++ b/internewshid/hid/templates/hid/categories_column.html
@@ -1,8 +1,12 @@
 {% if categories %}
-<select name="category-{{ record.id }}" class="form-control">
-        <option value="" selected="selected">---------</option>
-        {% for cat in categories %}
-        <option value="{{ cat.0 }}"{% if cat.0 in selected %} selected="selected"{% endif %}>{{ cat.1 | title }}</option>
-        {% endfor %}
-    </select>
+    {% if form_disabled %}
+        {{ selected_long_names | join:", " }}
+    {% else %}
+        <select name="category-{{ record.id }}" class="form-control">
+            <option value="" selected="selected">---------</option>
+            {% for cat in categories %}
+            <option value="{{ cat.0 }}"{% if cat.0 in selected %} selected="selected"{% endif %}>{{ cat.1 | title }}</option>
+            {% endfor %}
+        </select>
+    {% endif %}
 {% endif %}
diff --git a/internewshid/hid/templates/hid/tabs/view_and_edit_buttons.html b/internewshid/hid/templates/hid/tabs/view_and_edit_buttons.html
index 1f67eb8b04567f0169b2044e51676796925effc3..7aa7a6d87a6c43504cbad29d787c19167afb4702 100644
--- a/internewshid/hid/templates/hid/tabs/view_and_edit_buttons.html
+++ b/internewshid/hid/templates/hid/tabs/view_and_edit_buttons.html
@@ -1,5 +1,7 @@
 {% load i18n %}
 {% load bootstrap3 %}
+
+{% if not form_disabled %}
 <div class="form-group table-button-group">
     <select name='batchaction-{{button_placement}}' class='form-control'>
         {% for group in actions %}
@@ -21,3 +23,4 @@
         </div>
     {% endif %}
 </div>
+{% endif %}
\ No newline at end of file
diff --git a/internewshid/hid/tests/add_edit_form_tests.py b/internewshid/hid/tests/add_edit_form_tests.py
index 9f347b21f3931fac09475b3870405045d621c40b..360806bb26b618714d75cdf297da6ebde2386931 100644
--- a/internewshid/hid/tests/add_edit_form_tests.py
+++ b/internewshid/hid/tests/add_edit_form_tests.py
@@ -174,3 +174,26 @@ def test_category_field_is_not_required(
     form = AddEditItemForm()
 
     assert not form.fields['category'].required
+
+
+@patch.object(AddEditItemForm, '_get_category_choices')
+@patch.object(AddEditItemForm, '_get_feedback_type_choices')
+@patch.object(AddEditItemForm, '_get_age_range_choices')
+def test_form_has_keyvalue_fields(
+        age_range_choices,
+        feedback_choices,
+        category_choices
+):
+    age_range_choices.return_value = None
+    feedback_choices.return_value = None
+    category_choices.return_value = None
+
+    form = AddEditItemForm(
+        keyvalues={
+            'CONTRIBUTER': 'TWB',
+            'URL': 'https://twitter.com/WelshDalaiLama/status/1248884952558718976',
+        }
+    )
+
+    assert 'item-keyvalue-CONTRIBUTER' in form.fields
+    assert 'item-keyvalue-URL' in form.fields
diff --git a/internewshid/hid/tests/add_edit_item_view_tests.py b/internewshid/hid/tests/add_edit_item_view_tests.py
index e14b796636c2f5463378a3304b051fe5866ba42f..bf8ac8ed825caf9af1e79c4fde0ce20e702de223 100644
--- a/internewshid/hid/tests/add_edit_item_view_tests.py
+++ b/internewshid/hid/tests/add_edit_item_view_tests.py
@@ -6,6 +6,7 @@ from django.template.response import TemplateResponse
 from django.test import RequestFactory
 from django.urls import reverse
 from django.utils import timezone
+from django.utils.translation import ugettext as _
 
 import pytest
 from mock import Mock, patch
@@ -781,7 +782,7 @@ def test_item_update_transport_exception_logs_message(view, update_form):
 
     assert_message(view.request,
                    messages.ERROR,
-                   "Not found.")
+                   _("Not found."))
 
 
 @pytest.mark.django_db
@@ -982,6 +983,27 @@ def test_form_initial_values_include_feedback_type(generic_item):
         assert initial['feedback_type'] == ['concern']
 
 
+@pytest.mark.django_db
+def test_form_initial_values_include_keyvalues(generic_item):
+    with patch('hid.views.item.transport.items.get') as get_item:
+        generic_item['values'] = {
+                'CONTRIBUTER': 'TWB',
+                'URL': 'https://twitter.com/WelshDalaiLama/status/1248884952558718976',
+        }
+
+        get_item.return_value = generic_item
+
+        (view, response) = make_request(
+            AddEditItemView,
+            'edit-item',
+            kwargs={'item_id': 103}
+        )
+
+        initial = view.get_initial()
+        assert initial['item-keyvalue-CONTRIBUTER'] == 'TWB'
+        assert initial['item-keyvalue-URL'] == 'https://twitter.com/WelshDalaiLama/status/1248884952558718976'
+
+
 @pytest.mark.django_db
 def test_feedback_disabled_if_user_does_not_have_permission(generic_item):
     with patch('hid.views.item.transport.items.get') as get_item:
@@ -996,7 +1018,7 @@ def test_feedback_disabled_if_user_does_not_have_permission(generic_item):
         view.request.user = user
 
         response = view.get(view.request, *view.args, **view.kwargs)
-        user.has_perm.assert_called_with('data_layer.can_change_message_body')
+        user.has_perm.assert_any_call('data_layer.can_change_message_body')
 
         form = response.context_data['form']
 
@@ -1017,8 +1039,22 @@ def test_feedback_enabled_if_user_has_permission(generic_item):
         view.request.user = user
 
         response = view.get(view.request, *view.args, **view.kwargs)
-        user.has_perm.assert_called_with('data_layer.can_change_message_body')
+        user.has_perm.assert_any_call('data_layer.can_change_message_body')
 
         form = response.context_data['form']
 
         assert form.fields['body'].disabled is False
+
+
+@pytest.mark.django_db
+def test_item_keyvalues_can_be_updated(view, update_form, item_type_taxonomy):
+    TaxonomyFactory(name='Age Ranges', slug='age-ranges')
+    transport.items.add_keyvalue(view.item['id'], 'CONTRIBUTER', 'TWB')
+    update_form.cleaned_data['item-keyvalue-CONTRIBUTER'] = 'TWB modified'
+
+    view.form_valid(update_form)
+    assert_no_messages(view.request, messages.ERROR)
+
+    item = transport.items.get(view.item['id'])
+
+    assert item['values']['CONTRIBUTER'] == 'TWB modified'
diff --git a/internewshid/hid/tests/itemtable_tests.py b/internewshid/hid/tests/itemtable_tests.py
index 0017956a05111b4b2eb492388db5ec889e107ba5..a513a24e62ff5b459a124af845608fd304e86a90 100644
--- a/internewshid/hid/tests/itemtable_tests.py
+++ b/internewshid/hid/tests/itemtable_tests.py
@@ -95,7 +95,9 @@ def test_render_category_passes_context_to_template(mock_loader):
     context = {
         'categories': categories,
         'selected': ['Repatriation'],
+        'selected_long_names': ['Repatriation'],
         'record': record,
+        'form_disabled': True
     }
 
     mock_template.render.assert_called_with(context)
diff --git a/internewshid/hid/tests/site_needs_authentication_tests.py b/internewshid/hid/tests/site_needs_authentication_tests.py
index 39bf8ddad25f4b7c3669946c7ee0bde481c6fd62..b8462e19352ae89f6a09c1dc9b5b56dd88cf6834 100644
--- a/internewshid/hid/tests/site_needs_authentication_tests.py
+++ b/internewshid/hid/tests/site_needs_authentication_tests.py
@@ -14,6 +14,7 @@ class SiteNeedsAuthenticationTests(FastDispatchMixin, TestCase):
 
     def test_dashboard_can_be_accessed_when_logged_in(self):
         self.user = User()
+        self.user._set_pk_val(1)
         response = self.fast_dispatch('dashboard')
 
         response.render()
@@ -22,7 +23,7 @@ class SiteNeedsAuthenticationTests(FastDispatchMixin, TestCase):
 
     def test_logout_view_logs_user_out(self):
         self.user = User()
-
+        self.user._set_pk_val(1)
         self.fast_dispatch('dashboard')
 
         # The user when logged out should be None or AnonymousUser
diff --git a/internewshid/hid/tests/views_tests.py b/internewshid/hid/tests/views_tests.py
index 72e2d599b792e5957bef2d79e0a36f94712c52b8..66036ee908d4a3fffc277e3b2f7adfd01e56e7c4 100644
--- a/internewshid/hid/tests/views_tests.py
+++ b/internewshid/hid/tests/views_tests.py
@@ -4,7 +4,7 @@ from django.test import RequestFactory
 from django.urls import reverse
 
 import pytest
-from mock import MagicMock
+from mock import MagicMock, Mock
 
 import transport
 from hid.constants import ITEM_TYPE_CATEGORY
@@ -85,6 +85,10 @@ def request_item():
     })
     request = fix_messages(request)
 
+    user = Mock()
+    user.has_perm.return_value = True
+    request.user = user
+
     return [request, item]
 
 
@@ -133,6 +137,10 @@ def test_process_items_removes_question_type(item_type_taxonomy):
         'next': 'http://localhost/testurl'
     })
 
+    user = Mock()
+    user.has_perm.return_value = True
+    request .user = user
+
     request = fix_messages(request)
     view_and_edit_table_form_process_items(request)
 
diff --git a/internewshid/hid/views/item.py b/internewshid/hid/views/item.py
index cdbeea2c51fa7df24c9bb1df876b883031c636e5..be07701525572db7ff8e416d57cffe4fa0064479 100644
--- a/internewshid/hid/views/item.py
+++ b/internewshid/hid/views/item.py
@@ -68,6 +68,8 @@ class AddEditItemView(FormView):
                     self.item_terms[taxonomy] = []
                 self.item_terms[taxonomy].append(term)
         elif item_type:
+            # TODO will this work with multiple item types?
+            # Does it ever run anyway without an Add screen? Should we remove it?
             matches = transport.terms.list(
                 taxonomy='item-types',
                 name=item_type
@@ -138,8 +140,11 @@ class AddEditItemView(FormView):
                 messages.INFO,
                 _('No action performed')
             )
-        if 'delete' in self.request.POST['action']:
-            return self._delete_item()
+
+        if not self._form_disabled():
+
+            if 'delete' in self.request.POST['action']:
+                return self._delete_item()
 
         return super(AddEditItemView, self).post(request, *args, **kwargs)
 
@@ -172,6 +177,8 @@ class AddEditItemView(FormView):
                     )
                 ),
             }
+            for key, value in self.item.get('values', {}).items():
+                initial[f'item-keyvalue-{key}'] = value
 
         taxonomy = ITEM_TYPE_CATEGORY.get('all')
         if (taxonomy and taxonomy in self.item_terms
@@ -204,6 +211,9 @@ class AddEditItemView(FormView):
         kwargs = self.get_form_kwargs()
 
         kwargs['feedback_disabled'] = self._feedback_disabled()
+        kwargs['form_disabled'] = self._form_disabled()
+        if self.item:
+            kwargs['keyvalues'] = self.item.get('values')
 
         return form_class(**kwargs)
 
@@ -217,6 +227,15 @@ class AddEditItemView(FormView):
 
         return True
 
+    def _form_disabled(self):
+        if not hasattr(self.request, 'user'):
+            return False
+
+        if self.request.user.has_perm('data_layer.change_message'):
+            return False
+
+        return True
+
     def get_context_data(self, **kwargs):
         """ Get the form's context data
 
@@ -227,11 +246,8 @@ class AddEditItemView(FormView):
 
         # Add item and form mode to the context
         context['item'] = self.item
-        # Stashing values in the context because of a naming clash with a method on self.item
-        # prevent us from referencing values directly in the template
-        if self.item:
-            context['keyvalues'] = self.item.get('values')
-        context['update'] = self.item is not None
+
+        context['update'] = not (self._form_disabled() or (self.item is None))
 
         # Add the width of the option row to the context
         option_row_widget_count = 1  # We always have 'created'
@@ -248,34 +264,38 @@ class AddEditItemView(FormView):
         taxonomy = ITEM_TYPE_CATEGORY.get('all')
         item_id = int(form.cleaned_data['id'])
 
-        try:
-            if item_id == 0:
-                self.item = self._create_item(form, taxonomy)
-                item_description = self._get_item_description()
-                message = _("{0} {1} successfully created.").format(
-                    item_description,
-                    self.item['id']
-                )
-                message_code = messages.SUCCESS
-            else:
-                self._update_item(item_id, form)
-                item_description = self._get_item_description()
-                message = _("{0} {1} successfully updated.").format(
-                    item_description,
-                    item_id,
-                )
-                message_code = messages.SUCCESS
-        except transport.exceptions.ItemNotUniqueException as e:
-            message = _("This record could not be saved because the body and "
-                        "timestamp clashed with an existing record")
-            message_code = messages.ERROR
-
-        except transport.exceptions.TransportException as e:
-            message = e.message.get('detail')
-            if message is None:
-                message = e.message
-
-            message_code = messages.ERROR
+        if not self._form_disabled():
+            try:
+                if item_id == 0:
+                    self.item = self._create_item(form, taxonomy)
+                    item_description = self._get_item_description()
+                    message = _("{0} {1} successfully created.").format(
+                        item_description,
+                        self.item['id']
+                    )
+                    message_code = messages.SUCCESS
+                else:
+                    self._update_item(item_id, form)
+                    item_description = self._get_item_description()
+                    message = _("{0} {1} successfully updated.").format(
+                        item_description,
+                        item_id,
+                    )
+                    message_code = messages.SUCCESS
+            except transport.exceptions.ItemNotUniqueException as e:
+                message = _("This record could not be saved because the body and "
+                            "timestamp clashed with an existing record")
+                message_code = messages.ERROR
+
+            except transport.exceptions.TransportException as e:
+                message = e.message.get('detail')
+                if message is None:
+                    message = e.message
+
+                message_code = messages.ERROR
+        else:
+            message_code = messages.SUCCESS
+            message = 'Read only form'
 
         return self._response(
             form.cleaned_data['next'],
@@ -291,14 +311,17 @@ class AddEditItemView(FormView):
 
         tags = {}
         regular_fields = {}
+        keyvalues = {}
 
         for (field_name, field_value) in data.items():
             if field_name in self.tag_fields:
                 tags[field_name] = field_value
+            elif field_name.startswith('item-keyvalue-'):
+                keyvalues[field_name[len('item-keyvalue-'):]] = field_value
             else:
                 regular_fields[field_name] = field_value
 
-        return category, tags, feedback_type, age_range, regular_fields
+        return category, tags, feedback_type, age_range, regular_fields, keyvalues
 
     def _add_tags(self, item_id, tags):
         for (taxonomy, value) in tags.items():
@@ -307,6 +330,10 @@ class AddEditItemView(FormView):
 
             transport.items.add_terms(item_id, taxonomy, term_names)
 
+    def _update_keyvalues(self, item_id, keyvalues):
+        for (key, value) in keyvalues.items():
+            transport.items.update_keyvalue(item_id, key, value)
+
     def _update_item(self, item_id, form):
         """ Update the given item
 
@@ -321,7 +348,7 @@ class AddEditItemView(FormView):
         """
 
         (category, tags, feedback_type, age_range,
-         regular_fields) = self._separate_form_data(form)
+         regular_fields, keyvalues) = self._separate_form_data(form)
 
         transport.items.update(item_id, regular_fields)
 
@@ -346,6 +373,8 @@ class AddEditItemView(FormView):
         else:
             transport.items.delete_all_terms(item_id, 'age-ranges')
 
+        self._update_keyvalues(item_id, keyvalues)
+
         self._add_tags(item_id, tags)
 
     def _create_item(self, form, taxonomy):
@@ -363,8 +392,10 @@ class AddEditItemView(FormView):
             Raises:
                 TransportException: On API errors
         """
+
+        # keyvalues not supported for form creation
         (category, tags, feedback_type, age_range,
-         regular_fields) = self._separate_form_data(form)
+         regular_fields, _) = self._separate_form_data(form)
 
         if not feedback_type:
             feedback_type = [self.item_type[0]['name']]
diff --git a/internewshid/hid/views/upload_spreadsheet.py b/internewshid/hid/views/upload_spreadsheet.py
index 6e0691d94b99620b7a9dd40d9b7aa4ad00cedf29..c3a6e9e0bfb16746be2a2be33b74d0f815995c98 100644
--- a/internewshid/hid/views/upload_spreadsheet.py
+++ b/internewshid/hid/views/upload_spreadsheet.py
@@ -20,28 +20,34 @@ class UploadSpreadsheetView(FormView):
         source = data['source']
         uploaded_file = data['file']
 
-        try:
-            importer = self.get_importer()
-            (saved, skipped) = importer.store_spreadsheet(
-                source, uploaded_file
-            )
-            all_messages = [
-                gettext("Upload successful!"),
-                ungettext("{0} entry has been added.",
-                          "{0} entries have been added.",
-                          saved).format(saved)
-            ]
-
-            if skipped > 0:
-                all_messages.append(
-                    ungettext("{0} duplicate entry was skipped.",
-                              "{0} duplicate entries were skipped.",
-                              skipped).format(skipped)
+        form_enabled = False
+        if hasattr(self.request, 'user'):
+            if self.request.user.has_perm('data_layer.change_message'):
+                form_enabled = True
+
+        if form_enabled:
+            try:
+                importer = self.get_importer()
+                (saved, skipped) = importer.store_spreadsheet(
+                    source, uploaded_file
                 )
-
-            messages.success(self.request, ' '.join(all_messages))
-        except SheetImportException as exc:
-            messages.error(self.request, str(exc))
+                all_messages = [
+                    gettext("Upload successful!"),
+                    ungettext("{0} entry has been added.",
+                              "{0} entries have been added.",
+                              saved).format(saved)
+                ]
+
+                if skipped > 0:
+                    all_messages.append(
+                        ungettext("{0} duplicate entry was skipped.",
+                                  "{0} duplicate entries were skipped.",
+                                  skipped).format(skipped)
+                    )
+
+                messages.success(self.request, ' '.join(all_messages))
+            except SheetImportException as exc:
+                messages.error(self.request, str(exc))
 
         return HttpResponseRedirect(self.get_success_url())
 
diff --git a/internewshid/local_settings.py.drc-dev b/internewshid/local_settings.py.drc-dev
index d1bb90940fc872e17adca5d1fba134dd1871aaa4..870590def79beb8a620d784f80039977338ae6dd 100644
--- a/internewshid/local_settings.py.drc-dev
+++ b/internewshid/local_settings.py.drc-dev
@@ -1,6 +1,7 @@
 import private_settings
 import json
 
+
 DEBUG = True
 ASSETS_DEBUG = DEBUG
 ASSETS_AUTO_BUILD = DEBUG
diff --git a/internewshid/rest_api/tests/keyvalue_tests.py b/internewshid/rest_api/tests/keyvalue_tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..6c4dd4dcf1357a54670261d9b58a1835fa492e1e
--- /dev/null
+++ b/internewshid/rest_api/tests/keyvalue_tests.py
@@ -0,0 +1,96 @@
+import pytest
+from rest_framework import status
+from rest_framework.test import APIRequestFactory
+
+from ..views import ItemViewSet
+from .item_create_view_tests import create_item
+
+
+def get_item(id):
+    view = ItemViewSet.as_view(actions={'get': 'retrieve'})
+    request = APIRequestFactory().get('')
+
+    return view(request, pk=id)
+
+
+@pytest.mark.django_db
+def test_keyvalue_created():
+    item = create_item(body='Text').data
+
+    kwargs = {
+        'key': 'CONTRIBUTER',
+        'value': 'TWB',
+    }
+
+    request = APIRequestFactory().post('', kwargs)
+    view = ItemViewSet.as_view(actions={'post': 'add_keyvalue'})
+    response = view(request, item_pk=item['id'])
+    assert status.is_success(response.status_code), response.data
+
+    updated_item = get_item(item['id']).data
+
+    values = updated_item['values']
+
+    assert values['CONTRIBUTER'] == 'TWB'
+
+
+@pytest.mark.django_db
+def test_add_keyvalue_raises_if_item_does_not_exist():
+    kwargs = {
+        'key': 'CONTRIBUTER',
+        'value': 'TWB',
+    }
+
+    request = APIRequestFactory().post('', kwargs)
+    view = ItemViewSet.as_view(actions={'post': 'add_keyvalue'})
+
+    unknown_item_id = 9999
+    response = view(request, item_pk=unknown_item_id)
+    assert response.status_code == status.HTTP_404_NOT_FOUND
+    assert response.data['detail'] == "Message matching query does not exist."
+
+
+@pytest.mark.django_db
+def test_keyvalue_updated():
+    item = create_item(body='Text').data
+
+    kwargs = {
+        'key': 'CONTRIBUTER',
+        'value': 'TWB',
+    }
+
+    request = APIRequestFactory().post('', kwargs)
+    view = ItemViewSet.as_view(actions={'post': 'add_keyvalue'})
+    response = view(request, item_pk=item['id'])
+    assert status.is_success(response.status_code), response.data
+
+    kwargs = {
+        'key': 'CONTRIBUTER',
+        'value': 'TWB modified',
+    }
+    request = APIRequestFactory().post('', kwargs)
+    view = ItemViewSet.as_view(actions={'post': 'update_keyvalue'})
+    response = view(request, item_pk=item['id'])
+    assert status.is_success(response.status_code), response.data
+
+    updated_item = get_item(item['id']).data
+
+    values = updated_item['values']
+
+    assert values['CONTRIBUTER'] == 'TWB modified'
+
+
+@pytest.mark.django_db
+def test_update_keyvalue_raises_if_item_does_not_exist():
+    kwargs = {
+        'key': 'CONTRIBUTER',
+        'value': 'TWB',
+    }
+
+    request = APIRequestFactory().post('', kwargs)
+    view = ItemViewSet.as_view(actions={'post': 'update_keyvalue'})
+
+    unknown_item_id = 9999
+    response = view(request, item_pk=unknown_item_id)
+    assert response.status_code == status.HTTP_404_NOT_FOUND
+    assert response.data['detail'] == "Message matching query does not exist."
diff --git a/internewshid/rest_api/views.py b/internewshid/rest_api/views.py
index 3d380834a293764bc780f4f87656a760f40b99ce..c0e9c53732964a49280128b243335b300340a2d8 100644
--- a/internewshid/rest_api/views.py
+++ b/internewshid/rest_api/views.py
@@ -193,7 +193,23 @@ class ItemViewSet(viewsets.ModelViewSet, BulkDestroyModelMixin):
 
         keyvalue = request.data
         key, _ = Key.objects.get_or_create(key=keyvalue['key'])
-        value = Value.objects.create(key=key, value=keyvalue['value'], message=item)
+        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 update_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
+
+        values = Value.objects.filter(key__key=keyvalue['key'], message=item)
+        values.update(value=keyvalue['value'])
 
         serializer = ItemSerializer(item)
         return Response(serializer.data, status=status.HTTP_200_OK)
diff --git a/internewshid/settings.py b/internewshid/settings.py
index 86a1dc854d798f0c5e45f9464050e6aee4364f74..57666a930bab7ccbe576888c2c9cb7131b99ff1a 100644
--- a/internewshid/settings.py
+++ b/internewshid/settings.py
@@ -1,6 +1,7 @@
 import collections
 import json
 import re
+import sys
 import warnings
 from copy import deepcopy
 from os import path
@@ -13,6 +14,8 @@ warnings.filterwarnings(
     message='Unable to import floppyforms.gis'
 )
 
+WE_ARE_TESTING = "pytest" in sys.modules
+
 BASE_DIR = path.abspath(path.dirname(__file__))
 
 DEBUG = False
@@ -189,7 +192,11 @@ LOGGING = {
 # the utf8mb4 encoding/collation whereby the constance model
 # must be overriden. We set the max_length to 190 instead of 255.
 # See https://github.com/jazzband/django-constance/issues/121
-CONSTANCE_BACKEND = 'data_layer.models.CustomConstanceBackend'
+
+if WE_ARE_TESTING:
+    CONSTANCE_BACKEND = 'data_layer.models.CustomConstanceTestBackend'
+else:
+    CONSTANCE_BACKEND = 'data_layer.models.CustomConstanceBackend'
 
 CONSTANCE_CONFIG = {
     'CONTEXT_LOCATION': ("Cox's Bazaar, Bangladesh",
diff --git a/internewshid/tabbed_page/views.py b/internewshid/tabbed_page/views.py
index eebbd535a7d2dc531263b3d987a5705a3a0aca66..00757fbaf218b80b5a0a8e0165194a696f7eba32 100644
--- a/internewshid/tabbed_page/views.py
+++ b/internewshid/tabbed_page/views.py
@@ -40,3 +40,14 @@ class TabbedPageView(TemplateView):
             if len(candidates) > 0:
                 self._active_tab = candidates[0]
         return self._active_tab
+
+    def get_context_data(self, *args, **kwargs):
+        context = super(TabbedPageView, self).get_context_data(*args, **kwargs) or {}
+
+        form_disabled = True
+        if hasattr(self.request, 'user'):
+            if self.request.user.has_perm('data_layer.change_message'):
+                form_disabled = False
+
+        context['form_disabled'] = form_disabled
+        return context
diff --git a/internewshid/templates/base_side.html b/internewshid/templates/base_side.html
index 3373243236367b3fa8f2dbea31fab288722218ac..1bca7eb873f929edae8288a1810c753cc19c1697 100644
--- a/internewshid/templates/base_side.html
+++ b/internewshid/templates/base_side.html
@@ -29,7 +29,7 @@
 
       <ul class="nav navbar-nav navbar-right nav-utilities">
 
-        {% if request.user.is_authenticated %}
+        {% if request.user.is_authenticated and not form_disabled %}
         {% url 'tabbed-page' config.DEFAULT_TABBED_PAGE_NAME config.DEFAULT_TAB_NAME as next_url %}
         {% trans 'data' as type_label %}
         <li class="dropdown">
@@ -42,7 +42,7 @@
         </li>
         {% endif %}
 
-        {% if request.user.is_authenticated %}
+        {% if request.user.is_authenticated and not form_disabled %}
         <li class="dropdown">
             <a class="dropdown-toggle" data-toggle="dropdown" href="#">
              {% trans "Export data" %} <span class="fa fa-caret-down"> </span>
@@ -54,7 +54,7 @@
             <!-- /.dropdown-user -->
         </li>
         {% endif %}
-        
+
         {% if request.user.is_authenticated %}
         <li class="dropdown">
             <a class="dropdown-toggle" data-toggle="dropdown" href="#">
@@ -62,7 +62,9 @@
             </a>
             <ul class="dropdown-menu dropdown-user">
                 <li><a href="{% url "tabbed-page" name="main" tab_name="all" %}"><span class="fa fa-user fa-fw"></span> {% blocktrans %}Your Account {{ request.user }}{% endblocktrans %}</a></li>
-                <li><a href="{% url "admin:index" %}"><span class="fa fa-gear fa-fw"></span>{% trans "Administration" %}</a></li>
+                {% if request.user.is_staff %}
+                    <li><a href="{% url "admin:index" %}"><span class="fa fa-gear fa-fw"></span>{% trans "Administration" %}</a></li>
+                {% endif %}
                 <li role="separator" class="divider"></li>
                 <li><a href="{% url "logout" %}?next={% url 'login' %}"><span class="fa fa-sign-out fa-fw"></span>{% trans "Log out" %}</a></li>
             </ul>
diff --git a/internewshid/transport/items.py b/internewshid/transport/items.py
index d45cb1d22de25d033d1de168a11d1659241062d0..0e390bf7f5111a7477f3a2d4f284f5e1f8a75a1d 100644
--- a/internewshid/transport/items.py
+++ b/internewshid/transport/items.py
@@ -203,6 +203,23 @@ def add_keyvalue(item_id, key, value):
     raise TransportException(response.data)
 
 
+def update_keyvalue(item_id, key, value):
+    view = get_view({'post': 'update_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):
     view = get_view({'post': 'delete_all_terms'})
 
diff --git a/internewshid/transport/tests/item_detail_tests.py b/internewshid/transport/tests/item_detail_tests.py
index 93f6577ec2d27485c420cc6430c938d254013bc3..74ffd351c8661f3a93db72b15d5cf78c27fc9624 100644
--- a/internewshid/transport/tests/item_detail_tests.py
+++ b/internewshid/transport/tests/item_detail_tests.py
@@ -1,5 +1,7 @@
 from datetime import datetime
 
+from django.utils.translation import ugettext as _
+
 import pytest
 
 from .. import items
@@ -24,7 +26,7 @@ def test_get_item_throws_exception_for_unknown_id():
     with pytest.raises(TransportException) as excinfo:
         items.get(UNKNOWN_ITEM_ID)
 
-    assert excinfo.value.message['detail'] == 'Not found.'
+    assert excinfo.value.message['detail'] == _('Not found.')
 
 
 @pytest.mark.django_db
diff --git a/internewshid/transport/tests/item_keyvalue_tests.py b/internewshid/transport/tests/item_keyvalue_tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..8d669089fb38bf44b87a4b25cba0438e82d58783
--- /dev/null
+++ b/internewshid/transport/tests/item_keyvalue_tests.py
@@ -0,0 +1,54 @@
+import pytest
+
+from transport import items
+
+from ..exceptions import TransportException
+
+
+@pytest.fixture
+def item_data():
+    item = {'body': "What is the cuse of ebola?"}
+    return items.create(item)
+
+
+@pytest.mark.django_db
+def test_keyvalues_can_be_added_to_item(item_data):
+    item_id = item_data['id']
+    response = items.add_keyvalue(item_id, 'CONTRIBUTER', 'TWB')
+
+    assert response['values'] == {'CONTRIBUTER': 'TWB'}
+
+
+@pytest.mark.django_db
+def test_keyvalues_can_be_updated_on_item(item_data):
+    item_id = item_data['id']
+    response = items.add_keyvalue(item_id, 'CONTRIBUTER', 'TWB')
+    response = items.update_keyvalue(item_id, 'CONTRIBUTER', 'TWB modified')
+
+    assert response['values'] == {'CONTRIBUTER': 'TWB modified'}
+
+
+@pytest.mark.django_db
+def test_add_raises_when_item_does_not_exist():
+    unknown_item_id = 9999
+    with pytest.raises(TransportException) as excinfo:
+        items.add_keyvalue(unknown_item_id, 'CONTRIBUTER', 'TWB modified')
+
+        assert excinfo.value.message['status_code'] == 404
+        assert excinfo.value.message['value'] == {
+            'CONTRIBUTER': 'TWB modified'
+        }
+        assert excinfo.value.message['item_id'] == 9999
+
+
+@pytest.mark.django_db
+def test_update_raises_when_item_does_not_exist():
+    unknown_item_id = 9999
+    with pytest.raises(TransportException) as excinfo:
+        items.update_keyvalue(unknown_item_id, 'CONTRIBUTER', 'TWB modified')
+
+        assert excinfo.value.message['status_code'] == 404
+        assert excinfo.value.message['value'] == {
+            'CONTRIBUTER': 'TWB modified'
+        }
+        assert excinfo.value.message['item_id'] == 9999
diff --git a/internewshid/transport/tests/item_list_options_tests.py b/internewshid/transport/tests/item_list_options_tests.py
index 1d509cd7f47f6dd413b71cf0a45a1c4972520f7b..810ed871bbe58ce5355bb6731124a791ab985349 100644
--- a/internewshid/transport/tests/item_list_options_tests.py
+++ b/internewshid/transport/tests/item_list_options_tests.py
@@ -1,117 +1,38 @@
 import pytest
-from django_dynamic_fixture import G
 
-from data_layer.models import Item
+from data_layer.tests.factories import ItemFactory
 from transport.items import list_options
 
 
-@pytest.mark.django_db
-def test_list_options_for_gender_retrieved_all_options():
-    items = G(Item, n=5)
-    expected_genders = [an_item.gender for an_item in items]
-    actual_genders = list(list_options('gender'))
-    assert expected_genders == actual_genders
-
-
 @pytest.mark.django_db
 def test_list_options_for_gender_unique():
-    G(Item, gender='male', n=2)
-    G(Item, gender='female', n=2)
-    G(Item, gender='xie')
-    expected_genders = ['female', 'male', 'xie']
-    actual_genders = list(list_options('gender'))
-    assert expected_genders == actual_genders
+    ItemFactory(gender='male')
+    ItemFactory(gender='male')
+    ItemFactory(gender='female')
+    ItemFactory(gender='female')
+    ItemFactory(gender='other')
+
+    genders = list(list_options('gender'))
+    assert genders == ['female', 'male', 'other']
 
 
 @pytest.mark.django_db
 def test_list_options_for_gender_exclude_blank():
-    G(Item, gender='', n=2)
-    G(Item, gender='female', n=2)
-    G(Item, gender='xie')
-    expected_genders = ['female', 'xie']
-    actual_genders = list(list_options('gender'))
-    assert expected_genders == actual_genders
+    ItemFactory(gender='')
+    ItemFactory(gender='female')
+    ItemFactory(gender='xie')
 
-
-@pytest.mark.django_db
-def test_list_options_for_location_retrieved_all_options():
-    items = G(Item, n=5)
-    expected_locations = [an_item.location for an_item in items]
-    actual_locations = list(list_options('location'))
-    assert expected_locations == actual_locations
+    genders = list(list_options('gender'))
+    assert '' not in genders
 
 
 @pytest.mark.django_db
 def test_list_options_for_location_unique():
-    G(Item, location='Cambridge', n=2)
-    G(Item, location='Brighton', n=2)
-    G(Item, location='London')
-    expected_locations = ['Brighton', 'Cambridge', 'London']
-    actual_locations = list(list_options('location'))
-    assert expected_locations == actual_locations
-
+    ItemFactory(location='Cambridge')
+    ItemFactory(location='Cambridge')
+    ItemFactory(location='Brighton')
+    ItemFactory(location='Brighton')
+    ItemFactory(location='London')
 
-@pytest.mark.django_db
-def test_list_options_for_location_exclude_blank():
-    G(Item, location='', n=2)
-    G(Item, location='Brighton', n=2)
-    G(Item, location='London')
-    expected_locations = ['Brighton', 'London']
     actual_locations = list(list_options('location'))
-    assert expected_locations == actual_locations
-
-
-@pytest.mark.django_db
-def test_list_options_for_contributor_retrieved_all_options():
-    items = G(Item, n=5)
-    expected_contributors = [an_item.contributor for an_item in items]
-    actual_contributors = list(list_options('contributor'))
-    assert expected_contributors == actual_contributors
-
-
-@pytest.mark.django_db
-def test_list_options_for_contributor_unique():
-    G(Item, contributor='Rojina Akter', n=2)
-    G(Item, contributor='Nur Ankis', n=2)
-    G(Item, contributor='Rashada')
-    expected_contributors = ['Nur Ankis', 'Rashada', 'Rojina Akter', ]
-    actual_contributors = list(list_options('contributor'))
-    assert expected_contributors == actual_contributors
-
-
-@pytest.mark.django_db
-def test_list_options_for_contributor_exclude_blank():
-    items = G(Item, contributor='', n=2)
-    items.extend(G(Item, contributor='Nur Ankis', n=2))
-    items.append(G(Item, contributor='Rashada'))
-    expected_contributors = ['Nur Ankis', 'Rashada']
-    actual_contributors = list(list_options('contributor'))
-    assert expected_contributors == actual_contributors
-
-
-@pytest.mark.django_db
-def test_list_options_for_collection_type_retrieved_all_options():
-    items = G(Item, n=5)
-    expected_collection_types = [an_item.collection_type for an_item in items]
-    actual_collection_types = list(list_options('collection_type'))
-    assert expected_collection_types == actual_collection_types
-
-
-@pytest.mark.django_db
-def test_list_options_for_collection_type_unique():
-    G(Item, collection_type='Rojina Akter', n=2)
-    G(Item, collection_type='Nur Ankis', n=2)
-    G(Item, collection_type='Rashada')
-    expected_collection_types = ['Nur Ankis', 'Rashada', 'Rojina Akter']
-    actual_collection_types = list(list_options('collection_type'))
-    assert expected_collection_types == actual_collection_types
-
-
-@pytest.mark.django_db
-def test_list_options_for_collection_type_exclude_blank():
-    items = G(Item, collection_type='', n=2)
-    items.extend(G(Item, collection_type='Nur Ankis', n=2))
-    items.append(G(Item, collection_type='Rashada'))
-    expected_collection_types = ['Nur Ankis', 'Rashada']
-    actual_collection_types = list(list_options('collection_type'))
-    assert expected_collection_types == actual_collection_types
+    assert actual_locations == ['Brighton', 'Cambridge', 'London']