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 User(BaseModel, UserMixin):
    """
    User represents an user for the application.

    Attributes:
        first_name (str): User's first name
        last_name (str): User's last name
        email (str): User's **valid** email
        password (str): User's password
        admin (bool): User's admin status. Defaults to ``False``

    .. NOTE::
        Each User resource must have an unique `email` field, meaning
        that there cannot be two user's registered with the same email.

        For this reason, when checking for user's existence, the server requires
        either the `uuid` of the user or its `email`.
    """
    uuid = UUIDField(unique=True)
    first_name = CharField()
    last_name = CharField()
    email = CharField(unique=True)
    password = CharField()
    admin = BooleanField(default=False)
    _schema = UserSchema

    @staticmethod
    def exists(email):
        """
        Check that an user exists by checking the email field.

        Args:
            email (str): User's email to check
        """
        try:
            User.get(User.email == email)
        except User.DoesNotExist:
            return False
        return True

    @staticmethod
    def hash_password(password):
        """
        Use passlib to get a crypted password.

        Args:
            password (str): password to hash

        Returns:
            str: hashed password
        """
        return pbkdf2_sha256.hash(password)

    def verify_password(self, password):
        """
        Verify a clear password against the stored hashed password of the user
        using passlib.

        Args:
            password (str): Password to verify against the hashed stored password
        Returns:
            bool: wether the given email matches the stored one
        """
        return pbkdf2_sha256.verify(password, self.password)

    def add_favorite(user, item):
        """Link the favorite item to user."""
        return Favorite.create(
            uuid=uuid4(),
            item=item,
            user=user,
        )

    def delete_favorite(self, obj):
        obj.delete_instance() 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_admin_user(email, psw, id=None):
    """
    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='John Admin',
        last_name='Doe',
        email=email,
        password=User.hash_password(psw),
        uuid=id or uuid.uuid4(),
        admin=True,
    ) 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 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 def open_with_auth(app, url, method, username, password, content_type, data):
    """
    Generic call to app for http request, required for requests that need
    to send a ``Basic Auth`` request to the server.
    """

    AUTH_TYPE = 'Basic'
    bytes_auth = bytes('{}:{}'.format(username, password), 'ascii')
    auth_str = '{} {}'.format(
        AUTH_TYPE, b64encode(bytes_auth).decode('ascii'))

    return app.open(url,
                    method=method,
                    headers={'Authorization': auth_str},
                    content_type=content_type,
                    data=data) 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 wrong_dump(data):
    """
    Give a wrong encoding(urlencode - like) to the given dictionary
    """
    return reduce(lambda x, y: "{}&{}".format(x, y), [
        "{}={}".format(k, v) for k, v in zip(data.keys(), data.values())])
import json
from http.client import (BAD_REQUEST, CONFLICT, CREATED, NO_CONTENT,
                         OK, UNAUTHORIZED)
from models import Address, Item, Order, User
from tests.test_case import TestCase
from tests.test_utils import (RESULTS, add_address, add_admin_user, add_user,
                              format_jsonapi_request, open_with_auth,
                              assert_valid_response, wrong_dump)
        resp = open_with_auth(self.app, API_ENDPOINT.format(user_path), 'PATCH',
                              email, TEST_USER_PSW, content_type, json.dumps(post_data))

        assert resp.status_code == OK

        expected_result = EXPECTED_RESULTS['patch_change1user_attribute__success']
        assert_valid_response(resp.data, expected_result)

        assert User.select().count() == 1
        assert User.get().admin is False
        assert User.get().first_name == 'new-first-name'

    def test_patch_change_all_user_attribute__success(self):
        email = 'mail@email.it'
        add_user(email, TEST_USER_PSW)

        post_data = format_jsonapi_request('user', {
            'first_name': 'new-first-name',
            'last_name': 'new-last-name',
            'email': 'new-email@email.it'
        })
        content_type = 'application/json'
        user_path = 'users/'
        resp = open_with_auth(self.app, API_ENDPOINT.format(user_path), 'PATCH',
                              email, TEST_USER_PSW, content_type, json.dumps(post_data))

        assert resp.status_code == OK

        expected_result = EXPECTED_RESULTS['patch_change_all_user_attribute__success']
        assert_valid_response(resp.data, expected_result)

        assert User.select().count() == 1
        assert User.get().admin is False
        assert User.get().first_name == 'new-first-name'
        assert User.get().last_name == 'new-last-name'
        assert User.get().email == 'new-email@email.it'

    def test_patch_user_other_user__fail(self):
        email = 'mail@email.it'
        add_user(email, TEST_USER_PSW)

        email2 = 'pippo@email.it'

        post_data = format_jsonapi_request('user', {'first_name': 'new-first-name'})
        content_type = 'application/json'
        user_path = 'users/'
        resp = open_with_auth(self.app, API_ENDPOINT.format(user_path), 'PATCH',
                              email2, TEST_USER_PSW, content_type, json.dumps(post_data))
        assert resp.status_code == UNAUTHORIZED

    def test_get_usersme__success(self):
        email = 'mail@email.it'
        user = add_user(email, TEST_USER_PSW)

        resp = open_with_auth(self.app, API_ENDPOINT.format('users/me/'), 'GET',
                              user.email, TEST_USER_PSW, None, None)

        assert resp.status_code == OK

        expected_result = EXPECTED_RESULTS['get_usersme__success']
        assert_valid_response(resp.data, expected_result)

        assert User.select().count() == 1
        assert User.get().admin is False
        assert User.get().email == email

    def test_get_users_otherme__success(self):
        email = 'mail@email.it'
        add_user(email, TEST_USER_PSW)

        email2 = 'pippo@email.it'

        resp = open_with_auth(self.app, API_ENDPOINT.format('users/me/'), 'GET',
                              email2, TEST_USER_PSW, None, None)

        assert resp.status_code == UNAUTHORIZED

    def test_delete_user__success(self):
        email = 'mail@email.it'
        add_user(email, TEST_USER_PSW)

        user_path = 'users/'
        resp = open_with_auth(self.app, API_ENDPOINT.format(user_path),
                              'DELETE', email, TEST_USER_PSW, None, None)

        assert resp.status_code == NO_CONTENT
        assert User.select().count() == 0

    def test_delete_user__cascade(self):
        Item.create(
            uuid='429994bf-784e-47cc-a823-e0c394b823e8',
            name='mario',
            price=20.20,
            description='svariati mariii',
            availability=1,
            category='scarpe',
        )
        Item.create(
            uuid='577ad826-a79d-41e9-a5b2-7955bcf03499',
            name='GINO',
            price=30.20,
            description='svariati GINIIIII',
            availability=1,
            category='accessori',
        )
        email = 'mail@email.it'
        user = add_user(email, TEST_USER_PSW)
        user1 = add_user('mail2@hotmail.com', TEST_USER_PSW)
        addr = add_address(user=user)
        addr1 = add_address(user=user1)
        Order.create(delivery_address=addr, user=user)
        order1 = Order.create(delivery_address=addr1, user=user1)

        user_path = 'users/'
        resp = open_with_auth(self.app, API_ENDPOINT.format(user_path), 'DELETE',
                              email, TEST_USER_PSW, None, None)
        assert resp.status_code == NO_CONTENT
        assert Address.select().count() == 1
        assert Order.select().count() == 1
        assert User.select().count() == 1

        addr2 = Address.get()