class Address(BaseModel):
    """
    The model Address represent a user address.
    Each address is releated to one user, but one user can have
    more addresses.

    Attributes:
        uuid (UUID): Address unique uuid
        user (:any:`User`): Foreign key pointing to the user `owner` of the address
        country (str): Country for the address, i.e. ``Italy``
        city (str): City name
        post_code (str): Postal code for the address
        address (str): Full address for the Address resource
        phone (str): Phone number for the Address
    """
    uuid = UUIDField(unique=True)
    user = ForeignKeyField(User, related_name='addresses')
    country = CharField()
    city = CharField()
    post_code = CharField()
    address = CharField()
    phone = CharField()

    _schema = AddressSchema class Item(BaseModel):
    """
    Item describes a product for the e-commerce platform.

    Attributes:
        uuid (UUID): Item UUID
        name (str): Name for the product
        price (decimal.Decimal): Price for a single product
        description (str): Product description
        availability (int): Quantity of items available
        category (str): Category group of the item

    """
    uuid = UUIDField(unique=True)
    name = CharField()
    price = DecimalField(auto_round=True)
    description = TextField()
    availability = IntegerField()
    category = TextField()
    _schema = ItemSchema
    _search_attributes = ['name', 'category', 'description']

    def __str__(self):
        return '{}, {}, {}, {}'.format(
            self.uuid,
            self.name,
            self.price,
            self.description)

    def is_favorite(self, item):
        for f in self.favorites:
            if f.item_id == item.id:
                return True
        return False class Order(BaseModel):
    """
    Orders represent an order placed by a `User`, containing one or more `Item`
    that have to be delivered to one of the user's `Address`.

    Attributes:
        uuid (UUID): Order's unique id
        total_price (:any:`decimal.Decimal`): Total price for the order
        delivery_address (:any:`Address`): Address specified for delivery
        user (:any:`User`): User that created the order
    """
    uuid = UUIDField(unique=True, default=uuid4)
    total_price = DecimalField(default=0)
    delivery_address = ForeignKeyField(Address, related_name="orders")
    user = ForeignKeyField(User, related_name="orders")
    _schema = OrderSchema

    class Meta:
        order_by = ('created_at',)

    @property
    def order_items(self):
        """
        Property that execute a cross-table query against :class:`models.OrderItem`
        to get a list of all OrderItem related to the callee order.

        Returns:
            list: :class:`models.OrderItem` related to the order.
        """

        query = (
            OrderItem
            .select(OrderItem, Order)
            .join(Order)
            .where(Order.uuid == self.uuid)
        )

        return [orderitem for orderitem in query]

    def empty_order(self):
        """
        Remove all the items from the order deleting all OrderItem related
        to this order and resetting the order's total_price value to 0.

        Returns:
            models.Order: The updated order
        """

        self.total_price = 0
        OrderItem.delete().where(OrderItem.order == self).execute()
        self.save()
        return self

    @staticmethod
    def create_order(user, address, items):
        """
        Create an Order and respective OrderItems. OrderItems are created
        in a single query as well as the Order. It also updates Items'
        availability.

        Args:
            user (models.User): order owner
            address (models.Address): order address
            items (dict): item updates entries as a dictionary, keys are
                items and values are new quantities to set. Example of
                argument:

                ..code-block:: python
                    items = {
                        Item.get(pk=1): 3,
                        Item.get(pk=2): 1,
                    }

        Returns:
            models.Order: The new order
        """
        total_price = sum(
            item.price * quantity for item, quantity in items.items())

        with database.atomic():
            order = Order.create(
                delivery_address=address,
                user=user,
                total_price=total_price,
            )
            order.update_items(items, update_total=False)
            return order

    def update_items(self, items, update_total=True, new_address=None):
        """
        Update Order and respective OrderItems by splitting in creation,
        deletion and updating queries, minimizing the interactions with the
        database. It also updates Items' availability.

        Args:
            items (dict): item updates entries as a dictionary, keys are
                items and values are new quantities to set. Example of
                argument:

                ..code-block:: python
                    items = {
                        Item.get(pk=1): 3,
                        Item.get(pk=2): 0,
                        Item.get(pk=3): 1,
                    }
            update_total (bool): if True the procedure updates order's
                total price. Default to True.
            new_address (models.Address): if not None the procedure updates
                the order with the given address. Default to None.

        Returns:
            models.Order: The new/updated order
        """
        to_create = {}
        to_remove = {}
        to_edit = {}
        total_price_difference = 0
        orderitems = self.order_items

        # split items in insert, delete and update sets
        for item, quantity in items.items():
            for orderitem in orderitems:
                if orderitem.item == item:
                    difference = quantity - orderitem.quantity
                    if quantity == 0:
                        to_remove[item] = orderitem.quantity
                    elif difference > item.availability:
                        raise InsufficientAvailabilityException(
                            item, quantity)
                    elif quantity < 0:
                        raise WrongQuantity()
                    else:
                        to_edit[item] = difference
                    total_price_difference += item.price * difference
                    break
            else:
                if quantity <= 0:
                    raise WrongQuantity()
                elif quantity > item.availability:
                    raise InsufficientAvailabilityException(
                        item, quantity)
                else:
                    to_create[item] = quantity
                total_price_difference += item.price * quantity

        with database.atomic():
            self.edit_items_quantity(to_edit)
            self.create_items(to_create)
            self.delete_items(to_remove)
            if update_total:
                self.total_price += total_price_difference
            if new_address:
                self.address = new_address
            if update_total or new_address:
                self.save()
        return self

    @database.atomic()
    def edit_items_quantity(self, items):
        """
        Update orderitems using a query for each item, and updates
        items' availability.

        Args:
            items (dict): item updates entries as a dictionary, keys are
                items and values are new quantities to set. Example of
                argument:

                ..code-block:: python
                    items = {
                        Item.get(pk=1): 3,
                        Item.get(pk=3): 1,
                    }
        Returns:
            Order: callee instance
        """
        if not items:
            return

        orderitems = OrderItem.select().where(
            OrderItem.item << [k for k in items.keys()],
            OrderItem.order == self)

        for orderitem in orderitems:
            for item, difference in items.items():
                if orderitem.item == item:
                    item.availability -= difference
                    item.save()
                    orderitem.quantity += difference
                    orderitem._calculate_subtotal()
                    orderitem.save()
                    break

    def delete_items(self, items):
        """
        Delete orderitems in a single query and updates items' availability.

        Args:
            items (dict): item entries as a dictionary, keys are
                items to delete and values are previously reserved quantities.
                Example of argument:

                ..code-block:: python
                    items = {
                        Item.get(pk=1): 3,
                        Item.get(pk=2): 2,
                    }
        """
        if not items:
            return

        with database.atomic():
            for item, quantity in items.items():
                item.availability += quantity
                item.save()
            OrderItem.delete().where(
                OrderItem.order == self).where(
                OrderItem.item << [k for k in items.keys()]).execute()

    def create_items(self, items):
        """
        Creates orderitems in a single query and updates items' availability.

        Args:
            items (dict): item entries as a dictionary, keys are
                items to create and values are new quantities to set.
                Example of argument:

                ..code-block:: python
                    items = {
                        Item.get(pk=1): 3,
                        Item.get(pk=2): 1,
                    }
        """
        if not items:
            return

        with database.atomic():
            for item, quantity in items.items():
                item.availability -= quantity
                item.save()

            OrderItem.insert_many([
                {
                    'order': self,
                    'item': item,
                    'quantity': quantity,
                    'subtotal': item.price * quantity,
                } for item, quantity in items.items()]).execute()

    def add_item(self, item, quantity=1):
        """
        Add items to the order. It updates item availability.

        Args:
            item (models.Item): the Item to add
            quantity (int): how many items to add

        Returns:
            order (models.Order): the updated order
        """
        return self.update_items({item: quantity})

    def remove_item(self, item, quantity=1):
        """
        Remove the given item from the order, reducing quantity of the relative
        OrderItem entity or deleting it if removing the last item
        ``(OrderItem.quantity == 0)``.
        It also restores the item availability.

        Args:
            item (models.Item): the Item to remove
            quantity (int): how many items to remove

        Returns:
            order (models.Order): the updated order
        """
        return self.update_items({item: -quantity}) class Picture(BaseModel):
    """
    A Picture model describes and points to a stored image file. Allows linkage
    between the image files and one :any:`Item` resource.

    Attributes:
        uuid (UUID): Picture's uuid
        extension (str): Extension of the image file the Picture's model refer to
        item (:any:`Item`): Foreign key referencing the Item related to the Picture.
            A ``pictures`` field can be used from ``Item`` to access the Item resource
            pictures
    """
    uuid = UUIDField(unique=True)
    extension = CharField()
    item = ForeignKeyField(Item, related_name='pictures')
    _schema = PictureSchema

    @property
    def filename(self):
        """Full name (uuid.ext) of the file that the Picture model reference."""
        return '{}.{}'.format(
            self.uuid,
            self.extension)

    def __str__(self):
        return '{}.{} -> item: {}'.format(
            self.uuid,
            self.extension,
            self.item.uuid) class AddressSchema(BaseSchema):
    """
    Schema representing :any:`Address` resources.
    All the fields of the schema, if present, must be not empty strings.

    Attributes:
        id: (``UUID``): Address id
        country (str): Address' country (i.e. `Italy`)
        city (str): Address' city name (i.e. `Florence`)
        post_code (str): Address' postal code
        address (str): Address' full address (i.e. `via rossi 10`)
        phone (str): Phone number registered with the address
        user (``Relationship``): field pointing to the :any:`User` that
            created the address.

    """

    class Meta:
        type_ = 'address'
        self_url_many = '/addresses/'
        self_url = '/addresses/{id}'
        self_url_kwargs = {'id': '<id>'}
        json_module = simplejson

    id = fields.Str(dump_only=True, attribute='uuid')
    country = fields.Str(required=True, validate=NOT_BLANK)
    city = fields.Str(required=True, validate=NOT_BLANK)
    post_code = fields.Str(required=True, validate=NOT_BLANK)
    address = fields.Str(required=True, validate=NOT_BLANK)
    phone = fields.Str(required=True, validate=NOT_BLANK)

    user = fields.Relationship(
        include_resource_linkage=True,
        type_='user', schema='UserSchema',
        id_field='uuid', required=True,
    ) class ItemSchema(BaseSchema):
    """
    Schema describing the Item resources.

    Attributes:
        id (``UUID``): Item's ID
        name (str): Item's name
        price (float): Single item price
        description (str): Item's description
        availability (int): Quantity of items of this type available in the store

        pictures (``Relationship``):list of :any:`Picture` related to the
            Currently parsed `Item`. If included it will attach all the
            pictures information within the `included` attribute of the generated json.
    """

    class Meta:
        type_ = 'item'
        self_url = '/items/{id}'
        self_url_kwargs = {'id': '<id>'}
        self_url_many = '/items/'

    id = fields.Str(dump_only=True, attribute='uuid')
    name = fields.Str(required=True, validate=NOT_BLANK)
    price = fields.Float(required=True, validate=MORE_THAN_ZERO)
    description = fields.Str(required=True, validate=NOT_BLANK)
    availability = fields.Int(required=True, validate=MORE_THAN_ZERO)
    category = fields.Str(required=True, validate=NOT_BLANK)

    pictures = fields.Relationship(
        include_resource_linkage=True,
        type_='picture', schema='PictureSchema',
        id_field='uuid', many=True,
    ) class OrderSchema(BaseSchema):
    """
    Schema describing an Order resource.

    Attributes:
        id (``UUID``): Order's ID

        date (:any:`datetime`): Order's creation date

        total_price (:any:`decimal.Decimal`): Order's total price,
            if passed through validation should be at least equal to 0

        delivery_address (``Relationship``): :any:`Address` specified for delivery.
            Should be `owned` by the order's author.

        items (``Relationship``):list of :any:`Item` in the order.
            Objects in the relationship list is generated through the
            :any:`OrderItemSchema`. If passed while validating should not be empty.

        user (``Relationship``): the :any:`models.User` that created the Order.

    """

    class Meta:
        type_ = 'order'
        self_url_many = '/orders/'
        self_url = '/orders/{id}'
        self_url_kwargs = {'id': '<id>'}
        json_module = simplejson

    id = fields.Str(dump_only=True, attribute='uuid')
    date = fields.DateTime(attribute='created_at', dump_only=True)
    total_price = fields.Decimal(dump_only=True, places=2,
                                 validate=MORE_THAN_ZERO,
                                 )

    delivery_address = fields.Relationship(
        required=True, include_resource_linkage=True,
        type_='address', schema='AddressSchema',
        id_field='uuid',
    )

    items = fields.Relationship(
        many=True, include_resource_linkage=True,
        type_='item', schema='OrderItemSchema',
        id_field='item.uuid',
        attribute='order_items', required=True,
        validate=NOT_EMPTY,
    )

    user = fields.Relationship(
        include_resource_linkage=True,
        type_='user', schema='UserSchema',
        id_field='uuid',
        required=True,
    ) class PictureSchema(BaseSchema):
    """
    Schema for describing :any:`Picture` resources.

    Attributes:
        id (uuid): Picture's ID
        extension (str): picture file's extension
        filename (str): full name for the picture
        item (``Relationship``): any:`Item` related to the picture
            resource.
    """

    class Meta:
        type_ = 'picture'
        self_url = '/pictures/{id}'
        self_url_kwargs = {'id': '<id>'}
        json_module = simplejson

    id = fields.Str(dump_only=True, attribute='uuid')
    # TODO: Make extensions validation rule oneOf
    extension = fields.Str(required=True, validate=NOT_BLANK)
    filename = fields.Str(dump_only=True)

    item = fields.Relationship(
        include_resource_linkage=True,
        type_='item', schema='ItemSchema',
        id_field='uuid', required=True,
    ) class UserSchema(BaseSchema):
    """
    Schema representing a :any:`User` resource.

    Attributes:
     id (``UUID``): User's id
     first_name (str): User's first name. If present must not be blank
     last_name (str): User's last name. If present must not be blank
     email (str): User's email. If present must be a valid email and not be blank
     password (str): User's password. If present must not be blank.
     admin (bool): wether the user is registered as admin or not

     orders (``Relationship``): field pointing to all the :any:`Order` created
        by the user.
     addresses (``Relationship``): field pointing to the user's :any:`Address` resources
    """

    class Meta:
        type_ = 'user'
        self_url_many = '/users/'
        self_url = '/users/{id}'
        self_url_kwargs = {'id': '<id>'}
        json_module = simplejson

    id = fields.Str(dump_only=True, attribute='uuid')
    first_name = fields.Str(required=True, validate=NOT_BLANK)
    last_name = fields.Str(required=True, validate=NOT_BLANK)
    email = fields.Email(required=True, validate=NOT_BLANK)
    password = fields.Str(required=True, validate=NOT_BLANK, load_only=True)
    admin = fields.Boolean(dump_only=True)

    orders = fields.Relationship(
        many=True, include_resource_linkage=True,
        type_='order', schema='OrderSchema',
        dump_only=True, id_field='uuid',
    )

    addresses = fields.Relationship(
        many=True, include_resource_linkage=True,
        type_='address', schema='AddressSchema',
        dump_only=True, id_field='uuid',
    ) def mock_uuid_generator():
