Esempio n. 1
0
class PublicSignalCreateSerializer(SignalValidationMixin,
                                   serializers.ModelSerializer):
    """
    This serializer allows anonymous users to report `signals.Signals`.

    Note: this is only used in the creation of new Signal instances, not to
    create the response body after a succesfull POST.
    """
    location = _NestedLocationModelSerializer()
    reporter = _NestedReporterModelSerializer()
    category = _NestedCategoryModelSerializer(source='category_assignment')

    extra_properties = SignalExtraPropertiesField(
        required=False,
        allow_null=True,
        validators=[
            ExtraPropertiesValidator(
                filename=os.path.join(os.path.dirname(__file__), '..',
                                      'json_schema', 'extra_properties.json'))
        ])

    incident_date_start = serializers.DateTimeField()

    class Meta(object):
        model = Signal
        fields = (
            'text',
            'text_extra',
            'location',
            'category',
            'reporter',
            'incident_date_start',
            'incident_date_end',
            'extra_properties',
        )

    def validate(self, data):
        """Make sure any extra data is rejected"""
        if hasattr(self, 'initial_data'):
            present_keys = set(self.initial_data)
            allowed_keys = set(self.fields)

            if present_keys - allowed_keys:
                raise ValidationError('Extra properties present: {}'.format(
                    ', '.join(present_keys - allowed_keys)))
        return super().validate(data)

    def create(self, validated_data):
        location_data = validated_data.pop('location')
        reporter_data = validated_data.pop('reporter')
        category_assignment_data = validated_data.pop('category_assignment')

        status_data = {"state": workflow.GEMELD}
        signal = Signal.actions.create_initial(validated_data, location_data,
                                               status_data,
                                               category_assignment_data,
                                               reporter_data)
        return signal
