From dd64130df856df81c0ad53fd1af38cbf6b6cd5fd Mon Sep 17 00:00:00 2001
From: Luke Murphy <lukewm@riseup.net>
Date: Wed, 24 Oct 2018 14:07:15 +0200
Subject: [PATCH] First stab at some location coverage.

---
 Pipfile                                       |   1 +
 Pipfile.lock                                  | 155 +++++++++++++-----
 internewshid/rest_api/serializers.py          |  26 +++
 .../rest_api/tests/location_coverage_tests.py |  46 ++++++
 internewshid/rest_api/urls.py                 |  15 +-
 internewshid/rest_api/views.py                |  10 +-
 6 files changed, 204 insertions(+), 49 deletions(-)
 create mode 100644 internewshid/rest_api/tests/location_coverage_tests.py

diff --git a/Pipfile b/Pipfile
index 73192b29..5fdb2388 100644
--- a/Pipfile
+++ b/Pipfile
@@ -30,6 +30,7 @@ openpyxl = "*"
 python-dateutil = "*"
 pytz = "*"
 django-constance = {version = "*", extras = ["database"]}
+rest-pandas = "*"
 
 [dev-packages]
 factory_boy = "*"
diff --git a/Pipfile.lock b/Pipfile.lock
index 4650e0fe..9e4d6d2f 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "2aa92a301153dd63c052458b6505a6e5e864ea6ab28df9aa9b93d34d9f869ee5"
+            "sha256": "179591b179de506537efcf057cc9b1fd3fb4e25ac6a67a124d268c38d1a4ab69"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -99,10 +99,10 @@
         },
         "django-tables2": {
             "hashes": [
-                "sha256:e601af8a63d053c0b12c1b9f883f14f081b9cbc313cbaa3633a96b6e136e05e5"
+                "sha256:0f39ef59a03a2bba1e8fc2fa5af5ebf45f98a078f1035197957965be2d76faea"
             ],
             "index": "pypi",
-            "version": "==2.0.1"
+            "version": "==2.0.2"
         },
         "django-widget-tweaks": {
             "hashes": [
@@ -119,11 +119,11 @@
         },
         "djangorestframework": {
             "hashes": [
-                "sha256:b6714c3e4b0f8d524f193c91ecf5f5450092c2145439ac2769711f7eba89a9d9",
-                "sha256:c375e4f95a3a64fccac412e36fb42ba36881e52313ec021ef410b40f67cddca4"
+                "sha256:607865b0bb1598b153793892101d881466bd5a991de12bd6229abb18b1c86136",
+                "sha256:63f76cbe1e7d12b94c357d7e54401103b2e52aef0f7c1650d6c820ad708776e5"
             ],
             "index": "pypi",
-            "version": "==3.8.2"
+            "version": "==3.9.0"
         },
         "djangorestframework-bulk": {
             "hashes": [
@@ -175,12 +175,71 @@
             "index": "pypi",
             "version": "==1.2.5"
         },
+        "numpy": {
+            "hashes": [
+                "sha256:032df9b6571c5f1d41ea6f6a189223208cb488990373aa686aca55570fcccb42",
+                "sha256:094f8a83e5bd0a44a7557fa24a46db6ba7d5299c389ddbc9e0e18722f567fb63",
+                "sha256:1c0c80e74759fa4942298044274f2c11b08c86230b25b8b819e55e644f5ff2b6",
+                "sha256:2aa0910eaeb603b1a5598193cc3bc8eacf1baf6c95cbc3955eb8e15fa380c133",
+                "sha256:2f5ebc7a04885c7d69e5daa05208faef4db7f1ae6a99f4d36962df8cd54cdc76",
+                "sha256:32a07241cb624e104b88b08dea2851bf4ec5d65a1f599d7735041ced7171fd7a",
+                "sha256:3c7959f750b54b445f14962a3ddc41b9eadbab00b86da55fbb1967b2b79aad10",
+                "sha256:3d8f9273c763a139a99e65c2a3c10f1109df30bedae7f011b10d95c538364704",
+                "sha256:63bca71691339d2d6f8a7c970821f2b12098a53afccc0190d4e1555e75e5223a",
+                "sha256:7ae9c3baff3b989859c88e0168ad10902118595b996bf781eaf011bb72428798",
+                "sha256:866a7c8774ccc7d603667fad95456b4cf56d79a2bb5a7648ac9f0082e0b9416e",
+                "sha256:8bc4b92a273659e44ca3f3a2f8786cfa39d8302223bcfe7df794429c63d5f5a1",
+                "sha256:919f65e0732195474897b1cafefb4d4e7c2bb8174a725e506b62e9096e4df28d",
+                "sha256:9d1598573d310104acb90377f0a8c2319f737084689f5eb18012becaf345cda5",
+                "sha256:9fff90c88bfaad2901be50453d5cd7897a826c1d901f0654ee1d73ab3a48cd18",
+                "sha256:a245464ddf6d90e2d6287e9cef6bcfda2a99467fdcf1b677b99cd0b6c7b43de2",
+                "sha256:a988db28f54e104a01e8573ceb6f28202b4c15635b1450b2e3b2b822c6564f9b",
+                "sha256:b12fe6f31babb9477aa0f9692730654b3ee0e71f33b4568170dfafd439caf0a2",
+                "sha256:b7599ff4acd23f5de983e3aec772153b1043e131487a5c6ad0f94b41a828877a",
+                "sha256:c9f4dafd6065c4c782be84cd67ceeb9b1d4380af60a7af32be10ebecd723385e",
+                "sha256:ce3622b73ccd844ba301c1aea65d36cf9d8331e7c25c16b1725d0f14db99aaf4",
+                "sha256:d0f36a24cf8061a2c03e151be3418146717505b9b4ec17502fa3bbdb04ec1431",
+                "sha256:d263f8f14f2da0c079c0297e829e550d8f2c4e0ffef215506bd1d0ddd2bff3de",
+                "sha256:d8837ff272800668aabdfe70b966631914b0d6513aed4fc1b1428446f771834d",
+                "sha256:ef694fe72a3995aa778a5095bda946e0d31f7efabd5e8063ad8c6238ab7d3f78",
+                "sha256:f1fd1a6f40a501ba4035f5ed2c1f4faa68245d1407bf97d2ee401e4f23d1720b",
+                "sha256:fa337b6bd5fe2b8c4e705f4102186feb9985de9bb8536d32d5129a658f1789e0",
+                "sha256:febd31cd0d2fd2509ca2ec53cb339f8bf593c1bd245b9fc55c1917a68532a0af"
+            ],
+            "markers": "python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.3.*'",
+            "version": "==1.15.3"
+        },
         "openpyxl": {
             "hashes": [
-                "sha256:22904d7bdfaaab33d65d50a0915a65eeb2f29c85d9ec53081563850678a29927"
+                "sha256:022c0f3fa1e873cc0ba20651c54dd5e6276fc4ff150b4060723add4fc448645e"
             ],
             "index": "pypi",
-            "version": "==2.5.8"
+            "version": "==2.5.9"
+        },
+        "pandas": {
+            "hashes": [
+                "sha256:11975fad9edbdb55f1a560d96f91830e83e29bed6ad5ebf506abda09818eaf60",
+                "sha256:12e13d127ca1b585dd6f6840d3fe3fa6e46c36a6afe2dbc5cb0b57032c902e31",
+                "sha256:1c87fcb201e1e06f66e23a61a5fea9eeebfe7204a66d99df24600e3f05168051",
+                "sha256:242e9900de758e137304ad4b5663c2eff0d798c2c3b891250bd0bd97144579da",
+                "sha256:26c903d0ae1542890cb9abadb4adcb18f356b14c2df46e4ff657ae640e3ac9e7",
+                "sha256:2e1e88f9d3e5f107b65b59cd29f141995597b035d17cc5537e58142038942e1a",
+                "sha256:31b7a48b344c14691a8e92765d4023f88902ba3e96e2e4d0364d3453cdfd50db",
+                "sha256:4fd07a932b4352f8a8973761ab4e84f965bf81cc750fb38e04f01088ab901cb8",
+                "sha256:5b24ca47acf69222e82530e89111dd9d14f9b970ab2cd3a1c2c78f0c4fbba4f4",
+                "sha256:647b3b916cc8f6aeba240c8171be3ab799c3c1b2ea179a3be0bd2712c4237553",
+                "sha256:66b060946046ca27c0e03e9bec9bba3e0b918bafff84c425ca2cc2e157ce121e",
+                "sha256:6efa9fa6e1434141df8872d0fa4226fc301b17aacf37429193f9d70b426ea28f",
+                "sha256:be4715c9d8367e51dbe6bc6d05e205b1ae234f0dc5465931014aa1c4af44c1ba",
+                "sha256:bea90da782d8e945fccfc958585210d23de374fa9294a9481ed2abcef637ebfc",
+                "sha256:d318d77ab96f66a59e792a481e2701fba879e1a453aefeebdb17444fe204d1ed",
+                "sha256:d785fc08d6f4207437e900ffead930a61e634c5e4f980ba6d3dc03c9581748c7",
+                "sha256:de9559287c4fe8da56e8c3878d2374abc19d1ba2b807bfa7553e912a8e5ba87c",
+                "sha256:f4f98b190bb918ac0bc0e3dd2ab74ff3573da9f43106f6dba6385406912ec00f",
+                "sha256:f71f1a7e2d03758f6e957896ed696254e2bc83110ddbc6942018f1a232dd9dad",
+                "sha256:fb944c8f0b0ab5c1f7846c686bc4cdf8cde7224655c12edcd59d5212cd57bec0"
+            ],
+            "version": "==0.23.4"
         },
         "pillow": {
             "hashes": [
@@ -221,19 +280,26 @@
         },
         "python-dateutil": {
             "hashes": [
-                "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0",
-                "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"
+                "sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93",
+                "sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"
             ],
             "index": "pypi",
-            "version": "==2.7.3"
+            "version": "==2.7.5"
         },
         "pytz": {
             "hashes": [
-                "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
-                "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
+                "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca",
+                "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6"
+            ],
+            "index": "pypi",
+            "version": "==2018.7"
+        },
+        "rest-pandas": {
+            "hashes": [
+                "sha256:cdfbc1baa5927f833e986b586f751b677a38d0360d827291937ee68a0ed7e105"
             ],
             "index": "pypi",
-            "version": "==2018.5"
+            "version": "==1.0.0"
         },
         "six": {
             "hashes": [
@@ -274,6 +340,7 @@
             "hashes": [
                 "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba",
                 "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed",
+                "sha256:0bf8cbbd71adfff0ef1f3a1531e6402d13b7b01ac50a79c97ca15f030dba6306",
                 "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95",
                 "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640",
                 "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd",
@@ -302,6 +369,7 @@
                 "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd",
                 "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d",
                 "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6",
+                "sha256:f05a636b4564104120111800021a92e43397bc12a5c72fed7036be8556e0029e",
                 "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80"
             ],
             "version": "==4.5.1"
@@ -339,17 +407,17 @@
         },
         "faker": {
             "hashes": [
-                "sha256:74b32991f8e08e4f2f84858b919eca253becfaec4b3fa5fcff7fdbd70d5d78b1",
-                "sha256:c2ce42dd8361e6d392276006d757532562463c8642b1086709584200b7fd7758"
+                "sha256:2621643b80a10b91999925cfd20f64d2b36f20bf22136bbdc749bb57d6ffe124",
+                "sha256:5ed822d31bd2d6edf10944d176d30dc9c886afdd381eefb7ba8b7aad86171646"
             ],
-            "version": "==0.9.1"
+            "version": "==0.9.2"
         },
         "funcsigs": {
             "hashes": [
                 "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca",
                 "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50"
             ],
-            "markers": "python_version < '3.3'",
+            "markers": "python_version < '3.0'",
             "version": "==1.0.2"
         },
         "futures": {
@@ -409,25 +477,26 @@
         },
         "pbr": {
             "hashes": [
-                "sha256:1be135151a0da949af8c5d0ee9013d9eafada71237eb80b3ba8896b4f12ec5dc",
-                "sha256:cf36765bf2218654ae824ec8e14257259ba44e43b117fd573c8d07a9895adbdd"
+                "sha256:8fc938b1123902f5610b06756a31b1e6febf0d105ae393695b0c9d4244ed2910",
+                "sha256:f20ec0abbf132471b68963bb34d9c78e603a5cf9e24473f14358e66551d47475"
             ],
-            "version": "==4.3.0"
+            "version": "==5.1.0"
         },
         "pluggy": {
             "hashes": [
-                "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1",
-                "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"
+                "sha256:447ba94990e8014ee25ec853339faf7b0fc8050cdc3289d4d71f7f410fb90095",
+                "sha256:bde19360a8ec4dfd8a20dcb811780a30998101f078fc7ded6162f0076f50508f"
             ],
-            "version": "==0.7.1"
+            "markers": "python_version >= '2.7' and python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.2.*' and python_version != '3.3.*'",
+            "version": "==0.8.0"
         },
         "py": {
             "hashes": [
-                "sha256:06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1",
-                "sha256:50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6"
+                "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
+                "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
             ],
             "index": "pypi",
-            "version": "==1.6.0"
+            "version": "==1.7.0"
         },
         "pycodestyle": {
             "hashes": [
@@ -438,11 +507,11 @@
         },
         "pydocstyle": {
             "hashes": [
-                "sha256:08a870edc94508264ed90510db466c6357c7192e0e866561d740624a8fc7d90c",
-                "sha256:4d5bcde961107873bae621f3d580c3e35a426d3687ffc6f8fb356f6628da5a97",
-                "sha256:af9fcccb303899b83bec82dc9a1d56c60fc369973223a5e80c3dfa9bdf984405"
+                "sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8",
+                "sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4",
+                "sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039"
             ],
-            "version": "==2.1.1"
+            "version": "==3.0.0"
         },
         "pyflakes": {
             "hashes": [
@@ -453,18 +522,18 @@
         },
         "pylava": {
             "hashes": [
-                "sha256:20d12927f866c2d31c7d16cc4201a02c36ad26212f36f13e6725e0c40502520e",
-                "sha256:667320f3a9eab9f3528b6a83e4e7e92eb8b4791a654402b2f48fd927f01a81a1"
+                "sha256:b2fc881e9112a36f5a8a97bdbbedfa1704bbb65c34afa630e6df31532ae20994",
+                "sha256:dcdecb6668cb8c4a38e0ee4fdb5f4d9488793af57bc040d89b0d1142f13b8622"
             ],
             "index": "pypi",
-            "version": "==0.2.1"
+            "version": "==0.2.2"
         },
         "pytest": {
             "hashes": [
-                "sha256:7e258ee50338f4e46957f9e09a0f10fb1c2d05493fa901d113a8dafd0790de4e",
-                "sha256:9332147e9af2dcf46cd7ceb14d5acadb6564744ddff1fe8c17f0ce60ece7d9a2"
+                "sha256:a9e5e8d7ab9d5b0747f37740276eb362e6a76275d76cebbb52c6049d93b475db",
+                "sha256:bf47e8ed20d03764f963f0070ff1c8fda6e2671fc5dd562a4d3b7148ad60f5ca"
             ],
-            "version": "==3.8.2"
+            "version": "==3.9.3"
         },
         "pytest-cov": {
             "hashes": [
@@ -498,19 +567,19 @@
         },
         "python-dateutil": {
             "hashes": [
-                "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0",
-                "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8"
+                "sha256:063df5763652e21de43de7d9e00ccf239f953a832941e37be541614732cdfc93",
+                "sha256:88f9287c0174266bb0d8cedd395cfba9c58e87e5ad86b2ce58859bc11be3cf02"
             ],
             "index": "pypi",
-            "version": "==2.7.3"
+            "version": "==2.7.5"
         },
         "pytz": {
             "hashes": [
-                "sha256:a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053",
-                "sha256:ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"
+                "sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca",
+                "sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6"
             ],
             "index": "pypi",
-            "version": "==2018.5"
+            "version": "==2018.7"
         },
         "scandir": {
             "hashes": [
diff --git a/internewshid/rest_api/serializers.py b/internewshid/rest_api/serializers.py
index c9ad5d11..7963643b 100644
--- a/internewshid/rest_api/serializers.py
+++ b/internewshid/rest_api/serializers.py
@@ -36,6 +36,17 @@ class TermSerializer(serializers.ModelSerializer):
     )
 
 
+class LocationCoverageTermSerializer(TermSerializer):
+    class Meta:
+        model = Term
+        fields = (
+            'long_name',
+        )
+
+    def to_representation(self, instance):
+        return instance.long_name
+
+
 class TermItemCountSerializer(serializers.ModelSerializer):
     class Meta:
         model = Term
@@ -88,3 +99,18 @@ class ItemSerializer(serializers.ModelSerializer):
         item.save()
 
         return item
+
+
+class LocationCoverageSerializer(ItemSerializer):
+    terms = LocationCoverageTermSerializer(many=True)
+
+    # https://github.com/wq/django-rest-pandas#date-formatting
+    timestamp = serializers.DateField(format=None)
+
+    class Meta:
+        model = Item
+        fields = (
+            'location',
+            'terms',
+            'timestamp',
+        )
diff --git a/internewshid/rest_api/tests/location_coverage_tests.py b/internewshid/rest_api/tests/location_coverage_tests.py
new file mode 100644
index 00000000..ecc29bc4
--- /dev/null
+++ b/internewshid/rest_api/tests/location_coverage_tests.py
@@ -0,0 +1,46 @@
+import csv
+from StringIO import StringIO
+
+from django.urls import reverse
+from django.utils import timezone
+
+import pytest
+from rest_framework.test import APIClient
+
+from data_layer.models import Item
+from taxonomies.models import Taxonomy, Term
+
+
+@pytest.fixture
+def client(django_user_model):
+    client = APIClient()
+    admin = django_user_model.objects.create(is_staff=True)
+    client.force_login(user=admin)
+    return client
+
+
+def test_location_coverage(client):
+    NOW = timezone.now().replace(microsecond=0)
+
+    taxonomy = Taxonomy.objects.create(name='topicbaz')
+    item1 = Item.objects.create(location='locationfoo', timestamp=NOW)
+
+    term = Term.objects.create(taxonomy=taxonomy, long_name='termbar')
+    item1.terms.add(term)
+    item1.save()
+
+    item2 = Item.objects.create()
+
+    response = client.get(reverse('location-coverage'))
+    assert response.accepted_media_type == 'text/csv'
+    reader = csv.DictReader(StringIO(response.content))
+
+    assert reader.fieldnames == ['row', 'location', 'terms', 'timestamp']
+
+    rendered = [item for item in reader]
+
+    assert rendered[0]['location'] == 'locationfoo'
+    assert rendered[0]['terms'] == '[termbar]'
+
+    assert rendered[1]['location'] == ''
+    assert rendered[1]['terms'] == '[]'
diff --git a/internewshid/rest_api/urls.py b/internewshid/rest_api/urls.py
index 1f25ac02..af86d100 100644
--- a/internewshid/rest_api/urls.py
+++ b/internewshid/rest_api/urls.py
@@ -1,8 +1,10 @@
-from django.conf import settings
+from django.conf.urls import url
 
 from rest_framework import routers
 
-from .views import ItemViewSet, TaxonomyViewSet, TermViewSet
+from .views import (
+    ItemViewSet, LocationCoverageView, TaxonomyViewSet, TermViewSet
+)
 
 router = routers.SimpleRouter()
 router.register(
@@ -19,5 +21,10 @@ router.register(
     TermViewSet,
 )
 
-
-urlpatterns = router.urls if settings.DEBUG else []
+urlpatterns = [
+    url(
+        r'location-coverage/$',
+        LocationCoverageView.as_view(),
+        name='location-coverage'
+    ),
+] + router.urls
diff --git a/internewshid/rest_api/views.py b/internewshid/rest_api/views.py
index 21b7b10a..556e586c 100644
--- a/internewshid/rest_api/views.py
+++ b/internewshid/rest_api/views.py
@@ -5,13 +5,14 @@ from rest_framework import status, viewsets
 from rest_framework.decorators import action
 from rest_framework.response import Response
 from rest_framework_bulk.mixins import BulkDestroyModelMixin
+from rest_pandas import PandasView
 
 from data_layer.models import Item
 from taxonomies.models import Taxonomy, Term
 
 from .serializers import (
-    ItemSerializer, TaxonomySerializer, TermItemCountSerializer,
-    TermSerializer
+    ItemSerializer, LocationCoverageSerializer, TaxonomySerializer,
+    TermItemCountSerializer, TermSerializer
 )
 
 
@@ -194,3 +195,8 @@ class TermViewSet(viewsets.ModelViewSet):
             items = items.filter(taxonomy__slug=taxonomy_slug)
 
         return items
+
+
+class LocationCoverageView(PandasView):
+    queryset = Item.objects.all()
+    serializer_class = LocationCoverageSerializer
-- 
GitLab