Better handling of expiration of GCUM and accidents report types.

This commit is contained in:
Lucas Verney 2018-07-10 15:55:45 +02:00
parent bc52d4f929
commit 9d6ed7e74c
8 changed files with 86 additions and 43 deletions

View File

@ -80,13 +80,6 @@ adapt its behavior:
You can use the `wsgi.py` script at the root of the git repository to serve You can use the `wsgi.py` script at the root of the git repository to serve
the server side part. the server side part.
#### Useful scripts
The `scripts` folder contain some useful scripts:
* `purge_old_gcum_and_accidents.py` deletes `gcum` and `accident` type report
older than one hour. You should set a crontask to call it regularly.
## Contributing ## Contributing

View File

@ -1,21 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
import os
import sys
SCRIPT_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.abspath(os.path.join(SCRIPT_DIRECTORY, '..')))
import arrow
from server.models import db, Report
if __name__ == "__main__":
db.connect()
one_hour_ago = arrow.utcnow().shift(hours=-1).datetime
nb = Report.delete().where(
((Report.type == 'accident') | (Report.type == 'gcum')) &
(Report.datetime < one_hour_ago)
).execute()
print("%d accident/GCUM reports purged." % nb)
if not db.is_closed():
db.close()

View File

@ -43,6 +43,7 @@ class Report(BaseModel):
datetime = peewee.DateTimeField( datetime = peewee.DateTimeField(
default=lambda: arrow.utcnow().replace(microsecond=0).datetime default=lambda: arrow.utcnow().replace(microsecond=0).datetime
) )
expiration_datetime = peewee.DateTimeField(null=True)
is_open = peewee.BooleanField(default=True) is_open = peewee.BooleanField(default=True)
upvotes = peewee.IntegerField(default=0) upvotes = peewee.IntegerField(default=0)
downvotes = peewee.IntegerField(default=0) downvotes = peewee.IntegerField(default=0)

View File

@ -3,6 +3,7 @@
""" """
Routes definitions Routes definitions
""" """
import arrow
import json import json
import bottle import bottle
@ -11,14 +12,9 @@ from server.models import Report
from server import jsonapi from server import jsonapi
@bottle.route('/api/v1/reports', ["GET", "OPTIONS"]) def get_reports(only_active=False):
def get_reports():
""" """
API v1 GET reports route. Get reports for the reports getting routes.
Example::
GET /api/v1/reports
.. note:: .. note::
@ -57,6 +53,11 @@ def get_reports():
query = Report.select() query = Report.select()
if filters: if filters:
query = query.where(*filters) query = query.where(*filters)
if only_active:
query = query.where(
(Report.expiration_datetime == None) |
(Report.expiration_datetime > arrow.utcnow().replace(microsecond=0).datetime)
)
query = query.order_by(*sorting) query = query.order_by(*sorting)
if page_number and page_size: if page_number and page_size:
query = query.paginate(page_number, page_size) query = query.paginate(page_number, page_size)
@ -69,6 +70,69 @@ def get_reports():
} }
@bottle.route('/api/v1/reports', ["GET", "OPTIONS"])
def get_all_reports():
"""
API v1 GET reports route. Get all reports.
Example::
GET /api/v1/reports
.. 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
.. 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"]) @bottle.route('/api/v1/reports', ["POST", "OPTIONS"])
def post_report(): def post_report():
""" """
@ -93,11 +157,17 @@ def post_report():
return jsonapi.JsonApiError(400, "Invalid JSON payload: " + str(exc)) return jsonapi.JsonApiError(400, "Invalid JSON payload: " + str(exc))
try: try:
r = Report.create( r = Report(
type=payload['type'], type=payload['type'],
lat=payload['lat'], lat=payload['lat'],
lng=payload['lng'] lng=payload['lng']
) )
# Handle expiration
if r.type in ['accident', 'gcum']:
r.expiration_datetime = (
arrow.utcnow().replace(microsecond=0).shift(hours=+1).datetime
)
r.save()
except KeyError as exc: except KeyError as exc:
return jsonapi.JsonApiError(400, "Invalid report payload: " + str(exc)) return jsonapi.JsonApiError(400, "Invalid report payload: " + str(exc))

View File

@ -21,8 +21,8 @@ export function saveReport(type, lat, lng) {
}); });
} }
export function getReports() { export function getActiveReports() {
return fetch(`${BASE_URL}api/v1/reports`) return fetch(`${BASE_URL}api/v1/reports/active`)
.then(response => response.json()) .then(response => response.json())
.then(response => response.data) .then(response => response.data)
.catch((exc) => { .catch((exc) => {

View File

@ -34,9 +34,9 @@ export default {
}, },
reportLabels: { reportLabels: {
accident: 'Accident', accident: 'Accident',
accidentDescription: 'Any accident on the road', accidentDescription: 'Any accident on the road (active for one hour).',
gcum: 'GCUM', gcum: 'GCUM',
gcumDescription: 'A car poorly parked on a bike lane. Such reports are automatically deleted after one hour, as they are by nature temporary.', gcumDescription: 'A car poorly parked on a bike lane. Such reports are automatically deleted after one hour, as they are by nature temporary (active for one hour).',
interrupt: 'Interruption', interrupt: 'Interruption',
interruptDescription: 'An interruption of the bike lane (works, unexpected end of the bike lane, etc.).', interruptDescription: 'An interruption of the bike lane (works, unexpected end of the bike lane, etc.).',
misc: 'Other', misc: 'Other',

View File

@ -34,9 +34,9 @@ export default {
}, },
reportLabels: { reportLabels: {
accident: 'Accident', accident: 'Accident',
accidentDescription: 'Un accident sur la route.', accidentDescription: 'Un accident sur la route (actif pour une heure).',
gcum: 'GCUM', gcum: 'GCUM',
gcumDescription: "Une voiture (mal) garée sur la piste cyclable. Ces signalements sont automatiquement supprimés au bout d'une heure car ils sont par essence temporaires.", gcumDescription: "Une voiture (mal) garée sur la piste cyclable. Ces signalements sont automatiquement supprimés au bout d'une heure car ils sont par essence temporaires (actif pour une heure).",
interrupt: 'Interruption', interrupt: 'Interruption',
interruptDescription: "Une interruption d'itinéraire cyclable (travaux, arrêt inattendu d'une piste cyclable, etc)", interruptDescription: "Une interruption d'itinéraire cyclable (travaux, arrêt inattendu d'une piste cyclable, etc)",
misc: 'Autre', misc: 'Autre',

View File

@ -14,7 +14,7 @@ import {
export function fetchReports({ commit }) { export function fetchReports({ commit }) {
commit(IS_LOADING); commit(IS_LOADING);
return api.getReports() return api.getActiveReports()
.then(reports => commit(STORE_REPORTS, { reports })) .then(reports => commit(STORE_REPORTS, { reports }))
.finally(() => commit(IS_DONE_LOADING)); .finally(() => commit(IS_DONE_LOADING));
} }