diff --git a/ember/.ember-cli b/ember/.ember-cli index 6f52478f37..644f89c031 100644 --- a/ember/.ember-cli +++ b/ember/.ember-cli @@ -6,5 +6,6 @@ Setting `disableAnalytics` to true will prevent any data from being sent. */ "disableAnalytics": false, - "outputPath": "../skylines/frontend/static" + "outputPath": "../skylines/frontend/static", + "proxy": "http://localhost:5000/" } diff --git a/ember/app/authenticators/cookie.js b/ember/app/authenticators/cookie.js index 574315fd4d..cb64ed0adc 100644 --- a/ember/app/authenticators/cookie.js +++ b/ember/app/authenticators/cookie.js @@ -5,7 +5,7 @@ export default Base.extend({ ajax: Ember.inject.service(), authenticate(email, password) { - return this.get('ajax').request('/session', { method: 'PUT', json: { email, password } }) + return this.get('ajax').request('/api/session', { method: 'PUT', json: { email, password } }) .then(() => this.get('ajax').request('/api/settings/')) .then(settings => ({ settings })); }, @@ -16,6 +16,6 @@ export default Base.extend({ }, invalidate(/* data */) { - return this.get('ajax').request('/session', { method: 'DELETE' }); + return this.get('ajax').request('/api/session', { method: 'DELETE' }); }, }); diff --git a/ember/app/routes/application.js b/ember/app/routes/application.js index 54801774af..f5cef1fb6b 100644 --- a/ember/app/routes/application.js +++ b/ember/app/routes/application.js @@ -56,10 +56,13 @@ export default Ember.Route.extend(ApplicationRouteMixin, { sessionAuthenticated() { const attemptedTransition = this.get('session.attemptedTransition'); + const inLoginRoute = this.controllerFor('application').get('inLoginRoute'); if (attemptedTransition) { attemptedTransition.retry(); this.set('session.attemptedTransition', null); + } else if (inLoginRoute) { + this.transitionTo('index'); } }, }); diff --git a/ember/app/routes/login.js b/ember/app/routes/login.js index f81116f6c4..91e47d152f 100644 --- a/ember/app/routes/login.js +++ b/ember/app/routes/login.js @@ -1,4 +1,14 @@ import Ember from 'ember'; import UnauthenticatedRouteMixin from 'ember-simple-auth/mixins/unauthenticated-route-mixin'; -export default Ember.Route.extend(UnauthenticatedRouteMixin); +export default Ember.Route.extend(UnauthenticatedRouteMixin, { + setupController() { + this._super(...arguments); + this.controllerFor('application').set('inLoginRoute', true); + }, + + resetController() { + this._super(...arguments); + this.controllerFor('application').set('inLoginRoute', false); + }, +}); diff --git a/ember/app/utils/map-click-handler.js b/ember/app/utils/map-click-handler.js index e5473c3477..88be7a6603 100644 --- a/ember/app/utils/map-click-handler.js +++ b/ember/app/utils/map-click-handler.js @@ -242,7 +242,7 @@ const MapClickHandler = Ember.Object.extend({ * @param {Number} lat Latitude. */ getLocationInfo(lon, lat) { - let req = $.ajax(`/api/v0/mapitems?lon=${lon}&lat=${lat}`); + let req = $.ajax(`/api/mapitems?lon=${lon}&lat=${lat}`); req.done(data => this.showLocationData(data)); req.fail(() => this.showLocationData(null)); }, diff --git a/ember/bower.json b/ember/bower.json index 302812dc5d..4b030a44cb 100644 --- a/ember/bower.json +++ b/ember/bower.json @@ -13,7 +13,6 @@ "Flot": "flot#0.8.3", "flot-marks": "https://github.com/TobiasLohner/flot-marks.git#f09ded70f5a229a38ba0b9cfa92dbb448ca4daaf", "jquery": "1.10.2", - "jQuery-ajaxTransport-XDomainRequest": "jquery-ajaxtransport-xdomainrequest#1.0.3", "sidebar-v2": "0.2.1", "eonasdan-bootstrap-datetimepicker": "https://github.com/TobiasLohner/bootstrap-datetimepicker.git#c36342415a1be8fa013548402bf01718ca93d454", "BigScreen": "bigscreen#2.0.4" diff --git a/ember/ember-cli-build.js b/ember/ember-cli-build.js index feadc07f3e..91dc662fe0 100644 --- a/ember/ember-cli-build.js +++ b/ember/ember-cli-build.js @@ -31,8 +31,6 @@ module.exports = function(defaults) { // please specify an object with the list of modules as keys // along with the exports of each module as its value. - app.import('bower_components/jQuery-ajaxTransport-XDomainRequest/jquery.xdomainrequest.min.js'); - app.import('vendor/openlayers/ol3cesium.js'); app.import('vendor/openlayers/ol.css'); diff --git a/skylines/api/views/__init__.py b/skylines/api/views/__init__.py index 67eb183f01..fd1809914e 100644 --- a/skylines/api/views/__init__.py +++ b/skylines/api/views/__init__.py @@ -16,7 +16,6 @@ def register(app): from .airports import airports_blueprint from .airspace import airspace_blueprint from .clubs import clubs_blueprint - from .mapitems import mapitems_blueprint from .search import search_blueprint from .users import users from .user import user @@ -42,7 +41,6 @@ def require_user_agent(): app.register_blueprint(airports_blueprint) app.register_blueprint(airspace_blueprint) app.register_blueprint(clubs_blueprint) - app.register_blueprint(mapitems_blueprint) app.register_blueprint(search_blueprint) app.register_blueprint(user) app.register_blueprint(users) diff --git a/skylines/api/views/airports.py b/skylines/api/views/airports.py index 743a11cd40..89dbf9197a 100644 --- a/skylines/api/views/airports.py +++ b/skylines/api/views/airports.py @@ -12,10 +12,9 @@ } -@airports_blueprint.route('/airports/') -@airports_blueprint.route('/airports', endpoint='list') +@airports_blueprint.route('/airports', strict_slashes=False) @use_args(bbox_args) -def _list(args): +def list(args): airports = api.get_airports_by_bbox(args['bbox']) return jsonify(airports) diff --git a/skylines/api/views/airspace.py b/skylines/api/views/airspace.py index bbf62f1a4b..3ac6085b47 100644 --- a/skylines/api/views/airspace.py +++ b/skylines/api/views/airspace.py @@ -7,8 +7,7 @@ airspace_blueprint = Blueprint('airspace', 'skylines') -@airspace_blueprint.route('/airspace/') -@airspace_blueprint.route('/airspace', endpoint='list') -def _list(): +@airspace_blueprint.route('/airspace', strict_slashes=False) +def list(): location = parse_location(request.args) return jsonify(api.get_airspaces_by_location(location)) diff --git a/skylines/api/views/user.py b/skylines/api/views/user.py index ffc5d79da2..4cc2b91c60 100644 --- a/skylines/api/views/user.py +++ b/skylines/api/views/user.py @@ -8,8 +8,7 @@ user = Blueprint('user', 'skylines') -@user.route('/user/') -@user.route('/user') +@user.route('/user', strict_slashes=False) @oauth.required() def read(): user = User.get(request.user_id) diff --git a/skylines/api/views/users.py b/skylines/api/views/users.py index f57bc33e81..e1cd52659d 100644 --- a/skylines/api/views/users.py +++ b/skylines/api/views/users.py @@ -10,10 +10,9 @@ users = Blueprint('users', 'skylines') -@users.route('/users/') -@users.route('/users', endpoint='list') +@users.route('/users', strict_slashes=False) @use_args(pagination_args) -def _list(args): +def list(args): offset = (args['page'] - 1) * args['per_page'] limit = args['per_page'] diff --git a/skylines/api/views/waves.py b/skylines/api/views/waves.py index a1ac0e9af5..b8db41721d 100644 --- a/skylines/api/views/waves.py +++ b/skylines/api/views/waves.py @@ -7,8 +7,7 @@ waves_blueprint = Blueprint('waves', 'skylines') -@waves_blueprint.route('/mountain_wave_project/') -@waves_blueprint.route('/mountain_wave_project', endpoint='list') -def _list(): +@waves_blueprint.route('/mountain_wave_project', strict_slashes=False) +def list(): location = parse_location(request.args) return jsonify(api.get_waves_by_location(location)) diff --git a/skylines/app.py b/skylines/app.py index 519bc1012f..69add401e5 100644 --- a/skylines/app.py +++ b/skylines/app.py @@ -2,7 +2,6 @@ import config from flask import Flask -from raven.contrib.flask import Sentry from skylines.api.middleware import HTTPMethodOverrideMiddleware @@ -23,20 +22,18 @@ def __init__(self, name='skylines', config_file=None, *args, **kw): def add_sqlalchemy(self): """ Create and configure SQLAlchemy extension """ - from skylines.database import db, migrate + from skylines.database import db db.init_app(self) - migrate.init_app(self, db) def add_cache(self): """ Create and attach Cache extension """ - from flask.ext.cache import Cache - self.cache = Cache(self, with_jinja2_ext=False) + from skylines.frontend.cache import cache + cache.init_app(self) def add_login_manager(self): """ Create and attach Login extension """ - from flask.ext.login import LoginManager - self.login_manager = LoginManager() - self.login_manager.init_app(self) + from skylines.frontend.login import login_manager + login_manager.init_app(self) def add_logging_handlers(self): if self.debug: return @@ -61,16 +58,14 @@ def add_logging_handlers(self): self.logger.addHandler(file_handler) def add_sentry(self): - sentry_dsn = self.config.get('SENTRY_DSN') - if sentry_dsn: - Sentry(self, dsn=sentry_dsn) + from skylines.sentry import sentry + sentry.init_app(self) def add_celery(self): from skylines.worker.celery import celery celery.init_app(self) - return celery - def initialize_lib(self): + def load_egm96(self): from skylines.lib.geoid import load_geoid load_geoid(self) @@ -78,8 +73,7 @@ def initialize_lib(self): def create_app(*args, **kw): app = SkyLines(*args, **kw) app.add_sqlalchemy() - app.add_cache() - app.initialize_lib() + app.add_sentry() return app @@ -87,7 +81,6 @@ def create_http_app(*args, **kw): app = create_app(*args, **kw) app.add_logging_handlers() - app.add_sentry() app.add_celery() return app @@ -96,6 +89,8 @@ def create_http_app(*args, **kw): def create_frontend_app(*args, **kw): app = create_http_app('skylines.frontend', *args, **kw) + app.add_cache() + app.load_egm96() app.add_login_manager() import skylines.frontend.views diff --git a/skylines/commands/__init__.py b/skylines/commands/__init__.py index 3e5f787834..800e6e852a 100644 --- a/skylines/commands/__init__.py +++ b/skylines/commands/__init__.py @@ -1,7 +1,7 @@ import sys from flask.ext.script import Manager -from flask.ext.migrate import MigrateCommand +from flask.ext.migrate import Migrate, MigrateCommand from .shell import Shell from .server import Server, APIServer @@ -19,6 +19,7 @@ from .search import Search from skylines.app import create_app +from skylines.database import db from config import to_envvar @@ -27,7 +28,9 @@ def _create_app(config): print 'Config file "{}" not found.'.format(config) sys.exit(1) - return create_app() + app = create_app() + app.migrate = Migrate(app, db) + return app manager = Manager(_create_app) diff --git a/skylines/database.py b/skylines/database.py index d91e8059c2..d132ae90ca 100644 --- a/skylines/database.py +++ b/skylines/database.py @@ -1,5 +1,4 @@ from flask.ext.sqlalchemy import SQLAlchemy -from flask.ext.migrate import Migrate def query(cls, **kw): @@ -20,7 +19,6 @@ def exists(cls, **kw): db = SQLAlchemy(session_options=dict(expire_on_commit=False)) -migrate = Migrate() db.Model.flask_query = db.Model.query db.Model.query = classmethod(query) diff --git a/skylines/frontend/cache.py b/skylines/frontend/cache.py new file mode 100644 index 0000000000..0ea3d61436 --- /dev/null +++ b/skylines/frontend/cache.py @@ -0,0 +1,3 @@ +from flask.ext.cache import Cache + +cache = Cache() diff --git a/skylines/frontend/login.py b/skylines/frontend/login.py new file mode 100644 index 0000000000..8c39f5da43 --- /dev/null +++ b/skylines/frontend/login.py @@ -0,0 +1,23 @@ +import base64 + +from flask.ext.login import LoginManager + +from skylines.model import User + +login_manager = LoginManager() + + +@login_manager.user_loader +def load_user(user_id): + return User.get(user_id) + + +@login_manager.header_loader +def load_user_from_header(header_val): + try: + header_val = header_val.replace('Basic ', '', 1) + header_val = base64.b64decode(header_val) + email, password = header_val.split(':', 1) + return User.by_credentials(email, password) + except: + return None diff --git a/skylines/frontend/views/__init__.py b/skylines/frontend/views/__init__.py index 7b95e1e7ac..a5f522f2ac 100644 --- a/skylines/frontend/views/__init__.py +++ b/skylines/frontend/views/__init__.py @@ -12,6 +12,7 @@ from .flight import flight_blueprint from .flights import flights_blueprint from .livetrack24 import lt24_blueprint +from .mapitems import mapitems_blueprint from .notifications import notifications_blueprint from .ranking import ranking_blueprint from .search import search_blueprint @@ -31,25 +32,27 @@ def register(app): register_i18n(app) register_login(app) - app.register_blueprint(about_blueprint) - app.register_blueprint(airport_blueprint) - app.register_blueprint(aircraft_models_blueprint) app.register_blueprint(assets_blueprint) - app.register_blueprint(club_blueprint) - app.register_blueprint(clubs_blueprint) app.register_blueprint(files_blueprint) - app.register_blueprint(flight_blueprint) - app.register_blueprint(flights_blueprint) app.register_blueprint(lt24_blueprint) - app.register_blueprint(notifications_blueprint) - app.register_blueprint(ranking_blueprint) - app.register_blueprint(search_blueprint) - app.register_blueprint(settings_blueprint) - app.register_blueprint(statistics_blueprint) - app.register_blueprint(timeline_blueprint) - app.register_blueprint(track_blueprint) - app.register_blueprint(tracking_blueprint) - app.register_blueprint(upload_blueprint) - app.register_blueprint(user_blueprint) - app.register_blueprint(users_blueprint) app.register_blueprint(widgets_blueprint) + + app.register_blueprint(about_blueprint, url_prefix='/api') + app.register_blueprint(airport_blueprint, url_prefix='/api') + app.register_blueprint(aircraft_models_blueprint, url_prefix='/api') + app.register_blueprint(club_blueprint, url_prefix='/api') + app.register_blueprint(clubs_blueprint, url_prefix='/api') + app.register_blueprint(flight_blueprint, url_prefix='/api') + app.register_blueprint(flights_blueprint, url_prefix='/api') + app.register_blueprint(mapitems_blueprint, url_prefix='/api') + app.register_blueprint(notifications_blueprint, url_prefix='/api') + app.register_blueprint(ranking_blueprint, url_prefix='/api') + app.register_blueprint(search_blueprint, url_prefix='/api') + app.register_blueprint(settings_blueprint, url_prefix='/api') + app.register_blueprint(statistics_blueprint, url_prefix='/api') + app.register_blueprint(timeline_blueprint, url_prefix='/api') + app.register_blueprint(track_blueprint, url_prefix='/api') + app.register_blueprint(tracking_blueprint, url_prefix='/api') + app.register_blueprint(upload_blueprint, url_prefix='/api') + app.register_blueprint(user_blueprint, url_prefix='/api') + app.register_blueprint(users_blueprint, url_prefix='/api') diff --git a/skylines/frontend/views/about.py b/skylines/frontend/views/about.py index 24ccff728f..0091f0aebf 100644 --- a/skylines/frontend/views/about.py +++ b/skylines/frontend/views/about.py @@ -5,7 +5,7 @@ about_blueprint = Blueprint('about', 'skylines') -@about_blueprint.route('/api/imprint') +@about_blueprint.route('/imprint') def imprint(): content = current_app.config.get( 'SKYLINES_IMPRINT', @@ -14,7 +14,7 @@ def imprint(): return jsonify(content=content) -@about_blueprint.route('/api/team') +@about_blueprint.route('/team') def skylines_team(): path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '..', 'AUTHORS.md') @@ -24,7 +24,7 @@ def skylines_team(): return jsonify(content=content) -@about_blueprint.route('/api/license') +@about_blueprint.route('/license') def license(): path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '..', 'LICENSE') diff --git a/skylines/frontend/views/aircraft_models.py b/skylines/frontend/views/aircraft_models.py index 4648981cc2..7f79acd72f 100644 --- a/skylines/frontend/views/aircraft_models.py +++ b/skylines/frontend/views/aircraft_models.py @@ -6,7 +6,7 @@ aircraft_models_blueprint = Blueprint('aircraft_models', 'skylines') -@aircraft_models_blueprint.route('/api/aircraft-models', strict_slashes=False) +@aircraft_models_blueprint.route('/aircraft-models', strict_slashes=False) def index(): models = AircraftModel.query() \ .order_by(AircraftModel.kind) \ diff --git a/skylines/frontend/views/airport.py b/skylines/frontend/views/airport.py index 27ba3221a8..d2a96c8902 100644 --- a/skylines/frontend/views/airport.py +++ b/skylines/frontend/views/airport.py @@ -6,7 +6,7 @@ airport_blueprint = Blueprint('airport', 'skylines') -@airport_blueprint.route('/api/airports/') +@airport_blueprint.route('/airports/') def index(airport_id): airport = get_requested_record(Airport, airport_id) diff --git a/skylines/frontend/views/club.py b/skylines/frontend/views/club.py index 71d45fc65a..ae2b448f3b 100644 --- a/skylines/frontend/views/club.py +++ b/skylines/frontend/views/club.py @@ -8,28 +8,20 @@ club_blueprint = Blueprint('club', 'skylines') -@club_blueprint.url_value_preprocessor -def _pull_user_id(endpoint, values): - g.club_id = values.pop('club_id') - g.club = get_requested_record(Club, g.club_id) +@club_blueprint.route('/clubs/', strict_slashes=False) +def read(club_id): + club = get_requested_record(Club, club_id) - -@club_blueprint.url_defaults -def _add_user_id(endpoint, values): - if hasattr(g, 'club_id'): - values.setdefault('club_id', g.club_id) - - -@club_blueprint.route('/api/clubs/', strict_slashes=False) -def read(): - json = ClubSchema().dump(g.club).data - json['isWritable'] = g.club.is_writable(g.current_user) + json = ClubSchema().dump(club).data + json['isWritable'] = club.is_writable(g.current_user) return jsonify(**json) -@club_blueprint.route('/api/clubs/', methods=['POST'], strict_slashes=False) -def update(): +@club_blueprint.route('/clubs/', methods=['POST'], strict_slashes=False) +def update(club_id): + club = get_requested_record(Club, club_id) + json = request.get_json() if json is None: return jsonify(error='invalid-request'), 400 @@ -42,13 +34,13 @@ def update(): if 'name' in data: name = data.get('name') - if name != g.club.name and Club.exists(name=name): + if name != club.name and Club.exists(name=name): return jsonify(error='duplicate-club-name'), 422 - g.club.name = name + club.name = name if 'website' in data: - g.club.website = data.get('website') + club.website = data.get('website') db.session.commit() diff --git a/skylines/frontend/views/clubs.py b/skylines/frontend/views/clubs.py index 5dd8555808..e13a117930 100644 --- a/skylines/frontend/views/clubs.py +++ b/skylines/frontend/views/clubs.py @@ -7,7 +7,7 @@ clubs_blueprint = Blueprint('clubs', 'skylines') -@clubs_blueprint.route('/api/clubs/', strict_slashes=False) +@clubs_blueprint.route('/clubs/', strict_slashes=False) def list(): clubs = Club.query().order_by(func.lower(Club.name)) diff --git a/skylines/frontend/views/flight.py b/skylines/frontend/views/flight.py index 1a90b06198..524952af63 100644 --- a/skylines/frontend/views/flight.py +++ b/skylines/frontend/views/flight.py @@ -3,24 +3,25 @@ from flask import Blueprint, request, abort, current_app, jsonify, g, make_response +from sqlalchemy import literal_column, and_ from sqlalchemy.orm import undefer_group, contains_eager from sqlalchemy.sql.expression import func from geoalchemy2.shape import to_shape from datetime import timedelta +from skylines.frontend.cache import cache from skylines.database import db from skylines.lib import files -from skylines.lib.dbutil import get_requested_record_list +from skylines.lib.dbutil import get_requested_record from skylines.lib.xcsoar_ import analyse_flight from skylines.lib.datetime import from_seconds_of_day from skylines.lib.geo import METERS_PER_DEGREE from skylines.lib.geoid import egm96_height from skylines.model import ( - User, Flight, Location, FlightComment, + User, Flight, Elevation, Location, FlightComment, Notification, Event, FlightMeetings, AircraftModel, ) from skylines.model.event import create_flight_comment_notifications -from skylines.model.flight import get_elevations_for_flight from skylines.schemas import fields, FlightSchema, FlightCommentSchema, FlightPhaseSchema, ContestLegSchema, Schema, ValidationError from skylines.worker import tasks from redis.exceptions import ConnectionError @@ -42,31 +43,10 @@ def _reanalyse_if_needed(flight): db.session.commit() -@flight_blueprint.url_value_preprocessor -def _pull_flight_id(endpoint, values): - g.flight_id = values.pop('flight_id') - - def _patch_query(q): return q.join(Flight.igc_file) \ - .options(contains_eager(Flight.igc_file)) \ - .filter(Flight.is_viewable(g.current_user)) - - -@flight_blueprint.before_request -def _query_flights(): - flights = get_requested_record_list( - Flight, g.flight_id, patch_query=_patch_query) - - g.flight = flights[0] - - map(_reanalyse_if_needed, flights) - - -@flight_blueprint.url_defaults -def _add_flight_id(endpoint, values): - if hasattr(g, 'flight_id'): - values.setdefault('flight_id', g.flight_id) + .options(contains_eager(Flight.igc_file)) \ + .filter(Flight.is_viewable(g.current_user)) def _get_flight_path(flight, threshold=0.001, max_points=3000): @@ -170,19 +150,25 @@ class NearFlightSchema(Schema): times = fields.Nested(MeetingTimeSchema, many=True) -@flight_blueprint.route('/api/flights/', strict_slashes=False) -def read(): - mark_flight_notifications_read(g.flight) +@flight_blueprint.route('/flights/', strict_slashes=False) +def read(flight_id): + flight = get_requested_record(Flight, flight_id, joinedload=[Flight.igc_file]) + + if not flight.is_viewable(g.current_user): + return jsonify(), 404 + + _reanalyse_if_needed(flight) + mark_flight_notifications_read(flight) - flight = FlightSchema().dump(g.flight).data + flight_json = FlightSchema().dump(flight).data if 'extended' not in request.args: - return jsonify(flight=flight) + return jsonify(flight=flight_json) - near_flights = FlightMeetings.get_meetings(g.flight).values() + near_flights = FlightMeetings.get_meetings(flight).values() near_flights = NearFlightSchema().dump(near_flights, many=True).data - comments = FlightCommentSchema().dump(g.flight.comments, many=True).data + comments = FlightCommentSchema().dump(flight.comments, many=True).data phases_schema = FlightPhaseSchema(only=( 'circlingDirection', @@ -197,7 +183,7 @@ def read(): 'glideRate', )) - phases = phases_schema.dump(g.flight.phases, many=True).data + phases = phases_schema.dump(flight.phases, many=True).data cruise_performance_schema = FlightPhaseSchema(only=( 'duration', @@ -210,7 +196,7 @@ def read(): 'count', )) - cruise_performance = cruise_performance_schema.dump(g.flight.cruise_performance).data + cruise_performance = cruise_performance_schema.dump(flight.cruise_performance).data circling_performance_schema = FlightPhaseSchema(only=( 'circlingDirection', @@ -221,17 +207,17 @@ def read(): 'altDiff', )) - circling_performance = circling_performance_schema.dump(g.flight.circling_performance, many=True).data + circling_performance = circling_performance_schema.dump(flight.circling_performance, many=True).data performance = dict(circling=circling_performance, cruise=cruise_performance) contest_leg_schema = ContestLegSchema() contest_legs = {} for type in ['classic', 'triangle']: - legs = g.flight.get_contest_legs('olc_plus', type) + legs = flight.get_contest_legs('olc_plus', type) contest_legs[type] = contest_leg_schema.dump(legs, many=True).data return jsonify( - flight=flight, + flight=flight_json, near_flights=near_flights, comments=comments, contest_legs=contest_legs, @@ -239,22 +225,27 @@ def read(): performance=performance) -@flight_blueprint.route('/api/flights//json') -def json(): +@flight_blueprint.route('/flights//json') +def json(flight_id): + flight = get_requested_record(Flight, flight_id, joinedload=[Flight.igc_file]) + + if not flight.is_viewable(g.current_user): + return jsonify(), 404 + # Return HTTP Status code 304 if an upstream or browser cache already # contains the response and if the igc file did not change to reduce # latency and server load # This implementation is very basic. Sadly Flask (0.10.1) does not have # this feature - last_modified = g.flight.time_modified \ + last_modified = flight.time_modified \ .strftime('%a, %d %b %Y %H:%M:%S GMT') modified_since = request.headers.get('If-Modified-Since') etag = request.headers.get('If-None-Match') if (modified_since and modified_since == last_modified) or \ - (etag and etag == g.flight.igc_file.md5): + (etag and etag == flight.igc_file.md5): return ('', 304) - trace = _get_flight_path(g.flight, threshold=0.0001, max_points=10000) + trace = _get_flight_path(flight, threshold=0.0001, max_points=10000) if not trace: abort(404) @@ -266,14 +257,14 @@ def json(): contests=trace['contests'], elevations_t=trace['elevations_t'], elevations_h=trace['elevations_h'], - sfid=g.flight.id, + sfid=flight.id, geoid=trace['geoid'], additional=dict( - registration=g.flight.registration, - competition_id=g.flight.competition_id))) + registration=flight.registration, + competition_id=flight.competition_id))) resp.headers['Last-Modified'] = last_modified - resp.headers['Etag'] = g.flight.igc_file.md5 + resp.headers['Etag'] = flight.igc_file.md5 return resp @@ -337,8 +328,13 @@ def _get_near_flights(flight, location, time, max_distance=1000): return flights -@flight_blueprint.route('/api/flights//near') -def near(): +@flight_blueprint.route('/flights//near') +def near(flight_id): + flight = get_requested_record(Flight, flight_id, joinedload=[Flight.igc_file]) + + if not flight.is_viewable(g.current_user): + return jsonify(), 404 + try: latitude = float(request.args['lat']) longitude = float(request.args['lon']) @@ -348,9 +344,9 @@ def near(): abort(400) location = Location(latitude=latitude, longitude=longitude) - time = from_seconds_of_day(g.flight.takeoff_time, time) + time = from_seconds_of_day(flight.takeoff_time, time) - flights = _get_near_flights(g.flight, location, time, 1000) + flights = _get_near_flights(flight, location, time, 1000) def add_flight_path(flight): trace = _get_flight_path(flight, threshold=0.0001, max_points=10000) @@ -363,9 +359,11 @@ def add_flight_path(flight): return jsonify(flights=map(add_flight_path, flights)) -@flight_blueprint.route('/api/flights/', methods=['POST'], strict_slashes=False) -def update(): - if not g.flight.is_writable(g.current_user): +@flight_blueprint.route('/flights/', methods=['POST'], strict_slashes=False) +def update(flight_id): + flight = get_requested_record(Flight, flight_id) + + if not flight.is_writable(g.current_user): return jsonify(), 403 json = request.get_json() @@ -390,18 +388,18 @@ def update(): if pilot_club_id != g.current_user.club_id or (pilot_club_id is None and pilot_id != g.current_user.id): return jsonify(error='pilot-disallowed'), 422 - if g.flight.pilot_id != pilot_id: - g.flight.pilot_id = pilot_id + if flight.pilot_id != pilot_id: + flight.pilot_id = pilot_id # pilot_name is irrelevant, if pilot_id is given - g.flight.pilot_name = None + flight.pilot_name = None # update club if pilot changed - g.flight.club_id = pilot_club_id + flight.club_id = pilot_club_id else: - g.flight.pilot_id = None + flight.pilot_id = None if 'pilot_name' in data: - g.flight.pilot_name = data['pilot_name'] + flight.pilot_name = data['pilot_name'] if 'co_pilot_id' in data: co_pilot_id = data['co_pilot_id'] @@ -417,17 +415,17 @@ def update(): or (co_pilot_club_id is None and co_pilot_id != g.current_user.id): return jsonify(error='co-pilot-disallowed'), 422 - g.flight.co_pilot_id = co_pilot_id + flight.co_pilot_id = co_pilot_id # co_pilot_name is irrelevant, if co_pilot_id is given - g.flight.co_pilot_name = None + flight.co_pilot_name = None else: - g.flight.co_pilot_id = None + flight.co_pilot_id = None if 'co_pilot_name' in data: - g.flight.co_pilot_name = data['co_pilot_name'] + flight.co_pilot_name = data['co_pilot_name'] - if g.flight.co_pilot_id is not None and g.flight.co_pilot_id == g.flight.pilot_id: + if flight.co_pilot_id is not None and flight.co_pilot_id == flight.pilot_id: return jsonify(error='copilot-equals-pilot'), 422 if 'model_id' in data: @@ -436,44 +434,48 @@ def update(): if model_id is not None and not AircraftModel.exists(id=model_id): return jsonify(error='unknown-aircraft-model'), 422 - g.flight.model_id = model_id + flight.model_id = model_id if 'registration' in data: - g.flight.registration = data['registration'] + flight.registration = data['registration'] if 'competition_id' in data: - g.flight.competition_id = data['competition_id'] + flight.competition_id = data['competition_id'] if 'privacy_level' in data: - g.flight.privacy_level = data['privacy_level'] + flight.privacy_level = data['privacy_level'] try: - tasks.analyse_flight.delay(g.flight.id) - tasks.find_meetings.delay(g.flight.id) + tasks.analyse_flight.delay(flight.id) + tasks.find_meetings.delay(flight.id) except ConnectionError: current_app.logger.info('Cannot connect to Redis server') - g.flight.time_modified = datetime.utcnow() + flight.time_modified = datetime.utcnow() db.session.commit() return jsonify() -@flight_blueprint.route('/api/flights/', methods=('DELETE',), strict_slashes=False) -def delete(): - if not g.flight.is_writable(g.current_user): +@flight_blueprint.route('/flights/', methods=('DELETE',), strict_slashes=False) +def delete(flight_id): + flight = get_requested_record(Flight, flight_id, joinedload=[Flight.igc_file]) + + if not flight.is_writable(g.current_user): abort(403) - files.delete_file(g.flight.igc_file.filename) - db.session.delete(g.flight) - db.session.delete(g.flight.igc_file) + files.delete_file(flight.igc_file.filename) + db.session.delete(flight) + db.session.delete(flight.igc_file) db.session.commit() return jsonify() -@flight_blueprint.route('/api/flights//comments', methods=('POST',)) -def add_comment(): +@flight_blueprint.route('/flights//comments', methods=('POST',)) +def add_comment(flight_id): + flight = get_requested_record(Flight, flight_id) + if not g.current_user: return jsonify(), 403 @@ -488,7 +490,7 @@ def add_comment(): comment = FlightComment() comment.user = g.current_user - comment.flight = g.flight + comment.flight = flight comment.text = data['text'] create_flight_comment_notifications(comment) @@ -496,3 +498,62 @@ def add_comment(): db.session.commit() return jsonify() + + +def get_elevations_for_flight(flight): + cached_elevations = cache.get('elevations_' + flight.__repr__()) + if cached_elevations: + return cached_elevations + + ''' + WITH src AS + (SELECT ST_DumpPoints(flights.locations) AS location, + flights.timestamps AS timestamps, + flights.locations AS locations + FROM flights + WHERE flights.id = 30000) + SELECT timestamps[(src.location).path[1]] AS timestamp, + ST_Value(elevations.rast, (src.location).geom) AS elevation + FROM elevations, src + WHERE src.locations && elevations.rast AND (src.location).geom && elevations.rast; + ''' + + # Prepare column expressions + location = Flight.locations.ST_DumpPoints() + + # Prepare cte + cte = db.session.query(location.label('location'), + Flight.locations.label('locations'), + Flight.timestamps.label('timestamps')) \ + .filter(Flight.id == flight.id).cte() + + # Prepare column expressions + timestamp = literal_column('timestamps[(location).path[1]]') + elevation = Elevation.rast.ST_Value(cte.c.location.geom) + + # Prepare main query + q = db.session.query(timestamp.label('timestamp'), + elevation.label('elevation')) \ + .filter(and_(cte.c.locations.intersects(Elevation.rast), + cte.c.location.geom.intersects(Elevation.rast))).all() + + if len(q) == 0: + return [] + + start_time = q[0][0] + start_midnight = start_time.replace(hour=0, minute=0, second=0, + microsecond=0) + + elevations = [] + for time, elevation in q: + if elevation is None: + continue + + time_delta = time - start_midnight + time = time_delta.days * 86400 + time_delta.seconds + + elevations.append((time, elevation)) + + cache.set('elevations_' + flight.__repr__(), elevations, timeout=3600 * 24) + + return elevations diff --git a/skylines/frontend/views/flights.py b/skylines/frontend/views/flights.py index e823df8a74..0cbb086cec 100644 --- a/skylines/frontend/views/flights.py +++ b/skylines/frontend/views/flights.py @@ -127,12 +127,12 @@ def _create_list(date=None, pilot=None, club=None, airport=None, return jsonify(**json) -@flights_blueprint.route('/api/flights/all') +@flights_blueprint.route('/flights/all') def all(): return _create_list(default_sorting_column='date', default_sorting_order='desc') -@flights_blueprint.route('/api/flights/date/') +@flights_blueprint.route('/flights/date/') def date(date, latest=False): try: if isinstance(date, (str, unicode)): @@ -147,7 +147,7 @@ def date(date, latest=False): return _create_list(date=date, default_sorting_column='score', default_sorting_order='desc') -@flights_blueprint.route('/api/flights/latest') +@flights_blueprint.route('/flights/latest') def latest(): query = db.session \ .query(func.max(Flight.date_local).label('date')) \ @@ -162,7 +162,7 @@ def latest(): return date(date_, latest=True) -@flights_blueprint.route('/api/flights/pilot/') +@flights_blueprint.route('/flights/pilot/') def pilot(id): pilot = get_requested_record(User, id) @@ -171,21 +171,21 @@ def pilot(id): return _create_list(pilot=pilot, default_sorting_column='date', default_sorting_order='desc') -@flights_blueprint.route('/api/flights/club/') +@flights_blueprint.route('/flights/club/') def club(id): club = get_requested_record(Club, id) return _create_list(club=club, default_sorting_column='date', default_sorting_order='desc') -@flights_blueprint.route('/api/flights/airport/') +@flights_blueprint.route('/flights/airport/') def airport(id): airport = get_requested_record(Airport, id) return _create_list(airport=airport, default_sorting_column='date', default_sorting_order='desc') -@flights_blueprint.route('/api/flights/unassigned') +@flights_blueprint.route('/flights/unassigned') def unassigned(): if not g.current_user: return jsonify(), 400 @@ -196,7 +196,7 @@ def unassigned(): return _create_list(filter=f, default_sorting_column='date', default_sorting_order='desc') -@flights_blueprint.route('/api/flights/list/') +@flights_blueprint.route('/flights/list/') def list(ids): if not ids: return jsonify(), 400 diff --git a/skylines/frontend/views/login.py b/skylines/frontend/views/login.py index 414b89da0c..3a306edbba 100644 --- a/skylines/frontend/views/login.py +++ b/skylines/frontend/views/login.py @@ -1,9 +1,6 @@ -import base64 - from flask import request, g, jsonify from flask.ext.login import login_user, logout_user, current_user -from skylines.frontend.ember import send_index from skylines.model import User from skylines.schemas import CurrentUserSchema, ValidationError @@ -11,20 +8,6 @@ def register(app): """ Register the /login and /logout routes on the given app """ - @app.login_manager.user_loader - def load_user(userid): - return User.get(userid) - - @app.login_manager.header_loader - def load_user_from_header(header_val): - try: - header_val = header_val.replace('Basic ', '', 1) - header_val = base64.b64decode(header_val) - email, password = header_val.split(':', 1) - return User.by_credentials(email, password) - except: - return None - @app.before_request def inject_current_user(): """ @@ -37,10 +20,7 @@ def inject_current_user(): else: g.current_user = current_user - @app.route('/login') - def login(): - return send_index() - + @app.route('/api/session', methods=('PUT',)) @app.route('/session', methods=('PUT',)) def create_session(): json = request.get_json() @@ -59,6 +39,7 @@ def create_session(): return jsonify() + @app.route('/api/session', methods=('DELETE',)) @app.route('/session', methods=('DELETE',)) def delete_session(): logout_user() diff --git a/skylines/api/views/mapitems.py b/skylines/frontend/views/mapitems.py similarity index 56% rename from skylines/api/views/mapitems.py rename to skylines/frontend/views/mapitems.py index 7b209d32b3..c5a1c3d546 100644 --- a/skylines/api/views/mapitems.py +++ b/skylines/frontend/views/mapitems.py @@ -1,15 +1,13 @@ -from flask import Blueprint, request +from flask import Blueprint, request, jsonify from skylines import api -from .json import jsonify -from .parser import parse_location +from skylines.api.views.parser import parse_location mapitems_blueprint = Blueprint('mapitems', 'skylines') -@mapitems_blueprint.route('/mapitems/') -@mapitems_blueprint.route('/mapitems', endpoint='list') -def _list(): +@mapitems_blueprint.route('/mapitems', strict_slashes=False) +def list(): location = parse_location(request.args) return jsonify({ 'airspaces': api.get_airspaces_by_location(location), diff --git a/skylines/frontend/views/notifications.py b/skylines/frontend/views/notifications.py index 4d60f27ccd..74c37a1600 100644 --- a/skylines/frontend/views/notifications.py +++ b/skylines/frontend/views/notifications.py @@ -29,7 +29,7 @@ def _filter_query(query, args): return query -@notifications_blueprint.route('/api/notifications', strict_slashes=False) +@notifications_blueprint.route('/notifications', strict_slashes=False) @login_required("You have to login to read notifications.") def list(): query = Notification.query(recipient=g.current_user) \ @@ -59,7 +59,7 @@ def get_event(notification): return jsonify(events=(map(convert_event, events))) -@notifications_blueprint.route('/api/notifications/clear', methods=('POST',)) +@notifications_blueprint.route('/notifications/clear', methods=('POST',)) @login_required("You have to login to clear notifications.") def clear(): def filter_func(query): diff --git a/skylines/frontend/views/ranking.py b/skylines/frontend/views/ranking.py index c05219846d..f7794c0793 100644 --- a/skylines/frontend/views/ranking.py +++ b/skylines/frontend/views/ranking.py @@ -69,7 +69,7 @@ def _parse_year(): return current_year -@ranking_blueprint.route('/api/ranking/pilots') +@ranking_blueprint.route('/ranking/pilots') def pilots(): data = _handle_request(User, 'pilot_id') @@ -89,7 +89,7 @@ def pilots(): return jsonify(ranking=json, total=g.paginators['result'].count) -@ranking_blueprint.route('/api/ranking/clubs') +@ranking_blueprint.route('/ranking/clubs') def clubs(): data = _handle_request(Club, 'club_id') @@ -109,7 +109,7 @@ def clubs(): return jsonify(ranking=json, total=g.paginators['result'].count) -@ranking_blueprint.route('/api/ranking/airports') +@ranking_blueprint.route('/ranking/airports') def airports(): data = _handle_request(Airport, 'takeoff_airport_id') diff --git a/skylines/frontend/views/search.py b/skylines/frontend/views/search.py index ed68cd4c05..0051695c05 100644 --- a/skylines/frontend/views/search.py +++ b/skylines/frontend/views/search.py @@ -11,7 +11,7 @@ MODELS = [User, Club, Airport] -@search_blueprint.route('/api/search', strict_slashes=False) +@search_blueprint.route('/search', strict_slashes=False) def index(): search_text = request.values.get('text', '').strip() if not search_text: diff --git a/skylines/frontend/views/settings.py b/skylines/frontend/views/settings.py index 4cc67803e1..ad75d7a97e 100644 --- a/skylines/frontend/views/settings.py +++ b/skylines/frontend/views/settings.py @@ -1,9 +1,8 @@ -from flask import Blueprint, request, abort, g, jsonify - +from flask import Blueprint, request, g, jsonify +from flask.ext.login import login_required from sqlalchemy.sql.expression import and_, or_ from skylines.database import db -from skylines.lib.dbutil import get_requested_record from skylines.model import User, Club, Flight, IGCFile from skylines.model.event import ( create_club_join_event @@ -13,38 +12,15 @@ settings_blueprint = Blueprint('settings', 'skylines') -@settings_blueprint.before_request -def handle_user_param(): - """ - Extracts the `user` parameter from request.values, queries the - corresponding User model and checks if the model is writeable by the - current user. - """ - - if request.endpoint == 'settings.html': - return - - if not g.current_user: - abort(401) - - g.user_id = request.values.get('user') - - if g.user_id: - g.user = get_requested_record(User, g.user_id) - else: - g.user = g.current_user - - if not g.user.is_writable(g.current_user): - abort(403) - - -@settings_blueprint.route('/api/settings', strict_slashes=False) +@settings_blueprint.route('/settings', strict_slashes=False) +@login_required def read(): schema = CurrentUserSchema(exclude=('id')) - return jsonify(**schema.dump(g.user).data) + return jsonify(**schema.dump(g.current_user).data) -@settings_blueprint.route('/api/settings', methods=['POST'], strict_slashes=False) +@settings_blueprint.route('/settings', methods=['POST'], strict_slashes=False) +@login_required def update(): json = request.get_json() if json is None: @@ -58,61 +34,61 @@ def update(): if 'email_address' in data: email = data.get('email_address') - if email != g.user.email_address and User.exists(email_address=email): + if email != g.current_user.email_address and User.exists(email_address=email): return jsonify(error='email-exists-already'), 422 - g.user.email_address = email + g.current_user.email_address = email if 'first_name' in data: - g.user.first_name = data.get('first_name') + g.current_user.first_name = data.get('first_name') if 'last_name' in data: - g.user.last_name = data.get('last_name') + g.current_user.last_name = data.get('last_name') if 'distance_unit' in data: - g.user.distance_unit = data.get('distance_unit') + g.current_user.distance_unit = data.get('distance_unit') if 'speed_unit' in data: - g.user.speed_unit = data.get('speed_unit') + g.current_user.speed_unit = data.get('speed_unit') if 'lift_unit' in data: - g.user.lift_unit = data.get('lift_unit') + g.current_user.lift_unit = data.get('lift_unit') if 'altitude_unit' in data: - g.user.altitude_unit = data.get('altitude_unit') + g.current_user.altitude_unit = data.get('altitude_unit') if 'tracking_callsign' in data: - g.user.tracking_callsign = data.get('tracking_callsign') + g.current_user.tracking_callsign = data.get('tracking_callsign') if 'tracking_delay' in data: - g.user.tracking_delay = data.get('tracking_delay') + g.current_user.tracking_delay = data.get('tracking_delay') if 'password' in data: if 'currentPassword' not in data: return jsonify(error='current-password-missing'), 422 - if not g.user.validate_password(data['currentPassword']): + if not g.current_user.validate_password(data['currentPassword']): return jsonify(error='wrong-password'), 403 - g.user.password = data['password'] - g.user.recover_key = None + g.current_user.password = data['password'] + g.current_user.recover_key = None - if 'club_id' in data and data['club_id'] != g.user.club_id: + if 'club_id' in data and data['club_id'] != g.current_user.club_id: club_id = data['club_id'] if club_id is not None and not Club.exists(id=club_id): return jsonify(error='unknown-club'), 422 - g.user.club_id = club_id + g.current_user.club_id = club_id - create_club_join_event(club_id, g.user) + create_club_join_event(club_id, g.current_user) # assign the user's new club to all of his flights that have # no club yet flights = Flight.query().join(IGCFile) flights = flights.filter(and_(Flight.club_id == None, - or_(Flight.pilot_id == g.user.id, - IGCFile.owner_id == g.user.id))) + or_(Flight.pilot_id == g.current_user.id, + IGCFile.owner_id == g.current_user.id))) for flight in flights: flight.club_id = club_id @@ -121,24 +97,27 @@ def update(): return jsonify() -@settings_blueprint.route('/api/settings/password/check', methods=['POST']) +@settings_blueprint.route('/settings/password/check', methods=['POST']) +@login_required def check_current_password(): json = request.get_json() if not json: return jsonify(error='invalid-request'), 400 - return jsonify(result=g.user.validate_password(json.get('password', ''))) + return jsonify(result=g.current_user.validate_password(json.get('password', ''))) -@settings_blueprint.route('/api/settings/tracking/key', methods=['POST']) +@settings_blueprint.route('/settings/tracking/key', methods=['POST']) +@login_required def tracking_generate_key(): - g.user.generate_tracking_key() + g.current_user.generate_tracking_key() db.session.commit() - return jsonify(key=g.user.tracking_key_hex) + return jsonify(key=g.current_user.tracking_key_hex) -@settings_blueprint.route('/api/settings/club', methods=['PUT']) +@settings_blueprint.route('/settings/club', methods=['PUT']) +@login_required def create_club(): json = request.get_json() if json is None: @@ -159,10 +138,10 @@ def create_club(): db.session.flush() # assign the user to the new club - g.user.club = club + g.current_user.club = club # create the "user joined club" event - create_club_join_event(club.id, g.user) + create_club_join_event(club.id, g.current_user) db.session.commit() diff --git a/skylines/frontend/views/statistics.py b/skylines/frontend/views/statistics.py index d05966e326..810e9c182a 100644 --- a/skylines/frontend/views/statistics.py +++ b/skylines/frontend/views/statistics.py @@ -8,8 +8,8 @@ statistics_blueprint = Blueprint('statistics', 'skylines') -@statistics_blueprint.route('/api/statistics', strict_slashes=False) -@statistics_blueprint.route('/api/statistics//') +@statistics_blueprint.route('/statistics', strict_slashes=False) +@statistics_blueprint.route('/statistics//') def index(page=None, id=None): name = None diff --git a/skylines/frontend/views/timeline.py b/skylines/frontend/views/timeline.py index dc56a553c2..54c4100086 100644 --- a/skylines/frontend/views/timeline.py +++ b/skylines/frontend/views/timeline.py @@ -9,7 +9,7 @@ timeline_blueprint = Blueprint('timeline', 'skylines') -@timeline_blueprint.route('/api/timeline') +@timeline_blueprint.route('/timeline') def list(): query = Event.query() \ .options(subqueryload('actor')) \ diff --git a/skylines/frontend/views/track.py b/skylines/frontend/views/track.py index 33f3a34f1f..b827f22bc6 100644 --- a/skylines/frontend/views/track.py +++ b/skylines/frontend/views/track.py @@ -1,10 +1,10 @@ from datetime import datetime, timedelta from math import log -from flask import Blueprint, request, abort, jsonify, g +from flask import Blueprint, request, abort, jsonify from sqlalchemy.sql.expression import and_ -from skylines.lib.dbutil import get_requested_record_list +from skylines.lib.dbutil import get_requested_record_list, get_requested_record from skylines.lib.helpers import color from skylines.lib.xcsoar_ import FlightPathFix from skylines.lib.geoid import egm96_height @@ -14,28 +14,6 @@ track_blueprint = Blueprint('track', 'skylines') - -@track_blueprint.url_value_preprocessor -def _pull_user_id(endpoint, values): - if request.endpoint == 'track.html': - return - - g.user_id = values.pop('user_id') - - g.pilots = get_requested_record_list( - User, g.user_id, joinedload=[User.club]) - - color_gen = color.generator() - for pilot in g.pilots: - pilot.color = color_gen.next() - - -@track_blueprint.url_defaults -def _add_user_id(endpoint, values): - if hasattr(g, 'user_id'): - values.setdefault('user_id', g.user_id) - - UNKNOWN_ELEVATION = -1000 @@ -118,24 +96,30 @@ def _get_flight_path(pilot, threshold=0.001, last_update=None): # Use `live` alias here since `/api/tracking/*` is filtered by the "EasyPrivacy" adblocker list... -@track_blueprint.route('/api/tracking/', strict_slashes=False) -@track_blueprint.route('/api/live/', strict_slashes=False) -def read(): - traces = map(_get_flight_path, g.pilots) +@track_blueprint.route('/tracking/', strict_slashes=False) +@track_blueprint.route('/live/', strict_slashes=False) +def read(user_ids): + pilots = get_requested_record_list(User, user_ids, joinedload=[User.club]) + + color_gen = color.generator() + for pilot in pilots: + pilot.color = color_gen.next() + + traces = map(_get_flight_path, pilots) if not any(traces): traces = None user_schema = UserSchema() pilots_json = [] - for pilot in g.pilots: + for pilot in pilots: json = user_schema.dump(pilot).data json['color'] = pilot.color pilots_json.append(json) flights = [] if traces: - for pilot, trace in zip(g.pilots, traces): + for pilot, trace in zip(pilots, traces): if trace: flights.append({ 'sfid': pilot.id, @@ -156,10 +140,10 @@ def read(): return jsonify(flights=flights, pilots=pilots_json) -@track_blueprint.route('/api/tracking//json') -@track_blueprint.route('/api/live//json') -def json(): - pilot = g.pilots[0] +@track_blueprint.route('/tracking//json') +@track_blueprint.route('/live//json') +def json(user_id): + pilot = get_requested_record(User, user_id, joinedload=[User.club]) last_update = request.values.get('last_update', 0, type=int) trace = _get_flight_path(pilot, threshold=0.001, last_update=last_update) diff --git a/skylines/frontend/views/tracking.py b/skylines/frontend/views/tracking.py index 1f12bd6c6f..a1774eeee5 100644 --- a/skylines/frontend/views/tracking.py +++ b/skylines/frontend/views/tracking.py @@ -1,5 +1,6 @@ -from flask import Blueprint, current_app, jsonify, g +from flask import Blueprint, jsonify, g +from skylines.frontend.cache import cache from skylines.lib.decorators import jsonp from skylines.model import TrackingFix, Airport, Follower from skylines.schemas import TrackingFixSchema, AirportSchema @@ -7,13 +8,13 @@ tracking_blueprint = Blueprint('tracking', 'skylines') -@tracking_blueprint.route('/api/tracking', strict_slashes=False) -@tracking_blueprint.route('/api/live', strict_slashes=False) +@tracking_blueprint.route('/tracking', strict_slashes=False) +@tracking_blueprint.route('/live', strict_slashes=False) def index(): fix_schema = TrackingFixSchema(only=('time', 'location', 'altitude', 'elevation', 'pilot')) airport_schema = AirportSchema(only=('id', 'name', 'countryCode')) - @current_app.cache.memoize(timeout=(60 * 60)) + @cache.memoize(timeout=(60 * 60)) def get_nearest_airport(track): airport = Airport.by_location(track.location, None) if not airport: @@ -41,7 +42,7 @@ def get_nearest_airport(track): return jsonify(friends=followers, tracks=tracks) -@tracking_blueprint.route('/api/tracking/latest.json') +@tracking_blueprint.route('/tracking/latest.json') @jsonp def latest(): fixes = [] diff --git a/skylines/frontend/views/upload.py b/skylines/frontend/views/upload.py index f05f2a0b2d..1e672c6101 100644 --- a/skylines/frontend/views/upload.py +++ b/skylines/frontend/views/upload.py @@ -12,6 +12,7 @@ from redis.exceptions import ConnectionError from sqlalchemy.sql.expression import func +from skylines.frontend.cache import cache from skylines.database import db from skylines.lib import files from skylines.lib.util import pressure_alt_to_qnh_alt @@ -147,7 +148,7 @@ def _encode_flight_path(fp, qnh): igc_start_time=fp[0].datetime, igc_end_time=fp[-1].datetime) -@upload_blueprint.route('/api/flights/upload/csrf') +@upload_blueprint.route('/flights/upload/csrf') @login_required("You have to login to upload flights.") def csrf(): if not g.current_user: @@ -156,7 +157,7 @@ def csrf(): return jsonify(token=generate_csrf()) -@upload_blueprint.route('/api/flights/upload', methods=('POST',), strict_slashes=False) +@upload_blueprint.route('/flights/upload', methods=('POST',), strict_slashes=False) def index_post(): if not g.current_user: return jsonify(error='authentication-required'), 403 @@ -270,8 +271,8 @@ def index_post(): # Store data in cache for image creation cache_key = hashlib.sha1(str(flight.id) + '_' + str(user.id)).hexdigest() - current_app.cache.set('upload_airspace_infringements_' + cache_key, infringements, timeout=15 * 60) - current_app.cache.set('upload_airspace_flight_path_' + cache_key, fp, timeout=15 * 60) + cache.set('upload_airspace_infringements_' + cache_key, infringements, timeout=15 * 60) + cache.set('upload_airspace_flight_path_' + cache_key, fp, timeout=15 * 60) airspace = db.session.query(Airspace) \ .filter(Airspace.id.in_(infringements.keys())) \ @@ -305,7 +306,7 @@ def index_post(): return jsonify(results=results, club_members=club_members, aircraft_models=aircraft_models) -@upload_blueprint.route('/api/flights/upload/verify', methods=('POST',)) +@upload_blueprint.route('/flights/upload/verify', methods=('POST',)) @login_required('You have to login to upload flights.') def verify(): json = request.get_json() @@ -374,14 +375,14 @@ def verify(): return jsonify() -@upload_blueprint.route('/api/flights/upload/airspace//.png') +@upload_blueprint.route('/flights/upload/airspace//.png') def airspace_image(cache_key, airspace_id): if not mapscript_available: abort(404) # get information from cache... - infringements = current_app.cache.get('upload_airspace_infringements_' + cache_key) - flight_path = current_app.cache.get('upload_airspace_flight_path_' + cache_key) + infringements = cache.get('upload_airspace_infringements_' + cache_key) + flight_path = cache.get('upload_airspace_flight_path_' + cache_key) # abort if invalid cache key if not infringements \ diff --git a/skylines/frontend/views/user.py b/skylines/frontend/views/user.py index 71b54ab6a6..9a2077c6a3 100644 --- a/skylines/frontend/views/user.py +++ b/skylines/frontend/views/user.py @@ -17,21 +17,6 @@ user_blueprint = Blueprint('user', 'skylines') -@user_blueprint.url_value_preprocessor -def _pull_user_id(endpoint, values): - if request.endpoint == 'user.html': - return - - g.user_id = values.pop('user_id') - g.user = get_requested_record(User, g.user_id) - - -@user_blueprint.url_defaults -def _add_user_id(endpoint, values): - if hasattr(g, 'user_id'): - values.setdefault('user_id', g.user_id) - - def _largest_flight(user, schema): flight = user.get_largest_flights() \ .filter(Flight.is_rankable()) \ @@ -73,11 +58,11 @@ class QuickStatsSchema(Schema): duration = fields.TimeDelta() -def _quick_stats(): +def _quick_stats(user): result = db.session.query(func.count('*').label('flights'), func.sum(Flight.olc_classic_distance).label('distance'), func.sum(Flight.duration).label('duration')) \ - .filter(Flight.pilot == g.user) \ + .filter(Flight.pilot == user) \ .filter(Flight.date_local > (date.today() - timedelta(days=365))) \ .filter(Flight.is_rankable()) \ .one() @@ -85,9 +70,9 @@ def _quick_stats(): return QuickStatsSchema().dump(result).data -def _get_takeoff_locations(): +def _get_takeoff_locations(user): locations = Location.get_clustered_locations( - Flight.takeoff_location_wkt, filter=and_(Flight.pilot == g.user, Flight.is_rankable())) + Flight.takeoff_location_wkt, filter=and_(Flight.pilot == user, Flight.is_rankable())) return [loc.to_lonlat() for loc in locations] @@ -103,28 +88,32 @@ def add_user_filter(query): db.session.commit() -@user_blueprint.route('/api/users/', strict_slashes=False) -def read(): - user_schema = CurrentUserSchema() if g.user == g.current_user else UserSchema() - user = user_schema.dump(g.user).data +@user_blueprint.route('/users/', strict_slashes=False) +def read(user_id): + user = get_requested_record(User, user_id) + + user_schema = CurrentUserSchema() if user == g.current_user else UserSchema() + user_json = user_schema.dump(user).data if g.current_user: - user['followed'] = g.current_user.follows(g.user) + user_json['followed'] = g.current_user.follows(user) if 'extended' in request.args: - user['distanceFlights'] = _distance_flights(g.user) - user['stats'] = _quick_stats() - user['takeoffLocations'] = _get_takeoff_locations() + user_json['distanceFlights'] = _distance_flights(user) + user_json['stats'] = _quick_stats(user) + user_json['takeoffLocations'] = _get_takeoff_locations(user) - mark_user_notifications_read(g.user) + mark_user_notifications_read(user) - return jsonify(**user) + return jsonify(**user_json) -@user_blueprint.route('/api/users//followers') -def followers(): +@user_blueprint.route('/users//followers') +def followers(user_id): + user = get_requested_record(User, user_id) + # Query list of pilots that are following the selected user - query = Follower.query(destination=g.user) \ + query = Follower.query(destination=user) \ .join('source') \ .options(contains_eager('source')) \ .options(subqueryload('source.club')) \ @@ -138,10 +127,12 @@ def followers(): return jsonify(followers=followers) -@user_blueprint.route('/api/users//following') -def following(): +@user_blueprint.route('/users//following') +def following(user_id): + user = get_requested_record(User, user_id) + # Query list of pilots that are following the selected user - query = Follower.query(source=g.user) \ + query = Follower.query(source=user) \ .join('destination') \ .options(contains_eager('destination')) \ .options(subqueryload('destination.club')) \ @@ -174,18 +165,20 @@ def add_current_user_follows(followers): follower['currentUserFollows'] = (follower['id'] in current_user_follows) -@user_blueprint.route('/api/users//follow') +@user_blueprint.route('/users//follow') @login_required -def follow(): - Follower.follow(g.current_user, g.user) - create_follower_notification(g.user, g.current_user) +def follow(user_id): + user = get_requested_record(User, user_id) + Follower.follow(g.current_user, user) + create_follower_notification(user, g.current_user) db.session.commit() return jsonify() -@user_blueprint.route('/api/users//unfollow') +@user_blueprint.route('/users//unfollow') @login_required -def unfollow(): - Follower.unfollow(g.current_user, g.user) +def unfollow(user_id): + user = get_requested_record(User, user_id) + Follower.unfollow(g.current_user, user) db.session.commit() return jsonify() diff --git a/skylines/frontend/views/users.py b/skylines/frontend/views/users.py index 0f769925c7..a6671ce20c 100644 --- a/skylines/frontend/views/users.py +++ b/skylines/frontend/views/users.py @@ -16,7 +16,7 @@ users_blueprint = Blueprint('users', 'skylines') -@users_blueprint.route('/api/users', strict_slashes=False) +@users_blueprint.route('/users', strict_slashes=False) def list(): users = User.query() \ .options(joinedload(User.club)) \ @@ -32,7 +32,7 @@ def list(): return jsonify(users=UserSchema(only=fields).dump(users, many=True).data) -@users_blueprint.route('/api/users', methods=['POST'], strict_slashes=False) +@users_blueprint.route('/users', methods=['POST'], strict_slashes=False) def new_post(): json = request.get_json() if json is None: @@ -55,7 +55,7 @@ def new_post(): return jsonify(user=UserSchema().dump(user).data) -@users_blueprint.route('/api/users/recover', methods=['POST']) +@users_blueprint.route('/users/recover', methods=['POST']) def recover_post(): json = request.get_json() if json is None: @@ -136,7 +136,7 @@ def recover_step2_post(json): return jsonify() -@users_blueprint.route('/api/users/check-email', methods=['POST']) +@users_blueprint.route('/users/check-email', methods=['POST']) def check_email(): json = request.get_json() if not json: diff --git a/skylines/model/flight.py b/skylines/model/flight.py index 411826e687..d23e110f69 100644 --- a/skylines/model/flight.py +++ b/skylines/model/flight.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta from bisect import bisect_left -from flask import current_app from sqlalchemy.dialects import postgresql from sqlalchemy.orm import deferred @@ -21,7 +20,6 @@ from .geo import Location from .igcfile import IGCFile from .aircraft_model import AircraftModel -from .elevation import Elevation from .contest_leg import ContestLeg @@ -488,62 +486,3 @@ def update_flight_path(flight): db.session.commit() return True - - -def get_elevations_for_flight(flight): - cached_elevations = current_app.cache.get('elevations_' + flight.__repr__()) - if cached_elevations: - return cached_elevations - - ''' - WITH src AS - (SELECT ST_DumpPoints(flights.locations) AS location, - flights.timestamps AS timestamps, - flights.locations AS locations - FROM flights - WHERE flights.id = 30000) - SELECT timestamps[(src.location).path[1]] AS timestamp, - ST_Value(elevations.rast, (src.location).geom) AS elevation - FROM elevations, src - WHERE src.locations && elevations.rast AND (src.location).geom && elevations.rast; - ''' - - # Prepare column expressions - location = Flight.locations.ST_DumpPoints() - - # Prepare cte - cte = db.session.query(location.label('location'), - Flight.locations.label('locations'), - Flight.timestamps.label('timestamps')) \ - .filter(Flight.id == flight.id).cte() - - # Prepare column expressions - timestamp = literal_column('timestamps[(location).path[1]]') - elevation = Elevation.rast.ST_Value(cte.c.location.geom) - - # Prepare main query - q = db.session.query(timestamp.label('timestamp'), - elevation.label('elevation')) \ - .filter(and_(cte.c.locations.intersects(Elevation.rast), - cte.c.location.geom.intersects(Elevation.rast))).all() - - if len(q) == 0: - return [] - - start_time = q[0][0] - start_midnight = start_time.replace(hour=0, minute=0, second=0, - microsecond=0) - - elevations = [] - for time, elevation in q: - if elevation is None: - continue - - time_delta = time - start_midnight - time = time_delta.days * 86400 + time_delta.seconds - - elevations.append((time, elevation)) - - current_app.cache.set('elevations_' + flight.__repr__(), elevations, timeout=3600 * 24) - - return elevations diff --git a/skylines/sentry.py b/skylines/sentry.py new file mode 100644 index 0000000000..20ddeef3cd --- /dev/null +++ b/skylines/sentry.py @@ -0,0 +1,3 @@ +from raven.contrib.flask import Sentry + +sentry = Sentry() diff --git a/skylines/tracking/server.py b/skylines/tracking/server.py index 86d7b5513f..bdd7d5aa5f 100644 --- a/skylines/tracking/server.py +++ b/skylines/tracking/server.py @@ -3,13 +3,13 @@ from datetime import datetime, time, timedelta from gevent.server import DatagramServer -from raven import Client as RavenClient from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.sql.expression import or_ from skylines.database import db from skylines.model import User, TrackingFix, Follower, Elevation +from skylines.sentry import sentry from skylines.tracking.crc import check_crc, set_crc # More information about this protocol can be found in the XCSoar @@ -280,17 +280,11 @@ def __handle(self, data, (host, port)): def handle(self, data, address): try: self.__handle(data, address) - except Exception as e: - if self.raven: - self.raven.captureException() - else: - print e + except Exception: + sentry.captureException() def serve_forever(self, **kwargs): if not self.app: raise RuntimeError('application not registered on server instance') - sentry_dsn = self.app.config.get('SENTRY_DSN') - self.raven = RavenClient(dsn=sentry_dsn) if sentry_dsn else None - super(TrackingServer, self).serve_forever(**kwargs)