Ejemplo n.º 1
0
    def default_filters(self, query) -> object:
        """Default filters to be applied to every query.

        This is supposed to be specialized by resource classes.
        :returns: A tuple of default filters to be applied to queries.
        """
        user = self.request.user
        model = self.model
        custom_filter = self.request.params.get('_custom_filter')
        if 'g:customers' in user.groups and custom_filter == 'customer_deliveries':
            query = query.filter(
                or_(
                    and_(model.state.in_(('accepted', 'refused', 'in_qa')),
                         model.accept_date.isnot(None)),
                    model.state == 'delivered'))
        elif custom_filter == 'late_first_submission':
            config_delta = timedelta(seconds=int(LATE_SUBMISSION_SECONDS))
            date_limit = datetime_utcnow() - config_delta
            query = query.filter(
                model.assignments.any(
                    and_(
                        Assignment.state == 'awaiting_assets',
                        Assignment.scheduled_datetime <= date_limit,
                        Assignment.last_approval_date.is_(None),
                    )))
        elif custom_filter == 'late_re_submission':
            config_delta = timedelta(seconds=int(LATE_SUBMISSION_SECONDS))
            date_limit = datetime_utcnow() - config_delta
            query = query.filter(
                model.assignments.any(
                    and_(Assignment.state == 'awaiting_assets',
                         Assignment.last_approval_date <= date_limit,
                         Assignment.submission_path.isnot(None))))
        return query
Ejemplo n.º 2
0
    def default_filters(self, query) -> object:
        """Default filters to be applied to every query.

        This is supposed to be specialized by resource classes.
        :returns: A tuple of default filters to be applied to queries.
        """
        user = self.request.user
        model = self.model
        custom_filter = self.request.params.get('_custom_filter')
        if 'g:professionals' in user.groups and custom_filter == 'pool':
            # disable security for this custom filter
            self.enable_security = False
            professional = Professional.get(user.id)
            pool_ids = [item.id for item in professional.pools]
            query = query.filter(model.pool_id.in_(pool_ids),
                                 model.state == 'published')
        elif custom_filter == 'late_first_submission':
            config_delta = timedelta(seconds=int(LATE_SUBMISSION_SECONDS))
            date_limit = datetime_utcnow() - config_delta
            query = query.filter(model.scheduled_datetime <= date_limit,
                                 model.state == 'awaiting_assets',
                                 model.last_approval_date.is_(None))
        elif custom_filter == 'late_re_submission':
            config_delta = timedelta(seconds=int(LATE_SUBMISSION_SECONDS))
            date_limit = datetime_utcnow() - config_delta
            query = query.filter(
                model.last_approval_date <= date_limit,
                model.state == 'awaiting_assets',
                model.submission_path.isnot(None),
            )
        return query
Ejemplo n.º 3
0
    def validate_requirement_items(self, key: str,
                                   values: t.Sequence) -> t.Sequence:
        """Validate if requirement_items payload is in the correct format.

        :param key: Attribute name.
        :param values: Requirement items payload.
        :return: Validated payload.
        """
        request = self.request
        user_id = str(request.user.id) if request else None
        current_value = list(
            self.requirement_items) if self.requirement_items else []
        if values:
            for item in values:
                if not item.get('created_by') and user_id:
                    item['created_by'] = user_id
                if not item.get('created_at'):
                    item['created_at'] = datetime_utcnow().isoformat()

        if values or current_value:
            requirements_schema = RequirementItems()
            try:
                values = requirements_schema.deserialize(values)
            except colander.Invalid as exc:
                raise ValidationError(
                    message='Invalid payload for requirement_items', name=key)

        return values
Ejemplo n.º 4
0
def now_utc():
    """Fixture to return a datetime now instance with timezone.

    :returns: datetime.now with timezone
    :rtype: datetime.datetime
    """
    return datetime_utcnow()
Ejemplo n.º 5
0
 def _update_history(
         self,
         transition: str,
         state_from: str,
         state_to: str,
         actor: UUID_TYPE='',
         message: str=''
 ):
     now = datetime_utcnow().isoformat()
     document = self.document
     context = self.context
     if context and not actor:
         actor = self._get_context_actor(context)
     key = self.history_key
     history = self._get_history_value_inner(document)
     entry = {
         'from': state_from,
         'to': state_to,
         'date': now,
         'actor': str(actor),
         'transition': transition,
         'message': message
     }
     history.append(entry)
     history = self._process_history(history)
     self._safe_set(document, key, history, False)
