Merge branch 'yohanboniface'
This commit is contained in:
commit
5312a6bd47
1 changed files with 102 additions and 79 deletions
181
backend.py
181
backend.py
|
@ -1,73 +1,106 @@
|
||||||
# backend.py
|
# backend.py
|
||||||
# openeventdatabase
|
# openeventdatabase
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
import psycopg2
|
import psycopg2
|
||||||
import uuid
|
import psycopg2.extras
|
||||||
import json
|
|
||||||
import codecs
|
|
||||||
|
|
||||||
def db_connect():
|
def db_connect():
|
||||||
try:
|
return psycopg2.connect(
|
||||||
db = psycopg2.connect(dbname="oedb")
|
dbname=os.getenv("DB_NAME", "oedb"),
|
||||||
except:
|
host=os.getenv("DB_HOST", None),
|
||||||
db_host = os.getenv("DB_HOST","localhost")
|
password=os.getenv("POSTGRES_PASSWORD", None),
|
||||||
db_user = os.getenv("DB_USER","oedb")
|
user=os.getenv("DB_USER", None))
|
||||||
db_password = os.getenv("POSTGRES_PASSWORD","")
|
|
||||||
db = psycopg2.connect(dbname="oedb",host=db_host,password=db_password,user=db_user)
|
|
||||||
|
|
||||||
return db
|
|
||||||
|
|
||||||
def standard_headers(resp):
|
class EventEncoder(json.JSONEncoder):
|
||||||
resp.set_header('X-Powered-By', 'OpenEventDatabase')
|
def default(self, o):
|
||||||
resp.set_header('Access-Control-Allow-Origin', '*')
|
if isinstance(o, datetime):
|
||||||
resp.set_header('Access-Control-Allow-Headers', 'X-Requested-With')
|
return o.isoformat()
|
||||||
|
try:
|
||||||
|
return super().default(o)
|
||||||
|
except TypeError:
|
||||||
|
return str(o)
|
||||||
|
|
||||||
|
|
||||||
|
def dumps(data):
|
||||||
|
return json.dumps(data, cls=EventEncoder)
|
||||||
|
|
||||||
|
|
||||||
|
class HeaderMiddleware:
|
||||||
|
|
||||||
|
def process_response(self, req, resp, resource):
|
||||||
|
resp.set_header('X-Powered-By', 'OpenEventDatabase')
|
||||||
|
resp.set_header('Access-Control-Allow-Origin', '*')
|
||||||
|
resp.set_header('Access-Control-Allow-Headers', 'X-Requested-With')
|
||||||
|
|
||||||
|
|
||||||
class StatsResource(object):
|
class StatsResource(object):
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
db = db_connect()
|
db = db_connect()
|
||||||
cur = db.cursor()
|
cur = db.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
||||||
cur.execute("SELECT count(*) as events_count, max(createdate) as last_created, max(lastupdate) as last_updated from events;")
|
cur.execute("SELECT count(*) as events_count, max(createdate) as last_created, max(lastupdate) as last_updated from events;")
|
||||||
stat = cur.fetchone()
|
stat = cur.fetchone()
|
||||||
cur.close()
|
cur.close()
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
standard_headers(resp)
|
resp.body = dumps(dict(stat))
|
||||||
resp.body = """{"events_count": %s, "last_created": "%s", "last_updated": "%s"}""" % (stat[0], stat[1],stat[2])
|
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
|
|
||||||
class EventsResource(object):
|
|
||||||
def on_get(self,req,resp):
|
class BaseEvent:
|
||||||
|
|
||||||
|
def row_to_feature(self, row):
|
||||||
|
properties = dict(row['events_tags'])
|
||||||
|
properties.update({
|
||||||
|
'createdate': row['createdate'],
|
||||||
|
'last_updated': row['lastupdate']
|
||||||
|
})
|
||||||
|
if "distance" in row:
|
||||||
|
properties['distance'] = row['distance']
|
||||||
|
return {
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": json.loads(row['geometry']),
|
||||||
|
"id": row['events_id'],
|
||||||
|
"properties": properties
|
||||||
|
}
|
||||||
|
|
||||||
|
def rows_to_collection(self, rows):
|
||||||
|
return {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [self.row_to_feature(r) for r in rows]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EventsResource(BaseEvent):
|
||||||
|
|
||||||
|
def on_get(self, req, resp):
|
||||||
db = db_connect()
|
db = db_connect()
|
||||||
cur = db.cursor()
|
cur = db.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
||||||
# get event geojson Feature
|
cur.execute("SELECT events_id, events_tags, createdate, lastupdate, st_asgeojson(geom) as geometry FROM events JOIN geo ON (hash=events_geo)")
|
||||||
cur.execute("""
|
resp.body = dumps(self.rows_to_collection(cur.fetchall()))
|
||||||
SELECT format('{"type":"Feature", "id": "'|| events_id::text ||'", "properties": '|| events_tags::text ||', "geometry":'|| st_asgeojson(geom)) ||' }'
|
|
||||||
FROM events
|
|
||||||
JOIN geo ON (hash=events_geo)""");
|
|
||||||
standard_headers(resp)
|
|
||||||
resp.body = """{"type": "FeatureCollection","features": [
|
|
||||||
"""+""",
|
|
||||||
""".join([x[0] for x in cur.fetchall()])+"""
|
|
||||||
]}"""
|
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
|
|
||||||
class EventResource(object):
|
|
||||||
def maybe_insert_geometry(self,geometry,cur):
|
class EventResource(BaseEvent):
|
||||||
|
def maybe_insert_geometry(self, geometry, cur):
|
||||||
# insert into geo table if not existing
|
# insert into geo table if not existing
|
||||||
cur.execute("""INSERT INTO geo (hash, geom, geom_center) SELECT *, st_centroid(geom) FROM (SELECT md5(ewkt) as hash, st_setsrid(st_geomfromewkt(ewkt),4326) as geom FROM (SELECT st_asewkt(st_geomfromgeojson( %s )) as ewkt) as g) as i ON CONFLICT DO NOTHING RETURNING hash;""",(geometry,))
|
cur.execute("""INSERT INTO geo (hash, geom, geom_center) SELECT *, st_centroid(geom) FROM (SELECT md5(ewkt) as hash, st_setsrid(st_geomfromewkt(ewkt),4326) as geom FROM (SELECT st_asewkt(st_geomfromgeojson( %s )) as ewkt) as g) as i ON CONFLICT DO NOTHING RETURNING hash;""", (geometry,))
|
||||||
# get its id (md5 hash)
|
# get its id (md5 hash)
|
||||||
h = cur.fetchone()
|
h = cur.fetchone()
|
||||||
if h is None:
|
if h is None:
|
||||||
cur.execute("""SELECT md5(st_asewkt(st_geomfromgeojson( %s )));""",(geometry,))
|
cur.execute("""SELECT md5(st_asewkt(st_geomfromgeojson( %s )));""", (geometry,))
|
||||||
h = cur.fetchone()
|
h = cur.fetchone()
|
||||||
return h
|
return h
|
||||||
|
|
||||||
def on_get(self, req, resp, id = None):
|
def on_get(self, req, resp, id=None):
|
||||||
standard_headers(resp)
|
|
||||||
db = db_connect()
|
db = db_connect()
|
||||||
cur = db.cursor()
|
cur = db.cursor(cursor_factory=psycopg2.extras.DictCursor)
|
||||||
if id is None:
|
if id is None:
|
||||||
# get query search parameters
|
# get query search parameters
|
||||||
|
|
||||||
|
@ -76,41 +109,42 @@ class EventResource(object):
|
||||||
event_bbox = cur.mogrify(" AND geom && ST_SetSRID(ST_MakeBox2D(ST_Point(%s,%s),ST_Point(%s,%s)),4326) ",tuple(req.params['bbox'])).decode("utf-8")
|
event_bbox = cur.mogrify(" AND geom && ST_SetSRID(ST_MakeBox2D(ST_Point(%s,%s),ST_Point(%s,%s)),4326) ",tuple(req.params['bbox'])).decode("utf-8")
|
||||||
event_dist = ""
|
event_dist = ""
|
||||||
elif 'near' in req.params:
|
elif 'near' in req.params:
|
||||||
# limit search with location+distance (long, lat, distance in meters)
|
# Limit search with location+distance
|
||||||
if len(req.params['near'])<3:
|
# (long, lat, distance in meters)
|
||||||
|
if len(req.params['near']) < 3:
|
||||||
dist = 1
|
dist = 1
|
||||||
else:
|
else:
|
||||||
dist = req.params['near'][2]
|
dist = req.params['near'][2]
|
||||||
event_bbox = cur.mogrify(" AND ST_Intersects(geom, ST_Buffer(st_setsrid(st_makepoint(%s,%s),4326)::geography,%s)::geometry) ",(req.params['near'][0], req.params['near'][1], dist)).decode("utf-8")
|
event_bbox = cur.mogrify(" AND ST_Intersects(geom, ST_Buffer(st_setsrid(st_makepoint(%s,%s),4326)::geography,%s)::geometry) ", (req.params['near'][0], req.params['near'][1], dist)).decode("utf-8")
|
||||||
event_dist = cur.mogrify(", 'distance', ST_Length(ST_ShortestLine(geom, st_setsrid(st_makepoint(%s,%s),4326))::geography) ",(req.params['near'][0], req.params['near'][1])).decode("utf-8")
|
event_dist = cur.mogrify("ST_Length(ST_ShortestLine(geom, st_setsrid(st_makepoint(%s,%s),4326))::geography) as distance,", (req.params['near'][0], req.params['near'][1])).decode("utf-8")
|
||||||
else:
|
else:
|
||||||
event_bbox = ""
|
event_bbox = ""
|
||||||
event_dist = ""
|
event_dist = ""
|
||||||
|
|
||||||
if 'when' in req.params:
|
if 'when' in req.params:
|
||||||
# limit search with fixed time
|
# limit search with fixed time
|
||||||
event_when = cur.mogrify("tstzrange(%s,%s,'[]')",(req.params['when'],req.params['when'])).decode("utf-8")
|
event_when = cur.mogrify("tstzrange(%s,%s,'[]')", (req.params['when'], req.params['when'])).decode("utf-8")
|
||||||
elif 'start' in req.params and 'stop' in req.params:
|
elif 'start' in req.params and 'stop' in req.params:
|
||||||
# limit search with fixed time (start to stop)
|
# limit search with fixed time (start to stop)
|
||||||
event_when = cur.mogrify("tstzrange(%s,%s,'[]')",(req.params['start'],req.params['stop'])).decode("utf-8")
|
event_when = cur.mogrify("tstzrange(%s,%s,'[]')", (req.params['start'], req.params['stop'])).decode("utf-8")
|
||||||
elif 'start' in req.params and 'stop' not in req.params:
|
elif 'start' in req.params and 'stop' not in req.params:
|
||||||
# limit search with fixed time (start to now)
|
# limit search with fixed time (start to now)
|
||||||
event_when = cur.mogrify("tstzrange(%s,now(),'[]')",(req.params['start'],)).decode("utf-8")
|
event_when = cur.mogrify("tstzrange(%s,now(),'[]')", (req.params['start'],)).decode("utf-8")
|
||||||
elif 'start' not in req.params and 'stop' in req.params:
|
elif 'start' not in req.params and 'stop' in req.params:
|
||||||
# limit search with fixed time (now to stop)
|
# limit search with fixed time (now to stop)
|
||||||
event_when = cur.mogrify("tstzrange(now(),%s,'[]')",(req.params['stop'],)).decode("utf-8")
|
event_when = cur.mogrify("tstzrange(now(),%s,'[]')", (req.params['stop'],)).decode("utf-8")
|
||||||
else:
|
else:
|
||||||
event_when = """tstzrange(now(),now(),'[]')"""
|
event_when = "tstzrange(now(),now(),'[]')"
|
||||||
|
|
||||||
if 'what' in req.params:
|
if 'what' in req.params:
|
||||||
# limit search based on "what"
|
# limit search based on "what"
|
||||||
event_what = cur.mogrify(" AND events_what LIKE %s ",(req.params['what']+"%",)).decode("utf-8")
|
event_what = cur.mogrify(" AND events_what LIKE %s ", (req.params['what']+"%",)).decode("utf-8")
|
||||||
else:
|
else:
|
||||||
event_what = ""
|
event_what = ""
|
||||||
|
|
||||||
if 'type' in req.params:
|
if 'type' in req.params:
|
||||||
# limit search based on type (scheduled, forecast, unscheduled)
|
# limit search based on type (scheduled, forecast, unscheduled)
|
||||||
event_type = cur.mogrify(" AND events_type = %s ",(req.params['type'],)).decode("utf-8")
|
event_type = cur.mogrify(" AND events_type = %s ", (req.params['type'],)).decode("utf-8")
|
||||||
else:
|
else:
|
||||||
event_type = ""
|
event_type = ""
|
||||||
|
|
||||||
|
@ -119,42 +153,32 @@ class EventResource(object):
|
||||||
if req.params['geom'] == 'full':
|
if req.params['geom'] == 'full':
|
||||||
event_geom = "geom"
|
event_geom = "geom"
|
||||||
|
|
||||||
# search recent active events
|
# Search recent active events.
|
||||||
cur.execute("""
|
sql = """SELECT events_id, events_tags, createdate, lastupdate, {event_dist} st_asgeojson({event_geom}) as geometry FROM events JOIN geo ON (hash=events_geo) {event_bbox} WHERE events_when && {event_when} {event_what} {event_type} ORDER BY createdate DESC LIMIT 200"""
|
||||||
SELECT '{"type":"Feature", "properties": '|| (events_tags::jsonb || jsonb_build_object('id',events_id,'createdate',createdate,'lastupdate',lastupdate """+event_dist+"""))::text ||', "geometry":'|| st_asgeojson("""+event_geom+""") ||' }' as feature
|
# No user generated content here, so format is safe.
|
||||||
FROM events
|
sql = sql.format(event_dist=event_dist, event_geom=event_geom,
|
||||||
JOIN geo ON (hash=events_geo) """ + event_bbox +"""
|
event_bbox=event_bbox, event_what=event_what,
|
||||||
WHERE events_when && """+ event_when + event_what + event_type +"""
|
event_when=event_when, event_type=event_type)
|
||||||
ORDER BY createdate DESC
|
cur.execute(sql)
|
||||||
LIMIT 200;
|
resp.body = dumps(self.rows_to_collection(cur.fetchall()))
|
||||||
""")
|
|
||||||
resp.body = """{"type":"FeatureCollection", "features": [
|
|
||||||
"""+""",
|
|
||||||
""".join([x[0] for x in cur.fetchall()])+"""
|
|
||||||
]}"""
|
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
else:
|
else:
|
||||||
# get single event geojson Feature by id
|
# Get single event geojson Feature by id.
|
||||||
cur.execute("""
|
cur.execute("SELECT events_id, events_tags, createdate, lastupdate, st_asgeojson(geom) as geometry FROM events JOIN geo ON (hash=events_geo) WHERE events_id=%s", [id])
|
||||||
SELECT format('{"type":"Feature", "properties": '|| (events_tags::jsonb || jsonb_build_object('id',events_id,'createdate',createdate,'lastupdate',lastupdate))::text ||', "geometry":'|| st_asgeojson(geom)) ||' }'
|
|
||||||
FROM events
|
|
||||||
JOIN geo ON (hash=events_geo)
|
|
||||||
WHERE events_id=%s;""", (id,))
|
|
||||||
|
|
||||||
e = cur.fetchone()
|
e = cur.fetchone()
|
||||||
if e is not None:
|
if e is not None:
|
||||||
resp.body = e[0]
|
resp.body = dumps(self.row_to_feature(e))
|
||||||
resp.status = falcon.HTTP_200
|
resp.status = falcon.HTTP_200
|
||||||
else:
|
else:
|
||||||
resp.status = falcon.HTTP_404
|
resp.status = falcon.HTTP_404
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
def insert_or_update(self, req, resp, id, query):
|
def insert_or_update(self, req, resp, id, query):
|
||||||
standard_headers(resp)
|
|
||||||
|
|
||||||
# get request body payload (geojson Feature)
|
# get request body payload (geojson Feature)
|
||||||
body = req.stream.read().decode('utf-8')
|
body = req.stream.read().decode('utf-8')
|
||||||
j=json.loads(body)
|
j = json.loads(body)
|
||||||
if "properties" not in j or "geometry" not in j:
|
if "properties" not in j or "geometry" not in j:
|
||||||
resp.body = "missing 'geometry' or 'properties' elements"
|
resp.body = "missing 'geometry' or 'properties' elements"
|
||||||
resp.status = falcon.HTTP_400
|
resp.status = falcon.HTTP_400
|
||||||
|
@ -174,12 +198,12 @@ WHERE events_id=%s;""", (id,))
|
||||||
db = db_connect()
|
db = db_connect()
|
||||||
cur = db.cursor()
|
cur = db.cursor()
|
||||||
# get the geometry part
|
# get the geometry part
|
||||||
geometry=json.dumps(j['geometry'])
|
geometry=dumps(j['geometry'])
|
||||||
h = self.maybe_insert_geometry(geometry,cur)
|
h = self.maybe_insert_geometry(geometry,cur)
|
||||||
params = (j['properties']['type'],j['properties']['what'],event_start, event_stop, bounds, json.dumps(j['properties']),h[0])
|
params = (j['properties']['type'], j['properties']['what'], event_start, event_stop, bounds, dumps(j['properties']), h[0])
|
||||||
if (id):
|
if id:
|
||||||
params = params + (id,)
|
params = params + (id,)
|
||||||
cur.execute(query,params)
|
cur.execute(query, params)
|
||||||
# get newly created event id
|
# get newly created event id
|
||||||
e = cur.fetchone()
|
e = cur.fetchone()
|
||||||
db.commit()
|
db.commit()
|
||||||
|
@ -196,10 +220,9 @@ WHERE events_id=%s;""", (id,))
|
||||||
self.insert_or_update(req, resp, id, """UPDATE events SET ( events_type, events_what, events_when, events_tags, events_geo) = (%s, %s, tstzrange(%s,%s,%s) , %s, %s) WHERE events_id = %s RETURNING events_id;""")
|
self.insert_or_update(req, resp, id, """UPDATE events SET ( events_type, events_what, events_when, events_tags, events_geo) = (%s, %s, tstzrange(%s,%s,%s) , %s, %s) WHERE events_id = %s RETURNING events_id;""")
|
||||||
|
|
||||||
def on_delete(self, req, resp, id):
|
def on_delete(self, req, resp, id):
|
||||||
standard_headers(resp)
|
|
||||||
db = db_connect()
|
db = db_connect()
|
||||||
cur = db.cursor()
|
cur = db.cursor()
|
||||||
cur.execute("""DELETE FROM events WHERE events_id = %s;""",(id,));
|
cur.execute("""DELETE FROM events WHERE events_id = %s;""", (id,));
|
||||||
db.commit()
|
db.commit()
|
||||||
cur.close()
|
cur.close()
|
||||||
db.close()
|
db.close()
|
||||||
|
@ -208,8 +231,8 @@ WHERE events_id=%s;""", (id,))
|
||||||
else:
|
else:
|
||||||
resp.status = falcon.HTTP_404
|
resp.status = falcon.HTTP_404
|
||||||
|
|
||||||
# falcon.API instances are callable WSGI apps
|
# Falcon.API instances are callable WSGI apps.
|
||||||
app = falcon.API()
|
app = falcon.API(middleware=[HeaderMiddleware()])
|
||||||
|
|
||||||
# Resources are represented by long-lived class instances
|
# Resources are represented by long-lived class instances
|
||||||
events = EventsResource()
|
events = EventsResource()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue