Skip to content

Ordering

OrderingFilterBackend

Bases: OrderingFilter

Custom OrderingFilter with additional features.

Provides: - Better support of openapi via drf-spectacular. - Extra kwargs for ordering fields.

Source code in saritasa_drf_tools/filters/ordering.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
class OrderingFilterBackend(filters.OrderingFilter):
    """Custom OrderingFilter with additional features.

    Provides:
    - Better support of openapi via drf-spectacular.
    - Extra kwargs for ordering fields.

    """

    def get_ordering(
        self,
        request: request.Request,
        queryset: models.QuerySet,
        view: views.APIView,
    ) -> collections.abc.Sequence[models.OrderBy]:
        """Adjust ordering params.

        Wrap ordering fields in `models.OrderBy` and pass extra ordering kwargs

        When using PostgreSQL and sorting by non-unique fields only (or without
        any ordering), non-deterministic results may occur, because the db
        doesn't guarantee particular output ordering without unique ordering
        fields
        (https://www.postgresql.org/docs/current/queries-order.html).
        Closed PR to change this in `django-rest-framework`:
        https://github.com/encode/django-rest-framework/pull/9109
        To fix this, "id" is added as the last ordering field if
        `add_pk_to_ordering` is provided (otherwise applied by default or
        controlled via global setting). If "id" is already present in
        ordering, it won't be duplicated.

        """
        ordering = (
            super().get_ordering(
                request,
                queryset,
                view,
            )
            or ()
        )
        default_add_pk_to_ordering = getattr(
            settings,
            "SARITASA_DRF_ORDERING_ADD_PK_TO_ORDERING",
            True,
        )
        add_pk_to_ordering = getattr(
            view,
            "add_pk_to_ordering",
            default_add_pk_to_ordering,
        )
        is_null_first = getattr(
            settings,
            "SARITASA_DRF_ORDERING_IS_NULL_FIRST",
            None,
        )
        is_null_last = getattr(
            settings,
            "SARITASA_DRF_ORDERING_IS_NULL_LAST",
            None,
        )

        ordering_fields_extra_kwargs = getattr(
            view,
            "ordering_fields_extra_kwargs",
            {},
        )
        fields_from_extra_kwargs = set(ordering_fields_extra_kwargs.keys())
        ordering_fields: set[str] = set(
            getattr(view, "ordering_fields", set()),
        )
        if unknown_fields := fields_from_extra_kwargs - ordering_fields:
            view_action = getattr(view, "action", "list")
            warnings.warn(
                f"Unknown ordering fields: {','.join(unknown_fields)}"
                f" defined(ordering_fields_extra_kwargs) in {self.__class__}"
                f"({view_action}).",
                stacklevel=2,
            )
        adjusted_ordering: list[models.OrderBy] = []
        for order_field in ordering:
            cleared_order_field = order_field.removeprefix("-")
            order_by_kwargs = {
                "descending": order_field.startswith("-"),
                "nulls_first": is_null_first,
                "nulls_last": is_null_last,
            }
            order_by_kwargs.update(
                ordering_fields_extra_kwargs.get(cleared_order_field, {}),
            )
            adjusted_ordering.append(
                models.OrderBy(
                    expression=models.F(cleared_order_field),
                    **order_by_kwargs,
                ),
            )
        if not add_pk_to_ordering:
            return adjusted_ordering
        return (
            *tuple(adjusted_ordering),
            models.OrderBy(
                expression=models.F("pk"),
            ),
        )

    def get_schema_operation_parameters(
        self,
        view: views.APIView,
    ) -> list[dict[str, typing.Any]]:
        """Prepare parameters for openapi schema.

        Check that view has `ordering_fields`.

        Check that `ordering_fields` contains valid set of fields. Actually,
        this check may perform some SQL queries during spec generation. Also,
        spec generation is not the best place for checking of source code
        (comparing to linters/django system checks/tests), but DRF doesn't
        validate `ordering_fields` for views while backend running.

        Extend view description with list of `ordering_fields`.

        """
        operation_parameters = super().get_schema_operation_parameters(
            view=view,
        )
        # Not using get_valid_fields since it is requires additional params
        ordering_fields: collections.abc.Sequence[str] | None = getattr(
            view,
            "ordering_fields",
            None,
        )
        if not ordering_fields:  # pragma: no cover
            from drf_spectacular import drainage

            drainage.warn(
                f"`ordering_fields` are not set up for {view.__class__}",
            )
            return operation_parameters

        self._validate_ordering_fields(view)

        formatted_fields = ", ".join(f"`{field}`" for field in ordering_fields)
        operation_parameters[0]["description"] = (
            "Which fields to use when ordering the results. A list "
            "fields separated by `,`. Example: `field1,field2`\n\n"
            f"Supported fields: {formatted_fields}.\n\n"
            "To reverse order just add `-` to field. Example:"
            "`field` -> `-field`"
        )
        return operation_parameters

    def _validate_ordering_fields(
        self,
        view: views.APIView,
    ) -> None:
        """Validate `ordering_fields` in view."""
        try:
            view.get_queryset().order_by(*view.ordering_fields)  # type: ignore
        except exceptions.FieldError as error:  # pragma: no cover
            from drf_spectacular import drainage

            view_action = getattr(view, "action", "list")
            drainage.warn(
                "`ordering_fields` contains non-existent"
                f" or non-related fields for action {view_action}."
                f" {error}",
            )

get_ordering(request, queryset, view)

Adjust ordering params.