Ejemplo n.º 6
0
    def test_workflow_self_assign(self, instance_obj, web_request, roles,
                                  role_name, origin_state):
        """Test Assignment workflow self_assign transition."""
        assignment, wf, request = self.prepare_obj_wf(instance_obj,
                                                      web_request,
                                                      roles[role_name],
                                                      origin_state)
        order = assignment.order
        order.state = 'received'
        order.availability = [
            '2027-10-12T12:00:00+00:00',
            '2027-10-13T12:00:00+00:00',
        ]

        with pytest.raises(WorkflowTransitionException) as excinfo:
            wf.self_assign()
        assert 'Field scheduled_datetime is required for this transition.' in str(
            excinfo)

        fields_payload = {
            'scheduled_datetime': datetime_utcnow() + timedelta(10),
            'professional_id': '23d94a43-3947-42fc-958c-09245ecca5f2'
        }
        wf.self_assign(fields=fields_payload)

        assert assignment.state == 'scheduled'
Ejemplo n.º 7
0
    def test_workflow_reschedule(self, instance_obj, web_request, roles,
                                 role_name, origin_state):
        """Test Assignment workflow reschedule transition."""
        now = datetime_utcnow()
        assignment, wf, request = self.prepare_obj_wf(instance_obj,
                                                      web_request,
                                                      roles[role_name],
                                                      origin_state)
        order = assignment.order
        order.state = 'scheduled'

        if role_name == 'professional':
            scheduled_datetime = now
            with pytest.raises(WorkflowTransitionException) as excinfo:
                wf.reschedule(
                    fields={'scheduled_datetime': scheduled_datetime})

            assert 'Shoot time should be at least one day in the future' in str(
                excinfo)

        scheduled_datetime = now + timedelta(10)
        wf.reschedule(fields={'scheduled_datetime': scheduled_datetime})
        assert assignment.state == 'scheduled'
        assert assignment.scheduled_datetime == scheduled_datetime
        assert order.scheduled_datetime == scheduled_datetime
Ejemplo n.º 8
0
    def __init__(self, guid: str, data: dict, actor: str, request_id: str):
        """Initialize the event.

        :param guid: ID of the object or data on this event
        :param data: Dictionary containing the payload to be used on the event message
        :param actor: The id of the user triggering this event.
        :param request_id: ID of the request triggering this event
        """
        self.data = data
        self.actor = actor if actor else ''
        self.id = str(uuid4())
        self.guid = guid
        self.request_id = request_id if request_id else ''
        self.created_at = datetime_utcnow()
Ejemplo n.º 9
0
def _move_order_accepted(order: Order) -> bool:
    """Move Order from delivered to accepted after a certain amount of working days.

    Task name: leica.task.order_accepted
    Task events:

        * leica.task.order_accepted.success
        * leica.task.order_accepted.failure

    :param order: Order to be processed.
    :return: Status of the transition
    """
    task_name = 'leica.task.order_accepted'
    status = False
    state = order.state
    last_deliver_date = order.last_deliver_date
    if state == 'delivered' and last_deliver_date:
        now = datetime_utcnow()
        project = order.project
        approval_window = project.approval_window
        allowed_accept_date = workday(last_deliver_date, approval_window)
        wf = order.workflow
        wf.context = SystemUser

        if now >= allowed_accept_date:
            if not wf.can_accept:
                assignment_states = [a.state for a in order.assignments]
                msg = 'Approval window for Order {id} expired but it can not be completed. ' \
                      'Assignment states: {states}'
                msg = msg.format(id=order.id, states=str(assignment_states))
            else:
                wf.accept(
                    message=
                    'Order automatic accepted after the end of the approval window.'
                )
                msg = 'Order {id} moved to completed. Delivery date: {deliver_date}'
                msg = msg.format(id=order.id, deliver_date=last_deliver_date)
                status = True

            # Trigger task event
            cache_region.invalidate(order)
            for assignment in order.assignments:
                cache_region.invalidate(assignment)
            event = LeicaTaskEvent(task_name=task_name,
                                   success=status,
                                   obj=order)
            event()
            logger.info(msg)
    return status