Esempio n. 2
0
class PrivateSignalSerializerList(SignalValidationMixin, HALSerializer):
    """
    This serializer is used for the list endpoint and when creating a new instance
    """
    serializer_url_field = PrivateSignalLinksField
    _display = DisplayField()

    signal_id = serializers.UUIDField(source='uuid',
                                      required=False,
                                      read_only=True)

    location = _NestedLocationModelSerializer(
        permission_classes=(SIAPermissions, ))

    status = _NestedStatusModelSerializer(
        required=False, permission_classes=(SignalCreateInitialPermission, ))

    category = _NestedCategoryModelSerializer(
        source='category_assignment',
        permission_classes=(SignalCreateInitialPermission, ))

    reporter = _NestedReporterModelSerializer(
        permission_classes=(SIAPermissions, ))

    priority = _NestedPriorityModelSerializer(
        required=False, permission_classes=(SIAPermissions, ))

    notes = _NestedNoteModelSerializer(
        many=True,
        required=False,
        permission_classes=(SignalCreateInitialPermission, ))

    type = _NestedTypeModelSerializer(
        required=False,
        permission_classes=(SIAPermissions, ),
        source='type_assignment',
    )

    directing_departments = _NestedDepartmentModelSerializer(
        source='directing_departments_assignment.departments',
        many=True,
        required=False,
        permission_classes=(SIAPermissions, ),
    )

    routing_departments = _NestedDepartmentModelSerializer(
        source='routing_assignment.departments',
        many=True,
        required=False,
        allow_null=True,
        permission_classes=(SIAPermissions, ),
    )

    has_attachments = serializers.SerializerMethodField()

    assigned_user_email = serializers.EmailField(
        source='user_assignment.user.email', required=False, allow_null=True)

    extra_properties = SignalExtraPropertiesField(
        required=False,
        allow_null=True,
        validators=[
            ExtraPropertiesValidator(
                filename=os.path.join(os.path.dirname(__file__), '..',
                                      'json_schema', 'extra_properties.json'))
        ])

    parent = serializers.PrimaryKeyRelatedField(required=False,
                                                read_only=False,
                                                write_only=True,
                                                queryset=Signal.objects.all())
    has_parent = serializers.SerializerMethodField()
    has_children = serializers.SerializerMethodField()

    attachments = PrivateSignalAttachmentRelatedField(
        view_name='private-signals-attachments-detail',
        many=True,
        required=False,
        read_only=False,
        write_only=True,
        queryset=Attachment.objects.all())

    class Meta:
        model = Signal
        list_serializer_class = _SignalListSerializer
        fields = ('_links', '_display', 'id', 'signal_id', 'source', 'text',
                  'text_extra', 'status', 'location', 'category', 'reporter',
                  'priority', 'type', 'created_at', 'updated_at',
                  'incident_date_start', 'incident_date_end',
                  'operational_date', 'has_attachments', 'extra_properties',
                  'notes', 'directing_departments', 'routing_departments',
                  'attachments', 'parent', 'has_parent', 'has_children',
                  'assigned_user_email')
        read_only_fields = (
            'created_at',
            'updated_at',
            'has_attachments',
            'has_parent',
            'has_children',
        )
        extra_kwargs = {
            'source': {
                'validators': [SignalSourceValidator()]
            },
        }

    def get_has_attachments(self, obj):
        return obj.attachments.exists()

    def get_has_parent(self, obj):
        return obj.parent_id is not None  # True is a parent_id is set, False if not

    def get_has_children(self, obj):
        return obj.children.exists()

    def validate(self, attrs):
        errors = {}
        if attrs.get('directing_departments_assignment') is not None:
            errors.update({
                'directing_departments_assignment':
                ['Directing departments cannot be set on initial creation']
            })

        if attrs.get('routing_assignment') is not None:
            errors.update({
                'routing_assignment': [
                    'Signal departments relation cannot be set on initial creation'
                ]
            })

        if attrs.get('status') is not None:
            errors.update(
                {'status': ['Status cannot be set on initial creation']})

        attachments = attrs.get('attachments')
        parent = attrs.get('parent')
        if attachments and parent is None:
            errors.update({
                'attachments': [
                    'Attachments can only be copied when creating a child Signal'
                ]
            })

        if attachments and parent:
            attachments_belong_to_parent = all([
                parent.pk == attachment._signal_id
                for attachment in attachments
            ])
            if not attachments_belong_to_parent:
                errors.update({
                    'attachments':
                    ['Attachments can only be copied from the parent Signal']
                })

        if errors:
            raise serializers.ValidationError(errors)

        return super().validate(attrs=attrs)

    def create(self, validated_data):
        # Set default status
        logged_in_user = self.context['request'].user
        INITIAL_STATUS = {
            'state': workflow.GEMELD,  # see models.py is already default
            'text': None,
            'user': logged_in_user.email,
        }

        # We require location and reporter to be set and to be valid.
        reporter_data = validated_data.pop('reporter')

        location_data = validated_data.pop('location')
        location_data['created_by'] = logged_in_user.email

        category_assignment_data = validated_data.pop('category_assignment')
        category_assignment_data['created_by'] = logged_in_user.email

        # We will use the priority and signal type on the incoming message if present.
        priority_data = validated_data.pop(
            'priority', {'priority': Priority.PRIORITY_NORMAL})
        priority_data['created_by'] = logged_in_user.email
        type_data = validated_data.pop('type_assignment', {})
        type_data['created_by'] = logged_in_user.email

        attachments = validated_data.pop(
            'attachments') if 'attachments' in validated_data else None

        signal = Signal.actions.create_initial(validated_data, location_data,
                                               INITIAL_STATUS,
                                               category_assignment_data,
                                               reporter_data, priority_data,
                                               type_data)

        if attachments:
            Signal.actions.copy_attachments(data=attachments, signal=signal)

        signal.refresh_from_db()
        return signal