Wrap ordering fields in models.OrderBy and pass extra ordering kwargs

When using PostgreSQL and sorting by non-unique fields only (or without any ordering), non-deterministic results may occur, because the db doesn't guarantee particular output ordering without unique ordering fields (https://www.postgresql.org/docs/current/queries-order.html). Closed PR to change this in django-rest-framework: https://github.com/encode/django-rest-framework/pull/9109 To fix this, "id" is added as the last ordering field if add_pk_to_ordering is provided (otherwise applied by default or controlled via global setting). If "id" is already present in ordering, it won't be duplicated.

Source code in saritasa_drf_tools/filters/ordering.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def get_ordering(
    self,
    request: request.Request,
    queryset: models.QuerySet,
    view: views.APIView,
) -> collections.abc.Sequence[models.OrderBy]:
    """Adjust ordering params.

    Wrap ordering fields in `models.OrderBy` and pass extra ordering kwargs

    When using PostgreSQL and sorting by non-unique fields only (or without
    any ordering), non-deterministic results may occur, because the db
    doesn't guarantee particular output ordering without unique ordering
    fields
    (https://www.postgresql.org/docs/current/queries-order.html).
    Closed PR to change this in `django-rest-framework`:
    https://github.com/encode/django-rest-framework/pull/9109
    To fix this, "id" is added as the last ordering field if
    `add_pk_to_ordering` is provided (otherwise applied by default or
    controlled via global setting). If "id" is already present in
    ordering, it won't be duplicated.

    """
    ordering = (
        super().get_ordering(
            request,
            queryset,
            view,
        )
        or ()
    )
    default_add_pk_to_ordering = getattr(
        settings,
        "SARITASA_DRF_ORDERING_ADD_PK_TO_ORDERING",
        True,
    )
    add_pk_to_ordering = getattr(
        view,
        "add_pk_to_ordering",
        default_add_pk_to_ordering,
    )
    is_null_first = getattr(
        settings,
        "SARITASA_DRF_ORDERING_IS_NULL_FIRST",
        None,
    )
    is_null_last = getattr(
        settings,
        "SARITASA_DRF_ORDERING_IS_NULL_LAST",
        None,
    )

    ordering_fields_extra_kwargs = getattr(
        view,
        "ordering_fields_extra_kwargs",
        {},
    )
    fields_from_extra_kwargs = set(ordering_fields_extra_kwargs.keys())
    ordering_fields: set[str] = set(
        getattr(view, "ordering_fields", set()),
    )
    if unknown_fields := fields_from_extra_kwargs - ordering_fields:
        view_action = getattr(view, "action", "list")
        warnings.warn(
            f"Unknown ordering fields: {','.join(unknown_fields)}"
            f" defined(ordering_fields_extra_kwargs) in {self.__class__}"
            f"({view_action}).",
            stacklevel=2,
        )
    adjusted_ordering: list[models.OrderBy] = []
    for order_field in ordering:
        cleared_order_field = order_field.removeprefix("-")
        order_by_kwargs = {
            "descending": order_field.startswith("-"),
            "nulls_first": is_null_first,
            "nulls_last": is_null_last,
        }
        order_by_kwargs.update(
            ordering_fields_extra_kwargs.get(cleared_order_field, {}),
        )
        adjusted_ordering.append(
            models.OrderBy(
                expression=models.F(cleared_order_field),
                **order_by_kwargs,
            ),
        )
    if not add_pk_to_ordering:
        return adjusted_ordering
    return (
        *tuple(adjusted_ordering),
        models.OrderBy(
            expression=models.F("pk"),
        ),
    )

get_schema_operation_parameters(view)

Prepare parameters for openapi schema.

Check that view has ordering_fields.

Check that ordering_fields contains valid set of fields. Actually, this check may perform some SQL queries during spec generation. Also, spec generation is not the best place for checking of source code (comparing to linters/django system checks/tests), but DRF doesn't validate ordering_fields for views while backend running.

Extend view description with list of ordering_fields.

Source code in saritasa_drf_tools/filters/ordering.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def get_schema_operation_parameters(
    self,
    view: views.APIView,
) -> list[dict[str, typing.Any]]:
    """Prepare parameters for openapi schema.

    Check that view has `ordering_fields`.

    Check that `ordering_fields` contains valid set of fields. Actually,
    this check may perform some SQL queries during spec generation. Also,
    spec generation is not the best place for checking of source code
    (comparing to linters/django system checks/tests), but DRF doesn't
    validate `ordering_fields` for views while backend running.

    Extend view description with list of `ordering_fields`.

    """
    operation_parameters = super().get_schema_operation_parameters(
        view=view,
    )
    # Not using get_valid_fields since it is requires additional params
    ordering_fields: collections.abc.Sequence[str] | None = getattr(
        view,
        "ordering_fields",
        None,
    )
    if not ordering_fields:  # pragma: no cover
        from drf_spectacular import drainage

        drainage.warn(
            f"`ordering_fields` are not set up for {view.__class__}",
        )
        return operation_parameters

    self._validate_ordering_fields(view)

    formatted_fields = ", ".join(f"`{field}`" for field in ordering_fields)
    operation_parameters[0]["description"] = (
        "Which fields to use when ordering the results. A list "
        "fields separated by `,`. Example: `field1,field2`\n\n"
        f"Supported fields: {formatted_fields}.\n\n"
        "To reverse order just add `-` to field. Example:"
        "`field` -> `-field`"
    )
    return operation_parameters