Ejemplo n.º 10
0
    def test_workflow_cancel_from_scheduled(self, instance_obj, web_request,
                                            roles, role_name, origin_state):
        """Test Assignment workflow cancel transition from scheduled."""
        now = datetime_utcnow()
        assignment, wf, request = self.prepare_obj_wf(instance_obj,
                                                      web_request,
                                                      roles[role_name],
                                                      origin_state)
        # Will not be available if in the past
        scheduled_datetime = now - timedelta(1)
        assignment.scheduled_datetime = scheduled_datetime
        assert 'cancel' not in wf.transitions

        # Will not be available if in the past
        scheduled_datetime = now + timedelta(10)
        assignment.scheduled_datetime = scheduled_datetime
        assert 'cancel' in wf.transitions
Ejemplo n.º 11
0
    def can_cancel(self):
        """Permission: Validate if user can move the Order to the cancelled state.

        Groups: g:pm, g:customers, r:project_manager, r:customer_user
        """
        order = self.document
        project = order.project
        assignment = order.assignments[-1] if order.assignments else None
        user = self.context
        allowed = True
        uploaded = False

        if assignment:
            uploaded = True if assignment.submission_path else False
            if G['customers'].value in user.groups:
                now = datetime_utcnow()
                scheduled_datetime = assignment.scheduled_datetime
                if scheduled_datetime:
                    date_diff = scheduled_datetime - now
                    if date_diff.days <= project.cancellation_window:
                        allowed = False

        return allowed and not uploaded
Ejemplo n.º 12
0
    def test_workflow_ready_for_upload(self, instance_obj, web_request, roles,
                                       role_name, origin_state):
        """Test Assignment workflow ready_for_upload transition from scheduled.

        System is now able to move the assignment to awaiting for assets
        """
        now = datetime_utcnow()
        assignment, wf, request = self.prepare_obj_wf(instance_obj,
                                                      web_request,
                                                      roles[role_name],
                                                      origin_state)
        assignment.scheduled_datetime = now + timedelta(1)
        assert 'ready_for_upload' in wf.transitions
        with pytest.raises(WorkflowTransitionException) as excinfo:
            wf.ready_for_upload()
        assert 'Scheduled date time needs to be in the past.' in str(excinfo)

        # Set scheduled date time to be in the past
        assignment.scheduled_datetime = now - timedelta(1)
        wf.ready_for_upload()
        assert assignment.state == 'awaiting_assets'
        scheduled_datetime = now + timedelta(10)
        assignment.scheduled_datetime = scheduled_datetime
Ejemplo n.º 13
0
class Schema(colander.MappingSchema):
    """Payload for the assigment queue."""

    id = colander.SchemaNode(colander.String(), validator=colander.uuid)
    """message id"""

    created_at = colander.SchemaNode(colander.DateTime(),
                                     validator=null_validator,
                                     missing=datetime_utcnow())
    """Event creation time"""

    actor = colander.SchemaNode(colander.String(),
                                validator=null_validator,
                                missing=None)
    """Actor of this message"""

    data = colander.SchemaNode(Dictionary())
    """Payload -- for interpolation -- to be used on the email."""

    guid = colander.SchemaNode(colander.String(), validator=colander.uuid)
    """GUID for the event."""

    event_name = colander.SchemaNode(colander.String(), validator=EventName)
    """Event name."""
