diff --git a/app/api/moves.py b/app/api/moves.py index 665f7158..b07133d2 100644 --- a/app/api/moves.py +++ b/app/api/moves.py @@ -13,10 +13,10 @@ from app.api.helpers.exceptions import ForbiddenException, UnprocessableEntity from app.api.helpers.permission_manager import has_access from app.api.helpers.utilities import has_relationships -from app.api.schema.moves import (MoveArchiveSchema, MoveCommentSchema, - MoveDippedSchema, MoveDroppedSchema, - MoveGrabbedSchema, MoveSeenSchema, - MoveWithCoordinatesSchema) +from app.api.schema.moves import (DefaultMoveSchema, MoveArchiveSchema, + MoveCommentSchema, MoveDippedSchema, + MoveDroppedSchema, MoveGrabbedSchema, + MoveSeenSchema) from app.models import db from app.models.geokret import Geokret from app.models.move import Move @@ -103,7 +103,7 @@ def create_object(self, data, kwargs): update_geokret_and_moves(move.geokret_id, move.id) return move - schema = MoveWithCoordinatesSchema + schema = DefaultMoveSchema get_schema_kwargs = {'context': {'current_identity': current_identity}} decorators = ( api.has_permission('auth_required', methods="POST"), @@ -180,8 +180,8 @@ def update_object(self, data, qs, kwargs): # Comming from Comment require tracking-code if old_move.type == MOVE_TYPE_COMMENT and not data.get('tracking_code'): - raise UnprocessableEntity("Tracking code is missing", - {'pointer': '/data/attributes/tracking-code'}) + raise UnprocessableEntity("Tracking code is missing", + {'pointer': '/data/attributes/tracking-code'}) # Now convert tracking-code to GeoKret ID if 'tracking_code' in data: geokrety_to_update.append(old_move.geokret.id) @@ -203,7 +203,7 @@ def update_object(self, data, qs, kwargs): model=Move, fetch_key_url="id"), ) methods = ('GET', 'PATCH', 'DELETE') - schema = MoveDippedSchema + schema = DefaultMoveSchema data_layer = { 'session': db.session, 'model': Move, @@ -212,7 +212,7 @@ def update_object(self, data, qs, kwargs): class MoveRelationship(ResourceRelationship): methods = ['GET'] - schema = MoveGrabbedSchema + schema = DefaultMoveSchema data_layer = { 'session': db.session, 'model': Move, diff --git a/app/api/schema/geokrety.py b/app/api/schema/geokrety.py index 2db66ca2..d6290d10 100644 --- a/app/api/schema/geokrety.py +++ b/app/api/schema/geokrety.py @@ -38,6 +38,7 @@ class Meta: description = fields.Str() missing = fields.Boolean(dump_only=True) distance = fields.Integer(dump_only=True) + archived = fields.Boolean(dump_only=True) caches_count = fields.Integer(dump_only=True) pictures_count = fields.Integer(dump_only=True) average_rating = fields.Float(dump_only=True) @@ -83,29 +84,28 @@ class Meta: related_view='v1.moves_list', related_view_kwargs={'geokret_id': ''}, many=True, - schema='MoveSchema', + schema='DefaultMoveSchema', type_='move', - # include_resource_linkage=True ) last_position = Relationship( - attribute='last_position_id', + attribute='last_position', self_view='v1.geokret_last_position', self_view_kwargs={'id': ''}, related_view='v1.move_details', related_view_kwargs={'geokret_last_position_id': ''}, - schema='MoveSchema', + schema='DefaultMoveSchema', type_='move', include_resource_linkage=True, ) last_move = Relationship( - attribute='last_move_id', + attribute='last_move', self_view='v1.geokret_last_move', self_view_kwargs={'id': ''}, related_view='v1.move_details', related_view_kwargs={'geokret_last_move_id': ''}, - schema='MoveSchema', + schema='DefaultMoveSchema', type_='move', include_resource_linkage=True, ) diff --git a/app/api/schema/moves.py b/app/api/schema/moves.py index 54c57439..8580a229 100644 --- a/app/api/schema/moves.py +++ b/app/api/schema/moves.py @@ -103,6 +103,26 @@ class Meta: ) +class DefaultMoveSchema(MoveSchema): + + class Meta: + type_ = 'move' + self_view = 'v1.move_details' + self_view_kwargs = {'id': ''} + self_view_many = 'v1.moves_list' + inflect = dasherize + ordered = True + dateformat = "%Y-%m-%dT%H:%M:%S" + + # fields managed differently depending on move type + waypoint = fields.Str(dump_only=True) + latitude = fields.Float(dump_only=True) + longitude = fields.Float(dump_only=True) + altitude = fields.Integer(dump_only=True) + country = fields.Str(dump_only=True) + distance = fields.Integer(dump_only=True) + + class MoveWithCoordinatesSchema(MoveSchema): @validates('waypoint') diff --git a/app/models/geokret.py b/app/models/geokret.py index a209ac32..bbfd5ff0 100644 --- a/app/models/geokret.py +++ b/app/models/geokret.py @@ -2,12 +2,14 @@ from datetime import datetime from flask import request -from sqlalchemy import ForeignKeyConstraint, event +from sqlalchemy import event from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm.exc import NoResultFound import bleach import characterentities -from app.api.helpers.data_layers import MOVE_TYPE_DIPPED +from app.api.helpers.data_layers import (MOVE_TYPE_ARCHIVED, MOVE_TYPE_COMMENT, + MOVE_TYPE_DIPPED) from app.api.helpers.utilities import has_attribute, round_microseconds from app.models import db from app.models.move import Move @@ -98,12 +100,7 @@ class Geokret(db.Model): nullable=True, key='owner_id', ) - - ForeignKeyConstraint( - ['owner_id'], ['user.id'], - use_alter=True, - name='fk_geokret_owner' - ) + owner = db.relationship("User", foreign_keys=[owner_id], backref="geokrety_owned") holder_id = db.Column( 'hands_of', @@ -111,18 +108,14 @@ class Geokret(db.Model): db.ForeignKey('gk-users.id', name='fk_geokret_holder'), key='holder_id', ) + holder = db.relationship("User", foreign_keys=[holder_id], backref="geokrety_held") - ForeignKeyConstraint( - ['holder_id'], ['user.id'], - use_alter=True, - name='fk_geokret_holder' - ) - - moves = db.relationship( + # This is used to compute the archived status + _moves = db.relationship( 'Move', - backref="geokret", foreign_keys="Move.geokret_id", cascade="all,delete", + lazy="dynamic", ) last_position_id = db.Column( @@ -133,11 +126,7 @@ class Geokret(db.Model): nullable=True, default=None, ) - - ForeignKeyConstraint( - ['last_position_id'], ['move.id'], - use_alter=True, name='fk_geokret_last_position' - ) + last_position = db.relationship("Move", foreign_keys=[last_position_id], backref="spotted_geokret") last_move_id = db.Column( 'ost_log_id', @@ -145,12 +134,7 @@ class Geokret(db.Model): db.ForeignKey('gk-ruchy.id', name='fk_last_move', ondelete="SET NULL"), key='last_move_id' ) - - ForeignKeyConstraint( - ['last_move_id'], ['move.id'], - use_alter=True, - name='fk_last_move' - ) + last_move = db.relationship("Move", foreign_keys=[last_move_id], backref="moves") # avatar_id = db.Column( # 'avatarid', @@ -159,6 +143,17 @@ class Geokret(db.Model): # key='avatar_id' # ) + @hybrid_property + def archived(self): + """Compute the archived status + """ + try: + last_move = self._moves.filter(Move.type != MOVE_TYPE_COMMENT)\ + .order_by(Move.moved_on_datetime.desc()).limit(1).one() + return last_move.type == MOVE_TYPE_ARCHIVED + except NoResultFound: + return False + @property def average_rating(self): # TODO note should come from database diff --git a/app/models/move.py b/app/models/move.py index 86bff2da..a354f371 100644 --- a/app/models/move.py +++ b/app/models/move.py @@ -1,6 +1,6 @@ from datetime import datetime, timedelta -from sqlalchemy import ForeignKeyConstraint, event +from sqlalchemy import event from sqlalchemy.dialects.mysql import DOUBLE from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm.exc import NoResultFound @@ -32,12 +32,7 @@ class Move(db.Model): nullable=False, default=None ) - - ForeignKeyConstraint( - ['geokret_id'], ['geokret.id'], - use_alter=True, - name='fk_geokret_moved' - ) + geokret = db.relationship("Geokret", foreign_keys=[geokret_id], backref=db.backref("moves")) latitude = db.Column( 'lat', @@ -126,6 +121,7 @@ class Move(db.Model): nullable=False, default=None, ) + author = db.relationship("User", foreign_keys=[author_id], backref="moves") username = db.Column( 'username', @@ -173,9 +169,6 @@ class Move(db.Model): nullable=True ) - # geokret = db.relationship('Geokret', - # backref=db.backref('moves', lazy=True)) - @hybrid_property def comment(self): return characterentities.decode(self._comment) diff --git a/app/models/user.py b/app/models/user.py index 14e9794e..0cae00a0 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -152,24 +152,6 @@ class User(db.Model): backref="user", cascade="all,delete", ) - geokrety_owned = db.relationship( - 'Geokret', - backref="owner", - foreign_keys="Geokret.owner_id", - cascade="all,delete", - ) - geokrety_held = db.relationship( - 'Geokret', - backref="holder", - foreign_keys="Geokret.holder_id", - cascade="all,delete", - ) - moves = db.relationship( - 'Move', - backref="author", - foreign_keys="Move.author_id", - cascade="all,delete", - ) @hybrid_property def password(self): diff --git a/tests/unittests/api/geokrety/test_geokrety_details.py b/tests/unittests/api/geokrety/test_geokrety_details.py index 0d95121f..3569b702 100644 --- a/tests/unittests/api/geokrety/test_geokrety_details.py +++ b/tests/unittests/api/geokrety/test_geokrety_details.py @@ -205,3 +205,28 @@ def test_geokret_details_has_relationships_moves_data(self): moves = self.blend_move(count=5, geokret=geokret, author=self.user_1, type=MOVE_TYPE_GRABBED) response = self.send_get(geokret.id, user=self.user_1, args={'include': 'moves'}) response.assertHasRelationshipMovesDatas(moves) + + @request_context + def test_geokret_details_has_archived_attribute(self): + geokret = self.blend_geokret(created_on_datetime="2018-12-29T21:39:13") + assert not geokret.archived + response = self.send_get(geokret.id, user=self.user_1) + response.assertHasAttribute('archived', False) + + self.blend_move(geokret=geokret, author=self.user_1, type=MOVE_TYPE_GRABBED, + moved_on_datetime="2018-12-29T21:40:39") + assert not geokret.archived + response = self.send_get(geokret.id, user=self.user_1) + response.assertHasAttribute('archived', False) + + self.blend_move(geokret=geokret, author=self.user_1, type=MOVE_TYPE_ARCHIVED, + moved_on_datetime="2018-12-29T21:40:45") + assert geokret.archived + response = self.send_get(geokret.id, user=self.user_1) + response.assertHasAttribute('archived', True) + + self.blend_move(geokret=geokret, author=self.user_1, type=MOVE_TYPE_GRABBED, + moved_on_datetime="2018-12-29T21:51:27") + assert not geokret.archived + response = self.send_get(geokret.id, user=self.user_1) + response.assertHasAttribute('archived', False) diff --git a/tests/unittests/api/moves/test_move_create_archive.py b/tests/unittests/api/moves/test_move_create_archive.py index a5d384a5..33620c6a 100644 --- a/tests/unittests/api/moves/test_move_create_archive.py +++ b/tests/unittests/api/moves/test_move_create_archive.py @@ -1,10 +1,16 @@ # -*- coding: utf-8 -*- +import urllib +from datetime import datetime, timedelta + from parameterized import parameterized -from app.api.helpers.data_layers import MOVE_TYPE_ARCHIVED +from app.api.helpers.data_layers import (MOVE_TYPE_ARCHIVED, MOVE_TYPE_COMMENT, + MOVE_TYPE_DIPPED, MOVE_TYPE_DROPPED) from tests.unittests.utils.base_test_case import BaseTestCase, request_context from tests.unittests.utils.payload.move import MovePayload +from tests.unittests.utils.responses.collections import \ + GeokretCollectionResponse from tests.unittests.utils.responses.move import MoveResponse @@ -19,6 +25,11 @@ def send_post(self, payload=None, code=201, user=None, content_type='application user=user, content_type=content_type).get_json()) + def send_get_gk_collection(self, args=None, **kwargs): + args_ = '' if args is None else urllib.urlencode(args) + url = "/v1/geokrety?%s" % (args_) + return GeokretCollectionResponse(self._send_get(url, **kwargs).get_json()) + @parameterized.expand([ ['user_1'], ['user_2'], @@ -39,4 +50,81 @@ def test_move_create_archive_as_owner(self, username): user = getattr(self, username) if username else None geokret = self.blend_geokret(owner=self.user_1) payload = MovePayload(MOVE_TYPE_ARCHIVED, geokret=geokret) - assert self.send_post(payload, user=user, code=201) + self.send_post(payload, user=user, code=201) + self.assertEqual(geokret.archived, True) + + @request_context + def test_archive_date_must_be_provided(self): + geokret = self.blend_geokret(owner=self.user_1) + payload = MovePayload(MOVE_TYPE_ARCHIVED, geokret=geokret) + payload['data']['attributes'].pop('moved-on-datetime') + response = self.send_post(payload, user=self.user_1, code=422) + response.assertRaiseJsonApiError('/data/attributes/moved-on-datetime') + + @request_context + def test_archive_date_must_not_be_before_born_date(self): + geokret = self.blend_geokret(owner=self.user_1, created_on_datetime='2018-12-29T16:33:46') + payload = MovePayload(MOVE_TYPE_ARCHIVED, geokret=geokret)\ + .set_moved_on_datetime('2018-12-29T16:33:00') + response = self.send_post(payload, user=self.user_1, code=422) + response.assertRaiseJsonApiError('/data/attributes/moved-on-datetime') + + @request_context + def test_archive_date_must_not_be_the_future(self): + geokret = self.blend_geokret(owner=self.user_1) + payload = MovePayload(MOVE_TYPE_ARCHIVED, geokret=geokret)\ + .set_moved_on_datetime(datetime.now() + timedelta(hours=6)) + response = self.send_post(payload, user=self.user_1, code=422) + response.assertRaiseJsonApiError('/data/attributes/moved-on-datetime') + + @request_context + def test_a_comment_may_be_provided(self): + geokret = self.blend_geokret(owner=self.user_1) + payload = MovePayload(MOVE_TYPE_ARCHIVED, geokret=geokret)\ + .set_comment("Some comment") + self.send_post(payload, user=self.user_1) + + @request_context + def test_moves_can_continue_after_archive(self): + geokret = self.blend_geokret(owner=self.user_1, created_on_datetime='2018-12-29T16:40:06') + payload = MovePayload(MOVE_TYPE_ARCHIVED, geokret=geokret)\ + .set_moved_on_datetime('2018-12-29T16:40:14') + self.send_post(payload, user=self.user_1) + self.assertTrue(geokret.archived) + payload = MovePayload(MOVE_TYPE_COMMENT, geokret=geokret)\ + .set_moved_on_datetime('2018-12-29T16:40:33')\ + .set_comment("This GeoKret is not dead") + self.send_post(payload, user=self.user_1) + self.assertTrue(geokret.archived) + payload = MovePayload(MOVE_TYPE_DIPPED, geokret=geokret)\ + .set_moved_on_datetime('2018-12-29T16:40:41')\ + .set_coordinates()\ + .set_comment("Will continue it's journey") + self.send_post(payload, user=self.user_1) + self.assertFalse(geokret.archived) + + @request_context + def test_archived_geokret_must_not_be_shown_in_cache_details(self): + geokret_1 = self.blend_geokret(owner=self.user_1, created_on_datetime='2018-12-29T16:43:28') + payload = MovePayload(MOVE_TYPE_DROPPED, geokret=geokret_1)\ + .set_moved_on_datetime('2018-12-29T16:44:57')\ + .set_coordinates()\ + .set_waypoint('ABC123') + self.send_post(payload, user=self.user_1) + + geokret_2 = self.blend_geokret(owner=self.user_1, created_on_datetime='2018-12-29T16:43:28') + payload = MovePayload(MOVE_TYPE_DROPPED, geokret=geokret_2)\ + .set_moved_on_datetime('2018-12-29T16:44:57')\ + .set_coordinates()\ + .set_waypoint('ABC123') + self.send_post(payload, user=self.user_1) + + response = self.send_get_gk_collection( + args={'filter': '[{"name":"last_position","op":"has","val":{"name":"waypoint","op":"eq","val":"ABC123"}}]'}) + response.assertCount(2) + + payload = MovePayload(MOVE_TYPE_ARCHIVED, geokret=geokret_1)\ + .set_moved_on_datetime('2018-12-29T16:45:03') + response = self.send_get_gk_collection( + args={'filter': '[{"name":"last_position__waypoint","op":"has","val":"ABC123"}]'}) + response.assertCount(2)