Esempio n. 3
0
class PublicSignalCreateSerializer(SignalValidationMixin, serializers.ModelSerializer):
    """
    This serializer allows anonymous users to report `signals.Signals`.

    Note: this is only used in the creation of new Signal instances, not to
    create the response body after a succesfull POST.
    """
    source = serializers.CharField(default='online', validators=[PublicSignalSourceValidator()])
    location = _NestedLocationModelSerializer()
    reporter = _NestedReporterModelSerializer()
    category = _NestedCategoryModelSerializer(source='category_assignment')
    incident_date_start = serializers.DateTimeField()
    extra_properties = serializers.JSONField(
        required=False,
        allow_null=True,
        validators=[
            ExtraPropertiesValidator(
                filename=os.path.join(
                    os.path.dirname(__file__), '..', 'json_schema', 'extra_properties.json'
                )
            )
        ]
    )

    # The Session containing the given answers of a Questionnaire
    session = serializers.UUIDField(default=None, write_only=True, required=False)

    class Meta(object):
        model = Signal
        fields = (
            'source',
            'text',
            'text_extra',
            'location',
            'category',
            'reporter',
            'incident_date_start',
            'incident_date_end',
            'extra_properties',
            'session',
        )

    def validate(self, data):  # noqa: C901
        """Make sure any extra data is rejected"""
        if hasattr(self, 'initial_data'):
            present_keys = set(self.initial_data)
            allowed_keys = set(self.fields)

            if present_keys - allowed_keys:
                raise ValidationError('Extra properties present: {}'.format(
                    ', '.join(present_keys - allowed_keys)
                ))

        errors = {}
        if 'session' in data and data['session']:
            """
            If a Session UUID is given the following checks must be valid before it can be connected to the Signal that
            is going to be created created.

                - The Session with given UUID must exists
                - The Session cannot be connected to a Signal
                - The Session cannot be expired
                - The Session must be frozen
            """
            try:
                session = Session.objects.get(uuid=data['session'])
            except Session.DoesNotExist as e:
                errors.update({'session': [f'{e}']})
            else:
                if session._signal_id:
                    errors.update({'session': ['Session already used']})
                elif not session.frozen and not session.is_expired:
                    errors.update({'session': ['Session not frozen']})
                elif session.too_late:
                    errors.update({'session': ['Session expired']})
                else:
                    data['session'] = session

        # SIG-4382 additional check on the extra_properties if the category is lantaarpaal-straatverlichting
        #
        # Disabled additional check. This check prevents the creation of a "child" signal in the
        # lantaarnpaal-straatverlichting category because there is no way to provide the streetlight
        #
        # if data.get('category_assignment').get('category').slug == 'lantaarnpaal-straatverlichting':
        #     validator = ExtraPropertiesValidator(
        #         filename=os.path.join(os.path.dirname(__file__), '..', 'json_schema',
        #                               'extra_properties_streetlights.json'))
        #     try:
        #         validator(data.get('extra_properties'))
        #     except ValidationError:
        #         errors.update({'extra_properties': [
        #             'Extra properties not valid for category "lantaarnpaal-straatverlichting"'
        #         ]})

        if errors:
            raise serializers.ValidationError(errors)

        return super().validate(data)

    def create(self, validated_data):
        location_data = validated_data.pop('location')
        reporter_data = validated_data.pop('reporter')
        category_assignment_data = validated_data.pop('category_assignment')
        session = validated_data.pop('session')

        return Signal.actions.create_initial(
            validated_data,
            location_data,
            {'state': workflow.GEMELD},
            category_assignment_data,
            reporter_data,
            priority_data=None,
            type_data=None,
            session=session,
        )
