Olá!
Foi um prazer usar esta biblioteca! Estou tão entusiasmado com toda a conveniência que ele oferece!
Obrigado por isso!
Meu objetivo agora é ter um ponto de extremidade que suporte a capacidade de pesquisa em parâmetros de modelo e ordenação também.
Eu tenho um modelo School
que tem um campo calculado chamado learner_enrolled_count
A resposta JSON é semelhante a esta:
{
"schools": [
{
"id": 6,
"name": "Piano Gym Six",
"courses": [
// ...
],
"learner_enrolled_count": 0
},
{
"id": 7,
"name": "Piano Gym Seven",
"courses": [
// ...
],
"learner_enrolled_count": 5
}
]
}
O learner_enrolled_count
é um campo calculado.
Eu li a documentação aqui:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#orderingfilter
e aqui:
https://django-filter.readthedocs.io/en/stable/ref/filters.html?highlight=order#adding -custom-filter-choices
Com base nisso, escrevi este conjunto de filtros aqui:
# ------------------------------------------------------------------------------
# Python Standard Libraries
# ------------------------------------------------------------------------------
# N/A
# ------------------------------------------------------------------------------
# Third-party Libraries
# ------------------------------------------------------------------------------
from django_filters import CharFilter
from django_filters import OrderingFilter
from django_filters.rest_framework import FilterSet
# ------------------------------------------------------------------------------
# Custom Libraries
# ------------------------------------------------------------------------------
from piano_gym_api.versions.v1.models.school_model import SchoolModel
# See:
# https://django-filter.readthedocs.io/en/stable/ref/filters.html#adding-custom-filter-choices
class SchoolOrderingFilter(OrderingFilter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.extra["choices"] += [
("learner_enrolled_count", "Learner Enrolled Count"),
("-learner_enrolled_count", "Learner Enrolled Count (descending)"),
]
def filter(self, query_set, values):
if(values is None):
return super().filter(query_set, values)
for value in values:
if value in ['learner_enrolled_count', '-learner_enrolled_count']:
return query_set.order_by(value)
return super().filter(query_set, values)
class SchoolFilter(FilterSet):
school_name = CharFilter(field_name="name",
lookup_expr="icontains")
# ---
course_name = CharFilter(field_name="school_course__name",
lookup_expr="icontains")
course_description = CharFilter(field_name="school_course__description",
lookup_expr="icontains")
# ---
lesson_name = CharFilter(field_name="school_lesson__name",
lookup_expr="icontains")
lesson_description = CharFilter(field_name="school_lesson__description",
lookup_expr="icontains")
# ---
flash_card_set_name = CharFilter(field_name="school_lesson__flash_card_set__name",
lookup_expr="icontains")
flash_card_set_description = CharFilter(field_name="school_lesson__flash_card_set__description",
lookup_expr="icontains")
# ---
headmaster_username = CharFilter(field_name="school_board__school_board_headmaster__learner__user__username",
lookup_expr="icontains")
board_member_username = CharFilter(field_name="school_board__school_board_member__learner__user__username",
lookup_expr="icontains")
# See:
# https://django-filter.readthedocs.io/en/stable/ref/filters.html#orderingfilter
o = SchoolOrderingFilter(
# tuple-mapping retains order
fields=(
("learner_enrolled_count", "learner_enrolled_count"),
),
# labels do not need to retain order
field_labels={
"learner_enrolled_count": "Total learners enrolled in school",
}
)
class Meta:
model = SchoolModel
fields = [
"school_name",
# ---
"course_name",
"course_description",
# ---
"lesson_name",
"lesson_description",
# ---
"headmaster_username",
"board_member_username"
]
Esse problema é que ele não parece estar pedindo nada! Eu não tenho ideia do porquê. É tão estranho.
Se eu soltar um rastreamento de depuração no método filter
de SchoolOrderingFilter
, vejo que values
é None
. Não tenho certeza do que deveria ser.
O pedido que estou fazendo é parecido com este
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count
E a vista que recebe essa solicitação tem a seguinte aparência:
class SchoolViewSet(ViewSet):
# ...
def list(self, request):
all_school_models = SchoolModel.objects.getBrowseSchoolsData()
school_filter = SchoolFilter(request.GET, queryset=all_school_models)
paginator = HeaderLinkPagination()
current_page_results = paginator.paginate_queryset(school_filter.qs,
request)
all_schools_serializer = SchoolSerializer(current_page_results,
many=True)
response_data = {
"schools": all_schools_serializer.data
}
response_to_return = paginator.get_paginated_response(response_data)
return response_to_return
Acho que não está muito claro para mim na documentação sobre como usar o recurso de filtragem E como usar a ordenação de campos calculada.
O que estou fazendo de errado? Estou entendendo mal essa funcionalidade? Sinto que estou executando as etapas corretas para isso, mas simplesmente não consigo fazer com que a funcionalidade ordering
desta biblioteca funcione!
Mais uma vez, obrigado por tudo!
Olá, @loganknecht. Eu tenho que dizer, obrigado por um relatório de problema tão maravilhoso. Começa bem e fica ainda melhor. Tão claro. Então, obrigada.
Primeiro: parece que você definiu SchoolOrderingFilter
com o nome do campo o
:
o = SchoolOrderingFilter(...)
Então, quando você diz:
O pedido que estou fazendo é parecido com este
{{API_URL}}/api/v1/schools/?offset=5&limit=3&ordering=learner_enrolled_count
Estou esperando que o parâmetro da string de consulta seja o
também. Isso funciona: {{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count
?
Então (a parte que não tenho certeza em seu relatório) você diz que é um _campo computado_ - o que exatamente você quer dizer? ou seja, é um campo que aparece no modelo ou é uma propriedade Python, digamos?
Outra maneira de perguntar (parte de) é que order_by()
no queryset funciona com este campo? ou seja, isso funciona:
SchoolModel.objects.filter(...).order_by(learner_enrolled_count)
@carltongibson Isso é bem interessante! Não esperava que o
fosse o parâmetro ordering
definido! Eu não entendi isso da documentação 😂
o
O que é interessante é se eu usar a consulta
{{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count
Eu obtenho estes resultados:
{
"schools": [
{
"id": 6,
"name": "Piano Gym Six",
"courses": [
# ...
],
"learner_enrolled_count": 0
},
{
"id": 8,
"name": "Piano Gym Eight",
"courses": [
# ...
],
"learner_enrolled_count": 0
},
{
"id": 9,
"name": "Piano Gym Nine",
"courses": [
# ...
],
"learner_enrolled_count": 0
}
]
}
A parte interessante sobre isso, agora é que a escola que eu quero que seja ordenada no topo não está neste resultado de pesquisa. O que significa que provavelmente foi pedido, mas a paginação ainda está compensando os resultados. No entanto, quando usei learner_enrolled_count
ele está no último lugar.
{{API_URL}}/api/v1/schools/?limit=3&o=learner_enrolled_count
Então eu testei usando -learner_enrolled_count
(removendo o deslocamento e classificando em ordem decrescente)
{{API_URL}}/api/v1/schools/?limit=3&o=-learner_enrolled_count
e recebi esta resposta
{
"schools": [
{
"id": 7,
"name": "Piano Gym Seven",
"courses": [
# ...
],
"learner_enrolled_count": 1
},
{
"id": 1,
"name": "Piano Gym",
"courses": [
# ...
],
"learner_enrolled_count": 0
},
{
"id": 2,
"name": "Piano Gym Two",
"courses": [
# ...
],
"learner_enrolled_count": 0
}
]
}
Parece que funcionou!
learner_enrolled_count
não existe no próprio modelo. Usei uma anotação para calcular isso e injetá-la nos resultados.
Correndo o risco de mergulhar muito no modelo de dados, este ponto de extremidade deve ser um ponto de entrada único para filtrar e ordenar consultas para uma lista de escolas.
Tenho que otimizar este endpoint usando a recuperação de modelo assim:
class SchoolModelManager(Manager):
def getBrowseSchoolsData(self, *args, **kwargs):
"""Return all Schools that contain lessons with flash card sets.
Does not exclude empty sets, just requires that the school has something
to enroll in
"""
# WARNING: This MUST be imported here otherwise the compilation fails
# because of circular dependencies
from piano_gym_api.versions.v1.models.flash_card_model import FlashCardModel
# from piano_gym_api.versions.v1.models.flash_card_model import PlaySheetMusicFlashCardModel
# from piano_gym_api.versions.v1.models.flash_card_model import TrueOrFalseFlashCardModel
# from piano_gym_api.versions.v1.models.flash_card_set_model import FlashCardSetModel
from piano_gym_api.versions.v1.models.sheet_music_model import SheetMusicModel
# import pdb
# pdb.set_trace()
# --------------------
sheet_music_query_set = (SheetMusicModel.objects.all()
.select_related("school"))
play_sheet_music_flash_card_sheet_music_prefetch = Prefetch("playsheetmusicflashcardmodel__sheet_music",
sheet_music_query_set)
# --------------------
flash_card_query_set = (FlashCardModel.objects.all()
.select_related("flash_card_set",
"playsheetmusicflashcardmodel",
"school",
"trueorfalseflashcardmodel")
.prefetch_related(play_sheet_music_flash_card_sheet_music_prefetch))
flash_card_prefetch = Prefetch("flash_card_set__flash_card", flash_card_query_set)
# --------------------
school_lesson_query_set = (SchoolLessonModel.objects.all()
.select_related("course",
"flash_card_set",
"school")
.prefetch_related(flash_card_prefetch))
school_lesson_prefetch = Prefetch("school_lesson", school_lesson_query_set)
# --------------------
school_course_query_set = (SchoolCourseModel.objects.all()
.select_related("school")
.prefetch_related(school_lesson_prefetch))
school_course_prefetch = Prefetch("school_course", school_course_query_set)
# --------------------
query_set_to_return = (SchoolModel.objects.filter(school_lesson__flash_card_set__isnull=False)
.distinct()
# .annotate(learner_enrolled_count=Count("learner_enrolled_school", distinct=True))
.annotate(learner_enrolled_count=Case(
When(learner_enrolled_school__learner_enrolled_course__learner_enrolled_lesson__is_enrolled=True,
then=1),
default=0,
output_field=IntegerField())
).prefetch_related(school_course_prefetch))
return query_set_to_return
O bit calculated field
está na variável query_set_to_return
. O que ele faz é levar count
do total de alunos matriculados e annotates
para o campo learner_enrolled_count
. Não tenho ideia se essa é a maneira correta de fazer isso, mas parece funcionar 😂
Uma sugestão que eu faria é esclarecer na documentação, usando exemplos reais de url para solicitações ou texto simples e explicar que o
nos exemplos é o que está exposto como um parâmetro para pedido.
Parece estar funcionando corretamente agora que você esclareceu que o parâmetro o
é o que o filtro de ordenação é atribuído!
Se querysets
que estou fornecendo para filter
já tiverem um campo anotado - parece, a partir de meus testes, que não sou obrigado a fazer meu próprio OrderingFilter
.
Então, por causa disso, acho que posso usar um simples OrderingFilter
vez disso!
Por favor me corrija se eu estiver errado!
Acredito que isso resolve minha confusão! Obrigado!
Olá, @loganknecht. Ótimo, parece que está funcionando. 💃
Se você estiver usando uma anotação, deverá ser capaz de filtrar e ordenar com a classe padrão sim.
Vou fazer um ajuste nos documentos.
Obrigado pela sua contribuição.
@carltongibson e todos os outros, obrigado pela biblioteca incrível!
Comentários muito úteis
Olá, @loganknecht. Eu tenho que dizer, obrigado por um relatório de problema tão maravilhoso. Começa bem e fica ainda melhor. Tão claro. Então, obrigada.
Primeiro: parece que você definiu
SchoolOrderingFilter
com o nome do campoo
:Então, quando você diz:
Estou esperando que o parâmetro da string de consulta seja
o
também. Isso funciona:{{API_URL}}/api/v1/schools/?offset=5&limit=3&o=learner_enrolled_count
?Então (a parte que não tenho certeza em seu relatório) você diz que é um _campo computado_ - o que exatamente você quer dizer? ou seja, é um campo que aparece no modelo ou é uma propriedade Python, digamos?
Outra maneira de perguntar (parte de) é que
order_by()
no queryset funciona com este campo? ou seja, isso funciona: