diff --git a/ereuse_devicehub/resources/action/models.py b/ereuse_devicehub/resources/action/models.py index 26ed13d4..fe23d2e2 100644 --- a/ereuse_devicehub/resources/action/models.py +++ b/ereuse_devicehub/resources/action/models.py @@ -296,9 +296,10 @@ class ActionDevice(db.Model): primary_key=True) -class ActionWithMultipleDocuments(Action): +class ActionWithMultipleDocuments(ActionWithMultipleDevices): + # pass documents = relationship(Document, - backref=backref('actions_multiple', lazy=True, **_sorted_actions), + backref=backref('actions_multiple_docs', lazy=True, **_sorted_actions), secondary=lambda: ActionDocument.__table__, order_by=lambda: Document.id, collection_class=OrderedSet) @@ -1448,7 +1449,7 @@ class CancelReservation(Organize): """The act of cancelling a reservation.""" -class Confirm(JoinedTableMixin, ActionWithMultipleDevices): +class Confirm(JoinedTableMixin, ActionWithMultipleDocuments): """Users confirm the one action trade this confirmation it's link to trade and the devices that confirm """ @@ -1488,7 +1489,7 @@ class ConfirmRevoke(Confirm): return '<{0.t} {0.id} accepted by {0.user}>'.format(self) -class Trade(JoinedTableMixin, ActionWithMultipleDevices, ActionWithMultipleDocuments): +class Trade(JoinedTableMixin, ActionWithMultipleDocuments): """Trade actions log the political exchange of devices between users. Every time a trade action is performed, the old user looses its political possession, for example ownership, in favor of another diff --git a/ereuse_devicehub/resources/action/schemas.py b/ereuse_devicehub/resources/action/schemas.py index 9808d3ce..a8c7df85 100644 --- a/ereuse_devicehub/resources/action/schemas.py +++ b/ereuse_devicehub/resources/action/schemas.py @@ -499,7 +499,6 @@ class ConfirmRevoke(ActionWithMultipleDevices): class Trade(ActionWithMultipleDevices): __doc__ = m.Trade.__doc__ - document_id = SanitizedStr(validate=Length(max=STR_SIZE), data_key='documentID', required=False) date = DateTime(data_key='date', required=False) price = Float(required=False, data_key='price') user_to_id = SanitizedStr(validate=Length(max=STR_SIZE), data_key='userTo', missing='', diff --git a/ereuse_devicehub/resources/tradedocument/__init__.py b/ereuse_devicehub/resources/tradedocument/__init__.py new file mode 100644 index 00000000..0e75f6e8 --- /dev/null +++ b/ereuse_devicehub/resources/tradedocument/__init__.py @@ -0,0 +1,10 @@ +from teal.resource import Converters, Resource + +from ereuse_devicehub.resources.tradedocument import schemas +from ereuse_devicehub.resources.tradedocument.views import DocumentView + +class TradeDocumentDef(Resource): + SCHEMA = schemas.Document + VIEW = DocumentView + AUTH = True + ID_CONVERTER = Converters.string diff --git a/ereuse_devicehub/resources/tradedocument/models.py b/ereuse_devicehub/resources/tradedocument/models.py new file mode 100644 index 00000000..85db108b --- /dev/null +++ b/ereuse_devicehub/resources/tradedocument/models.py @@ -0,0 +1,115 @@ +import os + +from itertools import chain +from citext import CIText +from flask import current_app as app, g + +from sqlalchemy.dialects.postgresql import UUID +from ereuse_devicehub.db import db +from ereuse_devicehub.resources.user.models import User +from ereuse_devicehub.resources.models import STR_SM_SIZE, Thing, listener_reset_field_updated_in_actual_time + +from sqlalchemy import BigInteger, Boolean, Column, Float, ForeignKey, Integer, \ + Sequence, SmallInteger, Unicode, inspect, text +from sqlalchemy.ext.declarative import declared_attr +from sqlalchemy.orm import ColumnProperty, backref, relationship, validates +from sqlalchemy.util import OrderedSet +from sqlalchemy_utils import ColorType +from teal.db import CASCADE_DEL, POLYMORPHIC_ID, POLYMORPHIC_ON, \ + check_lower, check_range +from teal.resource import url_for_resource + +from ereuse_devicehub.resources.utils import hashcode +from ereuse_devicehub.resources.enums import BatteryTechnology, CameraFacing, ComputerChassis, \ + DataStorageInterface, DisplayTech, PrinterTechnology, RamFormat, RamInterface, Severity, TransferState + + +class Document(Thing): + """This represent a document involved in a trade action. + Every document is added to a lot. + When this lot is converted in one trade, the action trade is added to the document + and the action trade need to be confirmed for the both users of the trade. + This confirmation can be revoked and this revoked need to be ConfirmRevoke for have + some efect. + + This documents can be invoices or list of devices or certificates of erasure of + one disk. + + Like a Devices one document have actions and is possible add or delete of one lot + if this lot don't have a trade + + The document is saved in the database + + """ + + id = Column(BigInteger, Sequence('device_seq'), primary_key=True) + id.comment = """The identifier of the device for this database. Used only + internally for software; users should not use this. + """ + # type = Column(Unicode(STR_SM_SIZE), nullable=False) + date = Column(db.DateTime) + date.comment = """The date of document, some documents need to have one date + """ + id_document = Column(CIText()) + id_document.comment = """The id of one document like invoice so they can be linked.""" + description = Column(db.CIText()) + description.comment = """A description of document.""" + owner_id = db.Column(UUID(as_uuid=True), + db.ForeignKey(User.id), + nullable=False, + default=lambda: g.user.id) + owner = db.relationship(User, primaryjoin=owner_id == User.id) + file_name = Column(db.CIText()) + file_name.comment = """This is the name of the file when user up the document.""" + file_name_disk = Column(db.CIText()) + file_name_disk.comment = """This is the name of the file as devicehub save in server.""" + + __table_args__ = ( + db.Index('document_id', id, postgresql_using='hash'), + # db.Index('type_doc', type, postgresql_using='hash') + ) + + @property + def actions(self) -> list: + """All the actions where the device participated, including: + + 1. Actions performed directly to the device. + 2. Actions performed to a component. + 3. Actions performed to a parent device. + + Actions are returned by descending ``created`` time. + """ + return sorted(self.actions_multiple_docs, key=lambda x: x.created) + + @property + def path_to_file(self) -> str: + """The path of one file is defined by the owner, file_name and created time. + + """ + base = app.config['PATH_DOCUMENTS_STORAGE'] + file_name = "{0.date}-{0.filename}".format(self) + base = os.path.join(base, g.user.email, file_name) + return sorted(self.actions_multiple_docs, key=lambda x: x.created) + + def last_action_of(self, *types): + """Gets the last action of the given types. + + :raise LookupError: Device has not an action of the given type. + """ + try: + # noinspection PyTypeHints + actions = self.actions + actions.sort(key=lambda x: x.created) + return next(e for e in reversed(actions) if isinstance(e, types)) + except StopIteration: + raise LookupError('{!r} does not contain actions of types {}.'.format(self, types)) + + def _warning_actions(self, actions): + return sorted(ev for ev in actions if ev.severity >= Severity.Warning) + + + def __lt__(self, other): + return self.id < other.id + + def __str__(self) -> str: + return '{0.file_name}'.format(self) diff --git a/ereuse_devicehub/resources/tradedocument/schemas.py b/ereuse_devicehub/resources/tradedocument/schemas.py new file mode 100644 index 00000000..b71b85d7 --- /dev/null +++ b/ereuse_devicehub/resources/tradedocument/schemas.py @@ -0,0 +1,14 @@ +from marshmallow.fields import DateTime, Integer +from teal.marshmallow import SanitizedStr + +from ereuse_devicehub.resources.schemas import Thing +from ereuse_devicehub.resources.tradedocument import models as m + + +class Document(Thing): + __doc__ = m.Document.__doc__ + id = Integer(description=m.Document.id.comment, dump_only=True) + date = DateTime(required=False, description=m.Document.date.comment) + id_document = SanitizedStr(default='', description=m.Document.id_document.comment) + description = SanitizedStr(default='', description=m.Document.description.comment) + file_name = SanitizedStr(default='', description=m.Document.file_name.comment) diff --git a/ereuse_devicehub/resources/tradedocument/views.py b/ereuse_devicehub/resources/tradedocument/views.py new file mode 100644 index 00000000..cc30e228 --- /dev/null +++ b/ereuse_devicehub/resources/tradedocument/views.py @@ -0,0 +1,35 @@ + +import marshmallow +from flask import g, current_app as app, render_template, request, Response +from flask.json import jsonify +from flask_sqlalchemy import Pagination +from marshmallow import fields, fields as f, validate as v, Schema as MarshmallowSchema +from teal.resource import View + +from ereuse_devicehub import auth +from ereuse_devicehub.db import db +from ereuse_devicehub.query import SearchQueryParser, things_response +from ereuse_devicehub.resources.tradedocument.models import Document + +class DocumentView(View): + + # @auth.Auth.requires_auth + def one(self, id: str): + document = Document.query.filter_by(id=id).first() + return self.schema.jsonify(document) + + # @auth.Auth.requires_auth + def post(self): + """Posts an action.""" + json = request.get_json(validate=False) + resource_def = app.resources[json['type']] + + a = resource_def.schema.load(json) + Model = db.Model._decl_class_registry.data[json['type']]() + action = Model(**a) + db.session.add(action) + db.session().final_flush() + ret = self.schema.jsonify(action) + ret.status_code = 201 + db.session.commit() + return ret diff --git a/tests/test_trade.py b/tests/test_trade.py index 98d98ac7..587dfb23 100644 --- a/tests/test_trade.py +++ b/tests/test_trade.py @@ -56,7 +56,6 @@ def test_offer_without_to(user: UserClient): 'userFrom': user.email, 'price': 10, 'date': "2020-12-01T02:00:00+00:00", - 'documentID': '1', 'lot': lot['id'], 'confirm': False, 'code': 'MAX' @@ -84,7 +83,6 @@ def test_offer_without_to(user: UserClient): 'userFrom': user.email, 'price': 10, 'date': "2020-12-01T02:00:00+00:00", - 'documentID': '1', 'lot': lot['id'], 'confirm': False, 'code': 'MAX' @@ -107,7 +105,6 @@ def test_offer_without_to(user: UserClient): 'userFrom': user.email, 'price': 10, 'date': "2020-12-01T02:00:00+00:00", - 'documentID': '1', 'lot': lot2.id, 'confirm': False, 'code': 'MAX' @@ -138,7 +135,6 @@ def test_offer_without_from(user: UserClient, user2: UserClient): 'userTo': user2.email, 'price': 10, 'date': "2020-12-01T02:00:00+00:00", - 'documentID': '1', 'lot': lot.id, 'confirm': False, 'code': 'MAX' @@ -183,7 +179,6 @@ def test_offer_without_users(user: UserClient): 'devices': [device.id], 'price': 10, 'date': "2020-12-01T02:00:00+00:00", - 'documentID': '1', 'lot': lot.id, 'confirm': False, 'code': 'MAX' @@ -217,7 +212,6 @@ def test_offer(user: UserClient): 'userTo': user2.email, 'price': 10, 'date': "2020-12-01T02:00:00+00:00", - 'documentID': '1', 'lot': lot.id, 'confirm': True, } @@ -244,7 +238,6 @@ def test_offer_without_devices(user: UserClient): 'userTo': user2.email, 'price': 10, 'date': "2020-12-01T02:00:00+00:00", - 'documentID': '1', 'lot': lot['id'], 'confirm': True, } @@ -272,7 +265,6 @@ def test_endpoint_confirm(user: UserClient, user2: UserClient): 'userTo': user2.email, 'price': 10, 'date': "2020-12-01T02:00:00+00:00", - 'documentID': '1', 'lot': lot['id'], 'confirm': True, } @@ -313,7 +305,6 @@ def test_confirm_revoke(user: UserClient, user2: UserClient): 'userTo': user2.email, 'price': 10, 'date': "2020-12-01T02:00:00+00:00", - 'documentID': '1', 'lot': lot['id'], 'confirm': True, } @@ -392,7 +383,6 @@ def test_usecase_confirmation(user: UserClient, user2: UserClient): 'userTo': user.email, 'price': 10, 'date': "2020-12-01T02:00:00+00:00", - 'documentID': '1', 'lot': lot['id'], 'confirm': True, } @@ -580,7 +570,6 @@ def test_confirmRevoke(user: UserClient, user2: UserClient): 'userTo': user.email, 'price': 10, 'date': "2020-12-01T02:00:00+00:00", - 'documentID': '1', 'lot': lot['id'], 'confirm': True, }