Esempio n. 4
0
class PrivateSignalSerializerDetail(HALSerializer, AddressValidationMixin):
    """
    This serializer is used for the detail endpoint and when updating the instance
    """
    serializer_url_field = PrivateSignalLinksFieldWithArchives
    _display = DisplayField()

    location = _NestedLocationModelSerializer(
        required=False, permission_classes=(SIAPermissions, ))

    status = _NestedStatusModelSerializer(
        required=False, permission_classes=(SignalChangeStatusPermission, ))

    category = _NestedCategoryModelSerializer(
        source='category_assignment',
        required=False,
        permission_classes=(SignalChangeCategoryPermission, ))

    reporter = _NestedReporterModelSerializer(
        required=False, permission_classes=(SIAPermissions, ))

    priority = _NestedPriorityModelSerializer(
        required=False, permission_classes=(SIAPermissions, ))

    notes = _NestedNoteModelSerializer(
        many=True,
        required=False,
        permission_classes=(SignalCreateNotePermission, ))

    type = _NestedTypeModelSerializer(
        required=False,
        permission_classes=(SIAPermissions, ),
        source='type_assignment',
    )

    directing_departments = _NestedDepartmentModelSerializer(
        source='directing_departments_assignment.departments',
        many=True,
        required=False,
        permission_classes=(SIAPermissions, ),
    )

    routing_departments = _NestedDepartmentModelSerializer(
        source='routing_assignment.departments',
        many=True,
        required=False,
        allow_null=True,
        permission_classes=(SIAPermissions, ),
    )

    has_attachments = serializers.SerializerMethodField()

    assigned_user_email = serializers.EmailField(
        source='user_assignment.user.email', required=False, allow_null=True)

    extra_properties = SignalExtraPropertiesField(
        required=False,
        validators=[
            ExtraPropertiesValidator(
                filename=os.path.join(os.path.dirname(__file__), '..',
                                      'json_schema', 'extra_properties.json'))
        ])

    attachments = PrivateSignalAttachmentRelatedField(
        view_name='private-signals-attachments-detail',
        many=True,
        required=False,
        read_only=True)

    class Meta:
        model = Signal
        fields = (
            '_links',
            '_display',
            'category',
            'id',
            'has_attachments',
            'location',
            'status',
            'reporter',
            'priority',
            'notes',
            'type',
            'source',
            'text',
            'text_extra',
            'extra_properties',
            'created_at',
            'updated_at',
            'incident_date_start',
            'incident_date_end',
            'directing_departments',
            'routing_departments',
            'attachments',
            'assigned_user_email',
        )
        read_only_fields = (
            'id',
            'has_attachments',
        )

    def get_has_attachments(self, obj):
        return obj.attachments.exists()

    def update(self, instance, validated_data):  # noqa
        """
        Perform update on nested models.

        Note:
        - Reporter cannot be updated via the API.
        - Atomic update (all fail/succeed), django signals on full success (see
          underlying update_multiple method of actions SignalManager).
        """
        if not instance.is_parent and validated_data.get(
                'directing_departments_assignment') is not None:
            raise serializers.ValidationError(
                'Directing departments can only be set on a parent Signal')

        user_email = self.context['request'].user.email

        for _property in [
                'location', 'status', 'category_assignment', 'priority'
        ]:
            if _property in validated_data:
                data = validated_data[_property]
                data['created_by'] = user_email

        if 'type_assignment' in validated_data:
            type_data = validated_data.pop('type_assignment')
            type_data['created_by'] = user_email
            validated_data['type'] = type_data

        if 'notes' in validated_data and validated_data['notes']:
            note_data = validated_data['notes'][0]
            note_data['created_by'] = user_email

        if 'directing_departments_assignment' in validated_data and validated_data[
                'directing_departments_assignment']:
            validated_data['directing_departments_assignment'][
                'created_by'] = user_email

        if 'routing_assignment' in validated_data and validated_data[
                'routing_assignment']:
            validated_data['routing_assignment']['created_by'] = user_email

        if 'user_assignment' in validated_data and validated_data[
                'user_assignment']:
            validated_data['created_by'] = user_email

        signal = Signal.actions.update_multiple(validated_data, instance)
        return signal
