From 62474d6150776f47496746c07b15e19e22ab63c9 Mon Sep 17 00:00:00 2001 From: capooti Date: Thu, 29 Nov 2018 17:42:54 -0500 Subject: [PATCH] Port the worldmap map notes application. Fixes #4101 --- geonode/contrib/worldmap/mapnotes/__init__.py | 0 geonode/contrib/worldmap/mapnotes/admin.py | 12 +++ .../mapnotes/migrations/0001_initial.py | 29 ++++++ .../worldmap/mapnotes/migrations/__init__.py | 0 geonode/contrib/worldmap/mapnotes/models.py | 19 ++++ .../templates/mapnotes/annotation.html | 71 ++++++++++++++ geonode/contrib/worldmap/mapnotes/tests.py | 95 +++++++++++++++++++ geonode/contrib/worldmap/mapnotes/urls.py | 8 ++ geonode/contrib/worldmap/mapnotes/views.py | 86 +++++++++++++++++ geonode/settings.py | 1 + geonode/social/signals.py | 2 +- geonode/urls.py | 1 + requirements.txt | 1 + 13 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 geonode/contrib/worldmap/mapnotes/__init__.py create mode 100644 geonode/contrib/worldmap/mapnotes/admin.py create mode 100644 geonode/contrib/worldmap/mapnotes/migrations/0001_initial.py create mode 100644 geonode/contrib/worldmap/mapnotes/migrations/__init__.py create mode 100644 geonode/contrib/worldmap/mapnotes/models.py create mode 100644 geonode/contrib/worldmap/mapnotes/templates/mapnotes/annotation.html create mode 100644 geonode/contrib/worldmap/mapnotes/tests.py create mode 100644 geonode/contrib/worldmap/mapnotes/urls.py create mode 100644 geonode/contrib/worldmap/mapnotes/views.py diff --git a/geonode/contrib/worldmap/mapnotes/__init__.py b/geonode/contrib/worldmap/mapnotes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/contrib/worldmap/mapnotes/admin.py b/geonode/contrib/worldmap/mapnotes/admin.py new file mode 100644 index 00000000000..45c12b7cb89 --- /dev/null +++ b/geonode/contrib/worldmap/mapnotes/admin.py @@ -0,0 +1,12 @@ +from .models import MapNote +from django.contrib import admin + + +class MapNoteAdmin(admin.ModelAdmin): + list_display = ('id', 'map', 'title', 'content', 'owner', 'created_dttm', 'modified_dttm') + date_hierarchy = 'created_dttm' + search_fields = ['title', 'content'] + ordering = ('-created_dttm',) + + +admin.site.register(MapNote, MapNoteAdmin) diff --git a/geonode/contrib/worldmap/mapnotes/migrations/0001_initial.py b/geonode/contrib/worldmap/mapnotes/migrations/0001_initial.py new file mode 100644 index 00000000000..7eed7d45437 --- /dev/null +++ b/geonode/contrib/worldmap/mapnotes/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.16 on 2018-11-29 19:08 +from __future__ import unicode_literals + +from django.conf import settings +import django.contrib.gis.db.models.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + operations = [ + migrations.CreateModel( + name='MapNote', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('geometry', django.contrib.gis.db.models.fields.GeometryField(blank=True, null=True, srid=4326)), + ('created_dttm', models.DateTimeField(auto_now_add=True)), + ('modified_dttm', models.DateTimeField(auto_now=True)), + ('content', models.TextField(blank=True, null=True, verbose_name='Content')), + ('title', models.CharField(blank=True, max_length=255, null=True, verbose_name='Title')), + ('map', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='maps.Map')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/geonode/contrib/worldmap/mapnotes/migrations/__init__.py b/geonode/contrib/worldmap/mapnotes/migrations/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/geonode/contrib/worldmap/mapnotes/models.py b/geonode/contrib/worldmap/mapnotes/models.py new file mode 100644 index 00000000000..a62d6a2cb79 --- /dev/null +++ b/geonode/contrib/worldmap/mapnotes/models.py @@ -0,0 +1,19 @@ +from django.contrib.gis.db import models +from geonode.people.models import Profile +from geonode.maps.models import Map +from django.utils.translation import ugettext_lazy as _ + + +class MapNote(models.Model): + geometry = models.GeometryField(srid=4326, null=True, blank=True) + owner = models.ForeignKey(Profile) + map = models.ForeignKey(Map) + created_dttm = models.DateTimeField(auto_now_add=True) + modified_dttm = models.DateTimeField(auto_now=True) + content = models.TextField(_('Content'), blank=True, null=True) + title = models.CharField(_('Title'), max_length=255, blank=True, null=True) + + def owner_id(self): + return self.owner.id + + objects = models.GeoManager() diff --git a/geonode/contrib/worldmap/mapnotes/templates/mapnotes/annotation.html b/geonode/contrib/worldmap/mapnotes/templates/mapnotes/annotation.html new file mode 100644 index 00000000000..1101d5d7310 --- /dev/null +++ b/geonode/contrib/worldmap/mapnotes/templates/mapnotes/annotation.html @@ -0,0 +1,71 @@ +{% load i18n %} +{% load dialogos_tags %} +
+ + +