Ejemplo n.º 14
0
class TestLeadOrderView(BaseVersionedTestView):
    """Test LeadOrderService view."""

    base_path = '/orders'
    dependencies = [
        (models.Professional, 'data/professionals.json'),
        (models.Customer, 'data/customers.json'),
        (models.Project, 'data/projects.json'),
        (models.CustomerUserProfile, 'data/customer_profiles.json'),
        (models.InternalUserProfile, 'data/internal_profiles.json'),
        (models.LeadOrder, 'data/leadorders.json')
    ]
    serialize_attrs = [
        'path', '_roles', '_actors', 'customer', 'project', 'timezone', 'assignment',
        'assignments', 'requirement_items'
    ]
    ignore_validation_fields = [
        'state_history', 'state', 'location', 'requirements', 'number_required_assets'
    ]
    file_path = 'data/leadorders.json'
    model = models.LeadOrder
    initial_wf_state = 'new'
    UPDATE_SUCCESS_MESSAGE = ''
    NOT_FOUND_MESSAGE = ''
    update_map = {
        'title': 'New LeadOrder Title changed!',
        'price': 10000,
        'price_currency': 'USD',
        'availability': [
            (datetime_utcnow() + timedelta(days=14)).isoformat(),
            (datetime_utcnow() + timedelta(days=21)).isoformat()
        ]
    }

    def test_unsuccessful_creation(self, obj_payload, app):
        """Test unsuccessful creation of a new model."""
        payload = obj_payload
        payload['id'] = 'e93b5902-c15e-4b47-8e01-a93df6ea7211'
        # Use a project that does not allow creation of new orders
        payload['project_id'] = '4a068a1b-3646-4acf-937d-15563853e388'
        request = app.post_json(self.base_path, payload, headers=self.headers, status=403)

        assert request.status_code == 403
        result = request.json
        assert result['status'] == 'error'
        assert result['message'] == 'Unauthorized'

    def test_creation_disabled(self, obj_payload, app):
        """Test unsuccessful creation of a new model as creation is disabled."""
        payload = obj_payload.copy()
        del(payload['availability'])
        payload['id'] = 'e93b5902-c15e-4b47-8e01-a93df6ea7211'
        method = 'briefy.leica.views.order.order_creation_enabled'
        with mock.patch(method) as mock_creation:
            mock_creation.return_value = False
            request = app.post_json(self.base_path, payload, headers=self.headers, status=403)

        assert request.status_code == 403
        result = request.json
        assert result['status'] == 'error'
        assert result['message'] == 'Unauthorized'

    @pytest.mark.parametrize('file_path, position', [('data/leadorders.json', 0)])
    @pytest.mark.parametrize('filter_payload, total', LISTING_FILTERS_PAYLOADS)
    def test_collection_get_with_filters(self, app, get_file_payload, filter_payload, total):
        """Test collection_get endpoint with special filters."""
        leadorder_payload = get_file_payload
        location_payload = leadorder_payload['location']
        location_payload.pop('id')

        # create new order location instance
        models.OrderLocation.create(location_payload)

        base_path = self.get_base_path_with_query_str(filter_payload)
        request = app.get(base_path, headers=self.headers, status=200)
        result = request.json
        assert 'data' in result
        assert 'total' in result
        assert result['total'] == len(result['data']) == total
