def _notify_consent_service(self, validated_data): """ Trigger the update_contact_consent task with the current version of `validated_data`. The actual enqueuing of the task happens in the on_commit hook so it won't actually notify the consent service unless the database transaction was successful. """ if 'accepts_dit_email_marketing' not in validated_data: # If no consent value in request body return # Remove the accepts_dit_email_marketing from validated_data accepts_dit_email_marketing = validated_data.pop( 'accepts_dit_email_marketing') # If consent value in POST, notify combiner = DataCombiner(self.instance, validated_data) request = self.context.get('request', None) transaction.on_commit( lambda: update_contact_consent.apply_async( args=( combiner.get_value('email'), accepts_dit_email_marketing, ), kwargs={ 'modified_at': now().isoformat(), 'zipkin_headers': get_zipkin_headers(request), }, ), )
def validate(self, data): """ Performs cross-field validation after individual fields have been validated. Ensures that either a person or company name has been provided, as well as an email address or phone number. """ errors = {} data_combiner = DataCombiner(self.instance, data) company_name = data_combiner.get_value('company_name') trading_name = data_combiner.get_value('trading_name') company = data_combiner.get_value('company') first_name = data_combiner.get_value('first_name') last_name = data_combiner.get_value('last_name') telephone_number = data_combiner.get_value('telephone_number') email = data_combiner.get_value('email') has_company_name = any((company_name, company, trading_name)) has_contact_name = first_name and last_name if not (has_company_name or has_contact_name): errors['company_name'] = NAME_REQUIRED_MESSAGE errors['first_name'] = NAME_REQUIRED_MESSAGE errors['last_name'] = NAME_REQUIRED_MESSAGE if not (email or telephone_number): errors['telephone_number'] = CONTACT_REQUIRED_MESSAGE errors['email'] = CONTACT_REQUIRED_MESSAGE if errors: raise serializers.ValidationError(errors) return data
def __call__(self, data=None, order=None): """ Check that: - vat_status is specified - if vat_status == eu: - vat_verified is specified - if vat_verified == True: - vat_number is specified """ data_combiner = DataCombiner(order, data) vat_status = data_combiner.get_value('vat_status') if not vat_status: raise ValidationError({ 'vat_status': [self.message], }) if vat_status == VATStatus.eu: vat_verified = data_combiner.get_value('vat_verified') if vat_verified is None: raise ValidationError({ 'vat_verified': [self.message], }) vat_number = data_combiner.get_value('vat_number') if vat_verified and not vat_number: raise ValidationError({ 'vat_number': [self.message], })
def validate(instance=None, update_data=None, fields=None, next_stage=False): """Validates an investment project for the current stage. :param instance: Model instance (for update operations only) :param update_data: New data to update or create the instance with :param fields: Fields to restrict validation to :param next_stage: Perform validation for the next stage (rather than the current stage) :return: dict containing errors for incomplete fields """ combiner = DataCombiner(instance, update_data, model=InvestmentProject) desired_stage = combiner.get_value('stage') or Stage.prospect.value desired_stage_order = _get_desired_stage_order(desired_stage, next_stage) errors = {} for field, req_stage in (InvestmentProjectStageValidationConfig. get_required_fields_after_stage().items()): if _should_skip_rule(field, fields, desired_stage_order, req_stage.order): continue if field_incomplete(combiner, field): errors[field] = REQUIRED_MESSAGE for field, rule in (InvestmentProjectStageValidationConfig. get_conditional_rules_after_stage().items()): if _should_skip_rule(field, fields, desired_stage_order, rule.stage.order): continue if _check_rule(combiner, rule) and field_incomplete(combiner, field): errors[field] = REQUIRED_MESSAGE return errors
def __call__(self, data=None, order=None): """Validate that all the fields required are set.""" data_combiner = DataCombiner(order, data) meta = order._meta errors = defaultdict(list) # direct required fields for field_name in self.REQUIRED_FIELDS: field = meta.get_field(field_name) if isinstance(field, models.ManyToManyField): value = data_combiner.get_value_to_many(field_name) else: value = data_combiner.get_value(field_name) if not value: errors[field_name] = [self.message] # extra validators extra_errors = self._run_extra_validators(data, order) for field, field_errors in extra_errors.items(): errors[field] += field_errors if errors: raise ValidationError(errors)
def __call__(self, data, serializer): """ Performs validation. TODO: this method has do be simplified once `company` field is removed. """ instance = serializer.instance company_has_changed = not instance or ( 'company' in data and data['company'] != instance.company) companies_have_changed = not instance or ('companies' in data and set( data['companies']) != set(instance.companies.all())) contacts_have_changed = not instance or ('contacts' in data and set( data['contacts']) != set(instance.contacts.all())) if not (company_has_changed or companies_have_changed or contacts_have_changed): return combiner = DataCombiner(instance, data) company = combiner.get_value('company') if company_has_changed and company: companies = (company, ) else: companies = combiner.get_value_to_many('companies') contacts = combiner.get_value_to_many('contacts') if any(contact.company not in companies for contact in contacts): raise serializers.ValidationError( 'The interaction contacts must belong to the specified company.', code='inconsistent_contacts_and_company', )
def test_get_value_id_instance(self): """Tests getting a foreign key from an instance.""" subinstance = Mock() subinstance.id = 1234 instance = Mock(field1=subinstance) data_combiner = DataCombiner(instance, None) assert data_combiner.get_value_id('field1') == str(subinstance.id)
def get_incomplete_fields(instance, fields): """Returns a list of fields that are incomplete.""" combiner = DataCombiner(instance, {}, model=InvestorProfile) incomplete_fields = [] for field in fields: if field_incomplete(combiner, field): incomplete_fields.append(field) return incomplete_fields
def test_get_value_id_value(self): """Tests getting a foreign key from update data.""" subinstance = Mock() subinstance.id = 1234 new_subinstance = Mock() new_subinstance.id = 456 instance = Mock(field1=subinstance) data_combiner = DataCombiner(instance, {'field1': new_subinstance}) assert data_combiner.get_value_id('field1') == str(new_subinstance.id)
def _validate_theme(self, data): """Make sure that a theme is not unset once it has been set for an interaction.""" combiner = DataCombiner(self.instance, data) if self.instance and self.instance.theme and not combiner.get_value('theme'): error = { 'theme': [ self.error_messages['cannot_unset_theme'], ], } raise serializers.ValidationError(error, code='cannot_unset_theme')
def __call__(self, data): """Validate that contact works at company.""" data_combiner = DataCombiner(self.instance, data) company = data_combiner.get_value(self.company_field) contact = data_combiner.get_value(self.contact_field) if contact.company != company: raise ValidationError({ self.contact_field: self.message, })
def test_is_field_to_many_for_normal_field(self, monkeypatch): """Tests that test_is_field_to_many() returns False for a normal field.""" instance = Mock() model = Mock() mock_field_info = Mock(relations={}) monkeypatch.setattr( validate_utils, '_get_model_field_info', Mock(return_value=mock_field_info), ) data_combiner = DataCombiner(instance, None, model=model) assert not data_combiner.is_field_to_many('field1')
def _reset_vat_fields_if_necessary(self, data): """If vat_status is set and != 'eu', vat_number and vat_verified are reset.""" data_combiner = DataCombiner(self.instance, data) vat_status = data_combiner.get_value('vat_status') if vat_status and vat_status != VATStatus.eu: data['vat_number'] = '' data['vat_verified'] = None return data
def validate(self, data): """ Performs cross-field validation and adds extra fields to data. """ data = super().validate(data) combiner = DataCombiner(self.instance, data) self._populate_address_fields(combiner, data) return data
def test_get_value_auto_normal_field(self, monkeypatch): """Tests that get_value_auto() returns value for a normal field.""" instance = Mock(field1=123) model = Mock() mock_field_info = Mock(relations={}) monkeypatch.setattr( validate_utils, '_get_model_field_info', Mock(return_value=mock_field_info), ) data_combiner = DataCombiner(instance, None, model=model) assert data_combiner.get_value_auto('field1') == 123 assert data_combiner['field1'] == 123
def __call__(self, data): """Validate editable fields depending on the order status.""" if not self.instance or self.instance.status not in self.mapping: return combiner = DataCombiner(self.instance, data, model=self.instance.__class__) editable_fields = self.mapping[self.instance.status] for field in combiner.data: if field not in editable_fields and self._has_changed(field, combiner): raise ValidationError({field: self.message})
def __call__(self, data, serializer): """Validate that contact works at company.""" instance = getattr(serializer, 'instance', None) data_combiner = DataCombiner(instance, data) company = data_combiner.get_value(self.company_field) contact = data_combiner.get_value(self.contact_field) if contact.company != company: raise ValidationError({ self.contact_field: self.message, })
def validate(self, data): """ Performs cross-field validation and adds extra fields to data. """ combiner = DataCombiner(self.instance, data) if {'global_headquarters', 'headquarter_type'} & data.keys(): headquarter_type_id = combiner.get_value_id('headquarter_type') global_headquarters_id = combiner.get_value_id( 'global_headquarters') if (headquarter_type_id is not None and UUID(headquarter_type_id) == UUID(HeadquarterType.ghq.value.id) and global_headquarters_id is not None): message = self.error_messages[ 'subsidiary_cannot_be_a_global_headquarters'] raise serializers.ValidationError({ 'headquarter_type': message, }) combiner = DataCombiner(self.instance, data) return data
def test_is_field_to_many_for_to_many_field(self, monkeypatch): """Tests that test_is_field_to_many() returns True for a to-many field.""" instance = Mock() model = Mock() mock_field_info = Mock( relations={ 'field1': Mock(to_many=True), }, ) monkeypatch.setattr( validate_utils, '_get_model_field_info', Mock(return_value=mock_field_info), ) data_combiner = DataCombiner(instance, None, model=model) assert data_combiner.is_field_to_many('field1')
def test_get_value_auto_to_many(self, monkeypatch): """Tests that get_value_auto() returns a list-like object for a to-many field.""" instance = Mock(field1=MagicMock()) instance.field1.all.return_value = [123] model = Mock() mock_field_info = Mock( relations={'field1': Mock(to_many=True)}, ) monkeypatch.setattr( validate_utils, '_get_model_field_info', Mock(return_value=mock_field_info), ) data_combiner = DataCombiner(instance, None, model=model) assert data_combiner.get_value_auto('field1') == [123] assert data_combiner['field1'] == [123]
def test_get_value_auto_instance(self, monkeypatch): """Tests that get_value_auto() returns the ID for a foreign key.""" subinstance = Mock() subinstance.id = 1234 instance = Mock(field1=subinstance) model = Mock() mock_field_info = Mock( relations={'field1': Mock(to_many=False)}, ) monkeypatch.setattr( validate_utils, '_get_model_field_info', Mock(return_value=mock_field_info), ) data_combiner = DataCombiner(instance, None, model=model) assert data_combiner.get_value_auto('field1') == str(subinstance.id) assert data_combiner['field1'] == str(subinstance.id)
def __call__(self, data): """ Performs validation. """ if not self.instance: return existing_interaction_complete = self.instance.status == Interaction.STATUSES.complete combiner = DataCombiner(self.instance, data) new_status = combiner.get_value('status') update_changes_status = new_status != self.instance.status if existing_interaction_complete and update_changes_status: raise ValidationError( 'The status of a complete interaction cannot change.', code='complete_interaction_status_cannot_change', )
def __call__(self, data): """Validate the address fields.""" data_combiner = DataCombiner(self.instance, data) data_combined = { field_name: data_combiner.get_value(field_name) for field_name in self.fields_mapping.keys() } if not self._should_validate(data_combined): return errors = self._validate_fields(data_combined) if errors: raise ValidationError(errors)
def validate(self, attrs): """Performs cross-field validation.""" attrs = super().validate(attrs) errors = {} combiner = DataCombiner(self.instance, attrs) validators = (self._validate_related_trade_agreements, ) for validator in validators: errors.update(validator(combiner)) if errors: raise serializers.ValidationError(errors) return attrs
def _has_changed(self, field, combiner): """ :returns: True if the data value for `field` has changed compared to its instance value. """ field_value = combiner.get_value_auto(field) instance_value = DataCombiner( self.instance, {}, model=self.instance.__class__, ).get_value_auto(field) # if it's a queryset, evaluate it if hasattr(instance_value, 'all'): instance_value = list(instance_value) return field_value != instance_value
def __call__(self, data, serializer): """ Performs validation. """ instance = serializer.instance if not instance: return existing_interaction_complete = instance.status == Interaction.Status.COMPLETE combiner = DataCombiner(instance, data) new_status = combiner.get_value('status') update_changes_status = new_status != instance.status if existing_interaction_complete and update_changes_status: raise serializers.ValidationError( 'The status of a complete interaction cannot change.', code='complete_interaction_status_cannot_change', )
def __call__(self, data, serializer): """Performs validation.""" data_combiner = DataCombiner(serializer.instance, data) service = data_combiner.get_value('service') service_answers = data_combiner.get_value('service_answers') expected_questions = { str(question.pk): question for question in service.interaction_questions.all() } if service else {} self._validate_type_and_truthiness(expected_questions, service_answers) if service_answers is not None: self._validate_questions(expected_questions, service_answers)
def _update_status(self, data): """Updates the project status when the stage changes to or from Won.""" old_stage = self.instance.stage if self.instance else None new_stage = data.get('stage') if not new_stage or new_stage == old_stage: return combiner = DataCombiner(instance=self.instance, update_data=data) new_status = combiner.get_value('status') if str(new_stage.id) == InvestmentProjectStage.won.value.id: data['status'] = InvestmentProject.STATUSES.won elif (old_stage and str(old_stage.id) == InvestmentProjectStage.won.value.id and new_status == InvestmentProject.STATUSES.won): data['status'] = InvestmentProject.STATUSES.ongoing
def validate(self, data): """Performs cross-field validation.""" errors = {} combiner = DataCombiner(self.instance, data) validators = ( self._validate_lead_team, self._validate_dates, self._validate_uk_region, ) for validator in validators: errors.update(validator(combiner)) if errors: raise serializers.ValidationError(errors) return data
def _get_updated_status(instance, data): """Updates the project status when the stage changes to or from Won.""" old_stage = instance.stage if instance else None new_stage = data.get('stage') if not new_stage or new_stage == old_stage: return None combiner = DataCombiner(instance=instance, update_data=data) new_status = combiner.get_value('status') if str(new_stage.id) == InvestmentProjectStage.won.value.id: return InvestmentProject.Status.WON elif ( old_stage and str(old_stage.id) == InvestmentProjectStage.won.value.id and new_status == InvestmentProject.Status.WON ): return InvestmentProject.Status.ONGOING return None