class PropositionSerializer(serializers.ModelSerializer): """Proposition serialiser for view only endpoints.""" investment_project = NestedInvestmentProjectField() adviser = NestedAdviserField() created_by = NestedAdviserField() modified_by = NestedAdviserField() class Meta: model = Proposition fields = ( 'id', 'investment_project', 'adviser', 'deadline', 'status', 'name', 'scope', 'details', 'created_on', 'created_by', 'modified_on', 'modified_by', ) read_only_fields = fields
class InvestmentActivitySerializer(serializers.ModelSerializer): """Serializer for an Investment Activity.""" created_by = NestedAdviserField(read_only=True) modified_by = NestedAdviserField(read_only=True) text = serializers.CharField(required=True) activity_type = NestedRelatedField( InvestmentActivityType, default=default_activity_type, ) class Meta: model = InvestmentActivity fields = ( 'id', 'text', 'activity_type', 'created_by', 'created_on', 'modified_on', 'modified_by', ) read_only_fields = ( 'id', 'created_on', 'created_by', )
class EvidenceDocumentSerializer(serializers.ModelSerializer): """Serializer for Investment Project Evidence Documents.""" tags = NestedRelatedField(EvidenceTag, many=True, allow_empty=False) av_clean = serializers.BooleanField(source='document.av_clean', read_only=True) created_by = NestedAdviserField(read_only=True) status = serializers.CharField(source='document.status', read_only=True) uploaded_on = serializers.DateTimeField(source='document.uploaded_on', read_only=True) modified_by = NestedAdviserField(read_only=True) investment_project = NestedInvestmentProjectField(read_only=True) class Meta: model = EvidenceDocument fields = ( 'id', 'tags', 'comment', 'av_clean', 'created_by', 'created_on', 'modified_by', 'modified_on', 'uploaded_on', 'investment_project', 'original_filename', 'url', 'status', ) read_only_fields = ( 'url', 'created_on', ) extra_kwargs = { 'comment': { 'default': '' }, } def create(self, validated_data): """Create evidence document.""" evidence_document = EvidenceDocument.objects.create( investment_project_id=self.context['request']. parser_context['kwargs']['project_pk'], original_filename=validated_data['original_filename'], comment=validated_data.get('comment', ''), created_by=self.context['request'].user, ) evidence_document.tags.set(validated_data['tags']) return evidence_document
class InteractionDITParticipantSerializer(serializers.ModelSerializer): """ Interaction DIT participant serialiser. Used as a field in InteractionSerializer. """ adviser = NestedAdviserField() # team is read-only as it is set from the adviser when a participant is added to # an interaction team = NestedRelatedField(Team, read_only=True) @classmethod def many_init(cls, *args, **kwargs): """Initialises a many=True instance of the serialiser with a custom list serialiser.""" child = cls(context=kwargs.get('context')) return InteractionDITParticipantListSerializer(child=child, *args, **kwargs) class Meta: model = InteractionDITParticipant fields = ('adviser', 'team') # Explicitly set validator as extra protection against a unique together validator being # added. # (UniqueTogetherValidator would not function correctly when multiple items are being # updated at once.) validators = []
class PropositionDocumentSerializer(serializers.ModelSerializer): """Serializer for Investment Project Proposition Documents.""" av_clean = serializers.BooleanField(source='document.av_clean', read_only=True) created_by = NestedAdviserField(read_only=True) status = serializers.CharField(source='document.status', read_only=True) uploaded_on = serializers.DateTimeField(source='document.uploaded_on', read_only=True) class Meta: model = PropositionDocument fields = ( 'id', 'av_clean', 'created_by', 'created_on', 'uploaded_on', 'original_filename', 'url', 'status', ) read_only_fields = ('url', 'created_on') def create(self, validated_data): """Create proposition document.""" return PropositionDocument.objects.create( proposition_id=self.context['request'].parser_context['kwargs'] ['proposition_pk'], original_filename=validated_data['original_filename'], created_by=self.context['request'].user, )
class QuoteSerializer(serializers.ModelSerializer): """Quote DRF serializer.""" created_by = NestedAdviserField(read_only=True) cancelled_by = NestedAdviserField(read_only=True) accepted_by = NestedRelatedField(Contact, read_only=True) terms_and_conditions = serializers.CharField( source='terms_and_conditions.content', default='', ) def preview(self): """Same as create but without saving the changes.""" self.instance = self.create(self.validated_data, commit=False) return self.instance def cancel(self): """Call `order.reopen` to cancel this quote.""" order = self.context['order'] current_user = self.context['current_user'] order.reopen(by=current_user) self.instance = order.quote return self.instance def create(self, validated_data, commit=True): """Call `order.generate_quote` instead of creating the object directly.""" order = self.context['order'] current_user = self.context['current_user'] order.generate_quote(by=current_user, commit=commit) return order.quote class Meta: model = Quote fields = [ 'created_on', 'created_by', 'cancelled_on', 'cancelled_by', 'accepted_on', 'accepted_by', 'expires_on', 'content', 'terms_and_conditions', ] read_only_fields = fields
class NestedIProjectTeamMemberSerializer(serializers.ModelSerializer): """Serialiser for investment project team members when nested in the main investment project object. Used to exclude the investment project from the serialised representation. """ adviser = NestedAdviserField() class Meta: model = InvestmentProjectTeamMember fields = ('adviser', 'role')
class IProjectTeamMemberSerializer(serializers.ModelSerializer): """Serialiser for investment project team members.""" investment_project = NestedRelatedField(InvestmentProject) adviser = NestedAdviserField() @classmethod def many_init(cls, *args, **kwargs): """ Initialises a many-item instance of the serialiser using custom logic. This disables the unique together validator in the child serialiser, as it's incompatible with many-item update operations (as it mistakenly fails existing rows). """ child = cls(context=kwargs.get('context'), validators=()) return IProjectTeamMemberListSerializer(child=child, *args, **kwargs) class Meta: model = InvestmentProjectTeamMember fields = ('investment_project', 'adviser', 'role')
class EventSerializer(serializers.ModelSerializer): """Event serialiser.""" default_error_messages = { 'lead_team_not_in_teams': ugettext_lazy('Lead team must be in teams array.'), 'end_date_before_start_date': ugettext_lazy('End date cannot be before start date.'), 'uk_region_non_uk_country': ugettext_lazy('Cannot specify a UK region for a non-UK country.', ), } end_date = serializers.DateField() event_type = NestedRelatedField('event.EventType') location_type = NestedRelatedField('event.LocationType', required=False, allow_null=True) organiser = NestedAdviserField(required=False, allow_null=True) lead_team = NestedRelatedField('metadata.Team') teams = NestedRelatedField('metadata.Team', many=True, allow_empty=False) address_country = NestedRelatedField('metadata.Country') uk_region = NestedRelatedField('metadata.UKRegion', required=False, allow_null=True) related_programmes = NestedRelatedField( 'event.Programme', many=True, required=False, allow_empty=True, ) service = NestedRelatedField('metadata.Service') start_date = serializers.DateField() 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 _validate_lead_team(self, combiner): errors = {} lead_team = combiner.get_value('lead_team') teams = combiner.get_value_to_many('teams') if lead_team not in teams: errors['lead_team'] = self.error_messages['lead_team_not_in_teams'] return errors def _validate_dates(self, combiner): errors = {} start_date = combiner.get_value('start_date') end_date = combiner.get_value('end_date') if start_date and end_date and end_date < start_date: errors['end_date'] = self.error_messages[ 'end_date_before_start_date'] return errors def _validate_uk_region(self, combiner): errors = {} address_country_id = combiner.get_value_id('address_country') uk_region = combiner.get_value('uk_region') if address_country_id is None: return errors is_uk = address_country_id == Country.united_kingdom.value.id if is_uk and not uk_region: errors['uk_region'] = self.error_messages['required'] elif not is_uk and uk_region: errors['uk_region'] = self.error_messages[ 'uk_region_non_uk_country'] return errors class Meta: model = Event fields = ( 'address_1', 'address_2', 'address_country', 'address_country', 'address_county', 'address_postcode', 'address_town', 'archived_documents_url_path', 'disabled_on', 'end_date', 'event_type', 'id', 'lead_team', 'location_type', 'name', 'notes', 'organiser', 'related_programmes', 'start_date', 'teams', 'service', 'uk_region', ) read_only_fields = ( 'archived_documents_url_path', 'disabled_on', )
class InteractionSerializer(serializers.ModelSerializer): """V3 interaction serialiser.""" default_error_messages = { 'invalid_for_non_service_delivery': ugettext_lazy('This field is only valid for service deliveries.', ), 'invalid_for_service_delivery': ugettext_lazy('This field is not valid for service deliveries.', ), 'invalid_for_non_interaction': ugettext_lazy('This field is only valid for interactions.', ), 'invalid_for_non_interaction_or_service_delivery': ugettext_lazy( 'This value is only valid for interactions and service deliveries.', ), 'invalid_for_non_event': ugettext_lazy( 'This field is only valid for event service deliveries.', ), 'invalid_when_no_policy_feedback': ugettext_lazy( 'This field is only valid when policy feedback has been provided.', ), 'too_many_contacts_for_event_service_delivery': ugettext_lazy( 'Only one contact can be provided for event service deliveries.', ), 'one_participant_field': ugettext_lazy( 'If dit_participants is provided, dit_adviser and dit_team must be omitted.', ), 'cannot_unset_theme': ugettext_lazy("A theme can't be removed once set.", ), } company = NestedRelatedField(Company) contacts = NestedRelatedField( Contact, many=True, allow_empty=False, extra_fields=( 'name', 'first_name', 'last_name', 'job_title', ), ) created_by = NestedAdviserField(read_only=True) archived_by = NestedAdviserField(read_only=True) # dit_adviser has been replaced by dit_participants but is retained for temporary backwards # compatibility # TODO: Remove following deprecation period dit_adviser = NestedAdviserField(required=False) # TODO: Remove required=False once dit_adviser and dit_team have been removed dit_participants = InteractionDITParticipantSerializer( many=True, allow_empty=False, required=False, ) # dit_team has been replaced by dit_participants but is retained for temporary backwards # compatibility # TODO: Remove following deprecation period dit_team = NestedRelatedField(Team, required=False) communication_channel = NestedRelatedField( CommunicationChannel, required=False, allow_null=True, ) is_event = serializers.BooleanField(required=False, allow_null=True) event = NestedRelatedField(Event, required=False, allow_null=True) investment_project = NestedInvestmentProjectField(required=False, allow_null=True) modified_by = NestedAdviserField(read_only=True) service = NestedRelatedField(Service, required=False, allow_null=True) service_delivery_status = NestedRelatedField( ServiceDeliveryStatus, required=False, allow_null=True, ) policy_areas = NestedRelatedField(PolicyArea, many=True, required=False, allow_empty=True) policy_issue_types = NestedRelatedField( PolicyIssueType, allow_empty=True, many=True, required=False, ) def validate(self, data): """ Validates and cleans the data. This removes the semi-virtual field is_event from the data. This is removed because the value is not stored; it is instead inferred from contents of the the event field during serialisation. It also: - checks that if `dit_participants` has been provided, `dit_adviser` or `dit_team` haven't also been provided - copies the first value in `contacts` to `contact` - copies the first value in `dit_participants` to `dit_adviser` and `dit_team` """ self._validate_theme(data) has_dit_adviser_or_dit_team = {'dit_adviser', 'dit_team'} & data.keys() has_dit_participants = 'dit_participants' in data if has_dit_adviser_or_dit_team and has_dit_participants: error = { api_settings.NON_FIELD_ERRORS_KEY: [ self.error_messages['one_participant_field'], ], } raise serializers.ValidationError(error, code='one_participant_field') if 'is_event' in data: del data['is_event'] # If dit_participants has been provided, this copies the first participant to # dit_adviser and dit_team (for backwards compatibility). # TODO Remove once dit_adviser and dit_team removed from the database. if 'dit_participants' in data: first_participant = data['dit_participants'][0] data['dit_adviser'] = first_participant['adviser'] data['dit_team'] = first_participant['adviser'].dit_team # Ensure that archived=False is set for creations/updates, when the # existing instance does not have a value for it # TODO: remove this once we give archived a model-level default if not self.instance or self.instance.archived is None: data['archived'] = False return data @atomic def create(self, validated_data): """ Create an interaction. Overridden to handle updating of dit_participants. """ return self._create_or_update(validated_data) @atomic def update(self, instance, validated_data): """ Create an interaction. Overridden to handle updating of dit_participants. """ return self._create_or_update(validated_data, instance=instance, is_update=True) 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 _create_or_update(self, validated_data, instance=None, is_update=False): dit_participants = validated_data.pop('dit_participants', None) if is_update: interaction = super().update(instance, validated_data) else: interaction = super().create(validated_data) # If dit_participants has not been provided, create, update and remove participants using # the provided dit_adviser and dit_team values # TODO: Remove the 'if dit_participants is None' part once dit_adviser and dit_team have # been removed from the API. if dit_participants is None: InteractionDITParticipant.objects.update_or_create( interaction=interaction, adviser=interaction.dit_adviser, defaults={ 'team': interaction.dit_team, }, ) InteractionDITParticipant.objects.filter( interaction=interaction, ).exclude( adviser=interaction.dit_adviser, ).delete() else: self._save_dit_participants(interaction, dit_participants) return interaction def _save_dit_participants(self, interaction, validated_dit_participants): """ Updates the DIT participants for an interaction. This compares the provided list of participants with the current list, and adds and removes participants as necessary. This is based on example code in DRF documentation for ListSerializer. Note that adviser's team is also saved in the participant when a participant is added to an interaction, so that if the adviser later moves team, the interaction is still recorded against the original team. """ old_adviser_mapping = { dit_participant.adviser: dit_participant for dit_participant in interaction.dit_participants.all() } old_advisers = old_adviser_mapping.keys() new_advisers = { dit_participant['adviser'] for dit_participant in validated_dit_participants } # Create new DIT participants for adviser in new_advisers - old_advisers: InteractionDITParticipant( adviser=adviser, interaction=interaction, team=adviser.dit_team, ).save() # Delete removed DIT participants for adviser in old_advisers - new_advisers: old_adviser_mapping[adviser].delete() class Meta: model = Interaction extra_kwargs = { # Date is a datetime in the model, but only the date component is used # (at present). Setting the formats as below effectively makes the field # behave like a date field without changing the schema and breaking the # v1 API. 'date': { 'format': '%Y-%m-%d', 'input_formats': ['%Y-%m-%d'] }, 'grant_amount_offered': { 'min_value': 0 }, 'net_company_receipt': { 'min_value': 0 }, 'status': { 'default': Interaction.STATUSES.complete, 'allow_null': False }, 'location': { 'default': '' }, 'theme': { 'allow_blank': False, 'default': None, }, } fields = ( 'id', 'company', 'contacts', 'created_on', 'created_by', 'event', 'is_event', 'status', 'kind', 'modified_by', 'modified_on', 'date', 'dit_adviser', 'dit_participants', 'dit_team', 'communication_channel', 'grant_amount_offered', 'investment_project', 'net_company_receipt', 'service', 'service_delivery_status', 'subject', 'theme', 'notes', 'archived_documents_url_path', 'policy_areas', 'policy_feedback_notes', 'policy_issue_types', 'was_policy_feedback_provided', 'location', 'archived', 'archived_by', 'archived_on', 'archived_reason', ) read_only_fields = ( 'archived_documents_url_path', 'archived', 'archived_by', 'archived_on', 'archived_reason', ) validators = [ HasAssociatedInvestmentProjectValidator(), ContactsBelongToCompanyValidator(), StatusChangeValidator(), RulesBasedValidator( # If dit_adviser and dit_team are *omitted* (note that they already have # allow_null=False) we assume that dit_participants is being used, and return an # error if it is empty. # TODO: Remove once dit_adviser and dit_team have been removed. ValidationRule( 'required', OperatorRule('dit_participants', bool), when=AllIsBlankRule('dit_adviser', 'dit_team'), ), # If dit_adviser has been provided, double-check that dit_team is also set. # TODO: Remove once dit_adviser and dit_team have been removed. ValidationRule( 'required', OperatorRule('dit_adviser', bool), when=AndRule( OperatorRule('dit_team', bool), OperatorRule('dit_participants', not_), ), ), # If dit_team has been provided, double-check that dit_adviser is also set. # TODO: Remove once dit_adviser and dit_team have been removed. ValidationRule( 'required', OperatorRule('dit_team', bool), when=AndRule( OperatorRule('dit_adviser', bool), OperatorRule('dit_participants', not_), ), ), ValidationRule( 'required', OperatorRule('communication_channel', bool), when=AndRule( EqualsRule('kind', Interaction.KINDS.interaction), EqualsRule('status', Interaction.STATUSES.complete), ), ), ValidationRule( 'required', OperatorRule('service', bool), when=EqualsRule('status', Interaction.STATUSES.complete), ), ValidationRule( 'invalid_for_non_interaction', OperatorRule('investment_project', not_), when=EqualsRule('kind', Interaction.KINDS.service_delivery), ), ValidationRule( 'invalid_for_service_delivery', OperatorRule('communication_channel', not_), when=EqualsRule('kind', Interaction.KINDS.service_delivery), ), ValidationRule( 'invalid_for_non_service_delivery', OperatorRule('is_event', is_blank), OperatorRule('event', is_blank), OperatorRule('service_delivery_status', is_blank), OperatorRule('grant_amount_offered', is_blank), OperatorRule('net_company_receipt', is_blank), when=EqualsRule('kind', Interaction.KINDS.interaction), ), ValidationRule( 'invalid_when_no_policy_feedback', OperatorRule('policy_issue_types', not_), OperatorRule('policy_areas', not_), OperatorRule('policy_feedback_notes', not_), when=OperatorRule('was_policy_feedback_provided', not_), ), ValidationRule( 'required', OperatorRule('policy_areas', bool), OperatorRule('policy_issue_types', bool), OperatorRule('policy_feedback_notes', is_not_blank), when=OperatorRule('was_policy_feedback_provided', bool), ), ValidationRule( 'required', OperatorRule('is_event', is_not_blank), when=EqualsRule('kind', Interaction.KINDS.service_delivery), ), ValidationRule( 'required', OperatorRule('event', bool), when=OperatorRule('is_event', bool), ), ValidationRule( 'too_many_contacts_for_event_service_delivery', OperatorRule('contacts', lambda value: len(value) <= 1), when=OperatorRule('is_event', bool), ), ValidationRule( 'invalid_for_non_event', OperatorRule('event', not_), when=OperatorRule('is_event', not_), ), ), ]
class BaseInteractionSerializer(serializers.ModelSerializer): """ Interaction serialiser. Note that interactions can also be created and/or modified by: - the standard admin site functionality - the import interactions tool in the admin site - the calendar meeting invite processing tool If you're making changes to interaction functionality you should consider if any changes are required to the functionality listed above as well. Also note that the import interactions tool also uses the validators from this class, the calendar meeting invite processing tool uses the serializer as a whole to create interactions. """ default_error_messages = { 'invalid_for_investment': gettext_lazy( "This value can't be selected for investment interactions.", ), 'invalid_for_non_trade_agreement': gettext_lazy( 'This field is only valid for trade agreement interactions.', ), 'invalid_for_non_service_delivery': gettext_lazy('This field is only valid for service deliveries.', ), 'invalid_for_service_delivery': gettext_lazy('This field is not valid for service deliveries.', ), 'invalid_for_non_interaction': gettext_lazy('This field is only valid for interactions.', ), 'invalid_for_non_interaction_or_service_delivery': gettext_lazy( 'This value is only valid for interactions and service deliveries.', ), 'invalid_for_non_event': gettext_lazy( 'This field is only valid for event service deliveries.', ), 'invalid_when_no_policy_feedback': gettext_lazy( 'This field is only valid when policy feedback has been provided.', ), 'invalid_when_no_related_trade_agreement': gettext_lazy( 'This field is only valid when there are related trade agreements.', ), 'too_many_contacts_for_event_service_delivery': gettext_lazy( 'Only one contact can be provided for event service deliveries.', ), 'cannot_unset_theme': gettext_lazy("A theme can't be removed once set.", ), 'invalid_when_no_countries_discussed': gettext_lazy( 'This field is only valid when countries were discussed.', ), 'invalid_for_update': gettext_lazy('This field is invalid for interaction updates.', ), } INVALID_FOR_UPDATE = gettext_lazy( 'This field is invalid for interaction updates.', ) INVALID_COMPANY_OR_COMPANIES = gettext_lazy( 'Only either a company or companies can be provided.', ) company = NestedRelatedField(Company, required=False) companies = NestedRelatedField( Company, many=True, allow_empty=True, # TODO: the field has to be required, once `company` is removed. required=False, ) contacts = NestedRelatedField( Contact, many=True, allow_empty=False, extra_fields=( 'name', 'first_name', 'last_name', 'job_title', ), ) created_by = NestedAdviserField(read_only=True) archived_by = NestedAdviserField(read_only=True) dit_participants = InteractionDITParticipantSerializer( many=True, allow_empty=False, ) communication_channel = NestedRelatedField( CommunicationChannel, required=False, allow_null=True, ) is_event = serializers.BooleanField(required=False, allow_null=True) event = NestedRelatedField(Event, required=False, allow_null=True) investment_project = NestedInvestmentProjectField(required=False, allow_null=True) large_capital_opportunity = NestedRelatedField( LargeCapitalOpportunity, required=False, allow_null=True, ) modified_by = NestedAdviserField(read_only=True) service = NestedRelatedField(Service, required=False, allow_null=True) service_answers = serializers.JSONField(required=False) service_delivery_status = NestedRelatedField( ServiceDeliveryStatus, required=False, allow_null=True, ) policy_areas = NestedRelatedField(PolicyArea, many=True, required=False, allow_empty=True) policy_issue_types = NestedRelatedField( PolicyIssueType, allow_empty=True, many=True, required=False, ) export_countries = InteractionExportCountrySerializer( allow_empty=True, many=True, required=False, ) company_referral = NestedCompanyReferralDetail(read_only=True) def validate_service(self, value): """Make sure only a service without children can be assigned.""" if value and value.children.count() > 0: raise serializers.ValidationError( SERVICE_LEAF_NODE_NOT_SELECTED_MESSAGE) return value def validate_were_countries_discussed(self, were_countries_discussed): """ Make sure `were_countries_discussed` field is not being updated. Updates are not allowed on this field. """ if self.instance is None: return were_countries_discussed raise serializers.ValidationError(self.INVALID_FOR_UPDATE) def validate_export_countries(self, export_countries): """ Make sure `export_countries` field is not being updated. updates are not allowed on this field. """ if self.instance is None: return export_countries raise serializers.ValidationError(self.INVALID_FOR_UPDATE) def to_internal_value(self, data): """ Add support for both `company` and `companies` field. TODO: this method should be removed once `company` field is removed. """ if 'company' in data and 'companies' in data: raise serializers.ValidationError( { NON_FIELD_ERRORS: [self.INVALID_COMPANY_OR_COMPANIES], }, ) if 'company' not in data and 'companies' not in data: self.fields['company'].required = True return super().to_internal_value(data) def validate(self, data): """ Validates and cleans the data. This removes the semi-virtual field is_event from the data. This is removed because the value is not stored; it is instead inferred from contents of the the event field during serialisation. It copies company field to companies and other way around. """ self._validate_theme(data) if 'is_event' in data: del data['is_event'] # Ensure that archived=False is set for creations/updates, when the # existing instance does not have a value for it # TODO: remove this once we give archived a model-level default if not self.instance or self.instance.archived is None: data['archived'] = False # TODO: this should be removed when `company` field is removed. if 'company' in data and data.get('company'): data['companies'] = [data['company']] elif 'companies' in data and len(data.get('companies') or []) > 0: data['company'] = data['companies'][0] return data @atomic def create(self, validated_data): """ Create an interaction. Overridden to handle updating of dit_participants and export_countries. """ dit_participants = validated_data.pop('dit_participants') export_countries = validated_data.pop('export_countries', []) interaction = super().create(validated_data) self._save_dit_participants(interaction, dit_participants) self._save_export_countries(interaction, export_countries) return interaction @atomic def update(self, instance, validated_data): """ Create an interaction. Overridden to handle updating of dit_participants and export_countries. """ dit_participants = validated_data.pop('dit_participants', None) export_countries = validated_data.pop('export_countries', None) interaction = super().update(instance, validated_data) # For PATCH requests, dit_participants may not be being updated if dit_participants is not None: self._save_dit_participants(interaction, dit_participants) if export_countries is not None: self._save_export_countries(interaction, export_countries) return interaction 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 _save_dit_participants(self, interaction, validated_dit_participants): """ Updates the DIT participants for an interaction. This compares the provided list of participants with the current list, and adds and removes participants as necessary. This is based on example code in DRF documentation for ListSerializer. Note that adviser's team is also saved in the participant when a participant is added to an interaction, so that if the adviser later moves team, the interaction is still recorded against the original team. """ old_adviser_mapping = { dit_participant.adviser: dit_participant for dit_participant in interaction.dit_participants.all() } old_advisers = old_adviser_mapping.keys() new_advisers = { dit_participant['adviser'] for dit_participant in validated_dit_participants } # Create new DIT participants for adviser in new_advisers - old_advisers: InteractionDITParticipant( adviser=adviser, interaction=interaction, team=adviser.dit_team, ).save() # Delete removed DIT participants for adviser in old_advisers - new_advisers: old_adviser_mapping[adviser].delete() def _save_export_countries(self, interaction, validated_export_countries): """ Adds export countries related to an interaction. Update is not allowed yet. An attempt to update will result in `NotImplementedError` exception. Syncs interaction export countries into company export countries. """ existing_country_mapping = { export_country.country: export_country for export_country in interaction.export_countries.all() } new_country_mapping = { item['country']: item for item in validated_export_countries } for new_country, export_data in new_country_mapping.items(): status = export_data['status'] if new_country in existing_country_mapping: # TODO: updates are not supported yet raise NotImplementedError() InteractionExportCountry.objects.create( country=new_country, interaction=interaction, status=status, created_by=interaction.created_by, ) # Sync company_CompanyExportCountry model # NOTE: current date is preferred over future interaction date current_date = now() record_date = current_date if interaction.date > current_date else interaction.date interaction.company.add_export_country( new_country, status, record_date, interaction.created_by, )
class BaseEventSerializer(serializers.ModelSerializer): """Common functionality between V3 and V4 endpoint""" default_error_messages = { 'lead_team_not_in_teams': gettext_lazy('Lead team must be in teams array.'), 'end_date_before_start_date': gettext_lazy('End date cannot be before start date.'), 'uk_region_non_uk_country': gettext_lazy('Cannot specify a UK region for a non-UK country.', ), } end_date = serializers.DateField() event_type = NestedRelatedField('event.EventType') location_type = NestedRelatedField('event.LocationType', required=False, allow_null=True) organiser = NestedAdviserField() lead_team = NestedRelatedField('metadata.Team') teams = NestedRelatedField('metadata.Team', many=True, allow_empty=False) address_country = NestedRelatedField('metadata.Country') uk_region = NestedRelatedField('metadata.UKRegion', required=False, allow_null=True) related_programmes = NestedRelatedField( 'event.Programme', many=True, required=False, allow_empty=True, ) service = NestedRelatedField('metadata.Service') start_date = serializers.DateField() def validate_service(self, value): """Make sure only a service without children can be assigned.""" if value and value.children.count() > 0: raise serializers.ValidationError( SERVICE_LEAF_NODE_NOT_SELECTED_MESSAGE) return value 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 _validate_lead_team(self, combiner): errors = {} lead_team = combiner.get_value('lead_team') teams = combiner.get_value_to_many('teams') if lead_team not in teams: errors['lead_team'] = self.error_messages['lead_team_not_in_teams'] return errors def _validate_dates(self, combiner): errors = {} start_date = combiner.get_value('start_date') end_date = combiner.get_value('end_date') if start_date and end_date and end_date < start_date: errors['end_date'] = self.error_messages[ 'end_date_before_start_date'] return errors def _validate_uk_region(self, combiner): errors = {} address_country_id = combiner.get_value_id('address_country') uk_region = combiner.get_value('uk_region') if address_country_id is None: return errors is_uk = address_country_id == Country.united_kingdom.value.id if is_uk and not uk_region: errors['uk_region'] = self.error_messages['required'] elif not is_uk and uk_region: errors['uk_region'] = self.error_messages[ 'uk_region_non_uk_country'] return errors
class OrderAssigneeSerializer(serializers.ModelSerializer): """DRF serializer for an adviser assigned to an order.""" adviser = NestedAdviserField(required=True, allow_null=False) estimated_time = serializers.IntegerField(required=False, min_value=0) actual_time = serializers.IntegerField(required=False, allow_null=True, min_value=0) is_lead = serializers.BooleanField(required=False) default_error_messages = { 'readonly': gettext_lazy('This field cannot be changed at this stage.', ), } class Meta: list_serializer_class = OrderAssigneeListSerializer model = OrderAssignee fields = [ 'adviser', 'estimated_time', 'actual_time', 'is_lead', ] validators = ( RulesBasedValidator( # can't be changed when in draft, quote_awaiting_acceptance or quote_accepted ValidationRule( 'readonly', OperatorRule('actual_time', is_blank), when=OrderInStatusRule([ OrderStatus.draft, OrderStatus.quote_awaiting_acceptance, OrderStatus.quote_accepted, ], ), ), # can't be changed when in quote_awaiting_acceptance, quote_accepted or paid ValidationRule( 'readonly', OperatorRule('estimated_time', is_blank), when=OrderInStatusRule([ OrderStatus.quote_awaiting_acceptance, OrderStatus.quote_accepted, OrderStatus.paid, ], ), ), # can't be changed when in quote_awaiting_acceptance, quote_accepted or paid ValidationRule( 'readonly', OperatorRule('is_lead', is_blank), when=OrderInStatusRule([ OrderStatus.quote_awaiting_acceptance, OrderStatus.quote_accepted, OrderStatus.paid, ], ), ), ), ) def delete(self, instance): """Deletes the instance.""" instance.delete()
class BusinessLeadSerializer(serializers.ModelSerializer): """Business lead serialiser.""" company = NestedRelatedField( Company, required=False, allow_null=True, ) address_country = NestedRelatedField( meta_models.Country, required=False, allow_null=True, ) archived = serializers.BooleanField(read_only=True) archived_on = serializers.DateTimeField(read_only=True) archived_reason = serializers.CharField(read_only=True) archived_by = NestedAdviserField(read_only=True) created_by = NestedAdviserField(read_only=True) modified_by = NestedAdviserField(read_only=True) 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 class Meta: model = BusinessLead fields = ( 'id', 'first_name', 'last_name', 'job_title', 'company_name', 'trading_name', 'company', 'telephone_number', 'email', 'address_1', 'address_2', 'address_town', 'address_county', 'address_country', 'address_postcode', 'telephone_alternative', 'email_alternative', 'contactable_by_dit', 'contactable_by_uk_dit_partners', 'contactable_by_overseas_dit_partners', 'accepts_dit_email_marketing', 'contactable_by_email', 'contactable_by_phone', 'notes', 'archived', 'archived_on', 'archived_reason', 'archived_by', 'created_by', 'modified_by', ) extra_kwargs = { 'archived': {'read_only': True}, 'archived_on': {'read_only': True}, 'archived_reason': {'read_only': True}, 'archived_by': {'read_only': True}, }
class IProjectSerializer(PermittedFieldsModelSerializer, NoteAwareModelSerializer): """Serialiser for investment project endpoints.""" default_error_messages = { 'only_pm_or_paa_can_move_to_verify_win': ugettext_lazy( 'Only the Project Manager or Project Assurance Adviser can move the project' ' to the ‘Verify win’ stage.', ), 'only_ivt_can_move_to_won': ugettext_lazy( 'Only the Investment Verification Team can move the project to the ‘Won’ stage.', ), } incomplete_fields = serializers.SerializerMethodField() project_code = serializers.CharField(read_only=True) investment_type = NestedRelatedField(meta_models.InvestmentType) stage = NestedRelatedField(meta_models.InvestmentProjectStage, required=False) country_lost_to = NestedRelatedField(meta_models.Country, required=False, allow_null=True) country_investment_originates_from = NestedRelatedField( meta_models.Country, required=False, allow_null=True, ) investor_company = NestedRelatedField(Company, required=True, allow_null=False) investor_company_country = NestedRelatedField(meta_models.Country, read_only=True) investor_type = NestedRelatedField(InvestorType, required=False, allow_null=True) intermediate_company = NestedRelatedField(Company, required=False, allow_null=True) level_of_involvement = NestedRelatedField(Involvement, required=False, allow_null=True) likelihood_to_land = NestedRelatedField(LikelihoodToLand, required=False, allow_null=True) specific_programme = NestedRelatedField(SpecificProgramme, required=False, allow_null=True) client_contacts = NestedRelatedField( Contact, many=True, required=True, allow_null=False, allow_empty=False, ) client_relationship_manager = NestedAdviserField(required=True, allow_null=False) client_relationship_manager_team = NestedRelatedField(meta_models.Team, read_only=True) referral_source_adviser = NestedAdviserField(required=True, allow_null=False) referral_source_activity = NestedRelatedField( meta_models.ReferralSourceActivity, required=True, allow_null=False, ) referral_source_activity_website = NestedRelatedField( meta_models.ReferralSourceWebsite, required=False, allow_null=True, ) referral_source_activity_marketing = NestedRelatedField( meta_models.ReferralSourceMarketing, required=False, allow_null=True, ) fdi_type = NestedRelatedField(meta_models.FDIType, required=False, allow_null=True) sector = NestedRelatedField(meta_models.Sector, required=True, allow_null=False) business_activities = NestedRelatedField( meta_models.InvestmentBusinessActivity, many=True, required=True, allow_null=False, allow_empty=False, ) archived_by = NestedAdviserField(read_only=True) project_manager_request_status = NestedRelatedField( ProjectManagerRequestStatus, required=False, allow_null=True, ) # Value fields fdi_value = NestedRelatedField(meta_models.FDIValue, required=False, allow_null=True) average_salary = NestedRelatedField( meta_models.SalaryRange, required=False, allow_null=True, ) value_complete = serializers.SerializerMethodField() associated_non_fdi_r_and_d_project = NestedRelatedField( InvestmentProject, required=False, allow_null=True, extra_fields=('name', 'project_code'), ) # Requirements fields competitor_countries = NestedRelatedField(meta_models.Country, many=True, required=False) # Note: uk_region_locations is the possible UK regions at the start of the project (not the # actual/final UK regions at the end of the project) uk_region_locations = NestedRelatedField(meta_models.UKRegion, many=True, required=False) actual_uk_regions = NestedRelatedField(meta_models.UKRegion, many=True, required=False) delivery_partners = NestedRelatedField(InvestmentDeliveryPartner, many=True, required=False) strategic_drivers = NestedRelatedField( meta_models.InvestmentStrategicDriver, many=True, required=False, ) uk_company = NestedRelatedField(Company, required=False, allow_null=True) requirements_complete = serializers.SerializerMethodField() # Team fields project_manager = NestedAdviserField(required=False, allow_null=True) project_assurance_adviser = NestedAdviserField(required=False, allow_null=True) project_manager_team = NestedRelatedField(meta_models.Team, read_only=True) project_assurance_team = NestedRelatedField(meta_models.Team, read_only=True) team_members = NestedIProjectTeamMemberSerializer(many=True, read_only=True) team_complete = serializers.SerializerMethodField() # SPI fields project_arrived_in_triage_on = serializers.DateField(required=False, allow_null=True) proposal_deadline = serializers.DateField(required=False, allow_null=True) stage_log = NestedInvestmentProjectStageLogSerializer(many=True, read_only=True) def save(self, **kwargs): """Saves when and who assigned a project manager for the first time.""" if ('project_manager' in self.validated_data and (self.instance is None or self.instance.project_manager is None)): kwargs['project_manager_first_assigned_on'] = now() kwargs['project_manager_first_assigned_by'] = self.context[ 'current_user'] super().save(**kwargs) def validate_estimated_land_date(self, value): """Validate estimated land date.""" if value or (self.instance and self.instance.allow_blank_estimated_land_date): return value raise serializers.ValidationError(REQUIRED_MESSAGE) def validate(self, data): """Validates the object after individual fields have been validated. Performs stage-dependent validation of the different sections. When transitioning stage, all fields required for the new stage are validated. In other cases, only the fields being modified are validated. If a project ends up in an invalid state, this avoids the user being unable to rectify the situation. """ if not self.instance: # Required for validation as DRF does not allow defaults for read-only fields data['allow_blank_possible_uk_regions'] = False self._check_if_investment_project_can_be_moved_to_verify_win(data) self._check_if_investment_project_can_be_moved_to_won(data) self._validate_for_stage(data) self._update_status(data) self._track_project_manager_request(data) return data def _track_project_manager_request(self, data): """ If a project manager has been requested track the request by setting the project_manager_requested_on timestamp. """ pm_requested = ProjectManagerRequestStatusValue.requested.value.id if ('project_manager_request_status' in data and str( data['project_manager_request_status'].pk) == pm_requested and (self.instance is None or self.instance.project_manager_requested_on is None)): data['project_manager_requested_on'] = now() def _check_if_investment_project_can_be_moved_to_verify_win(self, data): # only project manager or project assurance adviser can move a project to verify win if self.instance and 'stage' in data: current_user_id = self.context['current_user'].id allowed_users_ids = ( self.instance.project_manager_id, self.instance.project_assurance_adviser_id, ) if (str(data['stage'].id) == InvestmentProjectStage.verify_win.value.id and self.instance.stage.order < data['stage'].order and current_user_id not in allowed_users_ids): errors = { 'stage': self.default_error_messages[ 'only_pm_or_paa_can_move_to_verify_win'], } raise serializers.ValidationError(errors) def _check_if_investment_project_can_be_moved_to_won(self, data): # Investment Verification Team can only move a project to won if self.instance and 'stage' in data: current_user = self.context['current_user'] permission_name = f'investment.{InvestmentProjectPermission.change_stage_to_won}' if (str(data['stage'].id) == InvestmentProjectStage.won.value.id and self.instance.stage.order < data['stage'].order and not current_user.has_perm(permission_name)): errors = { 'stage': self.default_error_messages['only_ivt_can_move_to_won'], } raise serializers.ValidationError(errors) def _validate_for_stage(self, data): fields = None if self.partial and 'stage' not in data: fields = data.keys() errors = validate(self.instance, data, fields=fields) if errors: raise serializers.ValidationError(errors) def get_incomplete_fields(self, instance): """Returns the names of the fields that still need to be completed in order to move to the next stage. """ return tuple(validate(instance=instance, next_stage=True)) def get_value_complete(self, instance): """Whether the value fields required to move to the next stage are complete.""" return not validate( instance=instance, fields=VALUE_FIELDS, next_stage=True, ) def get_requirements_complete(self, instance): """Whether the requirements fields required to move to the next stage are complete.""" return not validate( instance=instance, fields=REQUIREMENTS_FIELDS, next_stage=True, ) def get_team_complete(self, instance): """Whether the team fields required to move to the next stage are complete.""" return not validate( instance=instance, fields=TEAM_FIELDS, next_stage=True, ) 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 class Meta: model = InvestmentProject fields = ALL_FIELDS permissions = { f'investment.{InvestmentProjectPermission.view_investmentproject_document}': 'archived_documents_url_path', } read_only_fields = ( 'allow_blank_estimated_land_date', 'allow_blank_possible_uk_regions', 'archived', 'archived_on', 'archived_reason', 'archived_documents_url_path', 'comments', 'project_manager_requested_on', 'gross_value_added', )
class InteractionSerializer(serializers.ModelSerializer): """ Interaction serialiser. Note that interactions can also be created and/or modified by: - the standard admin site functionality - the import interactions tool in the admin site - the calendar meeting invite processing tool If you're making changes to interaction functionality you should consider if any changes are required to the functionality listed above as well. Also note that the import interactions tool also uses the validators from this class, the calendar meeting invite processing tool uses the serializer as a whole to create interactions. """ default_error_messages = { 'invalid_for_investment': gettext_lazy( "This value can't be selected for investment interactions.", ), 'invalid_for_non_service_delivery': gettext_lazy( 'This field is only valid for service deliveries.', ), 'invalid_for_service_delivery': gettext_lazy( 'This field is not valid for service deliveries.', ), 'invalid_for_non_interaction': gettext_lazy( 'This field is only valid for interactions.', ), 'invalid_for_non_interaction_or_service_delivery': gettext_lazy( 'This value is only valid for interactions and service deliveries.', ), 'invalid_for_non_event': gettext_lazy( 'This field is only valid for event service deliveries.', ), 'invalid_when_no_policy_feedback': gettext_lazy( 'This field is only valid when policy feedback has been provided.', ), 'too_many_contacts_for_event_service_delivery': gettext_lazy( 'Only one contact can be provided for event service deliveries.', ), 'cannot_unset_theme': gettext_lazy( "A theme can't be removed once set.", ), 'invalid_when_no_countries_discussed': gettext_lazy( 'This field is only valid when countries were discussed.', ), 'invalid_for_update': gettext_lazy( 'This field is invalid for interaction updates.', ), } INVALID_FOR_UPDATE = gettext_lazy( 'This field is invalid for interaction updates.', ) company = NestedRelatedField(Company) contacts = NestedRelatedField( Contact, many=True, allow_empty=False, extra_fields=( 'name', 'first_name', 'last_name', 'job_title', ), ) created_by = NestedAdviserField(read_only=True) archived_by = NestedAdviserField(read_only=True) dit_participants = InteractionDITParticipantSerializer( many=True, allow_empty=False, ) communication_channel = NestedRelatedField( CommunicationChannel, required=False, allow_null=True, ) is_event = serializers.BooleanField(required=False, allow_null=True) event = NestedRelatedField(Event, required=False, allow_null=True) investment_project = NestedInvestmentProjectField(required=False, allow_null=True) modified_by = NestedAdviserField(read_only=True) service = NestedRelatedField(Service, required=False, allow_null=True) service_answers = serializers.JSONField(required=False) service_delivery_status = NestedRelatedField( ServiceDeliveryStatus, required=False, allow_null=True, ) policy_areas = NestedRelatedField(PolicyArea, many=True, required=False, allow_empty=True) policy_issue_types = NestedRelatedField( PolicyIssueType, allow_empty=True, many=True, required=False, ) export_countries = InteractionExportCountrySerializer( allow_empty=True, many=True, required=False, ) company_referral = NestedCompanyReferralDetail(read_only=True) def validate_service(self, value): """Make sure only a service without children can be assigned.""" if value and value.children.count() > 0: raise serializers.ValidationError(SERVICE_LEAF_NODE_NOT_SELECTED_MESSAGE) return value def validate_were_countries_discussed(self, were_countries_discussed): """ Make sure `were_countries_discussed` field is not being updated. Updates are not allowed on this field. """ if self.instance is None: return were_countries_discussed raise serializers.ValidationError(self.INVALID_FOR_UPDATE) def validate_export_countries(self, export_countries): """ Make sure `export_countries` field is not being updated. updates are not allowed on this field. """ if self.instance is None: return export_countries raise serializers.ValidationError(self.INVALID_FOR_UPDATE) def validate(self, data): """ Validates and cleans the data. This removes the semi-virtual field is_event from the data. This is removed because the value is not stored; it is instead inferred from contents of the the event field during serialisation. """ self._validate_theme(data) if 'is_event' in data: del data['is_event'] # Ensure that archived=False is set for creations/updates, when the # existing instance does not have a value for it # TODO: remove this once we give archived a model-level default if not self.instance or self.instance.archived is None: data['archived'] = False return data @atomic def create(self, validated_data): """ Create an interaction. Overridden to handle updating of dit_participants and export_countries. """ dit_participants = validated_data.pop('dit_participants') export_countries = validated_data.pop('export_countries', []) interaction = super().create(validated_data) self._save_dit_participants(interaction, dit_participants) self._save_export_countries(interaction, export_countries) return interaction @atomic def update(self, instance, validated_data): """ Create an interaction. Overridden to handle updating of dit_participants and export_countries. """ dit_participants = validated_data.pop('dit_participants', None) export_countries = validated_data.pop('export_countries', None) interaction = super().update(instance, validated_data) # For PATCH requests, dit_participants may not be being updated if dit_participants is not None: self._save_dit_participants(interaction, dit_participants) if export_countries is not None: self._save_export_countries(interaction, export_countries) return interaction 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 _save_dit_participants(self, interaction, validated_dit_participants): """ Updates the DIT participants for an interaction. This compares the provided list of participants with the current list, and adds and removes participants as necessary. This is based on example code in DRF documentation for ListSerializer. Note that adviser's team is also saved in the participant when a participant is added to an interaction, so that if the adviser later moves team, the interaction is still recorded against the original team. """ old_adviser_mapping = { dit_participant.adviser: dit_participant for dit_participant in interaction.dit_participants.all() } old_advisers = old_adviser_mapping.keys() new_advisers = { dit_participant['adviser'] for dit_participant in validated_dit_participants } # Create new DIT participants for adviser in new_advisers - old_advisers: InteractionDITParticipant( adviser=adviser, interaction=interaction, team=adviser.dit_team, ).save() # Delete removed DIT participants for adviser in old_advisers - new_advisers: old_adviser_mapping[adviser].delete() def _save_export_countries(self, interaction, validated_export_countries): """ Adds export countries related to an interaction. Update is not allowed yet. An attempt to update will result in `NotImplementedError` exception. Syncs interaction export countries into company export countries. """ existing_country_mapping = { export_country.country: export_country for export_country in interaction.export_countries.all() } new_country_mapping = { item['country']: item for item in validated_export_countries } for new_country, export_data in new_country_mapping.items(): status = export_data['status'] if new_country in existing_country_mapping: # TODO: updates are not supported yet raise NotImplementedError() InteractionExportCountry.objects.create( country=new_country, interaction=interaction, status=status, created_by=interaction.created_by, ) # Sync company_CompanyExportCountry model # NOTE: current date is preferred over future interaction date current_date = now() record_date = current_date if interaction.date > current_date else interaction.date interaction.company.add_export_country( new_country, status, record_date, interaction.created_by, ) class Meta: model = Interaction extra_kwargs = { # Date is a datetime in the model, but only the date component is used # (at present). Setting the formats as below effectively makes the field # behave like a date field without changing the schema and breaking the # v1 API. 'date': {'format': '%Y-%m-%d', 'input_formats': ['%Y-%m-%d']}, 'grant_amount_offered': {'min_value': 0}, 'net_company_receipt': {'min_value': 0}, 'status': {'default': Interaction.Status.COMPLETE}, 'theme': { 'allow_blank': False, 'default': None, }, } fields = ( 'id', 'company', 'contacts', 'created_on', 'created_by', 'event', 'is_event', 'status', 'kind', 'modified_by', 'modified_on', 'date', 'dit_participants', 'communication_channel', 'grant_amount_offered', 'investment_project', 'net_company_receipt', 'service', 'service_answers', 'service_delivery_status', 'subject', 'theme', 'notes', 'archived_documents_url_path', 'policy_areas', 'policy_feedback_notes', 'policy_issue_types', 'was_policy_feedback_provided', 'were_countries_discussed', 'export_countries', 'archived', 'archived_by', 'archived_on', 'archived_reason', 'company_referral', ) read_only_fields = ( 'archived_documents_url_path', 'archived', 'archived_by', 'archived_on', 'archived_reason', ) # Note: These validators are also used by the admin site import interactions tool # (see the admin_csv_import sub-package) validators = [ HasAssociatedInvestmentProjectValidator(), ContactsBelongToCompanyValidator(), StatusChangeValidator(), ServiceAnswersValidator(), DuplicateExportCountryValidator(), RulesBasedValidator( ValidationRule( 'required', OperatorRule('communication_channel', bool), when=AndRule( EqualsRule('kind', Interaction.Kind.INTERACTION), EqualsRule('status', Interaction.Status.COMPLETE), ), ), ValidationRule( 'required', OperatorRule('service', bool), when=EqualsRule('status', Interaction.Status.COMPLETE), ), ValidationRule( 'invalid_for_investment', EqualsRule('kind', Interaction.Kind.INTERACTION), when=EqualsRule('theme', Interaction.Theme.INVESTMENT), ), ValidationRule( 'invalid_for_non_interaction', OperatorRule('investment_project', not_), when=EqualsRule('kind', Interaction.Kind.SERVICE_DELIVERY), ), ValidationRule( 'invalid_for_service_delivery', OperatorRule('communication_channel', not_), when=EqualsRule('kind', Interaction.Kind.SERVICE_DELIVERY), ), ValidationRule( 'invalid_for_non_service_delivery', OperatorRule('is_event', is_blank), OperatorRule('event', is_blank), OperatorRule('service_delivery_status', is_blank), when=EqualsRule('kind', Interaction.Kind.INTERACTION), ), ValidationRule( 'invalid_when_no_policy_feedback', OperatorRule('policy_issue_types', not_), OperatorRule('policy_areas', not_), OperatorRule('policy_feedback_notes', not_), when=OperatorRule('was_policy_feedback_provided', not_), ), ValidationRule( 'required', OperatorRule('policy_areas', bool), OperatorRule('policy_issue_types', bool), OperatorRule('policy_feedback_notes', is_not_blank), when=OperatorRule('was_policy_feedback_provided', bool), ), ValidationRule( 'required', OperatorRule('is_event', is_not_blank), when=EqualsRule('kind', Interaction.Kind.SERVICE_DELIVERY), ), ValidationRule( 'too_many_contacts_for_event_service_delivery', OperatorRule('contacts', lambda value: len(value) <= 1), when=OperatorRule('is_event', bool), ), ValidationRule( 'invalid_for_investment', OperatorRule('were_countries_discussed', not_), OperatorRule('export_countries', not_), when=EqualsRule('theme', Interaction.Theme.INVESTMENT), ), ValidationRule( 'required', OperatorRule('were_countries_discussed', is_not_blank), when=AndRule( IsObjectBeingCreated(), InRule( 'theme', [Interaction.Theme.EXPORT, Interaction.Theme.OTHER], ), ), ), ValidationRule( 'required', OperatorRule('export_countries', is_not_blank), when=AndRule( OperatorRule('were_countries_discussed', bool), InRule( 'theme', [Interaction.Theme.EXPORT, Interaction.Theme.OTHER], ), ), ), ValidationRule( 'invalid_when_no_countries_discussed', OperatorRule('export_countries', is_blank), when=AndRule( IsObjectBeingCreated(), OperatorRule('were_countries_discussed', not_), InRule( 'theme', [Interaction.Theme.EXPORT, Interaction.Theme.OTHER], ), ), ), # These two rules are only checked for service deliveries as there's a separate # check that event is blank for interactions above which takes precedence (to # avoid duplicate or contradictory error messages) ValidationRule( 'required', OperatorRule('event', bool), when=AndRule( OperatorRule('is_event', bool), EqualsRule('kind', Interaction.Kind.SERVICE_DELIVERY), ), ), ValidationRule( 'invalid_for_non_event', OperatorRule('event', not_), when=AndRule( OperatorRule('is_event', not_), EqualsRule('kind', Interaction.Kind.SERVICE_DELIVERY), ), ), ), ]
class InteractionSerializer(serializers.ModelSerializer): """V3 interaction serialiser.""" default_error_messages = { 'invalid_for_non_service_delivery': ugettext_lazy('This field is only valid for service deliveries.', ), 'invalid_for_service_delivery': ugettext_lazy('This field is not valid for service deliveries.', ), 'invalid_for_non_interaction': ugettext_lazy('This field is only valid for interactions.', ), 'invalid_for_non_event': ugettext_lazy( 'This field is only valid for event service deliveries.', ), 'invalid_for_non_policy_feedback': ugettext_lazy('This field is only valid for policy feedback.', ), 'one_policy_area_field': ugettext_lazy( 'Only one of policy_area and policy_areas should be provided.', ), } company = NestedRelatedField(Company) contact = NestedRelatedField( Contact, extra_fields=( 'name', 'first_name', 'last_name', 'job_title', ), ) dit_adviser = NestedAdviserField() created_by = NestedAdviserField(read_only=True) dit_team = NestedRelatedField(Team) communication_channel = NestedRelatedField( CommunicationChannel, required=False, allow_null=True, ) is_event = serializers.NullBooleanField(required=False) event = NestedRelatedField(Event, required=False, allow_null=True) investment_project = NestedInvestmentProjectField(required=False, allow_null=True) modified_by = NestedAdviserField(read_only=True) service = NestedRelatedField(Service) service_delivery_status = NestedRelatedField( ServiceDeliveryStatus, required=False, allow_null=True, ) policy_areas = NestedRelatedField(PolicyArea, many=True, required=False, allow_empty=True) policy_issue_type = NestedRelatedField( PolicyIssueType, required=False, allow_null=True, ) def validate(self, data): """ Removes the semi-virtual field is_event from the data. This is removed because the value is not stored; it is instead inferred from contents of the the event field during serialisation. """ if 'is_event' in data: del data['is_event'] return data class Meta: model = Interaction extra_kwargs = { # Date is a datetime in the model, but only the date component is used # (at present). Setting the formats as below effectively makes the field # behave like a date field without changing the schema and breaking the # v1 API. 'date': { 'format': '%Y-%m-%d', 'input_formats': ['%Y-%m-%d'] }, 'grant_amount_offered': { 'min_value': 0 }, 'net_company_receipt': { 'min_value': 0 }, } fields = ( 'id', 'company', 'contact', 'created_on', 'created_by', 'event', 'is_event', 'kind', 'modified_by', 'modified_on', 'date', 'dit_adviser', 'dit_team', 'communication_channel', 'grant_amount_offered', 'investment_project', 'net_company_receipt', 'service', 'service_delivery_status', 'subject', 'notes', 'archived_documents_url_path', 'policy_areas', 'policy_issue_type', ) read_only_fields = ('archived_documents_url_path', ) validators = [ KindPermissionValidator(), HasAssociatedInvestmentProjectValidator(), RulesBasedValidator( ValidationRule( 'required', OperatorRule('communication_channel', bool), when=InRule( 'kind', [ Interaction.KINDS.interaction, Interaction.KINDS.policy_feedback, ], ), ), ValidationRule( 'invalid_for_non_interaction', OperatorRule('investment_project', not_), when=InRule( 'kind', [ Interaction.KINDS.service_delivery, Interaction.KINDS.policy_feedback, ], ), ), ValidationRule( 'invalid_for_service_delivery', OperatorRule('communication_channel', not_), when=EqualsRule('kind', Interaction.KINDS.service_delivery), ), ValidationRule( 'invalid_for_non_service_delivery', OperatorRule('is_event', is_blank), OperatorRule('event', is_blank), OperatorRule('service_delivery_status', is_blank), OperatorRule('grant_amount_offered', is_blank), OperatorRule('net_company_receipt', is_blank), when=InRule( 'kind', [ Interaction.KINDS.interaction, Interaction.KINDS.policy_feedback, ], ), ), ValidationRule( 'invalid_for_non_policy_feedback', OperatorRule('policy_areas', not_), OperatorRule('policy_issue_type', is_blank), when=InRule( 'kind', [ Interaction.KINDS.interaction, Interaction.KINDS.service_delivery, ], ), ), ValidationRule( 'required', OperatorRule('is_event', is_not_blank), when=EqualsRule('kind', Interaction.KINDS.service_delivery), ), ValidationRule( 'required', OperatorRule('policy_areas', bool), OperatorRule('policy_issue_type', is_not_blank), when=EqualsRule('kind', Interaction.KINDS.policy_feedback), ), ValidationRule( 'required', OperatorRule('event', bool), when=OperatorRule('is_event', bool), ), ValidationRule( 'invalid_for_non_event', OperatorRule('event', not_), when=OperatorRule('is_event', not_), ), ValidationRule( 'required', OperatorRule('notes', is_not_blank), when=OperatorRule('is_event', not_), ), ), ]