def mock_datetime(*args):
    def __init__(self, cls):
    def __call__(self, created_at=mock_datetime(), uuid=None, **query):
def get_all_models_names():
def add_user(email, password, id=None, first_name='John', last_name='Doe'):
def add_admin_user(email, psw, id=None):
def add_address(user, country='Italy', city='Pistoia', post_code='51100',
                address='Via Verdi 12', phone='3294882773', id=None):
def add_favorite(user, item, id=None):
def json_favorite(item):
def add_item(name='Item Test', price='15.99',
             description='test test test', id=None, category='scarpe'):
def open_with_auth(app, url, method, username, password, content_type, data):
def clean_images():
def setup_images():
def count_order_items(order):
def format_jsonapi_request(type_, data):
def assert_valid_response(data, expected):
    def sort_data_lists(data, attribute, key):
    def included_sorter(i): return i['type']
    def errors_sorter(e): return e['source']['pointer']
def wrong_dump(data):
class MockModelCreate:
    AUTH_TYPE = 'Basic'
    RESULTS = json.load(fo) class TestCase:
    """
    Created TestCase to avoid duplicated code in the other tests
    """
    TEST_DB = SqliteDatabase(':memory:')

    @classmethod
    def setup_class(cls):
        """
        When a Test is created override the ``database`` attribute for all
        the tables with a SqliteDatabase in memory.
        """
        for table in TABLES:
            table._meta.database = cls.TEST_DB
            table.create_table(fail_silently=True)

        cls.app = app.test_client()

    def setup_method(self):
        """
        When setting up a new test method clear all the tables
        """
        for table in TABLES:
            table.delete().execute() RESULTS = json.load(fo) def add_address(user, country='Italy', city='Pistoia', post_code='51100',
                address='Via Verdi 12', phone='3294882773', id=None):

    return Address.create(
        uuid=id or uuid.uuid4(),
        user=user,
        country=country,
        city=city,
        post_code=post_code,
        address=address,
        phone=phone,
    ) def add_user(email, password, id=None, first_name='John', last_name='Doe'):
    """
    Create a single user in the test database.
    If an email is provided it will be used, otherwise it will be generated
    by the function before adding the User to the database.
    """
    email = email or 'johndoe{}@email.com'.format(int(random.random() * 100))

    return User.create(
        first_name=first_name,
        last_name=last_name,
        email=email,
        password=User.hash_password(password),
        uuid=id or uuid.uuid4(),
    ) def assert_valid_response(data, expected):
    """
    Take a flask app response.data and the expected test result, normalize them
    sorting the `included` and `errors` lists if present, then assert their
    equality
    """
    def sort_data_lists(data, attribute, key):
        """
        sort a given data structure's attribute(list) using the given key function
        """
        try:
            data[attribute] = sorted(data[attribute], key=key)
        except KeyError:
            pass

    # sort functions for included and errors attributes
    def included_sorter(i): return i['type']

    def errors_sorter(e): return e['source']['pointer']

    try:
        # lazy load of the response data, so we can pass either the parsed json
        # response or the json string
        data = json.loads(data)
    except TypeError:
        # data has already been parsed
        pass

    # ensure that both `data` and `expected` are lists while working on them
    data_items = data if isinstance(data, list) else [data]
    expected_items = expected if isinstance(expected, list) else [expected]

    for item in data_items:
        # Sort the included and errors lists of the response.data if present
        sort_data_lists(item, 'included', included_sorter)
        sort_data_lists(item, 'errors', errors_sorter)

    for item in expected_items:
        # Sort the lists of the expected results if present
        sort_data_lists(item, 'included', included_sorter)
        sort_data_lists(item, 'errors', errors_sorter)

    assert data == expected def format_jsonapi_request(type_, data):
    """
    Given the attributes and relationships of a resource, compile the jsonapi
    post data for the request.
    All key - value pairs except ``relationships`` will be mapped inside
    ``['data']['attributes']`` of the request.
    Relationships key value will be mapped inside ``['data']['relationship']``

    .. NOTE::
        All relationship **must** map to the related field inside the Schema of
        the type and have a ``type`` and ``id`` properties.

    .. code-block:: python

        data = {
            "<attribute field": "bar"
            "relationships": {
                "<field_name>": {
                    "type": "<Resource type_>",
                    "id": < Resource id >
                },
                "<field_name>": [
                    {
                        "type": "item",
                        "id": < Resource id >,
                        "<metadata>": < metadata_value(ie. quantity of items) >
                    }
                ]
            }
        }

    """
    rels = {}
    if 'relationships' in data:
        rels = {k: {'data': v} for k, v in data['relationships'].items()}
        del data['relationships']

    retval = {
        'data': {
            'type': type_,
            'attributes': data,
            'relationships': rels
        }
    }
    return retval
