diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a74d9d44e..7fc5b4b2d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,7 +94,7 @@ jobs: python-version: 3.6 - run: pip install black==18.9b0 - - run: black config migrations skylines tests *.py --check + - run: black config migrations skylines tests *.py --check --diff deploy: name: Deploy diff --git a/skylines/api/oauth.py b/skylines/api/oauth.py index dc21cb8430..ca426fc85a 100644 --- a/skylines/api/oauth.py +++ b/skylines/api/oauth.py @@ -119,7 +119,7 @@ def tokengetter(access_token=None, refresh_token=None): @staticmethod def tokensetter(token, request, *args, **kwargs): - """ Save a new token to the database. + """Save a new token to the database. :param token: Token dictionary containing access and refresh tokens, plus token type. :param request: Request dictionary containing information about the client and user. diff --git a/skylines/api/views/tracking.py b/skylines/api/views/tracking.py index 92749091bf..db1eed6d11 100644 --- a/skylines/api/views/tracking.py +++ b/skylines/api/views/tracking.py @@ -43,7 +43,7 @@ def get_nearest_airport(track): ) tracks = [] - for t in TrackingFix.get_latest(): + for t in TrackingFix.get_from_time(): nearest_airport = get_nearest_airport(t) track = fix_schema.dump(t).data @@ -66,8 +66,17 @@ def get_nearest_airport(track): @tracking_blueprint.route("/tracking/latest.json") @jsonp def latest(): + """ + Supported query parameter: + - from_time: Returns only the fixes after `from_time` expressed as a UNIX + timestamp. The maximum age of the returned fixes is 6h. + """ fixes = [] - for fix in TrackingFix.get_latest(): + from_time = request.values.get("from_time", 0, type=int) + + for fix in TrackingFix.get_from_time( + from_time=from_time, max_age=timedelta(hours=6) + ): json = dict( time=fix.time.isoformat() + "Z", location=fix.location.to_wkt(), @@ -95,13 +104,22 @@ def latest(): @tracking_blueprint.route("/tracking/", strict_slashes=False) @tracking_blueprint.route("/live/", strict_slashes=False) def read(user_ids): + """ + Supported query parameter: + - from_time: Returns only the fixes after `from_time` expressed as a UNIX + timestamp. The maximum age of the fix is 12 hours. + """ + from_time = request.values.get("from_time", 0, type=int) + pilots = get_requested_record_list(User, user_ids, joinedload=[User.club]) color_gen = color.generator() for pilot in pilots: pilot.color = next(color_gen) - traces = list(map(_get_flight_path, pilots)) + traces = list( + map(lambda pilot: _get_flight_path(pilot, from_time=from_time), pilots) + ) if not any(traces): traces = None @@ -142,10 +160,23 @@ def read(user_ids): @tracking_blueprint.route("/tracking//json") @tracking_blueprint.route("/live//json") def json(user_id): + """ + Supported query parameters: + - last_update: Returns only the fixes after the `last_update` expressed in + seconds from the first fix, + - from_time: Returns only the fixes after `from_time` expressed as a UNIX + timestamp. + + Specifying both parameters is equivalent to ANDing the conditions. + The maximum age of the fixes is 12h. + """ pilot = get_requested_record(User, user_id, joinedload=[User.club]) last_update = request.values.get("last_update", 0, type=int) + from_time = request.values.get("from_time", 0, type=int) - trace = _get_flight_path(pilot, threshold=0.001, last_update=last_update) + trace = _get_flight_path( + pilot, threshold=0.001, last_update=last_update, from_time=from_time + ) if not trace: abort(404) @@ -160,8 +191,8 @@ def json(user_id): ) -def _get_flight_path(pilot, threshold=0.001, last_update=None): - fp = _get_flight_path2(pilot, last_update=last_update) +def _get_flight_path(pilot, threshold=0.001, last_update=None, from_time=None): + fp = _get_flight_path2(pilot, last_update=last_update, from_time=from_time) if not fp: return None @@ -217,7 +248,7 @@ def _get_flight_path(pilot, threshold=0.001, last_update=None): ) -def _get_flight_path2(pilot, last_update=None): +def _get_flight_path2(pilot, last_update=None, from_time=None): query = TrackingFix.query().filter( and_( TrackingFix.pilot == pilot, @@ -245,6 +276,10 @@ def _get_flight_path2(pilot, last_update=None): >= start_fix.time + timedelta(seconds=(last_update - start_time)) ) + if from_time: + from_datetime_utc = datetime.utcfromtimestamp(from_time) + query = query.filter(TrackingFix.time >= from_datetime_utc) + result = [] for fix in query: location = fix.location diff --git a/skylines/lib/igc.py b/skylines/lib/igc.py index f3a5e2ef46..eeb0ccc3bc 100644 --- a/skylines/lib/igc.py +++ b/skylines/lib/igc.py @@ -17,8 +17,8 @@ def read_igc_headers(f): - """ Read IGC file headers from a file-like object, a list of strings or a - file if the parameter is a path. """ + """Read IGC file headers from a file-like object, a list of strings or a + file if the parameter is a path.""" if is_string(f): try: diff --git a/skylines/model/tracking.py b/skylines/model/tracking.py index 7dbe6d1102..a4bb40f58a 100644 --- a/skylines/model/tracking.py +++ b/skylines/model/tracking.py @@ -71,7 +71,7 @@ def max_age_filter(cls, max_age): Returns a filter that makes sure that the fix is not older than a certain time. - The delay parameter can be either a datetime.timedelta or a numeric + The max_age parameter can be either a datetime.timedelta or a numeric value that will be interpreted as hours. """ @@ -81,7 +81,22 @@ def max_age_filter(cls, max_age): return cls.time >= datetime.utcnow() - max_age @classmethod - def get_latest(cls, max_age=timedelta(hours=6)): + def get_from_time(cls, from_time=0, max_age=timedelta(hours=6)): + """ + Creates a query returning fixes from the timestamp from_time having a + maximum age of max_age. + + The max_age parameter can be either a datetime.timedelta or a numeric + value that will be interpreted as hours. + """ + if is_int(max_age) or isinstance(max_age, float): + max_age = timedelta(hours=max_age) + + # from_time is only taken into account if more recent than max_age. + from_datetime_utc = datetime.utcfromtimestamp(from_time) + age = datetime.utcnow() - from_datetime_utc + age_filter = TrackingFix.max_age_filter(min(age, max_age)) + # Add a db.Column to the inner query with # numbers ordered by time for each pilot row_number = db.over( @@ -92,7 +107,7 @@ def get_latest(cls, max_age=timedelta(hours=6)): subq = ( db.session.query(cls.id, row_number.label("row_number")) .join(cls.pilot) - .filter(cls.max_age_filter(max_age)) + .filter(age_filter) .filter(cls.time_visible <= datetime.utcnow()) .filter(cls.location_wkt != None) .subquery() diff --git a/tests/api/views/tracking/__init__.py b/tests/api/views/tracking/__init__.py index c448204868..dd240fc723 100644 --- a/tests/api/views/tracking/__init__.py +++ b/tests/api/views/tracking/__init__.py @@ -1,3 +1,6 @@ +from time import mktime + + def decode_time(encoded_time): """Decodes an encoded time string""" index = 0 @@ -34,3 +37,7 @@ def get_fixes_times_seconds(fixes): seconds.append(int((time - start_time).total_seconds() + start_second_of_day)) return seconds + + +def to_timestamp(dtime): + return int(mktime(dtime.timetuple())) diff --git a/tests/api/views/tracking/latest_test.py b/tests/api/views/tracking/latest_test.py index 19925c2915..86d54d6ed8 100644 --- a/tests/api/views/tracking/latest_test.py +++ b/tests/api/views/tracking/latest_test.py @@ -1,6 +1,7 @@ from mock import patch from datetime import datetime, timedelta from tests.data import add_fixtures, users, live_fix +from tests.api.views.tracking import to_timestamp def test_get_latest_default_max_age(db_session, client): @@ -19,6 +20,9 @@ def test_get_latest_default_max_age(db_session, client): add_fixtures(db_session, john, fix, latest_fix, jane, old_fix) with patch("skylines.model.tracking.datetime") as datetime_mock: + datetime_mock.utcfromtimestamp.side_effect = lambda *args, **kw: datetime.utcfromtimestamp( + *args, **kw + ) datetime_mock.utcnow.return_value = utcnow res = client.get("/tracking/latest.json") @@ -38,3 +42,89 @@ def test_get_latest_default_max_age(db_session, client): } ] } + + +def test_get_latest_filtered_by_from_time(db_session, client): + utcnow = datetime(year=2020, month=12, day=20, hour=12) + from_time = utcnow - timedelta(minutes=5) + + # This fix datetime is from_time, it should be returned + john = users.john() + fix_john = live_fix.create(john, from_time, 11, 21) + + # This fix is before from_time and should not be returned + jane = users.jane() + fix_jane = live_fix.create(jane, from_time - timedelta(minutes=10), 12, 22) + + add_fixtures(db_session, john, fix_john, jane, fix_jane) + + with patch("skylines.model.tracking.datetime") as datetime_mock: + datetime_mock.utcfromtimestamp.side_effect = lambda *args, **kw: datetime.utcfromtimestamp( + *args, **kw + ) + datetime_mock.utcnow.return_value = utcnow + + res = client.get( + "/tracking/latest.json?from_time={from_time}".format( + from_time=to_timestamp(from_time) + ) + ) + + assert res.status_code == 200 + assert res.json == { + u"fixes": [ + { + u"airspeed": 10, + u"altitude": 100, + u"ground_speed": 10, + u"location": u"POINT(11.0 21.0)", + u"pilot": {u"id": john.id, u"name": u"John Doe"}, + u"time": u"2020-12-20T11:55:00Z", + u"track": 0, + u"vario": 0, + } + ] + } + + +def test_get_from_time_max_6h(db_session, client): + utcnow = datetime(year=2020, month=12, day=20, hour=12) + from_time = utcnow - timedelta(hours=7) + + # This fix age is 7h, it should not be returned + john = users.john() + fix_john = live_fix.create(john, from_time, 11, 21) + + # This fix age is 10mn, it should be returned + jane = users.jane() + fix_jane = live_fix.create(jane, utcnow - timedelta(minutes=10), 12, 22) + + add_fixtures(db_session, john, fix_john, jane, fix_jane) + + with patch("skylines.model.tracking.datetime") as datetime_mock: + datetime_mock.utcfromtimestamp.side_effect = lambda *args, **kw: datetime.utcfromtimestamp( + *args, **kw + ) + datetime_mock.utcnow.return_value = utcnow + + res = client.get( + "/tracking/latest.json?from_time={from_time}".format( + from_time=to_timestamp(from_time) + ) + ) + + assert res.status_code == 200 + assert res.json == { + u"fixes": [ + { + u"airspeed": 10, + u"altitude": 100, + u"ground_speed": 10, + u"location": u"POINT(12.0 22.0)", + u"pilot": {u"id": jane.id, u"name": u"Jane Doe"}, + u"time": u"2020-12-20T11:50:00Z", + u"track": 0, + u"vario": 0, + } + ] + } diff --git a/tests/api/views/tracking/live_multiple_test.py b/tests/api/views/tracking/live_multiple_test.py deleted file mode 100644 index cafcee30e7..0000000000 --- a/tests/api/views/tracking/live_multiple_test.py +++ /dev/null @@ -1,101 +0,0 @@ -from mock import patch -from datetime import datetime, timedelta - -from tests.data import add_fixtures, users, live_fix -from tests.api.views.tracking import get_fixes_times_seconds, decode_time - - -def test_get_live_default_max_age(db_session, client): - """The default max_age is 12 hours""" - utcnow = datetime(year=2020, month=12, day=20, hour=20) - - john = users.john() - john_fixes = [] - for age_hour in range(14, 0, -1): - time = utcnow - timedelta(hours=age_hour) - john_fixes.append(live_fix.create(john, time, 10, 20)) - - jane = users.jane() - jane_fixes = [] - for age_hour in range(14, 0, -1): - time = utcnow - timedelta(hours=age_hour, minutes=30) - jane_fixes.append(live_fix.create(jane, time, 11, 21)) - - add_fixtures(db_session, john, *(john_fixes + jane_fixes)) - - with patch("skylines.model.tracking.datetime") as datetime_mock: - datetime_mock.utcnow.return_value = utcnow - - res = client.get("/live/{john},{jane}".format(john=john.id, jane=jane.id)) - - assert res.status_code == 200 - json = res.json - - assert json == { - u"flights": [ - { - u"additional": {u"color": u"#2b56d8", u"competition_id": u"JD"}, - u"barogram_h": u"eE???????????", - u"barogram_t": u"_gw@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", - u"contests": None, - u"elevations_h": u"????????????", - u"elevations_t": u"_gw@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", - u"enl": u"", - u"geoid": 26.504, - u"points": u"_gayB_c`|@??????????????????????", - u"sfid": john.id, - }, - { - u"additional": {u"color": u"#822bd8", u"competition_id": u"JD"}, - u"barogram_h": u"eE??????????", - u"barogram_t": u"owz@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", - u"contests": None, - u"elevations_h": u"???????????", - u"elevations_t": u"owz@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", - u"enl": u"", - u"geoid": 25.013, - u"points": u"_qd_C_mcbA????????????????????", - u"sfid": jane.id, - }, - ], - u"pilots": [ - { - u"club": None, - u"color": u"#2b56d8", - u"firstName": u"John", - u"followers": 0, - u"following": 0, - u"id": john.id, - u"lastName": u"Doe", - u"name": u"John Doe", - u"trackingCallsign": None, - u"trackingDelay": 0, - }, - { - u"club": None, - u"color": u"#822bd8", - u"firstName": u"Jane", - u"followers": 0, - u"following": 0, - u"id": jane.id, - u"lastName": u"Doe", - u"name": u"Jane Doe", - u"trackingCallsign": None, - u"trackingDelay": 0, - }, - ], - } - - expected_fixes = list( - filter(lambda f: f.time >= utcnow - timedelta(hours=12), john_fixes) - ) - assert decode_time( - json[u"flights"][0][u"barogram_t"] - ) == get_fixes_times_seconds(expected_fixes) - - expected_fixes = list( - filter(lambda f: f.time >= utcnow - timedelta(hours=12), jane_fixes) - ) - assert decode_time( - json[u"flights"][1][u"barogram_t"] - ) == get_fixes_times_seconds(expected_fixes) diff --git a/tests/api/views/tracking/live_pilot_test.py b/tests/api/views/tracking/live_pilot_test.py new file mode 100644 index 0000000000..b5a09d0d25 --- /dev/null +++ b/tests/api/views/tracking/live_pilot_test.py @@ -0,0 +1,246 @@ +from mock import patch +from datetime import datetime, timedelta + +from tests.data import add_fixtures, users, live_fix +from tests.api.views.tracking import get_fixes_times_seconds, decode_time, to_timestamp + + +def test_get_live_default_max_age(db_session, client): + """The default max_age is 12 hours""" + utcnow = datetime(year=2020, month=12, day=20, hour=20) + + john = users.john() + fixes = [] + for age_hour in range(14, 0, -1): + time = utcnow - timedelta(hours=age_hour) + fixes.append(live_fix.create(john, time, 10, 20)) + + add_fixtures(db_session, john, *fixes) + + with patch("skylines.model.tracking.datetime") as datetime_mock: + datetime_mock.utcfromtimestamp.side_effect = lambda *args, **kw: datetime.utcfromtimestamp( + *args, **kw + ) + datetime_mock.utcnow.return_value = utcnow + + res = client.get("/live/{id}/json".format(id=john.id)) + + assert res.status_code == 200 + json = res.json + + assert json == { + u"barogram_h": u"eE???????????", + u"barogram_t": u"_gw@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", + u"elevations": u"????????????", + u"enl": u"", + u"geoid": 26.504, + u"points": u"_gayB_c`|@??????????????????????", + u"sfid": john.id, + } + + expected_fixes = list( + filter(lambda f: f.time >= utcnow - timedelta(hours=12), fixes) + ) + assert decode_time(json[u"barogram_t"]) == get_fixes_times_seconds( + expected_fixes + ) + + +def test_get_live_filter_by_last_update(db_session, client): + utcnow = datetime(year=2020, month=12, day=20, hour=20) + last_update = 50000 + start_time = datetime(year=2020, month=12, day=20) + timedelta(seconds=last_update) + + john = users.john() + fixes = [] + for age_hour in range(14, 0, -1): + time = utcnow - timedelta(hours=age_hour) + fixes.append(live_fix.create(john, time, 10, 20)) + + add_fixtures(db_session, john, *fixes) + + with patch("skylines.model.tracking.datetime") as datetime_mock: + datetime_mock.utcfromtimestamp.side_effect = lambda *args, **kw: datetime.utcfromtimestamp( + *args, **kw + ) + datetime_mock.utcnow.return_value = utcnow + + res = client.get( + "/live/{id}/json?last_update={last_update}".format( + id=john.id, last_update=last_update + ) + ) + + assert res.status_code == 200 + json = res.json + + print(decode_time(json[u"barogram_t"])) + print(get_fixes_times_seconds(fixes)) + + assert json == { + u"barogram_h": u"eE?????", + u"barogram_t": u"_maB_`F_`F_`F_`F_`F", + u"elevations": u"??????", + u"enl": u"", + u"geoid": 26.504, + u"points": u"_gayB_c`|@??????????", + u"sfid": john.id, + } + + expected_fixes = list(filter(lambda f: f.time >= start_time, fixes)) + assert decode_time(json[u"barogram_t"]) == get_fixes_times_seconds( + expected_fixes + ) + + +def test_get_live_filter_by_from_time(db_session, client): + utcnow = datetime(year=2020, month=12, day=20, hour=20) + from_time = datetime(year=2020, month=12, day=20) + timedelta(seconds=50000) + + john = users.john() + fixes = [] + for age_hour in range(14, 0, -1): + time = utcnow - timedelta(hours=age_hour) + fixes.append(live_fix.create(john, time, 10, 20)) + + add_fixtures(db_session, john, *fixes) + + with patch("skylines.model.tracking.datetime") as datetime_mock: + datetime_mock.utcfromtimestamp.side_effect = lambda *args, **kw: datetime.utcfromtimestamp( + *args, **kw + ) + datetime_mock.utcnow.return_value = utcnow + + res = client.get( + "/live/{id}/json?from_time={from_time}".format( + id=john.id, from_time=to_timestamp(from_time) + ) + ) + + assert res.status_code == 200 + json = res.json + + assert json == { + u"barogram_h": u"eE?????", + u"barogram_t": u"_maB_`F_`F_`F_`F_`F", + u"elevations": u"??????", + u"enl": u"", + u"geoid": 26.504, + u"points": u"_gayB_c`|@??????????", + u"sfid": john.id, + } + + expected_fixes = list(filter(lambda f: f.time >= from_time, fixes)) + assert decode_time(json[u"barogram_t"]) == get_fixes_times_seconds( + expected_fixes + ) + + # Test that max age = 12h + res = client.get("/live/{id}/json?from_time=0".format(id=john.id)) + + assert res.status_code == 200 + json = res.json + + assert json == { + u"barogram_h": u"eE???????????", + u"barogram_t": u"_gw@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", + u"elevations": u"????????????", + u"enl": u"", + u"geoid": 26.504, + u"points": u"_gayB_c`|@??????????????????????", + u"sfid": john.id, + } + + expected_fixes = list( + filter(lambda f: f.time >= utcnow - timedelta(hours=12), fixes) + ) + assert decode_time(json[u"barogram_t"]) == get_fixes_times_seconds( + expected_fixes + ) + + +def test_get_live_filter_by_last_update_and_from_time(db_session, client): + utcnow = datetime(year=2020, month=12, day=20, hour=20) + last_update = 50000 + start_time = datetime(year=2020, month=12, day=20) + timedelta(seconds=last_update) + from_time = datetime(year=2020, month=12, day=20) + timedelta(seconds=40000) + + john = users.john() + fixes = [] + for age_hour in range(14, 0, -1): + time = utcnow - timedelta(hours=age_hour) + fixes.append(live_fix.create(john, time, 10, 20)) + + add_fixtures(db_session, john, *fixes) + + with patch("skylines.model.tracking.datetime") as datetime_mock: + datetime_mock.utcfromtimestamp.side_effect = lambda *args, **kw: datetime.utcfromtimestamp( + *args, **kw + ) + datetime_mock.utcnow.return_value = utcnow + + # from_time is earlier than last_update + res = client.get( + "/live/{id}/json?last_update={last_update}&from_time={from_time}".format( + id=john.id, last_update=last_update, from_time=to_timestamp(from_time) + ) + ) + + assert res.status_code == 200 + json = res.json + + print(decode_time(json[u"barogram_t"])) + print(get_fixes_times_seconds(fixes)) + + assert json == { + u"barogram_h": u"eE?????", + u"barogram_t": u"_maB_`F_`F_`F_`F_`F", + u"elevations": u"??????", + u"enl": u"", + u"geoid": 26.504, + u"points": u"_gayB_c`|@??????????", + u"sfid": john.id, + } + + expected_fixes = list(filter(lambda f: f.time >= start_time, fixes)) + assert decode_time(json[u"barogram_t"]) == get_fixes_times_seconds( + expected_fixes + ) + + last_update = 40000 + start_time = datetime(year=2020, month=12, day=20) + timedelta(seconds=last_update) + from_time = datetime(year=2020, month=12, day=20) + timedelta(seconds=50000) + + # last_update is earlier than from_time + with patch("skylines.model.tracking.datetime") as datetime_mock: + datetime_mock.utcfromtimestamp.side_effect = lambda *args, **kw: datetime.utcfromtimestamp( + *args, **kw + ) + datetime_mock.utcnow.return_value = utcnow + + res = client.get( + "/live/{id}/json?last_update={last_update}&from_time={from_time}".format( + id=john.id, last_update=last_update, from_time=to_timestamp(from_time) + ) + ) + + assert res.status_code == 200 + json = res.json + + print(decode_time(json[u"barogram_t"])) + print(get_fixes_times_seconds(fixes)) + + assert json == { + u"barogram_h": u"eE?????", + u"barogram_t": u"_maB_`F_`F_`F_`F_`F", + u"elevations": u"??????", + u"enl": u"", + u"geoid": 26.504, + u"points": u"_gayB_c`|@??????????", + u"sfid": john.id, + } + + expected_fixes = list(filter(lambda f: f.time >= from_time, fixes)) + assert decode_time(json[u"barogram_t"]) == get_fixes_times_seconds( + expected_fixes + ) diff --git a/tests/api/views/tracking/live_pilots_test.py b/tests/api/views/tracking/live_pilots_test.py new file mode 100644 index 0000000000..33aa299657 --- /dev/null +++ b/tests/api/views/tracking/live_pilots_test.py @@ -0,0 +1,267 @@ +from mock import patch +from datetime import datetime, timedelta + +from tests.data import add_fixtures, users, live_fix +from tests.api.views.tracking import get_fixes_times_seconds, decode_time, to_timestamp + + +def test_get_live_default_max_age(db_session, client): + """The default max_age is 12 hours""" + utcnow = datetime(year=2020, month=12, day=20, hour=20) + + john = users.john() + john_fixes = [] + for age_hour in range(14, 0, -1): + time = utcnow - timedelta(hours=age_hour) + john_fixes.append(live_fix.create(john, time, 10, 20)) + + jane = users.jane() + jane_fixes = [] + for age_hour in range(14, 0, -1): + time = utcnow - timedelta(hours=age_hour, minutes=30) + jane_fixes.append(live_fix.create(jane, time, 11, 21)) + + add_fixtures(db_session, john, *(john_fixes + jane_fixes)) + + with patch("skylines.model.tracking.datetime") as datetime_mock: + datetime_mock.utcfromtimestamp.side_effect = lambda *args, **kw: datetime.utcfromtimestamp( + *args, **kw + ) + datetime_mock.utcnow.return_value = utcnow + + res = client.get("/live/{john},{jane}".format(john=john.id, jane=jane.id)) + + assert res.status_code == 200 + json = res.json + + assert json == { + u"flights": [ + { + u"additional": {u"color": u"#2b56d8", u"competition_id": u"JD"}, + u"barogram_h": u"eE???????????", + u"barogram_t": u"_gw@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", + u"contests": None, + u"elevations_h": u"????????????", + u"elevations_t": u"_gw@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", + u"enl": u"", + u"geoid": 26.504, + u"points": u"_gayB_c`|@??????????????????????", + u"sfid": john.id, + }, + { + u"additional": {u"color": u"#822bd8", u"competition_id": u"JD"}, + u"barogram_h": u"eE??????????", + u"barogram_t": u"owz@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", + u"contests": None, + u"elevations_h": u"???????????", + u"elevations_t": u"owz@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", + u"enl": u"", + u"geoid": 25.013, + u"points": u"_qd_C_mcbA????????????????????", + u"sfid": jane.id, + }, + ], + u"pilots": [ + { + u"club": None, + u"color": u"#2b56d8", + u"firstName": u"John", + u"followers": 0, + u"following": 0, + u"id": john.id, + u"lastName": u"Doe", + u"name": u"John Doe", + u"trackingCallsign": None, + u"trackingDelay": 0, + }, + { + u"club": None, + u"color": u"#822bd8", + u"firstName": u"Jane", + u"followers": 0, + u"following": 0, + u"id": jane.id, + u"lastName": u"Doe", + u"name": u"Jane Doe", + u"trackingCallsign": None, + u"trackingDelay": 0, + }, + ], + } + + expected_fixes = list( + filter(lambda f: f.time >= utcnow - timedelta(hours=12), john_fixes) + ) + assert decode_time( + json[u"flights"][0][u"barogram_t"] + ) == get_fixes_times_seconds(expected_fixes) + + expected_fixes = list( + filter(lambda f: f.time >= utcnow - timedelta(hours=12), jane_fixes) + ) + assert decode_time( + json[u"flights"][1][u"barogram_t"] + ) == get_fixes_times_seconds(expected_fixes) + + +def test_get_live_filter_by_from_time(db_session, client): + """The default max_age is 12 hours""" + utcnow = datetime(year=2020, month=12, day=20, hour=20) + from_time = utcnow - timedelta(hours=1) + + john = users.john() + john_fixes = [] + for age_hour in range(14, 0, -1): + time = utcnow - timedelta(hours=age_hour) + john_fixes.append(live_fix.create(john, time, 10, 20)) + + jane = users.jane() + jane_fixes = [] + for age_hour in range(14, 0, -1): + time = utcnow - timedelta(hours=age_hour, minutes=30) + jane_fixes.append(live_fix.create(jane, time, 11, 21)) + + add_fixtures(db_session, john, *(john_fixes + jane_fixes)) + + # Recent fixes (<= 1h) only + with patch("skylines.model.tracking.datetime") as datetime_mock: + datetime_mock.utcfromtimestamp.side_effect = lambda *args, **kw: datetime.utcfromtimestamp( + *args, **kw + ) + datetime_mock.utcnow.return_value = utcnow + + res = client.get( + "/live/{john},{jane}?from_time={from_time}".format( + john=john.id, jane=jane.id, from_time=to_timestamp(from_time) + ) + ) + + assert res.status_code == 200 + json = res.json + + assert json == { + u"flights": [ + { + u"additional": {u"color": u"#2b56d8", u"competition_id": u"JD"}, + u"barogram_h": u"eE", + u"barogram_t": u"_rdC", + u"contests": None, + u"elevations_h": u"?", + u"elevations_t": u"_rdC", + u"enl": u"", + u"geoid": 26.504, + u"points": u"_gayB_c`|@", + u"sfid": john.id, + } + ], + u"pilots": [ + { + u"club": None, + u"color": u"#2b56d8", + u"firstName": u"John", + u"followers": 0, + u"following": 0, + u"id": john.id, + u"lastName": u"Doe", + u"name": u"John Doe", + u"trackingCallsign": None, + u"trackingDelay": 0, + }, + { + u"club": None, + u"color": u"#822bd8", + u"firstName": u"Jane", + u"followers": 0, + u"following": 0, + u"id": jane.id, + u"lastName": u"Doe", + u"name": u"Jane Doe", + u"trackingCallsign": None, + u"trackingDelay": 0, + }, + ], + } + + assert len(json[u"flights"]) == 1 + + expected_fixes = list(filter(lambda f: f.time >= from_time, john_fixes)) + assert decode_time( + json[u"flights"][0][u"barogram_t"] + ) == get_fixes_times_seconds(expected_fixes) + + # Can not get fixes aged more than 12h + res = client.get( + "/live/{john},{jane}?from_time=0".format(john=john.id, jane=jane.id) + ) + + assert res.status_code == 200 + json = res.json + + assert json == { + u"flights": [ + { + u"additional": {u"color": u"#2b56d8", u"competition_id": u"JD"}, + u"barogram_h": u"eE???????????", + u"barogram_t": u"_gw@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", + u"contests": None, + u"elevations_h": u"????????????", + u"elevations_t": u"_gw@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", + u"enl": u"", + u"geoid": 26.504, + u"points": u"_gayB_c`|@??????????????????????", + u"sfid": john.id, + }, + { + u"additional": {u"color": u"#822bd8", u"competition_id": u"JD"}, + u"barogram_h": u"eE??????????", + u"barogram_t": u"owz@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", + u"contests": None, + u"elevations_h": u"???????????", + u"elevations_t": u"owz@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", + u"enl": u"", + u"geoid": 25.013, + u"points": u"_qd_C_mcbA????????????????????", + u"sfid": jane.id, + }, + ], + u"pilots": [ + { + u"club": None, + u"color": u"#2b56d8", + u"firstName": u"John", + u"followers": 0, + u"following": 0, + u"id": john.id, + u"lastName": u"Doe", + u"name": u"John Doe", + u"trackingCallsign": None, + u"trackingDelay": 0, + }, + { + u"club": None, + u"color": u"#822bd8", + u"firstName": u"Jane", + u"followers": 0, + u"following": 0, + u"id": jane.id, + u"lastName": u"Doe", + u"name": u"Jane Doe", + u"trackingCallsign": None, + u"trackingDelay": 0, + }, + ], + } + + expected_fixes = list( + filter(lambda f: f.time >= utcnow - timedelta(hours=12), john_fixes) + ) + assert decode_time( + json[u"flights"][0][u"barogram_t"] + ) == get_fixes_times_seconds(expected_fixes) + + expected_fixes = list( + filter(lambda f: f.time >= utcnow - timedelta(hours=12), jane_fixes) + ) + assert decode_time( + json[u"flights"][1][u"barogram_t"] + ) == get_fixes_times_seconds(expected_fixes) diff --git a/tests/api/views/tracking/live_test.py b/tests/api/views/tracking/live_test.py index 0a5a903813..d549bbceb7 100644 --- a/tests/api/views/tracking/live_test.py +++ b/tests/api/views/tracking/live_test.py @@ -1,87 +1,3 @@ -from mock import patch -from datetime import datetime, timedelta - -from tests.data import add_fixtures, users, live_fix -from tests.api.views.tracking import get_fixes_times_seconds, decode_time - - -def test_get_live_default_max_age(db_session, client): - """The default max_age is 12 hours""" - utcnow = datetime(year=2020, month=12, day=20, hour=20) - - john = users.john() - fixes = [] - for age_hour in range(14, 0, -1): - time = utcnow - timedelta(hours=age_hour) - fixes.append(live_fix.create(john, time, 10, 20)) - - add_fixtures(db_session, john, *fixes) - - with patch("skylines.model.tracking.datetime") as datetime_mock: - datetime_mock.utcnow.return_value = utcnow - - res = client.get("/live/{id}/json".format(id=john.id)) - - assert res.status_code == 200 - json = res.json - - assert json == { - u"barogram_h": u"eE???????????", - u"barogram_t": u"_gw@_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F_`F", - u"elevations": u"????????????", - u"enl": u"", - u"geoid": 26.504, - u"points": u"_gayB_c`|@??????????????????????", - u"sfid": john.id, - } - - expected_fixes = list( - filter(lambda f: f.time >= utcnow - timedelta(hours=12), fixes) - ) - assert decode_time(json[u"barogram_t"]) == get_fixes_times_seconds( - expected_fixes - ) - - -def test_get_live_filter_by_last_update(db_session, client): - utcnow = datetime(year=2020, month=12, day=20, hour=20) - last_update = 50000 - start_time = datetime(year=2020, month=12, day=20) + timedelta(seconds=last_update) - - john = users.john() - fixes = [] - for age_hour in range(14, 0, -1): - time = utcnow - timedelta(hours=age_hour) - fixes.append(live_fix.create(john, time, 10, 20)) - - add_fixtures(db_session, john, *fixes) - - with patch("skylines.model.tracking.datetime") as datetime_mock: - datetime_mock.utcnow.return_value = utcnow - - res = client.get( - "/live/{id}/json?last_update={last_update}".format( - id=john.id, last_update=last_update - ) - ) - - assert res.status_code == 200 - json = res.json - - print(decode_time(json[u"barogram_t"])) - print(get_fixes_times_seconds(fixes)) - - assert json == { - u"barogram_h": u"eE?????", - u"barogram_t": u"_maB_`F_`F_`F_`F_`F", - u"elevations": u"??????", - u"enl": u"", - u"geoid": 26.504, - u"points": u"_gayB_c`|@??????????", - u"sfid": john.id, - } - - expected_fixes = list(filter(lambda f: f.time >= start_time, fixes)) - assert decode_time(json[u"barogram_t"]) == get_fixes_times_seconds( - expected_fixes - ) +def test_get_live(client): + res = client.get("/live") + assert res.status_code == 200