diff --git a/flatisfy/cmds.py b/flatisfy/cmds.py index 8c0e9c0..08a9c5e 100644 --- a/flatisfy/cmds.py +++ b/flatisfy/cmds.py @@ -233,4 +233,5 @@ def serve(config): # standard logging server = web_app.QuietWSGIRefServer - app.run(host=config["host"], port=config["port"], server=server) + app.run(host=config["host"], port=config["port"], server=server, + debug=config["debug"]) diff --git a/flatisfy/config.py b/flatisfy/config.py index 268b0b6..5dc969c 100644 --- a/flatisfy/config.py +++ b/flatisfy/config.py @@ -63,6 +63,8 @@ DEFAULT_CONFIG = { "search_index": None, # Web app port "port": 8080, + # Debug mode for webserver + "debug": False, # Web app host to listen on "host": "127.0.0.1", # Web server to use to serve the webapp (see Bottle deployment doc) @@ -126,6 +128,7 @@ def validate_config(config, check_with_data): assert config["database"] is None or isinstance(config["database"], str) # noqa: E501 + assert isinstance(config["debug"], bool) assert isinstance(config["port"], int) assert isinstance(config["host"], str) assert config["webserver"] is None or isinstance(config["webserver"], str) # noqa: E501 diff --git a/flatisfy/web/app.py b/flatisfy/web/app.py index 545a32c..122662b 100644 --- a/flatisfy/web/app.py +++ b/flatisfy/web/app.py @@ -42,7 +42,6 @@ def _serve_static_file(filename): ) ) - def get_app(config): """ Get a Bottle app instance with all the routes set-up. @@ -51,7 +50,7 @@ def get_app(config): """ get_session = database.init_db(config["database"], config["search_index"]) - app = bottle.default_app() + app = bottle.Bottle() app.install(DatabasePlugin(get_session)) app.install(ConfigPlugin(config)) app.config.setdefault("canister.log_level", logging.root.level) @@ -60,24 +59,40 @@ def get_app(config): app.install(canister.Canister()) # Use DateAwareJSONEncoder to dump JSON strings # From http://stackoverflow.com/questions/21282040/bottle-framework-how-to-return-datetime-in-json-response#comment55718456_21282666. pylint: disable=locally-disabled,line-too-long - bottle.install( + app.install( bottle.JSONPlugin( json_dumps=functools.partial(json.dumps, cls=DateAwareJSONEncoder) ) ) - # API v1 routes - app.route("/api/v1/", "GET", api_routes.index_v1) + # Enable CORS + @app.hook('after_request') + def enable_cors(): + """ + Add CORS headers at each request. + """ + # The str() call is required as we import unicode_literal and WSGI + # headers list should have plain str type. + bottle.response.headers[str('Access-Control-Allow-Origin')] = '*' + bottle.response.headers[str('Access-Control-Allow-Methods')] = ( + 'PUT, GET, POST, DELETE, OPTIONS' + ) + bottle.response.headers[str('Access-Control-Allow-Headers')] = ( + 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token' + ) - app.route("/api/v1/time_to_places", "GET", + # API v1 routes + app.route("/api/v1/", ["GET", "OPTIONS"], api_routes.index_v1) + + app.route("/api/v1/time_to_places", ["GET", "OPTIONS"], api_routes.time_to_places_v1) - app.route("/api/v1/flats", "GET", api_routes.flats_v1) - app.route("/api/v1/flats/:flat_id", "GET", api_routes.flat_v1) - app.route("/api/v1/flats/:flat_id", "PATCH", + app.route("/api/v1/flats", ["GET", "OPTIONS"], api_routes.flats_v1) + app.route("/api/v1/flats/:flat_id", ["GET", "OPTIONS"], api_routes.flat_v1) + app.route("/api/v1/flats/:flat_id", ["PATCH", "OPTIONS"], api_routes.update_flat_v1) - app.route("/api/v1/ics/visits.ics", "GET", + app.route("/api/v1/ics/visits.ics", ["GET", "OPTIONS"], api_routes.ics_feed_v1) app.route("/api/v1/search", "POST", api_routes.search_v1) diff --git a/flatisfy/web/routes/api.py b/flatisfy/web/routes/api.py index 11e565d..13e7a6b 100644 --- a/flatisfy/web/routes/api.py +++ b/flatisfy/web/routes/api.py @@ -103,6 +103,10 @@ def flats_v1(config, db): :return: The available flats objects in a JSON ``data`` dict. """ + if bottle.request.method == 'OPTIONS': + # CORS + return '' + try: db_query = db.query(flat_model.Flat) @@ -139,6 +143,10 @@ def flat_v1(flat_id, config, db): :return: The flat object in a JSON ``data`` dict. """ + if bottle.request.method == 'OPTIONS': + # CORS + return {} + try: flat = db.query(flat_model.Flat).filter_by(id=flat_id).first() @@ -171,6 +179,10 @@ def update_flat_v1(flat_id, config, db): :return: The new flat object in a JSON ``data`` dict. """ + if bottle.request.method == 'OPTIONS': + # CORS + return {} + try: flat = db.query(flat_model.Flat).filter_by(id=flat_id).first() if not flat: @@ -204,6 +216,10 @@ def time_to_places_v1(config): :return: The JSON dump of the places to compute time to (dict of places names mapped to GPS coordinates). """ + if bottle.request.method == 'OPTIONS': + # CORS + return {} + try: places = {} for constraint_name, constraint in config["constraints"].items(): @@ -231,6 +247,10 @@ def search_v1(db, config): :return: The matching flat objects in a JSON ``data`` dict. """ + if bottle.request.method == 'OPTIONS': + # CORS + return {} + try: try: query = json.load(bottle.request.body)["query"] @@ -260,6 +280,10 @@ def ics_feed_v1(config, db): :return: The ICS feed for the visits. """ + if bottle.request.method == 'OPTIONS': + # CORS + return {} + cal = vobject.iCalendar() try: flats_with_visits = db.query(flat_model.Flat).filter(