Repository: data-for-change/anyway

Issue:
Store users location and address searches
Create a file that contains coordinates of where our users are (the ones who allowed GPS access) and the addresses that people searched using the search option.

Only the coordinates are important so let's keep the information stored as minimal and detached from the individual user as much as possible.

This will allow us at some point to create a new layer that will show "interest zones", and will help us a lot even before that to research and understand our audience.

<!---
@huboard:{"order":204.6953125,"milestone_order":231,"custom_state":""}
-->



Discussion:
Hey @galraij, is it important to label each location coordinate in the file as "user location" or "search location"?
@danielhers , why did you labelled the issue as frontend, do you see any work needed be done in end side?

Not necessarily, if that's easy to do it could be nice, if it takes more effort/time we can live with the coordinates alone.

‫ב-4 במאי 2015, בשעה 04:01, ‏‏Elad Karakuli ‏notifications@github.com כתב/ה:‬

> Hey @galraij, is it important to label each location coordinate in the file as "user location" or "search location"?
> @danielhers , why did you labelled the issue as frontend, do you see any work needed be done in end side?
> 
> —
> Reply to this email directly or view it on GitHub.

I suppose you need JS to gather this information and send it to the server.
That's why I labeled it as such.

On Mon, May 4, 2015 at 8:01 AM Gal Raij notifications@github.com wrote:

> Not necessarily, if that's easy to do it could be nice, if it takes more
> effort/time we can live with the coordinates alone.
> 
> ‫ב-4 במאי 2015, בשעה 04:01, ‏‏Elad Karakuli ‏notifications@github.com
> כתב/ה:‬
> 
> > Hey @galraij, is it important to label each location coordinate in the
> > file as "user location" or "search location"?
> > @danielhers , why did you labelled the issue as frontend, do you see any
> > work needed be done in end side?
> > 
> > —
> > Reply to this email directly or view it on GitHub.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/hasadna/anyway/issues/231#issuecomment-98589136.

we also going to need some kind of interface so i can access it from the android app

I suggest in the first step to just store it, later we can decide if and how to present it on the website + Android.

> On May 4, 2015, at 10:40, Samuel Regev notifications@github.com wrote:
> 
> we also going to need some kind of interface so i can access it from the android app
> 
> —
> Reply to this email directly or view it on GitHub https://github.com/hasadna/anyway/issues/231#issuecomment-98611283.


Resolving patch:
diff --git a/main.py b/main.py
--- a/main.py
+++ b/main.py
@@ -7,7 +7,7 @@
 import time
 
 import jinja2
-from flask import make_response, jsonify, render_template, Response, url_for
+from flask import make_response, jsonify, render_template, Response, url_for, session
 import flask.ext.assets
 from webassets.ext.jinja2 import AssetsExtension
 from webassets import Environment as AssetsEnvironment
@@ -40,6 +40,7 @@
 jinja_environment.assets_environment = assets_env
 
 MINIMAL_ZOOM = 16
+SESSION_HIGHLIGHTPOINT_KEY = 'gps_highlightpoint_created'
 
 @app.teardown_appcontext
 def shutdown_session(exception=None):
@@ -137,10 +138,61 @@ def discussion():
         except KeyError:
             return index(message=u"דיון לא חוקי")
     else:
-        marker = DiscussionMarker.parse(request.get_json(force=True))
-        db_session.add(marker)
+        disscuss = parse_data(DiscussionMarker, get_json_object(request))
+        if disscuss is None:
+            log_bad_request(request)
+            return make_response("")
+        return make_response(post_handler(disscuss))
+
+@app.route("/highlightpoints", methods=['POST'])
+@user_optional
+def highlightpoint():
+    highlight = parse_data(HighlightPoint, get_json_object(request))
+    if highlight is None:
+        log_bad_request(request)
+        return make_response("")
+
+    # if it's a user gps type (user location), only handle a single post request per session
+    if int(highlight.type) == HighlightPoint.HIGHLIGHT_TYPE_USER_GPS:
+        if not SESSION_HIGHLIGHTPOINT_KEY in session:
+            session[SESSION_HIGHLIGHTPOINT_KEY] = "saved"
+        else:
+            return make_response("")
+
+    return make_response(post_handler(highlight))
+
+# Post handler for a generic REST API
+def post_handler(obj):
+    try:
+        db_session.add(obj)
         db_session.commit()
