Skip to content

Commit a40d82b

Browse files
committed
Merge pull request #296 from mvexel/master
API changes
2 parents 15a272e + f404187 commit a40d82b

7 files changed

Lines changed: 132 additions & 74 deletions

File tree

maproulette/api/__init__.py

Lines changed: 104 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@
66
from flask import session, request, abort, url_for
77
from maproulette.helpers import get_random_task,\
88
get_challenge_or_404, get_task_or_404,\
9-
require_signedin, osmerror, challenge_exists,\
9+
require_signedin, osmerror, \
1010
json_to_task, refine_with_user_area, user_area_is_defined,\
11-
send_email, as_stats_dict
11+
send_email, as_stats_dict, challenge_exists
1212
from maproulette.models import Challenge, Task, Action, User, db
1313
from geoalchemy2.functions import ST_Buffer
1414
from geoalchemy2.shape import to_shape
1515
from sqlalchemy import func
1616
import geojson
1717
import json
1818
import markdown
19+
import re
1920

2021

2122
class ProtectedResource(Resource):
@@ -175,7 +176,10 @@ def put(self):
175176
abort(400)
176177
[session.pop(k, None) for k, v in payload.iteritems() if v is None]
177178
for k, v in payload.iteritems():
179+
if k not in me_fields.keys():
180+
abort(400, 'you cannot set this key')
178181
if v is not None:
182+
app.logger.debug('setting {k} to {v}'.format(k=k, v=v))
179183
session[k] = v
180184
return {}
181185

@@ -522,22 +526,48 @@ class AdminApiChallenge(Resource):
522526

523527
"""Admin challenge creation endpoint"""
524528

529+
def post(self, slug):
530+
if challenge_exists(slug):
531+
abort(409, 'This challenge already exists')
532+
if not re.match("^[\w\d_-]+$", slug):
533+
abort(400, 'slug should contain only a-z, A-Z, 0-9, _, -')
534+
try:
535+
payload = json.loads(request.data)
536+
except Exception:
537+
abort(400, "JSON bad")
538+
if 'title' not in payload:
539+
abort(400, "new challenge must have title")
540+
c = Challenge(slug, payload.get('title'))
541+
if 'title' in payload:
542+
c.title = payload.get('title')
543+
if 'geometry' in payload:
544+
c.geometry = payload.get('geometry')
545+
if 'description' in payload:
546+
c.description = payload.get('description')
547+
if 'blurb' in payload:
548+
c.blurb = payload.get('blurb')
549+
if 'help' in payload:
550+
c.help = payload.get('help')
551+
if 'instruction' in payload:
552+
c.instruction = payload.get('instruction')
553+
if 'active' in payload:
554+
c.active = payload.get('active')
555+
if 'difficulty' in payload:
556+
c.difficulty = payload.get('difficulty')
557+
db.session.add(c)
558+
db.session.commit()
559+
return {}, 201
560+
525561
def put(self, slug):
526-
exists = challenge_exists(slug)
562+
c = get_challenge_or_404(slug, abort_if_inactive=False)
563+
if not re.match("^[\w\d_-]+$", slug):
564+
abort(400, 'slug should contain only a-z, A-Z, 0-9, _, -')
527565
try:
528566
payload = json.loads(request.data)
529567
except Exception:
530568
abort(400, "JSON bad")
531-
if not exists and 'title' not in payload:
532-
abort(400, "No title")
533-
return {}
534-
if exists:
535-
app.logger.debug('challenge existed, retrieving')
536-
c = get_challenge_or_404(slug, abort_if_inactive=False)
537-
if 'title' in payload:
538-
c.title = payload.get('title')
539-
else:
540-
c = Challenge(slug, payload.get('title'))
569+
if 'title' in payload:
570+
c.title = payload.get('title')
541571
if 'geometry' in payload:
542572
c.geometry = payload.get('geometry')
543573
if 'description' in payload:
@@ -552,10 +582,9 @@ def put(self, slug):
552582
c.active = payload.get('active')
553583
if 'difficulty' in payload:
554584
c.difficulty = payload.get('difficulty')
555-
merged_c = db.session.merge(c)
556-
db.session.add(merged_c)
585+
db.session.add(c)
557586
db.session.commit()
558-
return {}
587+
return {}, 200
559588