import copy
import utils
from datetime import datetime
from models import Address, Item, Order, Picture
from schemas import (AddressSchema, ItemSchema, OrderSchema, PictureSchema,
                     UserSchema)
from tests import test_utils
from tests.test_case import TestCase
from tests.test_utils import (RESULTS, add_address, add_user,
                              assert_valid_response, format_jsonapi_request)

    def test_address_validate_input__fail(self):
        data = copy.deepcopy(self.data)
        data['country'] = ''
        del data['relationships']['user']

        post_data = format_jsonapi_request('address', data)
        errors = AddressSchema.validate_input(post_data)

        expected_result = EXPECTED_ADDRESSES['address_validate_input__fail']
        assert_valid_response(errors, expected_result)

    def test_get_address_json__success(self):

        data, errors = AddressSchema.jsonapi(self.addr)

        assert errors == {}
        assert type(data) is str

        expected_result = EXPECTED_ADDRESSES['get_address_json__success']
        assert_valid_response(data, expected_result)

    def test_address_json_include_user__success(self):
        data, errors = AddressSchema.jsonapi(self.addr, include_data=['user'])

        assert errors == {}
        assert type(data) is str

        expected_result = EXPECTED_ADDRESSES['get_address_json_include_user__success']
        assert_valid_response(data, expected_result)

    def test_get_addresses_list__success(self):
        add_address(
            self.user, id='4373d5d7-cae5-42bc-b218-d6fc6d18626f')

        addr_list = [a for a in Address.select()]
        data = AddressSchema.jsonapi_list(addr_list)

        expected_result = EXPECTED_ADDRESSES['get_addresses_list__success']
        assert_valid_response(data, expected_result)


