์ฌ๋ณด์ธ์! ์ฐ์ , ์ฐ๋ฆฌ ์ ํ๋ฆฌ์ผ์ด์ ์์ Django REST Framework์ ํจ๊ป ๋งค์ฐ ์ ์ฉํ ๊ฒ์ผ๋ก ํ๋ช ๋ ์ด ํ๋ก์ ํธ์ ๋ํด ๊ฐ์ฌ๋ฅผ ํํ๊ณ ์ถ์ต๋๋ค.
ํ์ฌ ๊ด๋ จ ๋ชจ๋ธ์ ํ๋๋ณ๋ก ์ง๋ ฌํ, ํํฐ๋ง ๋ฐ ์ ๋ ฌ ๋ฌธ์ ๋ก ์ด๋ ค์์ ๊ฒช๊ณ ์์ต๋๋ค. ์ด๋ฒ ํธ์์๋ ์ฃผ๋ฌธ์๋ง ์ง์คํ๊ณ ์ถ์ต๋๋ค.
๋ค์ ๋ชจ๋ธ๋ก ๋ด ์๋๋ฅผ ์ค๋ช ํ๊ฒ ์ต๋๋ค.
# models.py
from django.contrib.auth.models import User
from django.db import models
class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.PROTECT, null=False, blank=False)
created = models.DateTimeField(null=False, blank=False, auto_now_add=True)
submitted = models.DateTimeField(null=True, blank=True)
def __str__(self):
return f'Order #{self.id}'
class Product(models.Model):
name = models.CharField(null=False, blank=False, max_length=256)
price = models.DecimalField(null=False, blank=False, decimal_places=2, max_digits=12)
def __str__(self):
return self.name
class OrderLine(models.Model):
product = models.ForeignKey(Product, on_delete=models.PROTECT, null=False, blank=False, related_name='order_lines')
quantity = models.IntegerField(null=False, blank=False)
product_price = models.DecimalField(null=False, blank=False, decimal_places=2, max_digits=12)
total_price = models.DecimalField(null=False, blank=False, decimal_places=2, max_digits=12)
order = models.ForeignKey(Order, on_delete=models.CASCADE, null=False, blank=False, related_name='order_lines')
def __str__(self):
return f'{self.order}: {self.product.name} x{self.quantity}'
๊ทธ๋ฐ ๋ค์ ํํฐ๋ง ๋ฐ ์์๊ฐ ๊ตฌ์ฑ๋์ง ์์ DRF ViewSet์ผ๋ก ์์ํ๊ฒ ์ต๋๋ค.
# views.py
from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend, FilterSet, OrderingFilter
from .models import Order
class OrderFilterSet(FilterSet):
pass
class OrderViewSet(viewsets.ModelViewSet):
queryset = Order.objects.all()
serializer_class = OrderSerializer # the definition of OrderSerializer is irrelevant and not shown here
filter_backends = (DjangoFilterBackend, OrderingFilter)
filterset_class = OrderFilterSet
ordering_fields = ()
class Meta:
model = Order
์ด ๋ฌธ์ ์ ๋ฒ์์์ ๋ด ๋ชฉํ๋ ํด๋ผ์ด์ธํธ๊ฐ ๋ค์ ํ๋๋ฅผ ๊ธฐ์ค์ผ๋ก ์์ฒญ๋ ์ฃผ๋ฌธ ๋ชฉ๋ก์ ์ฃผ๋ฌธํ ์ ์๋๋ก ํ๋ ๊ฒ์ ๋๋ค.
ordering=[-]created
- ์ฃผ๋ฌธ ์์ฑ ๋ ์ง๋ณ ์ฃผ๋ฌธ( [-]
์ ์ฃผ๋ฌธ ์ค๋ช
์ ๋ํ ์ ํ์ -
๋ฅผ ๋ํ๋
๋๋ค.)ordering=[-]user
- ์ฃผ๋ฌธํ ์ฌ์ฉ์์ ์ ์ฒด ์ด๋ฆ์ผ๋ก ์ฃผ๋ฌธordering=[-]total_quantity
- ์ฃผ๋ฌธํ ์ ํ์ ์ด ์๋์ผ๋ก ์ฃผ๋ฌธ$ ViewSet
#$์ ๊ตฌ์ฑ๋ ordering_fields
์ ๊ธฐ๋ณธ ์ ๊ทผ ๋ฐฉ์์ ๋ชจ๋ธ ํ๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ ์ฃผ๋ฌธ๋ง ํ์ฉํฉ๋๋ค(p.1). ๋คํํ๋ https://django-filter.readthedocs.io/en/master/ref/filters.html#orderingfilter ์ ์ค๋ช
๋ ๋๋ก filters.OrderingFilter
๋ฅผ ํ์ ๋ถ๋ฅํ๋ ๊ณ ๊ธ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
# views.py
from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend, FilterSet, OrderingFilter
from .models import Order
class OrderOrderingFilter(OrderingFilter):
def __init__(self):
super().__init__(fields={
# example: model field
# Order by order creation date
'created': 'created',
# example: expression on related model
# Order by user full name
'user': '', # ???
# example: aggregate expression
'total_quantity': '' # ???
})
class OrderFilterSet(FilterSet):
ordering = OrderOrderingFilter()
class OrderViewSet(viewsets.ModelViewSet):
queryset = Order.objects.all()
serializer_class = OrderSerializer # the definition of OrderSerializer is irrelevant and not shown here
filter_backends = (DjangoFilterBackend,)
filterset_class = OrderFilterSet
class Meta:
model = Order
๊ทธ๋ฌ๋ ์ด ๊ณ ๊ธ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ๋๋ผ๋ django-filter์๋ ํ๋ ์ด๋ฆ์ด ํ์ํ ๊ฒ ๊ฐ์ต๋๋ค. ์ด ๋ฌธ์ ๋ฅผ ์ฐํํ๋ ์ ์ผํ ๋ฐฉ๋ฒ์ ํํฐ๋ง๋ QuerySet์ annotate
'ํ๋ ๊ฒ์
๋๋ค.
# ... the rest is omitted to brevity ...
class OrderOrderingFilter(OrderingFilter):
def __init__(self):
super().__init__(fields={
# ... the rest is omitted to brevity ...
'user_order': 'user',
})
def filter(self, qs, value):
if value:
qs = self._annotate(qs)
return super().filter(qs, value)
def _annotate(self, qs, value):
if 'user' in value or '-user' in value:
qs = qs.annotate(user_order=functions.Concat(
models.F('user__first_name'),
models.Value(' '),
models.F('user__last_name'),
))
return qs
์ด๋ค ์ฌ๋๋ค์ https://docs.djangoproject.com/en/2.1 ์ ์ค๋ช
๋ ๋๋ก .annotate
์ .filter
๋ฅผ ์์ ์ ์ผ๋ก ํผํฉํ ์ ์๊ธฐ ๋๋ฌธ์ ์ฌ๊ธฐ์ .annotate
๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๋์ฐํ ์๊ฐ์ด๋ผ๊ณ ๋งํ ์ ์์ต๋๋ค. /topics/db/aggregation/#order -of-annotate-and-filter-clauses. ๊ทธ๋ฌ๋ .annotate
ํธ์ถ์์ ์ง๊ณ๋ฅผ ์ฌ์ฉํ์ง ์๋ ํ ๊ด์ฐฎ์ผ๋ฏ๋ก ์ง๊ธ๊น์ง๋ ๊ด์ฐฎ์ต๋๋ค. (_๋๋ ์ค์ ๋ก ๋์ค์ ์ง๊ณ๋ก ๋์๊ฐ ๊ฒ์
๋๋ค_)
์ต์ ์ฝ๋ ์ํ์ ๋ ๋ค๋ฅธ ๋ฌธ์ ๋ ์ฝ๋๊ฐ ๋ฐ๋ณต์ ์ด๋ฉฐ ํธ๋ฆฌํ๊ฒ ์์๋ ์ ์๋ ๊ธฐ๋ณธ ํด๋์ค๋ก ์ถ์ถํ๋ ๊ฒ์ด ๋ ๋ซ๋ค๋ ๊ฒ์
๋๋ค. ์ง๊ธ์ django-filter์์ ์ ๊ณตํ๋ ์์ ํ ๋์์ธ OrderingFilter
ํด๋์ค๋ฅผ ์ง์ ์์ฑํ์ต๋๋ค. ์ ์ฒด ์์ค ์ฝ๋๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค. django-filter์์ ๋ง์ ๊ฒ์ ์ฐจ์ฉํ๋ค๋ ๊ฒ์ ์ ์ ์์ต๋๋ค.
import typing
from django.db import models
from django.db.models import expressions
from django.forms.utils import pretty_name
from django.utils.translation import ugettext_lazy as _
from django_filters import filters
from django_filters.constants import EMPTY_VALUES
__all__ = ('OrderingFilter',)
class OrderingFilter(filters.BaseCSVFilter, filters.ChoiceFilter):
"""
An alternative to django_filters.filter.OrderingFilter that allows to specify any Django ORM "expression" for ordering.
Usage examples:
class MyOrderingFilter(ddf.OrderingFilter):
def __init__(self):
super().__init__(fields={
# a model field
'id': 'id'
# an expression
'published_by':
functions.Concat(
expressions.F(f'published_user__first_name'),
expressions.Value(' '),
expressions.F(f'published_user__last_name')
),
# a complete field descriptor with custom field label
'reported_by': {
'label': 'Reporter',
'desc_label': 'Reporter (descending)', # optional, would be derived from 'label' anyway
'expr': functions.Concat(
expressions.F(f'reported_user__first_name'),
expressions.Value(' '),
expressions.F(f'reported_user__last_name')
),
}
})
For more information about expressions, please see the official Django documentation at
https://docs.djangoproject.com/en/2.1/ref/models/expressions/
"""
_fields: typing.Mapping[str, 'FieldDescriptor']
def __init__(self, fields: typing.Mapping[str, typing.Any]):
self._fields = normalize_fields(fields)
super().__init__(choices=build_choices(self._fields))
# <strong i="15">@override</strong>
def filter(self, qs: models.QuerySet, value: typing.Union[typing.List[str], None]):
return qs if value in EMPTY_VALUES else qs.order_by(*(expr for expr in map(self.get_ordering_expr, value)))
def get_ordering_expr(self, param) -> expressions.Expression:
descending = param.startswith('-')
param = param[1:] if descending else param
field_descriptor = self._fields.get(param)
return None if field_descriptor is None else \
field_descriptor.expr if not descending else field_descriptor.expr.desc()
def normalize_fields(fields: typing.Mapping[str, typing.Any]) -> typing.Mapping[str, 'FieldDescriptor']:
return dict((
param_name,
FieldDescriptor(param_name, {'expr': normalize_expr(field)} if isinstance(field, (str, expressions.Expression)) else field)
) for param_name, field in fields.items())
def normalize_expr(expr: typing.Union[str, expressions.Expression]):
return models.F(expr) if isinstance(expr, str) else expr
descending_fmt = _('%s (descending)')
class FieldDescriptor:
expr: models.Expression
def __init__(self, param_name: str, data: typing.Mapping[str, typing.Any]):
self.expr = normalize_expr(data['expr'])
self.label = data.get('label', _(pretty_name(param_name)))
self.desc_label = data.get('desc_label', descending_fmt.format(self.label))
def build_choices(fields: typing.Mapping[str, 'FieldDescriptor']):
choices = []
for param_name, field_descriptor in fields.items():
choices.append((param_name, field_descriptor.label))
choices.append((f'-{param_name}', field_descriptor.desc_label))
return choices
์ด ํด๋์ค๋ฅผ ์ฌ์ฉํ๋ฉด OrdersOrderingFilter
๊ฐ ๋ฉ์ง๊ณ ๊ฐ๊ฒฐํด์ง๋๋ค.
# ... the rest is omitted to brevity ...
class OrderOrderingFilter(ddf.OrderingFilter):
def __init__(self):
super().__init__(fields={
# ... the rest is omitted to brevity ...
'user': functions.Concat(
models.F('user__first_name'),
models.Value(' '),
models.F('user__last_name'),
),
})
์ด๊ฒ์ ๋ด๊ฐ ์ฌ๊ธฐ์ ์จ ์ง๋ฌธ์ ์ ๊ธฐํฉ๋๋ค.
_ django_filters.rest_framework.OrderingFilter
์์ Django ORM ํํ์์ผ๋ก ์์๋ฅผ ์ง์ ํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ ๊ฒ์ ๋ํด ์ด๋ป๊ฒ ์๊ฐํ์ญ๋๊น?_
๊ทธ๋ฌ๋ ๋๋ตํ๊ธฐ ์ ์ ์ฐ๋ฆฌ๋ 1, 2ํ์ด์ง๋ง ํ์๋ค๋ ๊ฒ์ ์๊ธฐ์์ผ ๋๋ฆฌ๊ฒ ์ต๋๋ค. p.3์ ํด๊ฒฐํ๋ ค๋ฉด ์ง๊ณ ํํ์์ด ํ์ํฉ๋๋ค.
aggregates.Sum(models.F('order_lines__quantity'))
.annotate
์์๋ ์ด ํํ์์ ์ฌ์ฉํ ์ ์์ต๋๋ค. QuerySet ํํฐ๋ง๊ณผ ํผํฉํ ๊ฒฐ๊ณผ๋ฅผ ์์ธกํ ์ ์๊ธฐ ๋๋ฌธ์
๋๋ค. Query.order(...)
ํธ์ถ์์๋ ์ด ํํ์์ ์ง์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
django.core.exceptions.FieldError: Using an aggregate in order_by() without also including it in annotate() is not allowed: Sum(F(order_lines__quantity))
๋ฐ๋ผ์ p.3์ ๋๋ฌํ ์ ์์ผ๋ฏ๋ก ViewSet๋ถํฐ ์์ํ์ฌ ๋ชจ๋ ๊ฒ์ ๊ดํตํ๋ ์๋ฃจ์
์ด ํ์ํฉ๋๋ค. Django-filters๋ณด๋ค Django REST Framework์ ๋ ๊ด๋ จ์ด ์์ต๋๋ค. ์ง๊ณ ํํ์ ์ง์ ์์ด django_filters.rest_framework.OrderingFilter
์์ ํํ์ ์ง์์ด ์ค์ ๋ก ํ์ํฉ๋๊น? ์ด๊ฒ์ ๋ง์ ์ด์ ์ ๊ฐ์ ธ์ค์ง ์๊ณ django-filter ์ฌ์ฉ์๋ฅผ ํผ๋์ค๋ฝ๊ฒ ํ ์ ์์ต๋๋ค.
๊ทํ์ ์๊ฒฌ์ ๊ธฐ๋ค๋ฆฌ๊ฒ ์ต๋๋ค. ์ํํ ์ ๋ณด๊ฐ ๋ง๋ค๋ ๊ฒ์ ์๋๋ค. ๋ด ํ ์คํธ ํ๋ก์ ํธ๊ฐ ๋์์ด ๋๊ธฐ๋ฅผ ๋ฐ๋๋๋ค. https://github.com/earshinov/django_sample/tree/master/django_sample/ordering_by_expression
์๋ ํ์ธ์ @earshinov์ ๋๋ค. ์ ๊ณ ํด ์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค. ๋งค์ฐ ํฅ๋ฏธ๋ก์ด. ๋ด๊ฐ ๊ทธ๊ฒ์ ๋ํด ์๊ฐํ์. ๊ทธ๋๊น์ง ์๋ตํ์ง ์์ผ๋ฉด 3์์ ์ ์๊ฒ Ping์ ๋ณด๋ด์ฃผ์ญ์์ค. ๐
๋ ๋ค๋ฅธ ์์ด๋์ด: ํ ๋จ๊ณ ๋ ๋์๊ฐ ํํฐ๊ฐ ํ๋๊ฐ ์๋ ์ฌ๋ฌ ํํ์ ๋๋ ํ๋ ์ด๋ฆ์ ์ง์ ํ๋๋ก ํ์ฉํ ์ ์์ต๋๋ค.
์ด์ (์ต์ 1):
class OrderOrderingFilter(ddf.OrderingFilter):
def __init__(self):
super().__init__(fields={
'user': functions.Concat(
models.F('user__first_name'),
models.Value(' '),
models.F('user__last_name'),
),
})
์ดํ(์ต์ 2):
class OrderOrderingFilter(ddf.OrderingFilter):
def __init__(self):
super().__init__(fields={
'user': ('user__first_name', 'user__last_name'),
})
๋ ๊ฒฝ์ฐ ๋ชจ๋ ordering=user
๋ ์ฌ์ฉ์์ ์ด๋ฆ์ ๋จผ์ , ์ฌ์ฉ์์ ๋ ๋ฒ์งธ ์ด๋ฆ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ๋ ฌํ์ง๋ง ์ต์
2๋ ์ฐ๊ธฐ๊ฐ ๋ ์ฝ๊ณ DB ์ฟผ๋ฆฌ์ ๊ด๋ จํ์ฌ ๋ ํจ์จ์ ์ผ ์ ์์ต๋๋ค.
์๋ ํ์ธ์ @carltongibson ,
Match์์ ํ์ ์์ฒญํ์ต๋๋ค. ๋ฒ์จ 5์์ ๋๋ค :)
ํํ์(ํ๋ ๋๋ ์ฌ๋ฌ ๊ฐ)์ ์ํ ์์ ์ง์ ์ ๋ํ ์ ์ ์์ผ๋ก ๋์๊ฐ์, ์ ๋ ์ด๊ฒ์ด ํผ์ฆ์ ๋๋ฝ๋ ๋ถ๋ถ์ด๋ผ๊ณ ์๊ฐํฉ๋๋ค.
๋ค ๊ฐ์ง ๊ธฐ๋ณธ ํ
์ด๋ธ ์์
์ด ํ์๋ฉ๋๋ค.
ใ
. ์ง๋ ฌํ
๋น. ~ํ์ด์ง ๋งค๊น~(์ด ํ ๋ก ๊ณผ ๊ด๋ จ์ด ์์)
์จ. ํํฐ๋ง
๋. ์ ๋ ฌ
DRF ๋ฐ django-filter๋ฅผ ์ฌ์ฉ ํ๋ฉด ์ ๋ ฌ์ ์ ์ธํ๊ณ ๊ด๋ จ ๋ชจ๋ธ์ ์ง์ํ์ฌ ์ด๋ฌํ ๋ชจ๋ ๊ฒ์ ๊ตฌํํ ์ ์์ต๋๋ค.
ใ
. ์ง๋ ฌํ๋ฅผ ์ํด ์ค์ฒฉ๋ ์ง๋ ฌ ๋ณํ๊ธฐ ๊ฐ ์์ต๋๋ค. ๋ํ ์ง๊ณ๋ ํ๋๋ฅผ ๋ฐํํด์ผ ํ๋ ๊ฒฝ์ฐ( count
์๊ฐ) ์ฟผ๋ฆฌ ์งํฉ์ .annotate()
ํ ์ ์์ต๋๋ค.
from django.db.models import aggregates, functions, F, Value
class ModelViewSet(...):
def get_queryset(self):
qs = Model.objects.all()
if self.action == 'list':
qs = qs.annotate(
author_full_name=functions.Trim(functions.Concat(
F('author__first_name'),
Value(' '),
F('author__last_name'),
)),
submodel_count=aggregates.Count('submodel'))
)
return qs
์จ. Django ํํ์์ผ๋ก ํํฐ๋งํ๊ธฐ ์ํด ์ฌ์ฉ์ ์ ์ ํํฐ๋ฅผ ๊ตฌํํ ์ ์์ต๋๋ค(์๋ ์ ์ฐธ์กฐ). ์ผ๋ฐ ์ค์นผ๋ผ ํํฐ( filters.NumberFilter
)๋ฅผ ์ฌ์ฉํ์ฌ ์ง๊ณ ํ๋( submodel_count
)๋ก ํํฐ๋งํ ์ ์์ต๋๋ค.
from django_filters import filters
from django_filters.rest_framework import FilterSet
class ModelFilterSet(FilterSet):
author = UserFilter(field_name='author', label='author', lookup_expr='icontains')
class UserFilter(filters.Filter):
"""A django_filters filter that implements filtering by user's full name.
def filter(self, qs: QuerySet, value: str) -> QuerySet:
# first_name <lookup_expr> <value> OR last_name <lookup_expr> <value>
return qs if not value else qs.filter(
Q(**{f'{self.field_name}__first_name__{self.lookup_expr}': value}) |
Q(**{f'{self.field_name}__last_name__{self.lookup_expr}': value})
)
๋. ์ ๋ ฌ์ ๋ํ ํด๊ฒฐ์ฑ ์ ์์ต๋๋ค :-(
๋ค์์ ์ง๊ธ๊น์ง ์ฌ์ฉํด์จ ์ฌ์ฉ์ ์ ์ OrderingFilter
์ ์์ ํ ๊ตฌํ์
๋๋ค.
class OrderingFilter(filters.BaseCSVFilter, filters.ChoiceFilter):
"""An alternative to :class:`django_filters.filters.OrderingFilter` that allows to specify any Django ORM expression for ordering.
Usage example:
.. code-block:: python
from django.db import models
from django.db.models import aggregates, expressions, fields
import ddl
class OrderOrderingFilter(ddl.OrderingFilter):
def __init__(self):
super().__init__(fields={
# a model field
'created': 'created'
# an expression
'submitted': expressions.ExpressionWrapper(
models.Q(submitted_date__isnull=False),
output_field=fields.BooleanField()
),
# multiple fields or expressions
'user': ('user__first_name', 'user__last_name'),
# a complete field descriptor with custom field label
'products': {
'label': 'Total number of items in the order',
# if not specified, `desc_label` would be derived from 'label' anyway
'desc_label': 'Total number of items in the order (descending)',
'expr': aggregates.Sum('order_lines__quantity'),
# it is also possible to filter by multiple fields or expressions here
#'exprs': (...)
},
})
For more information about expressions, see the official Django documentation at
https://docs.djangoproject.com/en/dev/ref/models/expressions/
"""
_fields: typing.Mapping[str, 'FieldDescriptor']
def __init__(self, fields: typing.Mapping[str, typing.Any]):
self._fields = normalize_fields(fields)
super().__init__(choices=build_choices(self._fields))
# <strong i="7">@override</strong>
def filter(self, qs: models.QuerySet, value: typing.Union[typing.List[str], None]):
return qs if value in EMPTY_VALUES else qs.order_by(*(itertools.chain(*(self.__get_ordering_exprs(param) for param in value))))
def __get_ordering_exprs(self, param) -> typing.Union[None, typing.List[expressions.Expression]]:
descending = param.startswith('-')
param = param[1:] if descending else param
field_descriptor = self._fields.get(param)
return () if field_descriptor is None else \
field_descriptor.exprs if not descending else \
(expr.desc() for expr in field_descriptor.exprs)
def normalize_fields(fields: typing.Mapping[str, typing.Any]) -> typing.Mapping[str, 'FieldDescriptor']:
return dict((
param_name,
FieldDescriptor(param_name, field if isinstance(field, collections.Mapping) else {'exprs': normalize_exprs(field)})
) for param_name, field in fields.items())
def normalize_exprs(exprs: typing.Union[
typing.Union[str, expressions.Expression],
typing.List[typing.Union[str, expressions.Expression]]
]) -> typing.List[expressions.Expression]:
# `exprs` is either a single expression or a Sequence of expressions
exprs = exprs if isinstance(exprs, collections.Sequence) and not isinstance(exprs, str) else (exprs,)
return [normalize_expr(expr) for expr in exprs]
def normalize_expr(expr: typing.Union[str, expressions.Expression]) -> expressions.Expression:
return models.F(expr) if isinstance(expr, str) else expr
descending_fmt = _('%s (descending)')
class FieldDescriptor:
exprs: typing.List[models.Expression]
def __init__(self, param_name: str, data: typing.Mapping[str, typing.Any]):
exprs = data.get('exprs') or data.get('expr')
if not exprs:
raise ValueError("Expected 'exprs' or 'expr'")
self.exprs = normalize_exprs(exprs)
self.label = data.get('label', _(pretty_name(param_name)))
self.desc_label = data.get('desc_label', descending_fmt.format(self.label))
def build_choices(fields: typing.Mapping[str, 'FieldDescriptor']):
choices = []
for param_name, field_descriptor in fields.items():
choices.append((param_name, field_descriptor.label))
choices.append((f'-{param_name}', field_descriptor.desc_label))
return choices
_์
๋ฐ์ดํธ_: normalize_fields
๋ฐ ๊ธฐํ ๋์ฐ๋ฏธ ๋ฉ์๋์ ๊ตฌํ์ด ํฌํจ๋์์ต๋๋ค.
์ข์. ๐
ํ ์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค.
Tbh ๋๋ ์ด๊ฒ์ ๋ํด ์๊ฐํ ์๊ฐ์ด ์์์ต๋๋ค.
์ฌ๊ธฐ์์ ๋น์ ์ ๋ชจ๋ ํ๋ฅญํ ์๊ฐ์์ ์ฐ๋ฆฌ๊ฐ ๋ง๋ค ์ ์๋ ์์ ๋ณํ์ ๋ํ ์ ์์ด ์์ต๋๊น? (์๋ง๋ PR๋ก ์์ผ๋ก ๋์๊ฐ๋ ๊ฒ์ด ๋ ์ฌ์ธ ์๋ ์์ต๋๋ค.)
์, ๊ทธ๋ฌํ ๋ณ๊ฒฝ ์ฌํญ์ ์ด์ ๋ฒ์ ๊ณผ ํธํ๋๋๋ก ํ๋ ๋ฐฉ๋ฒ์ด ์ฆ์ ๋ช ํํ์ง ์์ต๋๋ค. ์๊ฐํด ๋ด์ผ ํ ๊ฒ์ ๋๋ค. ๊ณง PR์ ๊ธฐ๋ํ์ง ๋ง์ญ์์ค(์ธํฐ๋ท์ด ๋ถ์กฑํ ๊ณณ์์ ํด๊ฐ๋ฅผ ๊ฐ ๊ฒ์ ๋๋ค).
๊ด์ฐฎ์. ๐
์๋๋ฅผ ํ์๊ฐ ์์ต๋๋ค. ์ฐ๋ฆฌ๊ฐ ์๊ฐํด ๋ณด๋ ๊ฒ์ด ์ข์ต๋๋ค.
@carltongibson , ์ข์ต๋๋ค. ์ฐ๋ฆฌ๋ ์ฝ๊ฐ ์๊ฐํ์ง๋ง OrderingFilter
์ ์ด์ ๊ตฌํ๊ณผ ์ ๊ตฌํ์ ๊ฒฐํฉํ๋ ์ ์ ํ ๋ฐฉ๋ฒ์ ์ฐพ์ง ๋ชปํ์ต๋๋ค.
๋ชจ๋ ๊ฒ์ ํ๋์ ํด๋์ค์ ๋ฃ์ผ๋ ค๋ ๊ฒฝ์ฐ ๊ฐ์ฅ ํฐ ๋ฌธ์ ๋ ์ด์ ๊ตฌํ์์ dict ํ๋๊ฐ model_field
- parameter_name
์์ ์ ์ฅํ๋ ๋ฐ๋ฉด ์ ๊ตฌํ์์๋ ๋ฐ๋ parameter_name
- model_field
( model_field
๋์ ์ ํํ์์ ์ ๋ฌํ ์ ์์ผ๋ฏ๋ก ๊ฐ์๋ง ์ ์ฅํ ์ ์์ง๋ง ์ฌ์ ์ ํค์๋ ์ ์ฅํ ์ ์๊ธฐ ๋๋ฌธ์ ๋ฐ๋์ ๊ทธ๋์ผ ํจ) .
๊ธฐ์ ์ ์ผ๋ก ์ฌ์ ์ ๋ฌธ์์ด๋ง ํฌํจ๋ ๊ฒฝ์ฐ "์ด์ ๋ฐฉ์์ผ๋ก" ํด์ํ๊ณ ๊ทธ๋ ์ง ์์ ๊ฒฝ์ฐ "์๋ก์ด ๋ฐฉ์์ผ๋ก" ํด์ํ์ฌ ์ด ๋ฌธ์ ๋ฅผ ๊ทน๋ณตํ๋ ๊ฒ์ด ๊ฐ๋ฅํฉ๋๋ค. ์ด ๊ฒฝ์ฐ ์ฌ์ฉ์๋ ํํ์์ ๊ธฐ์ค์ผ๋ก ์ ๋ ฌํด์ผ ํ ๋ ์ฌ์ ํญ๋ชฉ์ "๋ค์ง์ด์ผ" ํฉ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์์ ํํ์ด ์ ๊ฑฐ๋๋ฉด ์ฌ์ ํญ๋ชฉ์ "๋ค์ง๊ธฐ"์ ์ฃผ์ํ์ญ์์ค... ์ด๊ฒ์ ๋์๊ฒ ๋์ฐํ ์ฌ์ฉ์ ๊ฒฝํ์ฒ๋ผ ๋ค๋ฆฝ๋๋ค. ๊ทธ๊ฒ์ ๋ํ ๊ตฌํ์ ๋ณต์กํ๊ฒ ๋ง๋ค ๊ฒ์ ๋๋ค.
ExpressionOrderingFilter
์ ๊ฐ์ด ๋ค๋ฅธ ์ด๋ฆ์ผ๋ก ์ OrderingFilter๋ฅผ ์ด์ ๊ฒ๊ณผ ํจ๊ป ๋์
ํ๋ ๊ฒ์ด ๊ฐ๋ฅํ๋ค๊ณ ์๊ฐํ์ญ๋๊น?
๋ด๊ฐ ํ์ํ๋ ๋ฐ๋ก ๊ทธ ์๊ฐ์ ํ๋ฅญํฉ๋๋ค! @earshinov ๊ฐ์ฌํฉ๋๋ค!
์ด๋ป๊ฒ ํด์ผ ํ ๊น์?
MyModel.objects.all().order_by(F('price').desc(nulls_last=True))
์ฃผ๋ฌธ ํํฐ๋ฅผ ์ฌ์ฉํ์๊ฒ ์ต๋๊น?
o = ExpressionOrderingFilter(
fields={
'price': F('price').desc(nulls_last=True)
}
)
์ ์๋ํ๋ ๊ฒ ๊ฐ์ต๋๋ค!
์ด์ ๋จ์ ๊ฒ์ ์ฌ๋ฌ ํํฐ(ak)๋ฅผ ๊ฒฐํฉํ๋ ๋ฐฉ๋ฒ์ ์์๋ด๋ ๊ฒ๋ฟ์
๋๋ค. '๊ฐ๊ฒฉ'๊ณผ '์ฃผ์'์ ํ๋๋ก.
ach - ์ด์ ๋ํ ์๋ต์ ์์ํ์ง๋ง ๋ด ์ปดํจํฐ๊ฐ kaput ์ํ๊ฐ ๋์์ต๋๋ค.
์ฌ๊ธฐ์ ์ ์๋ ๋ณ๊ฒฝ ์ฌํญ์ด ํฉ๋ฆฌ์ ์ด๋ผ๊ณ ์๊ฐํฉ๋๋ค. model_field
- parameter_name
์์ ์ผ๋ฐ์ ์ผ๋ก ๋ชจ๋ธ์์ ๋
ธ์ถ๋ params/forms/etc๋ฅผ ํ์ํ๋ฏ๋ก ๋น์์๋ ์๋ฏธ๊ฐ ์์์ง๋ง ์ด๊ฒ์ด ํ์ํ ์ด์ ๋ ์์ต๋๋ค. ๋งคํ์ ๋ฐ๊พธ๋ ๊ฒ์ด ์๋ฏธ๊ฐ ์๊ณ ๋ ๋ณต์กํ ํํ์์ ํ์ฉํ ์ ์์ต๋๋ค.
๋ํ ์ฌ์ฉ ์ค๋จ ํ๋ก์ธ์ค๊ฐ ์์ฒญ๋๊ฒ ์ด๋ ค์ธ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ์ง ์์ผ๋ฉฐ ๊ธฐ๊บผ์ด ๋์๋๋ฆฌ๊ฒ ์ต๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก fields
๋ฐ field_labels
๋ params
๋ฐ param_labels
๋ก ์ ํ๋ฉ๋๋ค. ์ด์ ๋ฒ์ ๊ณผ์ ํธํ์ฑ์ ์ ์งํ๊ณ ์ด์ ์ธ์๋ฅผ ์ฌ์ฉํ๋ ์ฌ์ฉ์์๊ฒ ์ฌ์ฉ ์ค๋จ ์๋ฆผ์ ํ์ํ๋ฉด์ ํ๋์์ ๋ค๋ฅธ ๊ฒ์ผ๋ก ๋ณํํ๋ ๊ฒ์ ์ถฉ๋ถํ ์ฝ์ต๋๋ค.
ํ ๊ฐ์ง ๊ณ ๋ คํด์ผ ํ ์ฌํญ์ ๋ด๋ฆผ์ฐจ์ ๋์๋ฌธ์( param
vs -param
)์ ๋ํ ๋ณต์กํ ํํ์์ ์๋ ๋ณํ์
๋๋ค. ์๋ฅผ ๋ค์ด, ์ค๋ฆ์ฐจ์์ด .asc(nulls_last=True)
์ธ ๊ฒฝ์ฐ, ์ด๊ฒ์ ์ญํจ์๋ .desc(nulls_first=True)
์ด์ด์ผ ํฉ๋๊น, ์๋๋ฉด ์ ๋ ฌ ๋ฐฉํฅ์ ๊ด๊ณ์์ด null์ด ๋ง์ง๋ง์ผ๋ก ๋จ์ ์์ด์ผ ํฉ๋๊น?
์ข์, ์ํผ @rpkilby.
์ฐ๋ฆฌ๊ฐ ์ฌ๊ธฐ์ ์์ผ๋ก ๋์๊ฐ๋ ๊ฒ์ ๋ณด๊ฒ ๋์ด ๊ธฐ์ฉ๋๋ค. ๋์ ์ฃผ์ ๊ด์ฌ์ฌ๋ ์ฐ๋ฆฌ๊ฐ ํ๋ ์ผ์ ์ ๋๋ก ๋ฌธ์ํํ๋ ๊ฒ์ ๋๋ค. ์ฌ์ฉ์๋ ์ด๋ฏธ df ๋ฌธ์๋ฅผ ์กฐ๊ธ _๊ฐ๋จํ๊ฒ_ ์ฐพ์์ต๋๋ค. ๐ โ API๋ฅผ ์ถ๊ฐํ๊ฒ ๋์ด ๊ธฐ์์ง๋ง ๋ช ํํด์ผ ํฉ๋๋ค.
๊ทธ๋ฐ ์ผ์ ํ ์ฌ๋์ ๋ ธ๋ ฅ์ด ํ์ํ์ง ์์ต๋๋ค.
@rpkilby , @carltongibson , ๋ด ๋ณ๊ฒฝ ์ฌํญ์ ํ๋ก์ ํธ์ ํตํฉํ๋ ๋ฐ ๋์์ด ํ์ํ๋ฉด ์๊ฐ์ ํ ์ ํ ์ ์์ต๋๋ค. ํ์ง๋ง ๋ฐฉํฅ์ด ํ์ํฉ๋๋ค. ์ด๋์ ์ด๋ป๊ฒ ์์ํฉ๋๊น?
์๋ ํ์ธ์ @earshinov์ ๋๋ค. ๋ฌธ์ ์ด์์ ์์ฑํ๋ ๊ฒ์ผ๋ก ์์ํ๊ฒ ์ต๋๋ค. ์ฐ๋ฆฌ๊ฐ ํ๋ ์ด์ผ๊ธฐ๋ ๋ฌด์์ ๋๊น? ๊ฑฐ๊ธฐ์์ ์ฝ๋๊ฐ ๋ณ๊ฒฝ๋ฉ๋๋ค. PR, ๋น๋ก ๋จ์ง ์ด์์ด ์ฐ๋ฆฌ์๊ฒ ์ด์ผ๊ธฐํ ๋ฌด์ธ๊ฐ๋ฅผ ์ ๊ณตํ๋๋ผ๋.
๋ํ ํ
์คํธ ์ผ์ด์ค๊ฐ ์์ผ๋ฉด ํ
์คํธํ์ญ์์ค. ์ต์ข
๋ณ๊ฒฝ ์ฌํญ๊ณผ ์ผ์นํ๋๋ก ์กฐ์ ํด์ผ ํ ์๋ ์์ง๋ง ๋ค์ํ ๊ฒฝ์ฐ๋ฅผ ๊ณ ๋ คํ๋ฉด ๋งค์ฐ ๋์์ด ๋ฉ๋๋ค(์: .asc
๋ฅผ .desc
, ์ฒ๋ฆฌ nulls_fist
/ nulls_last
์ธ์ ๋ฑ).
๊ฐ์ฅ ์ ์ฉํ ๋๊ธ
ach - ์ด์ ๋ํ ์๋ต์ ์์ํ์ง๋ง ๋ด ์ปดํจํฐ๊ฐ kaput ์ํ๊ฐ ๋์์ต๋๋ค.
์ฌ๊ธฐ์ ์ ์๋ ๋ณ๊ฒฝ ์ฌํญ์ด ํฉ๋ฆฌ์ ์ด๋ผ๊ณ ์๊ฐํฉ๋๋ค.
model_field
-parameter_name
์์ ์ผ๋ฐ์ ์ผ๋ก ๋ชจ๋ธ์์ ๋ ธ์ถ๋ params/forms/etc๋ฅผ ํ์ํ๋ฏ๋ก ๋น์์๋ ์๋ฏธ๊ฐ ์์์ง๋ง ์ด๊ฒ์ด ํ์ํ ์ด์ ๋ ์์ต๋๋ค. ๋งคํ์ ๋ฐ๊พธ๋ ๊ฒ์ด ์๋ฏธ๊ฐ ์๊ณ ๋ ๋ณต์กํ ํํ์์ ํ์ฉํ ์ ์์ต๋๋ค.๋ํ ์ฌ์ฉ ์ค๋จ ํ๋ก์ธ์ค๊ฐ ์์ฒญ๋๊ฒ ์ด๋ ค์ธ ๊ฒ์ด๋ผ๊ณ ์๊ฐํ์ง ์์ผ๋ฉฐ ๊ธฐ๊บผ์ด ๋์๋๋ฆฌ๊ฒ ์ต๋๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก
fields
๋ฐfield_labels
๋params
๋ฐparam_labels
๋ก ์ ํ๋ฉ๋๋ค. ์ด์ ๋ฒ์ ๊ณผ์ ํธํ์ฑ์ ์ ์งํ๊ณ ์ด์ ์ธ์๋ฅผ ์ฌ์ฉํ๋ ์ฌ์ฉ์์๊ฒ ์ฌ์ฉ ์ค๋จ ์๋ฆผ์ ํ์ํ๋ฉด์ ํ๋์์ ๋ค๋ฅธ ๊ฒ์ผ๋ก ๋ณํํ๋ ๊ฒ์ ์ถฉ๋ถํ ์ฝ์ต๋๋ค.ํ ๊ฐ์ง ๊ณ ๋ คํด์ผ ํ ์ฌํญ์ ๋ด๋ฆผ์ฐจ์ ๋์๋ฌธ์(
param
vs-param
)์ ๋ํ ๋ณต์กํ ํํ์์ ์๋ ๋ณํ์ ๋๋ค. ์๋ฅผ ๋ค์ด, ์ค๋ฆ์ฐจ์์ด.asc(nulls_last=True)
์ธ ๊ฒฝ์ฐ, ์ด๊ฒ์ ์ญํจ์๋.desc(nulls_first=True)
์ด์ด์ผ ํฉ๋๊น, ์๋๋ฉด ์ ๋ ฌ ๋ฐฉํฅ์ ๊ด๊ณ์์ด null์ด ๋ง์ง๋ง์ผ๋ก ๋จ์ ์์ด์ผ ํฉ๋๊น?