Ejemplo n.º 15
0
class TestOrderView(BaseVersionedTestView):
    """Test OrderService view."""

    base_path = '/orders'
    dependencies = [
        (models.Professional, 'data/professionals.json'),
        (models.Customer, 'data/customers.json'),
        (models.Project, 'data/projects.json'),
        (models.Order, 'data/orders.json'),
    ]
    serialize_attrs = [
        'path', '_roles', '_actors', 'customer', 'project', 'timezone',
        'assignment', 'assignments', 'requirement_items'
    ]
    ignore_validation_fields = [
        'state_history', 'state', 'location', 'requirements',
        'number_required_assets'
    ]
    file_path = 'data/orders.json'
    model = models.Order
    initial_wf_state = 'received'
    UPDATE_SUCCESS_MESSAGE = ''
    NOT_FOUND_MESSAGE = ''
    update_map = {
        'title':
        'New Order Title changed!',
        'price':
        10000,
        'price_currency':
        'USD',
        'availability': [(datetime_utcnow() + timedelta(days=14)).isoformat(),
                         (datetime_utcnow() + timedelta(days=21)).isoformat()]
    }

    def test_get_collection_custom_filter_customer_deliveries(
            self, app, login_as_customer):
        """Test get a collection of items using custom filters."""
        user_payload, token = login_as_customer
        headers = self.headers
        headers['Authorization'] = f'JWT {token}'
        request = app.get('{base}?_custom_filter=customer_deliveries'.format(
            base=self.base_path),
                          headers=headers,
                          status=200)
        result = request.json
        assert 'data' in result
        assert 'total' in result
        assert result['total'] == len(result['data'])

    def test_get_collection_custom_filter_late_first_submission(self, app):
        """Test get a collection of items using custom filters."""
        request = app.get('{base}?_custom_filter=late_first_submission'.format(
            base=self.base_path),
                          headers=self.headers,
                          status=200)
        result = request.json
        assert 'data' in result
        assert 'total' in result
        assert result['total'] == len(result['data'])

    def test_get_collection_custom_filter_late_re_submission(self, app):
        """Test get a collection of items using custom filters."""
        request = app.get('{base}?_custom_filter=late_re_submission'.format(
            base=self.base_path),
                          headers=self.headers,
                          status=200)
        result = request.json
        assert 'data' in result
        assert 'total' in result
        assert result['total'] == len(result['data'])

    def test_put_invalid_additional_charges(self, app, obj_payload):
        """Deal with invalid values sent to additional_charges."""
        payload = obj_payload.copy()
        del (payload['availability'])
        obj_id = payload.pop('id')
        payload['additional_charges'] = [{
            'category':
            'wrong',
            'amount':
            1200,
            'reason':
            '',
            'created_by':
            '669a99c2-9bb3-443f-8891-e600a15e3c10'
        }]
        request = app.put_json('{base}/{id}'.format(base=self.base_path,
                                                    id=obj_id),
                               payload,
                               headers=self.headers,
                               status=400)
        result = request.json
        error = result['errors'][0]
        assert result['status'] == 'error'
        assert error['name'] == 'additional_charges'
        assert error['location'] == 'body'
        assert 'Invalid payload for additional_charges' in error['description']

    def test_put_valid_additional_charges(self, app, obj_payload):
        """Updating additional_charges should also update total_order_price."""
        payload = obj_payload.copy()
        del (payload['availability'])
        obj_id = payload.pop('id')
        payload['additional_charges'] = [{
            'category': 'other',
            'amount': 12000,
            'reason': 'A good reason',
            'created_by': '669a99c2-9bb3-443f-8891-e600a15e3c10',
            'invoice_number': '1DEF1',
            'invoice_date': '2017-07-21'
        }]
        request = app.put_json('{base}/{id}'.format(base=self.base_path,
                                                    id=obj_id),
                               payload,
                               headers=self.headers,
                               status=200)
        result = request.json
        additional_charges = result['additional_charges']
        assert len(additional_charges) == 1
        assert additional_charges[0]['created_at'].endswith('+00:00')
        assert additional_charges[0]['id'] != ''
        assert additional_charges[0]['amount'] == 12000
        assert additional_charges[0]['category'] == 'other'
        assert result[
            'total_order_price'] == result['actual_order_price'] + 12000

    def test_put_invalid_additional_charges_by_deleting(
            self, app, obj_payload):
        """It should not be possible to remove an invoiced charge."""
        payload = obj_payload.copy()
        del (payload['availability'])
        obj_id = payload.pop('id')
        payload['additional_charges'] = []
        request = app.put_json('{base}/{id}'.format(base=self.base_path,
                                                    id=obj_id),
                               payload,
                               headers=self.headers,
                               status=400)
        result = request.json
        error = result['errors'][0]
        assert result['status'] == 'error'
        assert error['name'] == 'additional_charges'
        assert error['location'] == 'body'
        assert 'Not possible to delete an already invoiced item.' in error[
            'description']

    def test_put_invalid_asset_type(self, app, obj_payload):
        """Asset type should match one of the possible values."""
        payload = obj_payload.copy()
        del (payload['availability'])
        obj_id = payload.pop('id')
        payload['asset_types'] = ['Foobar']
        request = app.put_json('{base}/{id}'.format(base=self.base_path,
                                                    id=obj_id),
                               payload,
                               headers=self.headers,
                               status=400)
        result = request.json
        error = result['errors'][0]
        assert result['status'] == 'error'
        assert error['name'] == 'asset_types'
        assert error['location'] == 'body'
        assert 'Invalid type of asset' in error['description']

    def test_put_invalid_number_of_asset_types(self, app, obj_payload):
        """Asset type supports only 1 item."""
        payload = obj_payload.copy()
        del (payload['availability'])
        obj_id = payload['id']
        payload['asset_types'] = ['Image', 'Matterport']
        request = app.put_json('{base}/{id}'.format(base=self.base_path,
                                                    id=obj_id),
                               payload,
                               headers=self.headers,
                               status=400)
        result = request.json
        error = result['errors'][0]
        assert result['status'] == 'error'
        assert error['name'] == 'asset_types'
        assert error['location'] == 'body'
        assert 'Invalid number of type of assets' in error['description']

    def test_unsuccessful_creation(self, obj_payload, app):
        """Test unsuccessful creation of a new model."""
        payload = obj_payload.copy()
        del (payload['availability'])
        payload['id'] = 'e93b5902-c15e-4b47-8e01-a93df6ea7211'
        # Use a project that does not allow creation of new orders
        payload['project_id'] = '4a068a1b-3646-4acf-937d-15563853e388'
        request = app.post_json(self.base_path,
                                payload,
                                headers=self.headers,
                                status=403)

        assert request.status_code == 403
        result = request.json
        assert result['status'] == 'error'
        assert result['message'] == 'Unauthorized'

    def test_creation_disabled(self, obj_payload, app):
        """Test unsuccessful creation of a new model as creation is disabled."""
        payload = obj_payload.copy()
        del (payload['availability'])
        payload['id'] = 'e93b5902-c15e-4b47-8e01-a93df6ea7211'
        method = 'briefy.leica.views.order.order_creation_enabled'
        with mock.patch(method) as mock_creation:
            mock_creation.return_value = False
            request = app.post_json(self.base_path,
                                    payload,
                                    headers=self.headers,
                                    status=403)

        assert request.status_code == 403
        result = request.json
        assert result['status'] == 'error'
        assert result['message'] == 'Unauthorized'

    @pytest.mark.parametrize('file_path, position', [('data/orders.json', 0)])
    @pytest.mark.parametrize('filter_payload, total', LISTING_FILTERS_PAYLOADS)
    def test_collection_get_with_filters(self, app, get_file_payload,
                                         filter_payload, total):
        """Test collection_get endpoint with special filters."""
        order_payload = get_file_payload
        location_payload = order_payload['location']
        location_payload.pop('id')
        # create new order location instance
        models.OrderLocation.create(location_payload)

        base_path = self.get_base_path_with_query_str(filter_payload)
        request = app.get(base_path, headers=self.headers, status=200)
        result = request.json
        assert 'data' in result
        assert 'total' in result
        assert result['total'] == len(result['data']) == total