-        return make_response(json.dumps(marker.serialize()))
+        return jsonify(obj.serialize())
+    except Exception as e:
+        logging.debug("could not handle a post for object:{0}, error:{1}".format(obj, e.message))
+        return ""
+
+# Safely parsing an object
+# cls: the ORM Model class that implement a parse method
+def parse_data(cls, data):
+    try:
+        return cls.parse(data) if data is not None else None
+    except Exception as e:
+        logging.debug("Could not parse the requested data, for class:{0}, data:{1}. Error:{2}".format(cls, data, e.message))
+        return
+
+def get_json_object(request):
+    try:
+        return request.get_json(force=True)
+    except Exception as e:
+        logging.debug("Could not get json from a request. request:{0}. Error:{1}".format(request, e.message))
+        return
+
+def log_bad_request(request):
+    try:
+        logging.debug("Bad {0} Request over {1}. Values: {2} {3}".format(request.method, request.url, request.form, request.args))
+    except AttributeError:
+        logging.debug("Bad request:{0}".format(str(request)))
+
 
 @app.route('/')
 def index(marker=None, message=None):
@@ -338,4 +390,3 @@ def is_visible(self):
     logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(message)s')
     app.run(debug=True)
 
-
diff --git a/models.py b/models.py
--- a/models.py
+++ b/models.py
@@ -3,8 +3,8 @@
 import json
 import logging
 
-from sqlalchemy import Column, Integer, String, Boolean, Float, DateTime, Text, BigInteger, Index, desc
-from sqlalchemy.orm import load_only
+from sqlalchemy import Column, Integer, String, Boolean, Float, ForeignKey, DateTime, Text, Index, desc
+from sqlalchemy.orm import relationship, load_only
 import datetime
 import localization
 from database import Base
@@ -61,16 +61,15 @@ def __unicode__(self):
 MARKER_TYPE_ACCIDENT = 1
 MARKER_TYPE_DISCUSSION = 2
 
+class Point(object):
+    id = Column(Integer, primary_key=True)
+    latitude = Column(Float())
+    longitude = Column(Float())
         
-class MarkerMixin(object):
-
-    id = Column(BigInteger, primary_key=True)
+class MarkerMixin(Point):
     type = Column(Integer)
     title = Column(String(100))
-    created = Column(DateTime, default=datetime.datetime.utcnow, index=True)
-    latitude = Column(Float())
-    longitude = Column(Float())
-
+    created = Column(DateTime, default=datetime.datetime.now, index=True)
 
     __mapper_args__ = {
         'polymorphic_on': type
@@ -84,6 +83,38 @@ def format_description(field, value):
         name = localization.get_field(field).decode(db_encoding)
         return u"{0}: {1}".format(name, value)
 
+class HighlightPoint(Point, Base):
+    __tablename__ = "highlight_markers"
+    __table_args__ = (
+        Index('highlight_long_lat_idx', 'latitude', 'longitude'),
+    )
+
+    HIGHLIGHT_TYPE_USER_SEARCH = 1
+    HIGHLIGHT_TYPE_USER_GPS = 2
+
+    created = Column(DateTime, default=datetime.datetime.now)
+    type = Column(Integer)
+
+    def serialize(self):
+        return {
+            "id": str(self.id),
+            "latitude": self.latitude,
+            "longitude": self.longitude,
+            "type": self.type,
+        }
+
+    def update(self, data):
+        self.type = data["type"]
+        self.latitude = data["latitude"]
+        self.longitude = data["longitude"]
+
+    @classmethod
+    def parse(cls, data):
+        return cls(
+            type=data["type"],
+            latitude=data["latitude"],
+            longitude=data["longitude"]
+        )
 
 class Marker(MarkerMixin, Base): # TODO rename to AccidentMarker
     __tablename__ = "markers"


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,40 @@
+import unittest
+from main import parse_data
+from models import Marker
+
+class TestParseData(unittest.TestCase):
+
+  def setUp(self):
+      self.marker_dummy = dict(type=1, title="test title", description="test description", latitude=1, longitude=1)
+      self.bad_marker_dummy = dict(type=1, title="No properties")
+
+  def tearDown(self):
+    self.marker_dummy = None
+    self.bad_marker_dummy = None
+
+  class NoParsedObject(object):
+    """docstring for ClassName"""
+    def __init__(self, arg):
+      self.arg = arg
+
+  def test_class_null(self): 
+    self.assertEqual(parse_data(None, self.marker_dummy), None)
+
+  def test_data_null(self):
+    self.assertEqual(parse_data(Marker, None), None)
+
+  def test_bad_data_format_marker_class(self):
+    self.assertEqual(parse_data(Marker, self.bad_marker_dummy), None)
+
+  def test_class_without_parse_method(self):
+    self.assertEqual(parse_data(self.NoParsedObject, self.marker_dummy), None)
+
+  def test_parse_marker(self):
+    marker  = parse_data(Marker, self.marker_dummy)
+    for key in self.marker_dummy:
+      self.assertEqual(marker.__dict__[key], self.marker_dummy[key])
+
+if __name__ == '__main__':
+    unittest.main()
+    suite = unittest.TestLoader().loadTestsFromTestCase(TestParseData)
+    unittest.TextTestRunner(verbosity=2).run(suite)