{{ annotation.title }}

+

{{ annotation.content|linebreaks }}

+
+

{% trans "Author" %} : {{ annotation.owner.username }}

+

{% trans "Date" %}: {{ annotation.modified_dttm }}

+ +

{% trans "Comments" %}

+
+ {% comments annotation as comments %} + {% for comment in comments %} +
+
+ {{ comment.comment|escape|urlize|safe }} +
+

{{ comment.author.get_full_name|default:comment.author|capfirst }} + commented + {% blocktrans with comment.submit_date|timesince as age %} + {{ age }} ago + {% endblocktrans %} + +

+
+ {% endfor %} + + {% if request.user.is_authenticated %} +

{% trans "Post a comment" %}

+ {% comment_form annotation as comment_form %} +
+ {% csrf_token %} +
+ {{ comment_form.comment }} +
+
+ +
+ +
+ {% else %} +

{% trans "Login to add a comment" %}

+ {% endif %} +
+
diff --git a/geonode/contrib/worldmap/mapnotes/tests.py b/geonode/contrib/worldmap/mapnotes/tests.py new file mode 100644 index 00000000000..2197311783e --- /dev/null +++ b/geonode/contrib/worldmap/mapnotes/tests.py @@ -0,0 +1,95 @@ +import json +from django.test import TestCase, Client + + +class MapNotesTest(TestCase): + fixtures = ['mapnotes_data.json'] + + def test_get_notes_map(self): + c = Client() + response = c.get("/annotations/1?bbox=-180.0,-90.0,180.0,90.0") + note_json = json.loads(response.content) + self.assertEquals(2, len(note_json["features"])) + for feature in note_json["features"]: + self.assertTrue(feature["id"] == 1 or feature["id"] == 2) + + response = c.get("/annotations/2?bbox=-180.0,-90.0,180.0,90.0") + note_json = json.loads(response.content) + self.assertEquals(1, len(note_json["features"])) + for feature in note_json["features"]: + self.assertTrue(feature["id"] == 3) + + def test_get_notes_bbox(self): + c = Client() + response = c.get("/annotations/1?bbox=-180.0,-90.0,-150.0,-60.0") + note_json = json.loads(response.content) + self.assertEquals(0, len(note_json["features"])) + + response = c.get("/annotations/1?bbox=-180.0,-90.0,0.0,0.0") + note_json = json.loads(response.content) + self.assertEquals(1, len(note_json["features"])) + + def test_get_specific_note(self): + c = Client() + response = c.get("/annotations/1/2") + note_json = json.loads(response.content) + self.assertEquals(1, len(note_json["features"])) + self.assertEquals(2, note_json["features"][0]["id"]) + self.assertEquals("Map Note 2", note_json["features"][0]["properties"]["title"]) + + def test_create_new_note(self): + json_payload = """ + {"type":"FeatureCollection", + "features":[ + {"type":"Feature","properties":{ + "title":"A new note", + "content":"This is my new note"}, + "geometry":{"type":"Point","coordinates":[0.08789062499998361,17.81145608856474]} + } + ]} + """ + c = Client() + c.login(username='bobby', password='bob') + response = c.post("/annotations/1", data=json_payload, content_type="application/json") + note_json = json.loads(response.content) + self.assertEquals(1, len(note_json["features"])) + self.assertEquals(4, note_json["features"][0]["id"]) + self.assertEquals([0.08789062499998361, 17.81145608856474], note_json["features"][0]["geometry"]["coordinates"]) + self.assertEquals("This is my new note", note_json["features"][0]["properties"]["content"]) + + def test_modify_existing_note(self): + json_payload = """ + {"type":"FeatureCollection", + "features":[ + {"type":"Feature","properties":{ + "title":"A modified note", + "content":"This is my new note, modified"}, + "geometry":{"type":"Point","coordinates":[0.38789062499998361,23.81145608856474]} + } + ]} + """ + c = Client() + c.login(username='bobby', password='bob') + response = c.post("/annotations/1/1", data=json_payload, content_type="application/json") + note_json = json.loads(response.content) + self.assertEquals(1, len(note_json["features"])) + self.assertEquals(1, note_json["features"][0]["id"]) + self.assertEquals([0.38789062499998361, 23.81145608856474], note_json["features"][0]["geometry"]["coordinates"]) + self.assertEquals("This is my new note, modified", note_json["features"][0]["properties"]["content"]) + self.assertEquals("A modified note", note_json["features"][0]["properties"]["title"]) + + def test_note_security(self): + json_payload = """ + {"type":"FeatureCollection", + "features":[ + {"type":"Feature","properties":{ + "title":"Dont modify me", + "content":"This note should not be edited"}, + "geometry":{"type":"Point","coordinates":[40.38789062499998361,43.81145608856474]} + } + ]} + """ + c = Client() + c.login(username='bobby', password='bob') + response = c.post("/annotations/1/2", data=json_payload, content_type="application/json") + self.assertEquals(403, response.status_code) diff --git a/geonode/contrib/worldmap/mapnotes/urls.py b/geonode/contrib/worldmap/mapnotes/urls.py new file mode 100644 index 00000000000..ff6bd9bc89c --- /dev/null +++ b/geonode/contrib/worldmap/mapnotes/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls import url +from .views import annotations, annotation_details + +urlpatterns = [ + url(r'^annotations/(?P[0-9]*)$', annotations, name='annotations'), + url(r'^annotations/(?P[0-9]*)/(?P[0-9]*)$', annotations, name='annotations'), + url(r'^annotations/(?P[0-9]*)/details$', annotation_details, name='annotation_details'), +] diff --git a/geonode/contrib/worldmap/mapnotes/views.py b/geonode/contrib/worldmap/mapnotes/views.py new file mode 100644 index 00000000000..04667b56501 --- /dev/null +++ b/geonode/contrib/worldmap/mapnotes/views.py @@ -0,0 +1,86 @@ +from django.http import HttpResponse +import django.contrib.gis.geos as geos +from django.contrib.gis.gdal.envelope import Envelope +from django.shortcuts import render +from vectorformats.Formats import Django, GeoJSON +from geonode.maps.models import Map +from .models import MapNote +from django.views.decorators.csrf import csrf_exempt +import re + + +def serialize(features, properties=None): + if not properties: + properties = ['title', 'description', 'owner_id'] + djf = Django.Django(geodjango="geometry", properties=properties) + geoj = GeoJSON.GeoJSON() + jsonstring = geoj.encode(djf.decode(features)) + return jsonstring + + +def applyGeometry(obj, feature): + geometry_type = feature.geometry['type'] + if geometry_type.startswith("Multi"): + geomcls = getattr(geos, feature.geometry['type'].replace("Multi", "")) + geoms = [] + for item in feature.geometry['coordinates']: + geoms.append(geomcls(*item)) + geom = getattr(geos, feature.geometry['type'])(geoms) + else: + geomcls = getattr(geos, feature.geometry['type']) + geom = geomcls(*feature.geometry['coordinates']) + obj.geometry = geom + for key, value in feature.properties.items(): + if key != 'owner_id': + setattr(obj, key, value) + obj.save() + return obj + + +@csrf_exempt +def annotations(request, mapid, id=None): + geoj = GeoJSON.GeoJSON() + if id is not None: + obj = MapNote.objects.get(pk=id) + map_obj = Map.objects.get(id=mapid) + if request.method == "DELETE": + if request.user.id == obj.owner_id or request.user.has_perm('maps.change_map', obj=map_obj): + obj.delete() + return HttpResponse(status=200) + else: + return HttpResponse(status=403) + elif request.method != "GET": + if request.user.id == obj.owner_id: + features = geoj.decode(request.raw_post_data) + obj = applyGeometry(obj, features[0]) + else: + return HttpResponse(status=403) + return HttpResponse(serialize([obj], ['title', 'content', 'owner_id']), status=200) + if request.method == "GET": + bbox = [float(n) for n in re.findall('[0-9\.\-]+', request.GET["bbox"])] + features = MapNote.objects.filter(map=Map.objects.get(pk=mapid), geometry__intersects=Envelope(bbox).wkt) + else: + if request.user.id is not None: + features = geoj.decode(request.body) + created_features = [] + for feature in features: + obj = MapNote(map=Map.objects.get(id=mapid), owner=request.user) + obj = applyGeometry(obj, feature) + created_features.append(obj) + features = created_features + else: + return HttpResponse(status=301) + data = serialize(features, ['title', 'content', 'owner_id']) + if 'callback' in request: + data = '%s(%s);' % (request['callback'], data) + return HttpResponse(data, "text/javascript") + return HttpResponse(data, "application/json") + + +@csrf_exempt +def annotation_details(request, id): + annotation = MapNote.objects.get(pk=id) + return render(request, 'mapnotes/annotation.html', { + "owner_id": request.user.id, + "annotation": annotation, + }) diff --git a/geonode/settings.py b/geonode/settings.py index 17f9ebe14b0..050b797d98d 100644 --- a/geonode/settings.py +++ b/geonode/settings.py @@ -1655,6 +1655,7 @@ 'geoexplorer-worldmap', 'geonode.contrib.worldmap.gazetteer', 'geonode.contrib.worldmap.wm_extra', + 'geonode.contrib.worldmap.mapnotes', 'geonode.contrib.createlayer', ) # WorldMap Gazetter settings diff --git a/geonode/social/signals.py b/geonode/social/signals.py index 8fb58ca288e..b53549cb289 100644 --- a/geonode/social/signals.py +++ b/geonode/social/signals.py @@ -202,7 +202,7 @@ def comment_post_save(instance, sender, created, **kwargs): """ Send a notification when a comment to a layer, map or document has been submitted """ - notice_type_label = '%s_comment' % instance.content_object.class_name.lower() + notice_type_label = '%s_comment' % instance.content_type.model.lower() recipients = get_notification_recipients(notice_type_label, instance.author) send_notification(recipients, notice_type_label, {"instance": instance}) diff --git a/geonode/urls.py b/geonode/urls.py index 94f7157a565..64b1ad652e0 100644 --- a/geonode/urls.py +++ b/geonode/urls.py @@ -76,6 +76,7 @@ if settings.USE_WORLDMAP: urlpatterns += [url(r'', include('geonode.contrib.worldmap.wm_extra.urls', namespace='worldmap'))] urlpatterns += [url(r'', include('geonode.contrib.worldmap.gazetteer.urls', namespace='gazetteer'))] + urlpatterns += [url(r'', include('geonode.contrib.worldmap.mapnotes.urls', namespace='mapnotes'))] urlpatterns += [ diff --git a/requirements.txt b/requirements.txt index f381caf0063..393a0a2577d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -190,6 +190,7 @@ datautil==0.4 dicttoxml==1.7.4 django-geoexplorer-worldmap==4.0.60 geopy==1.14.0 +vectorformats==0.1 #production uWSGI==2.0.17