Drop the active reports API route and use API filtering abilities

instead

Fix for #49.
This commit is contained in:
Lucas Verney 2018-10-18 09:23:52 +02:00
parent 0f03700f71
commit 4345c3f4e3
7 changed files with 68 additions and 128 deletions

View File

@ -23,6 +23,8 @@ versions, meaning that the `0.3.py` script handles the migration of the
database from the version immediately before `0.3` to the `0.3` version of the
app.
**Always** make a backup of your database prior to running a migration on it.
If you upgrade through several versions at once, you should run all the
migrations scripts for all the intermediate versions, in the ascending order.
There are currently no automated way to handle the updates of the database

View File

@ -56,11 +56,11 @@ If you want to make an `OR` condition, satisfying either one of the filters,
you should make two different API calls.
#### Commplex filtering
#### Complex filtering
You can also use more complex elaborations through the syntax
`filter[FIELD][OPERATION]=VALUE`. The available operations are `eq` (equal,
default operation), `ne` (not equal), `gt` (greater than), `ge` (greater or
`filter[FIELD][OPERATOR]=VALUE`. The available operators are `eq` (equal,
default operator), `ne` (not equal), `gt` (greater than), `ge` (greater or
equal), `lt` (lower than) and `le` (lower or equal). For instance,
```
@ -75,3 +75,14 @@ reports in a geographical bounding box:
```
> GET /api/v1/reports?filter[lat][gt]=LAT_MIN&filter[lat][lt]=LAT_MAX&filter[lng][gt]=LNG_MIN&filter[lng][lt]=LNG_MAX
```
All operators can have a trailing `?` to make them consider fields without
value as matching the condition as well. This means you can use something like
```
> /api/v1/reports?filter[expiration_datetime][gt?]=2018-10-18T09:06:38.538375
```
to get the all the reports with an expiration datetime in the future or no
expiration datetime, that is all currently active reports, assuming the
current datetime is `2018-10-18T09:06:38.538375`.

View File

@ -29,6 +29,7 @@ def run_migration():
migrate(
migrator.add_column('report', 'first_report_datetime',
peewee.DateTimeField(default=UTC_now)),
migrator.drop_column('report', 'is_open'),
)
query = Report.select()
for report in query:

View File

@ -10,7 +10,7 @@ import re
import bottle
FILTER_RE = re.compile(r"filter\[([A-z0-9_]+?)\](\[([A-z0-9_]+)\])?")
FILTER_RE = re.compile(r"filter\[([A-z0-9_]+?)\](\[([A-z0-9_]+\??)\])?")
class DateAwareJSONEncoder(json.JSONEncoder):
@ -76,21 +76,36 @@ def JsonApiParseQuery(query, model, default_sorting=None):
for value in query.getall(param):
# Handle operation
operation = filter_match.group(3)
if operation == 'eq' or operation is None:
filters.append(field == value)
if operation is None:
# Default operation is 'eq'
operation = 'eq'
# Handle '?' modifier
modifier_filter = None
if operation.endswith('?'):
modifier_filter = (field == None)
operation = operation.rstrip('?')
if operation == 'eq':
operation_filter = (field == value)
elif operation == 'ne':
filters.append(field != value)
operation_filter = (field != value)
elif operation == 'gt':
filters.append(field > value)
operation_filter = (field > value)
elif operation == 'ge':
filters.append(field >= value)
operation_filter = (field >= value)
elif operation == 'lt':
filters.append(field < value)
operation_filter = (field < value)
elif operation == 'le':
filters.append(field <= value)
operation_filter = (field <= value)
else:
raise ValueError("Invalid filtering operator provided.")
if modifier_filter:
filters.append(modifier_filter | operation_filter)
else:
filters.append(operation_filter)
# Handle pagination according to JSON API spec
page_number, page_size = 0, None
try:

View File

@ -48,7 +48,6 @@ class Report(BaseModel):
default=UTC_now
)
expiration_datetime = peewee.DateTimeField(null=True)
is_open = peewee.BooleanField(default=True)
upvotes = peewee.IntegerField(default=0)
downvotes = peewee.IntegerField(default=0)

View File

@ -37,9 +37,34 @@ def check_auth():
return
def get_reports(only_active=False):
@bottle.route('/api/v1/reports', ["GET", "OPTIONS"])
def get_all_reports():
"""
Get reports for the reports getting routes.
API v1 GET reports route. Get all reports.
Example::
> GET /api/v1/reports
{
"data": [
{
"attributes": {
"expiration_datetime": null,
"downvotes": 0,
"datetime": "2018-06-27T16:44:12+00:00",
"lat": 48.842005,
"upvotes": 1,
"lng": 2.386278,
"type": "interrupt",
},
"type": "reports",
"id": 1
},
]
}
.. note::
@ -78,11 +103,6 @@ def get_reports(only_active=False):
query = Report.select()
if filters:
query = query.where(*filters)
if only_active:
query = query.where(
(Report.expiration_datetime == None) |
(Report.expiration_datetime > UTC_now())
)
query = query.order_by(*sorting)
if page_number and page_size:
query = query.paginate(page_number, page_size)
@ -95,111 +115,6 @@ def get_reports(only_active=False):
}
@bottle.route('/api/v1/reports', ["GET", "OPTIONS"])
def get_all_reports():
"""
API v1 GET reports route. Get all reports.
Example::
> GET /api/v1/reports
{
"data": [
{
"attributes": {
"expiration_datetime": null,
"downvotes": 0,
"datetime": "2018-06-27T16:44:12+00:00",
"is_open": true,
"lat": 48.842005,
"upvotes": 1,
"lng": 2.386278,
"type": "interrupt",
},
"type": "reports",
"id": 1
},
]
}
.. note::
Filtering can be done through the ``filter`` GET param, according
to JSON API spec (http://jsonapi.org/recommendations/#filtering).
.. note::
By default no pagination is done. Pagination can be forced using
``page[size]`` to specify a number of items per page and
``page[number]`` to specify which page to return. Pages are numbered
starting from 0.
.. note::
Sorting can be handled through the ``sort`` GET param, according to
JSON API spec (http://jsonapi.org/format/#fetching-sorting).
:return: The available reports objects in a JSON ``data`` dict.
"""
return get_reports(only_active=False)
@bottle.route('/api/v1/reports/active', ["GET", "OPTIONS"])
def get_active_reports():
"""
API v1 GET reports route. Only get active reports (those with
``expiration_datetime`` in the future).
Example::
> GET /api/v1/reports/active
{
"data": [
{
"attributes": {
"expiration_datetime": null,
"downvotes": 0,
"datetime": "2018-06-27T16:44:12+00:00",
"is_open": true,
"lat": 48.842005,
"upvotes": 1,
"lng": 2.386278,
"type": "interrupt",
},
"type": "reports",
"id": 1
},
]
}
.. note::
Filtering can be done through the ``filter`` GET param, according
to JSON API spec (http://jsonapi.org/recommendations/#filtering).
.. note::
By default no pagination is done. Pagination can be forced using
``page[size]`` to specify a number of items per page and
``page[number]`` to specify which page to return. Pages are numbered
starting from 0.
.. note::
Sorting can be handled through the ``sort`` GET param, according to
JSON API spec (http://jsonapi.org/format/#fetching-sorting).
:return: The available reports objects in a JSON ``data`` dict.
"""
return get_reports(only_active=True)
@bottle.route('/api/v1/reports', ["POST", "OPTIONS"])
def post_report():
"""
@ -225,7 +140,6 @@ def post_report():
"upvotes": 0,
"lng": 2.385234797066081,
"type": "pothole",
"is_open": true,
},
"type": "reports",
@ -290,7 +204,6 @@ def upvote_report(id):
"upvotes": 1,
"lng": 2.385234797066081,
"type": "pothole",
"is_open": true,
},
"type": "reports",
@ -349,7 +262,6 @@ def downvote_report(id):
"upvotes": 0,
"lng": 2.385234797066081,
"type": "pothole",
"is_open": true,
},
"type": "reports",

View File

@ -24,7 +24,7 @@ export function saveReport(type, lat, lng) {
}
export function getActiveReports() {
return fetch(`${BASE_URL}api/v1/reports/active`)
return fetch(`${BASE_URL}api/v1/reports?filter[expiration_datetime][gt?]=${(new Date()).toISOString()}`)
.then(response => response.json())
.then(response => response.data)
.catch((exc) => {