diff --git a/ereuse_devicehub/migrations/versions/d22d230d2850_adding_author_action_device.py b/ereuse_devicehub/migrations/versions/d22d230d2850_adding_author_action_device.py new file mode 100644 index 00000000..ed58f669 --- /dev/null +++ b/ereuse_devicehub/migrations/versions/d22d230d2850_adding_author_action_device.py @@ -0,0 +1,43 @@ +"""adding author action_device + +Revision ID: d22d230d2850 +Revises: 1bb2b5e0fae7 +Create Date: 2021-11-10 17:37:12.304853 + +""" +import sqlalchemy as sa +from alembic import context +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'd22d230d2850' +down_revision = '1bb2b5e0fae7' +branch_labels = None +depends_on = None + + +def get_inv(): + INV = context.get_x_argument(as_dictionary=True).get('inventory') + if not INV: + raise ValueError("Inventory value is not specified") + return INV + + +def upgrade(): + op.add_column('action_device', + sa.Column('author_id', + postgresql.UUID(), + nullable=True), + schema=f'{get_inv()}') + op.create_foreign_key("fk_action_device_author", + "action_device", "user", + ["author_id"], ["id"], + ondelete="SET NULL", + source_schema=f'{get_inv()}', + referent_schema='common') + + +def downgrade(): + op.drop_constraint("fk_action_device_author", "device", type_="foreignkey", schema=f'{get_inv()}') + op.drop_column('action_device', 'author_id', schema=f'{get_inv()}') diff --git a/ereuse_devicehub/resources/action/__init__.py b/ereuse_devicehub/resources/action/__init__.py index 331d4dec..0d89f557 100644 --- a/ereuse_devicehub/resources/action/__init__.py +++ b/ereuse_devicehub/resources/action/__init__.py @@ -285,6 +285,11 @@ class RevokeDef(ActionDef): SCHEMA = schemas.Revoke +class ConfirmRevokeDef(ActionDef): + VIEW = None + SCHEMA = schemas.ConfirmRevoke + + class TradeDef(ActionDef): VIEW = None SCHEMA = schemas.Trade diff --git a/ereuse_devicehub/resources/action/models.py b/ereuse_devicehub/resources/action/models.py index b2fb2c18..e4e8c3b6 100644 --- a/ereuse_devicehub/resources/action/models.py +++ b/ereuse_devicehub/resources/action/models.py @@ -317,6 +317,14 @@ class ActionDevice(db.Model): index=True, server_default=db.text('CURRENT_TIMESTAMP')) created.comment = """When Devicehub created this.""" + author_id = Column(UUID(as_uuid=True), + ForeignKey(User.id), + nullable=False, + default=lambda: g.user.id) + # todo compute the org + author = relationship(User, + backref=backref('authored_actions_device', lazy=True, collection_class=set), + primaryjoin=author_id == User.id) def __init__(self, **kwargs) -> None: self.created = kwargs.get('created', datetime.now(timezone.utc)) diff --git a/ereuse_devicehub/resources/action/schemas.py b/ereuse_devicehub/resources/action/schemas.py index 0202a3a3..1f7f3fef 100644 --- a/ereuse_devicehub/resources/action/schemas.py +++ b/ereuse_devicehub/resources/action/schemas.py @@ -445,16 +445,13 @@ class ActionStatus(Action): @post_load def put_rol_user(self, data: dict): for dev in data['devices']: - if dev.trading in [None, 'Revoke']: - return data - trades = [ac for ac in dev.actions if ac.t == 'Trade'] if not trades: return data trade = trades[-1] - if trade.user_to != g.user: + if trade.user_from == g.user: data['rol_user'] = trade.user_to data['trade'] = trade @@ -588,6 +585,10 @@ class Revoke(ActionWithMultipleDevices): raise ValidationError(txt) +class ConfirmRevoke(Revoke): + pass + + class ConfirmDocument(ActionWithMultipleDocuments): __doc__ = m.Confirm.__doc__ action = NestedOn('Action', only_query='id') diff --git a/ereuse_devicehub/resources/action/views/trade.py b/ereuse_devicehub/resources/action/views/trade.py index 601d2b17..4aac48d4 100644 --- a/ereuse_devicehub/resources/action/views/trade.py +++ b/ereuse_devicehub/resources/action/views/trade.py @@ -220,15 +220,29 @@ class RevokeView(ConfirmMixin): raise ValidationError('Devices not exist.') lot = data['action'].lot + + revokeConfirmed = [] for dev in data['devices']: - if not dev.trading(lot) == 'TradeConfirmed': - txt = 'Some of devices do not have enough to confirm for to do a revoke' - ValidationError(txt) + if dev.trading(lot) == 'RevokeConfirmed': + # this device is revoked before + revokeConfirmed.append(dev) ### End check ### - ids = {d.id for d in data['devices']} - lot = data['action'].lot - self.model = delete_from_trade(lot, ids) + devices = {d for d in data['devices'] if d not in revokeConfirmed} + # self.model = delete_from_trade(lot, devices) + # TODO @cayop we dont need delete_from_trade + + drop_of_lot = [] + # import pdb; pdb.set_trace() + for dev in devices: + # import pdb; pdb.set_trace() + if dev.trading_for_web(lot) in ['NeedConfirmation', 'Confirm', 'NeedConfirmRevoke']: + drop_of_lot.append(dev) + dev.reset_owner() + + self.model = Revoke(action=lot.trade, user=g.user, devices=devices) + db.session.add(self.model) + lot.devices.difference_update(OrderedSet(drop_of_lot)) # class ConfirmRevokeView(ConfirmMixin): diff --git a/ereuse_devicehub/resources/action/views/views.py b/ereuse_devicehub/resources/action/views/views.py index 82e3f5e0..88c95438 100644 --- a/ereuse_devicehub/resources/action/views/views.py +++ b/ereuse_devicehub/resources/action/views/views.py @@ -235,6 +235,10 @@ class ActionView(View): revoke = trade_view.RevokeView(json, resource_def, self.schema) return revoke.post() + if json['type'] == 'ConfirmRevoke': + revoke = trade_view.RevokeView(json, resource_def, self.schema) + return revoke.post() + if json['type'] == 'RevokeDocument': revoke = trade_view.RevokeDocumentView(json, resource_def, self.schema) return revoke.post() diff --git a/ereuse_devicehub/resources/device/metrics.py b/ereuse_devicehub/resources/device/metrics.py index 3365e6cc..e60312a2 100644 --- a/ereuse_devicehub/resources/device/metrics.py +++ b/ereuse_devicehub/resources/device/metrics.py @@ -89,7 +89,6 @@ class Metrics(MetricsMix): trade['status_receiver_created'] = self.act.created return - # import pdb; pdb.set_trace() # necesitamos poder poner un cambio de estado de un trade mas antiguo que last_trade # lo mismo con confirm @@ -148,7 +147,8 @@ class Metrics(MetricsMix): if the action is one trade action, is possible than have a list of confirmations. Get the doble confirm for to know if this trade is confirmed or not. """ - if self.device.trading == 'TradeConfirmed': + # import pdb; pdb.set_trace() + if self.device.trading(self.act.lot) == 'TradeConfirmed': return True return False diff --git a/ereuse_devicehub/resources/device/models.py b/ereuse_devicehub/resources/device/models.py index b0d09848..842c009d 100644 --- a/ereuse_devicehub/resources/device/models.py +++ b/ereuse_devicehub/resources/device/models.py @@ -310,6 +310,80 @@ class Device(Thing): return history + @property + def tradings(self): + return {str(x.id): self.trading_for_web(x.lot) for x in self.actions if x.t == 'Trade'} + + def trading_for_web(self, lot): + """The trading state, or None if no Trade action has + ever been performed to this device. This extract the posibilities for to do. + This method is performed for show in the web.""" + if not hasattr(lot, 'trade'): + return + + Status = {0: 'Trade', + 1: 'Confirm', + 2: 'NeedConfirmation', + 3: 'TradeConfirmed', + 4: 'Revoke', + 5: 'NeedConfirmRevoke', + 6: 'RevokeConfirmed'} + + trade = lot.trade + user_from = trade.user_from + user_to = trade.user_to + user_from_confirm = False + user_to_confirm = False + user_from_revoke = False + user_to_revoke = False + status = 0 + + if not hasattr(trade, 'acceptances'): + return Status[status] + + for ac in self.actions: + if ac.t not in ['Confirm', 'Revoke']: + continue + + if ac.user not in [user_from, user_to]: + continue + + if ac.t == 'Confirm' and ac.action == trade: + if ac.user == user_from: + user_from_confirm = True + elif ac.user == user_to: + user_to_confirm = True + + if ac.t == 'Revoke' and ac.action == trade: + if ac.user == user_from: + user_from_revoke = True + elif ac.user == user_to: + user_to_revoke = True + + confirms = [user_from_confirm, user_to_confirm] + revokes = [user_from_revoke, user_to_revoke] + + if any(confirms): + status = 1 + if user_to_confirm and user_from == g.user: + status = 2 + if user_from_confirm and user_to == g.user: + status = 2 + + if all(confirms): + status = 3 + + if any(revokes): + status = 4 + if user_to_revoke and user_from == g.user: + status = 5 + if user_from_revoke and user_to == g.user: + status = 5 + if all(revokes): + status = 6 + + return Status[status] + def trading(self, lot): """The trading state, or None if no Trade action has ever been performed to this device. This extract the posibilities for to do""" @@ -330,30 +404,28 @@ class Device(Thing): user_from_revoke = False user_to_revoke = False status = 0 - confirms = {} - revokes = {} if not hasattr(trade, 'acceptances'): return Status[status] - acceptances = copy.copy(trade.acceptances) - acceptances = sorted(acceptances, key=lambda x: x.created) + for ac in self.actions: + if ac.t not in ['Confirm', 'Revoke']: + continue - for ac in acceptances: if ac.user not in [user_from, user_to]: continue - if ac.t == 'Confirm': + if ac.t == 'Confirm' and ac.action == trade: if ac.user == user_from: user_from_confirm = True elif ac.user == user_to: user_to_confirm = True - if ac.t == 'Revoke': + if ac.t == 'Revoke' and ac.action == trade: if ac.user == user_from: user_from_revoke = True elif ac.user == user_to: - user_to_revoke= True + user_to_revoke = True confirms = [user_from_confirm, user_to_confirm] revokes = [user_from_revoke, user_to_revoke] @@ -370,75 +442,6 @@ class Device(Thing): return Status[status] - def trading2(self): - """The trading state, or None if no Trade action has - ever been performed to this device. This extract the posibilities for to do""" - # trade = 'Trade' - confirm = 'Confirm' - need_confirm = 'NeedConfirmation' - double_confirm = 'TradeConfirmed' - revoke = 'Revoke' - revoke_pending = 'RevokePending' - confirm_revoke = 'ConfirmRevoke' - # revoke_confirmed = 'RevokeConfirmed' - - # return the correct status of trade depending of the user - - # #### CASES ##### - # User1 == owner of trade (This user have automatic Confirmation) - # ======================= - # if the last action is => only allow to do - # ========================================== - # Confirmation not User1 => Revoke - # Confirmation User1 => Revoke - # Revoke not User1 => ConfirmRevoke - # Revoke User1 => RevokePending - # RevokeConfirmation => RevokeConfirmed - # - # - # User2 == Not owner of trade - # ======================= - # if the last action is => only allow to do - # ========================================== - # Confirmation not User2 => Confirm - # Confirmation User2 => Revoke - # Revoke not User2 => ConfirmRevoke - # Revoke User2 => RevokePending - # RevokeConfirmation => RevokeConfirmed - - ac = self.last_action_trading - if not ac: - return - - first_owner = self.which_user_put_this_device_in_trace() - - if ac.type == confirm_revoke: - # can to do revoke_confirmed - return confirm_revoke - - if ac.type == revoke: - if ac.user == g.user: - # can todo revoke_pending - return revoke_pending - else: - # can to do confirm_revoke - return revoke - - if ac.type == confirm: - if not first_owner: - return - - if ac.user == first_owner: - if first_owner == g.user: - # can to do revoke - return confirm - else: - # can to do confirm - return need_confirm - else: - # can to do revoke - return double_confirm - @property def revoke(self): """If the actual trading state is an revoke action, this property show @@ -543,15 +546,16 @@ class Device(Thing): def which_user_put_this_device_in_trace(self): """which is the user than put this device in this trade""" actions = copy.copy(self.actions) - actions.sort(key=lambda x: x.created) actions.reverse() - last_ac = None # search the automatic Confirm for ac in actions: if ac.type == 'Trade': - return last_ac.user - if ac.type == 'Confirm': - last_ac = ac + # import pdb; pdb.set_trace() + action_device = [x.device for x in ac.actions_device if x.device == self][0] + if action_device.author: + return action_device.author + + return ac.author def change_owner(self, new_user): """util for change the owner one device""" diff --git a/ereuse_devicehub/resources/device/schemas.py b/ereuse_devicehub/resources/device/schemas.py index d9a658fe..bc18b995 100644 --- a/ereuse_devicehub/resources/device/schemas.py +++ b/ereuse_devicehub/resources/device/schemas.py @@ -1,7 +1,7 @@ import datetime from marshmallow import post_load, pre_load, fields as f -from marshmallow.fields import Boolean, Date, DateTime, Float, Integer, List, Str, String, UUID +from marshmallow.fields import Boolean, Date, DateTime, Float, Integer, List, Str, String, UUID, Dict from marshmallow.validate import Length, OneOf, Range from sqlalchemy.util import OrderedSet from stdnum import imei, meid @@ -50,12 +50,11 @@ class Device(Thing): description='The lots where this device is directly under.') rate = NestedOn('Rate', dump_only=True, description=m.Device.rate.__doc__) price = NestedOn('Price', dump_only=True, description=m.Device.price.__doc__) - # trading = EnumField(states.Trading, dump_only=True, description=m.Device.trading.__doc__) - trading = SanitizedStr(dump_only=True, description='') + tradings = Dict(dump_only=True, description='') physical = EnumField(states.Physical, dump_only=True, description=m.Device.physical.__doc__) - traking= EnumField(states.Traking, dump_only=True, description=m.Device.physical.__doc__) + traking = EnumField(states.Traking, dump_only=True, description=m.Device.physical.__doc__) usage = EnumField(states.Usage, dump_only=True, description=m.Device.physical.__doc__) - revoke = UUID(dump_only=True) + revoke = UUID(dump_only=True) physical_possessor = NestedOn('Agent', dump_only=True, data_key='physicalPossessor') production_date = DateTime('iso', description=m.Device.updated.comment, diff --git a/ereuse_devicehub/resources/lot/views.py b/ereuse_devicehub/resources/lot/views.py index d41f2008..12309888 100644 --- a/ereuse_devicehub/resources/lot/views.py +++ b/ereuse_devicehub/resources/lot/views.py @@ -305,6 +305,7 @@ def delete_from_trade(lot: Lot, ids: Set[int]): db.session.add(phantom_revoke) lot.devices.difference_update(without_confirms) + # TODO @cayop ?? we dont need this line lot.trade.devices = lot.devices return revoke diff --git a/tests/test_action.py b/tests/test_action.py index 888541f3..86e811e3 100644 --- a/tests/test_action.py +++ b/tests/test_action.py @@ -1516,8 +1516,8 @@ def test_usecase_confirmation(user: UserClient, user2: UserClient): 'type': 'Confirm', 'action': trade.id, 'devices': [ - snap1['device']['id'], - snap2['device']['id'], + snap1['device']['id'], + snap2['device']['id'], snap3['device']['id'], snap4['device']['id'], snap5['device']['id'], @@ -1535,7 +1535,7 @@ def test_usecase_confirmation(user: UserClient, user2: UserClient): assert trade.devices[-1].actions[-1].user == trade.user_from assert len(trade.devices[0].actions) == n_actions - # The manager remove one device of the lot and automaticaly + # The manager remove one device of the lot and automaticaly # is create one revoke action device_10 = trade.devices[-1] lot, _ = user.delete({}, @@ -1554,31 +1554,28 @@ def test_usecase_confirmation(user: UserClient, user2: UserClient): # the SCRAP confirms the revoke action request_confirm_revoke = { - 'type': 'ConfirmRevoke', - 'action': device_10.actions[-1].id, + 'type': 'Revoke', + 'action': trade.id, 'devices': [ snap10['device']['id'] ] } user2.post(res=models.Action, data=request_confirm_revoke) - assert device_10.actions[-1].t == 'ConfirmRevoke' + assert device_10.actions[-1].t == 'Revoke' assert device_10.actions[-2].t == 'Revoke' # assert len(trade.lot.devices) == len(trade.devices) == 9 # assert not device_10 in trade.devices # check validation error request_confirm_revoke = { - 'type': 'ConfirmRevoke', - 'action': device_10.actions[-1].id, + 'type': 'Revoke', + 'action': trade.id, 'devices': [ snap9['device']['id'] ] } - user2.post(res=models.Action, data=request_confirm_revoke, status=422) - - # The manager add again device_10 # assert len(trade.devices) == 9 lot, _ = user.post({}, @@ -1604,7 +1601,7 @@ def test_usecase_confirmation(user: UserClient, user2: UserClient): assert device_10.actions[-1].user == trade.user_from assert device_10.actions[-2].t == 'Confirm' assert device_10.actions[-2].user == trade.user_to - assert device_10.actions[-3].t == 'ConfirmRevoke' + assert device_10.actions[-3].t == 'Revoke' # assert len(device_10.actions) == 13 @@ -1712,6 +1709,7 @@ def test_confirmRevoke(user: UserClient, user2: UserClient): assert len(trade.devices) == 10 # the SCRAP confirms the revoke action + import pdb; pdb.set_trace() request_confirm_revoke = { 'type': 'ConfirmRevoke', 'action': device_10.actions[-2].id,