Ejemplo n.º 16
0
class TestAssignmentView(BaseVersionedTestView):
    """Test AssignmentService view."""

    base_path = '/assignments'
    dependencies = [
        (models.Professional, 'data/professionals.json'),
        (models.Customer, 'data/customers.json'),
        (models.Project, 'data/projects.json'),
        (models.Order, 'data/orders.json'),
        (models.Assignment, 'data/assignments.json'),
    ]
    serialize_attrs = [
        'path', '_roles', '_actors', 'order', 'customer', 'project',
        'timezone', 'professional', 'location'
    ]
    file_path = 'data/assignments.json'
    model = models.Assignment
    initial_wf_state = 'pending'
    check_versions_field = 'updated_at'
    UPDATE_SUCCESS_MESSAGE = ''
    NOT_FOUND_MESSAGE = ''
    update_map = {
        'updated_at':
        (datetime_utcnow() + datetime.timedelta(minutes=10)).isoformat(),
        'payable':
        False,
        'travel_expenses':
        1000,
        'payout_currency':
        'USD'
    }

    def test_get_collection_no_access_to_customer(self, app,
                                                  login_as_customer):
        """Test get a collection of Assignments as a customer."""
        user_payload, token = login_as_customer
        headers = self.headers
        headers['Authorization'] = f'JWT {token}'
        request = app.get('{base}'.format(base=self.base_path),
                          headers=headers,
                          status=403)
        result = request.json
        assert result['status'] == 'error'
        assert result['message'] == 'Unauthorized'

    def test_get_collection_custom_filter_pool(self, app,
                                               login_as_professional):
        """Test get a collection of items using custom filters."""
        user_payload, token = login_as_professional
        headers = self.headers
        headers['Authorization'] = f'JWT {token}'
        request = app.get(
            '{base}?_custom_filter=pool'.format(base=self.base_path),
            headers=headers,
            status=200)
        result = request.json
        assert 'data' in result
        assert 'total' in result
        assert result['total'] == len(result['data'])

    def test_get_collection_custom_filter_late_first_submission(self, app):
        """Test get a collection of items using custom filters."""
        request = app.get('{base}?_custom_filter=late_first_submission'.format(
            base=self.base_path),
                          headers=self.headers,
                          status=200)
        result = request.json
        assert 'data' in result
        assert 'total' in result
        assert result['total'] == len(result['data'])

    def test_get_collection_custom_filter_late_re_submission(self, app):
        """Test get a collection of items using custom filters."""
        request = app.get('{base}?_custom_filter=late_re_submission'.format(
            base=self.base_path),
                          headers=self.headers,
                          status=200)
        result = request.json
        assert 'data' in result
        assert 'total' in result
        assert result['total'] == len(result['data'])

    def test_put_invalid_asset_type(self, app, obj_payload):
        """Asset type should match one of the possible values."""
        payload = obj_payload
        obj_id = payload['id']
        payload['asset_types'] = ['Foobar']
        request = app.put_json('{base}/{id}'.format(base=self.base_path,
                                                    id=obj_id),
                               payload,
                               headers=self.headers,
                               status=400)
        result = request.json
        error = result['errors'][0]
        assert result['status'] == 'error'
        assert error['name'] == 'asset_types'
        assert error['location'] == 'body'
        assert 'Invalid type of asset' in error['description']

    def test_put_invalid_number_of_asset_types(self, app, obj_payload):
        """Asset type supports only 1 item."""
        payload = obj_payload
        obj_id = payload['id']
        payload['asset_types'] = ['Image', 'Matterport']
        request = app.put_json('{base}/{id}'.format(base=self.base_path,
                                                    id=obj_id),
                               payload,
                               headers=self.headers,
                               status=400)
        result = request.json
        error = result['errors'][0]
        assert result['status'] == 'error'
        assert error['name'] == 'asset_types'
        assert error['location'] == 'body'
        assert 'Invalid number of type of assets' in error['description']

    def test_workflow(self, app, instance_obj):
        """Test workflow endpoints."""
        payload = {
            'id':
            '264b3e66-c327-4bbd-9cc7-271716fce178',
            'professional_id':
            '23d94a43-3947-42fc-958c-09245ecca5f2',
            'owner':
            '23d94a43-3947-42fc-958c-09245ecca5f2',
            'uploaded_by':
            'f5c2199f-6ed7-4ff8-90df-d1a98249f5e7',
            'description':
            '',
            'filename':
            '2345.jpg',
            'source_path':
            'source/files/assignments/2345.jpg',
            'state':
            'pending',
            'width':
            5760,
            'height':
            3840,
            'content_type':
            'image/jpeg',
            'state_history': [{
                'actor': '',
                'date': '2016-09-28T20:08:37.217221+00:00',
                'from': 'created',
                'message': 'Imported in this state from Knack database',
                'to': 'validation',
                'transition': 'submit'
            }, {
                'actor': '',
                'date': '2016-09-28T20:08:37.217221+00:00',
                'from': 'validation',
                'message': 'Correct dimensions',
                'to': 'pending',
                'transition': 'validate'
            }],
            'size':
            4049867,
            'assignment_id':
            'c04dc102-7d3b-4574-a261-4bf72db571db',
            'title':
            'IMAGE01'
        }
        # Create the object using a new transaction
        with transaction.manager:
            models.Image.create(payload)

        obj_id = instance_obj.id
        state = instance_obj.state
        assert state == 'pending'

        # Endpoints
        endpoint = '{base}/{id}/transitions'.format(base=self.base_path,
                                                    id=obj_id)

        # List available transitions
        request = app.get(endpoint, headers=self.headers, status=200)
        result = request.json
        assert result['total'] == 4
        assert 'cancel' in result['transitions']
        assert 'publish' in result['transitions']
        assert 'assign' in result['transitions']

    @pytest.mark.parametrize('file_path, position', [('data/orders.json', 0)])
    @pytest.mark.parametrize('filter_payload, total', LISTING_FILTERS_PAYLOADS)
    def test_collection_get_with_filters(self, app, get_file_payload,
                                         filter_payload, total):
        """Test collection_get endpoint with special filters."""
        order_payload = get_file_payload
        location_payload = order_payload['location']
        location_payload.pop('id')
        # create new order location instance
        models.OrderLocation.create(location_payload)

        base_path = self.get_base_path_with_query_str(filter_payload)
        request = app.get(base_path, headers=self.headers, status=200)
        result = request.json
        assert 'data' in result
        assert 'total' in result
        assert result['total'] == len(result['data']) == total