class TestItemSchema(TestCase):
    def setup_method(self):
        super(TestItemSchema, self).setup_method()
        self.data = {
            'name': 'Test item',
            'price': 10.25,
            'description': 'Test item description',
            'availability': 73,
            'category': 'scarpe',
        }
        self.item1 = Item.create(
            uuid='25da606b-dbd3-45e1-bb23-ff1f84a5622a',
            name='Item 1',
            description='Item 1 description',
            price=5.24,
            availability=5,
            category='scarpe',
        )
        self.item2 = Item.create(
            uuid='08bd8de0-a4ac-459d-956f-cf6d8b8a7507',
            name='Item 2',
            description='Item 2 description',
            price=8,
            availability=10,
            category='accessori',
        )
        self.picture = Picture.create(
            item=self.item1,
            uuid='08bd8de0-a4ac-459d-956f-cf6d8b8a7507',
            extension='png',
        )

    def test_item_validate_input__success(self):
        post_data = format_jsonapi_request('item', self.data)
        errors = ItemSchema.validate_input(post_data)

        assert errors == {}

    def test_item_validate_input__fail(self):
        data = {
            'name': '',          # not empty
            'price': -2,         # more than 0
            'availability': -2,  # more than 0
            'description': '',   # not empty
            'category': '',      # not empty
        }
        post_data = format_jsonapi_request('item', data)
        errors = ItemSchema.validate_input(post_data)

        expected_result = EXPECTED_ITEMS['item_validate_input__fail']
        assert_valid_response(errors, expected_result)

    def test_get_item_json__success(self):
        data, errors = ItemSchema.jsonapi(self.item1)

        assert errors == {}
        expected_result = EXPECTED_ITEMS['get_item_json__success']
        assert_valid_response(data, expected_result)

    def test_get_items_list__success(self):
        data = ItemSchema.jsonapi_list([self.item1, self.item2])
        expected_result = EXPECTED_ITEMS['get_items_list__success']
        assert_valid_response(data, expected_result)

    def test_get_item_include_pictures__success(self):
        data, errors = ItemSchema.jsonapi(
            self.item1, include_data=['pictures'])

        assert errors == {}
        expected_result = EXPECTED_ITEMS['get_item_include_pictures__success']
        assert_valid_response(data, expected_result)


class TestPictureSchema(TestCase):
    @classmethod
    def setup_class(cls):
        super(TestPictureSchema, cls).setup_class()
        utils.get_image_folder = lambda: TEST_IMAGE_FOLDER

        test_utils.get_image_folder = utils.get_image_folder