560589
def delete(self, slug):
561590
"""delete a challenge"""
@@ -581,16 +610,32 @@ class AdminApiUpdateTask(Resource):
581610

582611
"""Challenge Task Create / Update endpoint"""
583612

584-
def put(self, slug, identifier):
585-
"""Create or update one task."""
613+
def post(self, slug, identifier):
614+
"""create one task."""
615+
616+
if not re.match("^[\w\d_-]+$", identifier):
617+
abort(400, 'identifier should contain only a-z, A-Z, 0-9, _, -')
586618

587619
# Parse the posted data
588-
t = json_to_task(slug, json.loads(request.data))
589-
merged_t = db.session.merge(t)
590-
db.session.add(merged_t)
620+
t = json_to_task(
621+
slug,
622+
json.loads(request.data))
623+
db.session.add(t)
591624
db.session.commit()
592625
return {}, 201
593626

627+
def put(self, slug, identifier):
628+
"""update one task."""
629+
630+
# Parse the posted data
631+
t = json_to_task(
632+
slug,
633+
json.loads(request.data),
634+
task=get_task_or_404(slug, identifier))
635+
db.session.add(t)
636+
db.session.commit()
637+
return {}, 200
638+
594639
def delete(self, slug, identifier):
595640
"""Delete a task"""
596641

@@ -606,7 +651,9 @@ class AdminApiUpdateTasks(Resource):
606651

607652
"""Bulk task create / update endpoint"""
608653

609-
def put(self, slug):
654+
def post(self, slug):
655+
656+
"""bulk create"""
610657

611658
# Get the posted data
612659
data = json.loads(request.data)
@@ -615,12 +662,44 @@ def put(self, slug):
615662
app.logger.debug('posting {number} tasks...'.format(number=len(data)))
616663

617664
if len(data) > app.config['MAX_TASKS_BULK_UPDATE']:
618-
abort(400, 'more than 5000 tasks in bulk update')
665+
abort(400, 'more than 5000 tasks in bulk create')
619666

620667
for task in data:
668+
if not 'identifier' in task:
669+
abort(400, 'task must have identifier')
670+
if not re.match("^[\w\d_-]+$", task['identifier']):
671+
abort(400, 'identifier should contain only a-z, A-Z, 0-9, _, -')
672+
if not 'geometries' in task:
673+
abort(400, 'new task must have geometries')
621674
t = json_to_task(slug, task)
622-
merged_t = db.session.merge(t)
623-
db.session.add(merged_t)
675+
db.session.add(t)
676+
677+
# commit all dirty tasks at once.
678+
db.session.commit()
679+
return {}, 200
680+
681+
def put(self, slug):
682+
683+
"""bulk update"""
684+
685+
# Get the posted data
686+
data = json.loads(request.data)
687+
688+
# debug output number of tasks being posted
689+
app.logger.debug('putting {number} tasks...'.format(number=len(data)))
690+
691+
if len(data) > app.config['MAX_TASKS_BULK_UPDATE']:
692+
abort(400, 'more than 5000 tasks in bulk update')
693+
694+
for task in data:
695+
if not 'identifier' in task:
696+
abort(400, 'task must have identifier')
697+
if not re.match("^[\w\d_-]+$", task['identifier']):
698+
abort(400, 'identifier should contain only a-z, A-Z, 0-9, _, -')
699+
t = json_to_task(slug,
700+
task,
701+
task=get_task_or_404(slug, task['identifier']))
702+
db.session.add(t)
624703

625704
# commit all dirty tasks at once.
626705
db.session.commit()

