コード例 #1
0
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
コード例 #2
0
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',
        )
コード例 #3
0
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
コード例 #4
0
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 = []
コード例 #5
0
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,
        )
コード例 #6
0
ファイル: serializers.py プロジェクト: uktrade/data-hub-api
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
コード例 #7
0
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')
コード例 #8
0
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')
コード例 #9
0
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',
        )
コード例 #10
0
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_),
                ),
            ),
        ]
コード例 #11
0
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,
            )
コード例 #12
0
ファイル: serializers.py プロジェクト: uktrade/data-hub-api
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
コード例 #13
0
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()
コード例 #14
0
ファイル: serializers.py プロジェクト: reupen/data-hub-api
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},
        }
コード例 #15
0
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',
        )
コード例 #16
0
ファイル: serializers.py プロジェクト: cgsunkel/data-hub-api
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),
                    ),
                ),
            ),
        ]
コード例 #17
0
ファイル: serializers.py プロジェクト: reupen/data-hub-api
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_),
                ),
            ),
        ]