Esempio n. 5
0
class PrivateSignalSerializerList(SignalValidationMixin, HALSerializer):
    """
    This serializer is used for the list endpoint and when creating a new instance
    """
    serializer_url_field = PrivateSignalLinksField
    _display = DisplayField()

    signal_id = serializers.UUIDField(source='uuid', required=False, read_only=True)

    location = _NestedLocationModelSerializer(
        permission_classes=(SIAPermissions,)
    )

    status = _NestedStatusModelSerializer(
        required=False,
        permission_classes=(SignalCreateInitialPermission,)
    )

    category = _NestedCategoryModelSerializer(
        source='category_assignment',
        permission_classes=(SignalCreateInitialPermission,)
    )

    reporter = _NestedReporterModelSerializer(
        permission_classes=(SIAPermissions,)
    )

    priority = _NestedPriorityModelSerializer(
        required=False,
        permission_classes=(SIAPermissions,)
    )

    notes = _NestedNoteModelSerializer(
        many=True,
        required=False,
        permission_classes=(SignalCreateInitialPermission,)
    )

    type = _NestedTypeModelSerializer(
        required=False,
        permission_classes=(SIAPermissions,),
        source='type_assignment',
    )

    directing_departments = _NestedDepartmentModelSerializer(
        source='directing_departments_assignment.departments',
        many=True,
        required=False,
        permission_classes=(SIAPermissions,),
    )

    routing_departments = _NestedDepartmentModelSerializer(
        source='routing_assignment.departments',
        many=True,
        required=False,
        allow_null=True,
        permission_classes=(SIAPermissions,),
    )

    has_attachments = serializers.SerializerMethodField()

    assigned_user_email = serializers.EmailField(source='user_assignment.user.email', required=False, allow_null=True)

    extra_properties = serializers.JSONField(
        required=False,
        allow_null=True,
        validators=[
            ExtraPropertiesValidator(
                filename=os.path.join(
                    os.path.dirname(__file__), '..', 'json_schema', 'extra_properties.json')
            )
        ]
    )

    parent = serializers.PrimaryKeyRelatedField(
        required=False,
        read_only=False,
        write_only=True,
        queryset=Signal.objects.all()
    )
    has_parent = serializers.SerializerMethodField()
    has_children = serializers.SerializerMethodField()

    attachments = PrivateSignalAttachmentRelatedField(view_name='private-signals-attachments-detail', many=True,
                                                      required=False, read_only=False, write_only=True,
                                                      queryset=Attachment.objects.all())

    # The Session containing the given answers of a Questionnaire
    session = serializers.UUIDField(default=None, write_only=True, required=False)

    id_display = serializers.CharField(source='get_id_display', read_only=True)

    class Meta:
        model = Signal
        list_serializer_class = _SignalListSerializer
        fields = (
            '_links',
            '_display',
            'id',
            'id_display',
            'signal_id',
            'source',
            'text',
            'text_extra',
            'status',
            'location',
            'category',
            'reporter',
            'priority',
            'type',
            'created_at',
            'updated_at',
            'incident_date_start',
            'incident_date_end',
            'operational_date',
            'has_attachments',
            'extra_properties',
            'notes',
            'directing_departments',
            'routing_departments',
            'attachments',
            'parent',
            'has_parent',
            'has_children',
            'assigned_user_email',
            'session'
        )
        read_only_fields = (
            'created_at',
            'updated_at',
            'has_attachments',
            'has_parent',
            'has_children',
        )
        extra_kwargs = {
            'source': {'validators': [PrivateSignalSourceValidator()]},
        }

    def get_has_attachments(self, obj):
        return obj.attachments.exists()

    def get_has_parent(self, obj):
        return obj.parent_id is not None  # True is a parent_id is set, False if not

    def get_has_children(self, obj):
        return obj.children.exists()

    def validate(self, attrs):  # noqa C901
        errors = {}
        if attrs.get('directing_departments_assignment') is not None:
            errors.update(
                {'directing_departments_assignment': ['Directing departments cannot be set on initial creation']}
            )

        if attrs.get('routing_assignment') is not None:
            errors.update(
                {'routing_assignment': ['Signal departments relation cannot be set on initial creation']}
            )

        if attrs.get('status') is not None:
            errors.update(
                {'status': ['Status cannot be set on initial creation']}
            )

        attachments = attrs.get('attachments')
        parent = attrs.get('parent')
        if attachments and parent is None:
            errors.update({'attachments': ['Attachments can only be copied when creating a child Signal']})

        if attachments and parent:
            attachments_belong_to_parent = all([parent.pk == attachment._signal_id for attachment in attachments])
            if not attachments_belong_to_parent:
                errors.update({'attachments': ['Attachments can only be copied from the parent Signal']})

        if 'session' in attrs and attrs['session']:
            """
            If a Session UUID is given the following checks must be valid before it can be connected to the Signal that
            is going to be created created.

                - The Session with given UUID must exists
                - The Session cannot be connected to a Signal
                - The Session cannot be expired
                - The Session must be frozen
            """
            try:
                session = Session.objects.get(uuid=attrs['session'])
            except Session.DoesNotExist as e:
                errors.update({'session': [f'{e}']})
            else:
                if session._signal_id:
                    errors.update({'session': ['Session already used']})
                elif not session.frozen and not session.is_expired:
                    errors.update({'session': ['Session not frozen']})
                elif session.too_late:
                    errors.update({'session': ['Session expired']})
                else:
                    attrs['session'] = session

        # SIG-4382 additional check on the extra_properties if the category is lantaarpaal-straatverlichting
        #
        # Disabled additional check. This check prevents the creation of a "child" signal in the
        # lantaarnpaal-straatverlichting category because there is no way to provide the streetlight
        #
        # if attrs.get('category_assignment').get('category').slug == 'lantaarnpaal-straatverlichting':
        #     validator = ExtraPropertiesValidator(filename=os.path.join(os.path.dirname(__file__), '..', 'json_schema',
        #                                                                'extra_properties_streetlights.json'))
        #     try:
        #         validator(attrs.get('extra_properties'))
        #     except ValidationError:
        #         errors.update({'extra_properties': [
        #             'Extra properties not valid for category "lantaarnpaal-straatverlichting"'
        #         ]})

        if errors:
            raise serializers.ValidationError(errors)

        return super().validate(attrs=attrs)

    def create(self, validated_data):
        # Set default status
        logged_in_user = self.context['request'].user
        INITIAL_STATUS = {
            'state': workflow.GEMELD,  # see models.py is already default
            'text': None,
            'user': logged_in_user.email,
        }

        # We require location and reporter to be set and to be valid.
        reporter_data = validated_data.pop('reporter')

        location_data = validated_data.pop('location')
        location_data['created_by'] = logged_in_user.email

        category_assignment_data = validated_data.pop('category_assignment')
        category_assignment_data['created_by'] = logged_in_user.email

        # We will use the priority and signal type on the incoming message if present.
        priority_data = validated_data.pop('priority', {
            'priority': Priority.PRIORITY_NORMAL
        })
        priority_data['created_by'] = logged_in_user.email
        type_data = validated_data.pop('type_assignment', {})
        type_data['created_by'] = logged_in_user.email

        attachments = validated_data.pop('attachments') if 'attachments' in validated_data else None
        session = validated_data.pop('session')

        signal = Signal.actions.create_initial(
            validated_data,
            location_data,
            INITIAL_STATUS,
            category_assignment_data,
            reporter_data,
            priority_data,
            type_data,
            session=session,
        )

        if attachments:
            Signal.actions.copy_attachments(data=attachments, signal=signal, created_by=logged_in_user.email)
            # Add history entries for every attachment that was copied. Only photos allowed for now.
            for attachment in Attachment.objects.filter(_signal=signal).order_by('created_at'):
                filename = os.path.basename(attachment.file.name)
                msg = f'Bijlage gekopieerd van hoofdmelding: {filename}'
                Signal.actions.create_note(data={'text': msg}, signal=signal)

        signal.refresh_from_db()
        return signal