maproulette/helpers.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -135,21 +135,13 @@ def get_random_task(challenge):
135135
return q.first() or None
136136

137137

138-
def json_to_task(slug, data):
138+
def json_to_task(slug, data, task=None):
139139
"""Parse task json coming in through the admin api"""
140140

141141
task_geometries = []
142142

143-
# task json needs to have identifier
144-
if not 'identifier' in data:
145-
abort(400, 'no identifier')
146-
147-
# if the task is new, it needs to have geometry
148-
if not 'geometries' in data:
149-
if not task_exists(slug, data['identifier']):
150-
abort(400, 'no geometries for new tasks')
151-
else:
152-
# extract the task geometries
143+
# extract the task geometries
144+
if 'geometries' in data:
153145
geometries = data.pop('geometries')
154146
# parse the geometries
155147
for feature in geometries['features']:
@@ -159,15 +151,16 @@ def json_to_task(slug, data):
159151
task_geometries.append(g)
160152

161153
# create the task
162-
t = Task(slug, data['identifier'], task_geometries)
154+
if task is None:
155+
task = Task(slug, data['identifier'], task_geometries)
163156

164157
# check for instruction
165158
if 'instruction' in data:
166-
t.instruction = data['instruction']
159+
task.instruction = data['instruction']
167160
# check for status
168161
if 'status' in data:
169-
t.status = data['status']
170-
return t
162+
task.status = data['status']
163+
return task
171164

172165

173166
def get_envelope(geoms):

maproulette/templates/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<div id="user">
88
{%- if session.get('osm_token') -%}
99
<script type="text/javascript">MRManager.loggedIn = "{{ session.display_name }}";</script>
10-
signed in as <a href="{{ url_for("me") }}">{{ session.display_name }}</a>. <a href="{{ url_for("logout") }}">sign out</a>
10+
signed in as {{ session.display_name }}. <a href="{{ url_for("logout") }}">sign out</a>
1111
{%- else -%}
1212
<script type="text/javascript">MRManager.loggedIn = false;</script>
1313
not signed in. <a href="{{ url_for("oauth_authorize") }}">sign in on OSM</a>

maproulette/templates/me.html

Lines changed: 0 additions & 20 deletions
This file was deleted.

maproulette/views/__init__.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,3 @@ def logout():
7777
if signed_in() or app.debug:
7878
session.destroy()
7979
return redirect('/')
80-
81-
82-
@app.route('/me')
83-
def me():
84-
"""Display a page about me with some stats
85-
and user settings."""
86-
return render_template('me.html')

profile.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from werkzeug.contrib.profiler import ProfilerMiddleware
2+
from maproulette import app
3+
4+
app.config['PROFILE'] = True
5+
app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30])
6+
app.run(debug=True)

test/test.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
import os
2-
import maproulette
2+
from maproulette import app
3+
from maproulette.models import db
34
import unittest
45
import tempfile
56

67

78
class MapRouletteTestCase(unittest.TestCase):
89

910
def setUp(self):
10-
self.db_fd, maproulette.app.config['DATABASE'] = tempfile.mkstemp()
11-
maproulette.app.config['TESTING'] = True
12-
self.app = maproulette.app.test_client()
13-
maproulette.init_db()
11+
self.db_fd, app.config['DATABASE'] = tempfile.mkstemp()
12+
app.config['TESTING'] = True
13+
self.app = app.test_client()
14+
db.create_all()
1415

1516
def tearDown(self):
17+
db.drop_all()
1618
os.close(self.db_fd)
17-
os.unlink(maproulette.app.config['DATABASE'])
19+
os.unlink(app.config['DATABASE'])
20+
21+
def test_empty_db(self):
22+
r = self.app.get('/api/challenges')
23+
assert r.data.startswith('[]')
24+
1825

1926
if __name__ == '__main__':
2027
unittest.main()

0 commit comments

Comments
 (0)