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
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
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
def now_utc(): """Fixture to return a datetime now instance with timezone. :returns: datetime.now with timezone :rtype: datetime.datetime """ return datetime_utcnow()
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)
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'
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
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()
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
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
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
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
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."""
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
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
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