Django filter testing - django

class BusinessPartnerFilter(SilBaseFilter):
active = django_filters.BooleanFilter(
name='date_deactivated', lookup_expr='isnull')
parent_name = django_filters.CharFilter(name='parent__name')
unmapped = django_filters.BooleanFilter(method='check_if_unmapped')
I have added the field 'unmapped' above and created the method filter below. Can someone please help me to write tests for the filter. I'm stuck.
class Meta(object):
model = models.BusinessPartner
fields = [
'name', 'bp_type', 'slade_code', 'parent', 'national_identifier',
'active', 'parent_name', 'unmapped'
]
def check_if_unmapped(self, queryset, field, value):
if value:
exclude_bps = [record.id for record in queryset if record.mapped == 0 and record.unmapped == 0]
return queryset.exclude(id__in=exclude_bps)
return queryset

You can either test the filter method in isolation, or test the evaluation of FilterSet.qs.
To test the filter method, you don't necessarily need a fully initialized FilterSet.
qs = BusinessPartner.objects.all()
f = BusinessPartnerFilter()
result = f.check_if_unmapped(qs, 'unmapped', True)
# assert something about the result
That said, it's not much more difficult to fully initialize the FilterSet and check the .qs.
qs = BusinessPartner.objects.all()
f = BusinessPartnerFilter(data={'unmapped': 'True'}, queryset=qs)
result = f.qs
# assert something about the result

Related

Django ORM distinct on only a subset of the queryset

Working in Django Rest Framework (DRF), django-filter, and PostgreSQL, and having an issue with one of our endpoints.
Assume the following:
# models.py
class Company(models.Model):
name = models.CharField(max_length=50)
class Venue(models.Model):
company = models.ForeignKey(to="Company", on_delete=models.CASCADE)
name = models.CharField(max_length=50)
# create some data
company1 = Company.objects.create(name="Proper Ltd")
company2 = Company.objects.create(name="MyCompany Ltd")
Venue.objects.create(name="Venue #1", company=company1)
Venue.objects.create(name="Venue #2", company=company1)
Venue.objects.create(name="Property #1", company=company2)
Venue.objects.create(name="Property #2", company=company2)
# viewset
class CompanyViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CompanyVenueSearchSerializer
queryset = (
Venue.objects.all()
.select_related("company")
.order_by("company__name")
)
permission_classes = (ReadOnly,)
http_method_names = ["get"]
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = CompanyVenueListFilter
pagination_class = None
# filterset
class CompanyVenueListFilter(filters.FilterSet):
text = filters.CharFilter(method="name_search")
def name_search(self, qs, name, value):
return qs.filter(
Q(name__icontains=value)
| Q(company__name__icontains=value)
)
class Meta:
model = Venue
fields = [
"name",
"company__name",
]
# serializer
class CompanyVenueSearchSerializer(serializers.ModelSerializer):
company_id = serializers.IntegerField(source="company.pk")
company_name = serializers.CharField(source="company.name")
venue_id = serializers.IntegerField(source="pk")
venue_name = serializers.CharField(source="name")
class Meta:
model = Venue
fields = (
"company_id",
"company_name",
"venue_id",
"venue_name",
)
We now want to allow the user to filter the results by sending a query in the request, e.g. curl -X GET https://example.com/api/company/?text=pr.
The serializer result will look something like:
[
{
"company_id":1,
"company_name":"Proper Ltd",
"venue_id":1,
"venue_name":"Venue #1"
},
{ // update ORM to exclude this dict
"company_id":1,
"company_name":"Proper Ltd",
"venue_id":2,
"venue_name":"Venue #1"
},
{
"company_id":2,
"company_name":"MyCompany Ltd",
"venue_id":3,
"venue_name":"Property #1"
},
{
"company_id":2,
"company_name":"MyCompany Ltd",
"venue_id":4,
"venue_name":"Property #1"
}
]
Expected result:
Want to rewrite the ORM query so that if the filter ("pr") matches the venue__name, return all venues. But if the filter matches the company__name, only return it once, thus in the example above the second dict in the list would be excluded/removed.
Is this possible?
What you can do is to filter Company that matches name filtering and annotate them with the first related Venue and then combine it's results with the second requirement to return venue with name=value
from django.db.models import OuterRef, Q, Subquery
value = "pr"
first_venue = Venue.objects.filter(company__in=OuterRef("id")).order_by("id")
company_qs = Company.objects.filter(name__icontains=value).annotate(
first_venue_id=Subquery(first_venue.values("id")[:1])
)
venue_qs = Venue.objects.filter(
Q(name__icontains=value)
| Q(id__in=company_qs.values_list("first_venue_id", flat=True))
)
The query executed when accessing values of venue_qs looks like
SELECT
"venues_venue"."id",
"venues_venue"."company_id",
"venues_venue"."name"
FROM
"venues_venue"
WHERE
(
UPPER("venues_venue"."name"::TEXT) LIKE UPPER(% pr %)
OR "venues_venue"."id" IN (
SELECT
(
SELECT
U0."id"
FROM
"venues_venue" U0
WHERE
U0."company_id" IN (V0."id")
ORDER BY
U0."id" ASC
LIMIT
1
) AS "first_venue_id"
FROM
"venues_company" V0
WHERE
UPPER(V0."name"::TEXT) LIKE UPPER(% pr %)
)
)
This is how the filter should look like
class CompanyVenueListFilter(filters.FilterSet):
text = filters.CharFilter(method="name_search")
def name_search(self, qs, name, value):
first_venue = Venue.objects.filter(company__in=OuterRef("id")).order_by("id")
company_qs = Company.objects.filter(name__icontains=value).annotate(
first_venue_id=Subquery(first_venue.values("id")[:1])
)
return qs.filter(
Q(name__icontains=value)
| Q(id__in=company_qs.values_list("first_venue_id", flat=True))
)
class Meta:
model = Venue
fields = [
"name",
"company__name",
]
Update for Django 3.2.16
Seems like the query above will not work for such version because it generated a query without parentheses in WHERE clause around V0."id", chunk of query looks like
WHERE
U0."company_id" IN V0."id"
and it makes PostgreSQL complain with error
ERROR: syntax error at or near "V0"
LINE 17: U0."company_id" IN V0."id"
For Django==3.2.16 the filtering method in CompanyVenueListFilter could look like following:
def name_search(self, qs, name, value):
company_qs = Company.objects.filter(name__icontains=value)
venues_qs = (
Venue.objects.filter(company__in=company_qs)
.order_by("company_id", "id")
.distinct("company_id")
)
return qs.filter(Q(name__icontains=value) | Q(id__in=venues_qs.values_list("id")))
The answer is based on other stackoverflow answer and django docs
Django manager annotate first element of m2m as fk
Subquery() expressions
We have a temporary solution, which we're a bit wary about but it seems to do its job. Won't tag this answer as accepted as we're still hoping that someone has a more pythonic/djangoistic solution to the problem.
# viewset
class CompanyViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = CompanyVenueSearchSerializer
queryset = (
Venue.objects.all()
.select_related("company")
.order_by("company__name")
)
permission_classes = (ReadOnly,)
http_method_names = ["get"]
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = CompanyVenueListFilter
pagination_class = None
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(queryset, many=True)
text = request.GET.get("text").lower()
first_idx = 0
to_remove = []
for data in serializer.data:
if text in data.get("name").lower() and text not in data.get("venue_name").lower():
if data.get("id") != first_idx:
"""We don't want to remove the first hit of a company whose name matches"""
first_idx = data.get("id")
continue
to_remove.append((data.get("id"), data.get("venue_id")))
return Response(
[
data
for data in serializer.data
if (data.get("id"), data.get("venue_id")) not in to_remove
],
status=status.HTTP_200_OK,
)

Custom FilterSet doesn't filter by two fields at the same time

I wrote custom FilterSet to filter queryset by two fields but it doesn't work properly when it's filtering on two fields at the same time.
my FilterSet:
class EventFilter(filters.FilterSet):
values = None
default = None
category = filters.ModelMultipleChoiceFilter(
queryset=EventCategory.objects.all(),
)
interval = filters.CharFilter(
method='filter_interval'
)
class Meta:
model = Event
fields = ('category', 'interval')
def filter_interval(self, queryset, name, value):
if self.request.query_params.get('current_time'):
try:
interval = getattr(self, f'get_{value}_interval')()
interval = list(map(lambda date: self.to_utc(date), interval))
return self.queryset.filter(Q(status=Event.STARTED) | (Q(status=Event.NOT_STARTED, start_at__range=interval)))
except Exception as e:
pass
return queryset
APIView:
class ListEventsAPIView(generics.ListAPIView):
serializer_class = ListEventsSerializer
filter_class = EventFilter
search_fields = 'title',
filter_backends = filters.SearchFilter, DjangoFilterBackend
def get_queryset(self):
return Event.objects.filter(Q(status=Event.STARTED) | (Q(status=Event.NOT_STARTED) & Q(start_at__gte=date)))
Here is generated SQL when I'm trying to filter only by category:
SELECT "*" FROM "events" WHERE (("events"."status" = 'started'
OR ("events"."status" = 'not_started'
AND "events"."start_at" >= '2019-06-19T13:24:26.444183+00:00'::timestamptz))
AND "events"."category_id" = 'JNPIZF54n5q')
When I'm filtering on both:
SELECT "*" FROM "events" WHERE (("events"."status" = 'started'
OR ("events"."status" = 'not_started' AND "events"."start_at" >= '2019-06-19T13:24:26.444183+00:00'::timestamptz))
AND ("events"."status" = 'started' OR ("events"."start_at" BETWEEN '2019-06-19T07:16:48.549000+00:00'::timestamptz AND '2019-06-30T20:59:59.000059+00:00'::timestamptz AND "events"."status" = 'not_started')))
Your issue is in this line:
return self.queryset.filter(Q(status=Event.STARTED) | (Q(status=Event.NOT_STARTED, start_at__range=interval)))
You're using queryset from FilterSet class itself. This queryset doesn't have any previous filters applied, so by using it you're cancelling another filter. Just remove self. from this line to use queryset that is passed to this function as a parameter and everything will work fine.

View Set - filter objects

I have a problem with filtering objects in View Set... I am trying to show objects only where field 'point' is null.
I always get error: NameError: name 'null' is not defined
Could you please HELP ME ?
My code:
class CompanyMapSerializer(serializers.ModelSerializer):
class Meta:
model = Company
fields = ('name', 'point', 'url', 'pk')
extra_kwargs = {
'url': {'view_name': 'api:company-detail'},
}
def to_representation(self, instance):
ret = super(CompanyMapSerializer, self).to_representation(instance)
ret['point'] = {
'latitude': instance.point.x,
'longitude': instance.point.y
}
return ret
And view set code:
class CompanyMapViewSet(viewsets.ModelViewSet):
queryset = Company.objects.filter(point = null)
serializer_class = CompanyMapSerializer
PageNumberPagination.page_size = 10000
Please help me.
You are not defining what null is, and Python doesn't recognize null as a primitive, you've got two options:
queryset = Company.objects.filter(point = None) # using None
queryset = Company.objects.filter(point__isnull = True) # explicitly asking for Null
These two queries are equally valid.

How override Flask-Admin's edit_form() maintaining previous values as placeholders

I'm tryin to override Flask-Admin's edit_form() in order to dynamically populate a SelectField. I managed to do so this way
class ProductForm(Form):
order = IntegerField('order')
name = TextField('name')
category = SelectField('category', choices=[])
image = ImageUploadField(label='Optional image',
base_path=app.config['UPLOAD_FOLDER_ABS'],
relative_path=app.config['UPLOAD_FOLDER_R'],
max_size=(200, 200, True),
endpoint='images',
)
class ProductsView(MyModelView):
create_template = 'admin/create-products.html'
edit_template = 'admin/edit-products.html'
column_list = ('order', 'name', 'category', 'image')
form = ProductForm
column_default_sort = 'order'
def edit_form(self, obj):
form = self._edit_form_class(get_form_data(), obj=obj)
cats = list(db.db.categories.find())
cats.sort(key=lambda x: x['order'])
sorted_cats = [(cat['name'], cat['name']) for cat in cats]
form.category.choices = sorted_cats
form.image.data = obj['image']
return form
The problem is now the form in the /edit/ view defaults name and order fields to empty unless i add these two lines to edit_form():
form.name.data = obj['name']
form.order.data = obj['order']
But if i do so the form will ignore every change (because i set form.field_name.data already?)
How do I preserve the old form values as "placeholders" while correctly overriding edit_form()?
I had a similar problem and thanks to the answer here I solved it;
Basically you set the default for the fields, and then call the individual fields process() method, passing in the currently set value.
from wtforms.utils import unset_value
form.name.default = obj['name']
form.name.process(None, form.name.data or unset_value)
form.order.default = obj['order']
form.order.process(None, form.order.data or unset_value)

replacing field in queryset before return

I have the following view -
class DeployFilterView(generics.ListAPIView):
serializer_class = DefinitionSerializer
def get_queryset(self):
jobname = self.request.GET.get('jobname')
if jobname.count("\\") == 1:
jobname = jobname.replace("\\", "")
queryset = Jobmst.objects.db_manager('Admiral').filter(jobmst_name=jobname).exclude(jobmst_prntname__isnull=False, jobmst_dirty='X')
else:
parent, job = jobname.rsplit('\\', 1)
queryset = Jobmst.objects.db_manager('Admiral').filter(jobmst_prntname=parent, jobmst_name=job).exclude(jobmst_dirty='X')
return queryset
In that view there's a value field called "jobmst_runbook" which has a character that doesn't translate with the DRF XML renderer. What I'd like to be able to do is scan the queryset for a particular character - SOH or \u0001
If it finds this character I want to remove it before doing the return queryset
I solved this by doing the logic in my serializer. The serializer is now looking for the object that's causing the failure and stripping out the character.
class DefinitionSerializer(serializers.ModelSerializer):
runbook_url = serializers.SerializerMethodField('get_url')
# dependencies = serializers.RelatedField(many=True)
jobdep = serializers.HyperlinkedRelatedField(
source='jobdep_set', # this is the model class name (and add set, this is how you call the reverse relation of bar)
view_name='jobdep-detail' # the name of the URL, required
)
# triggers = serializers.RelatedField(many=True)
trgmst = serializers.HyperlinkedRelatedField(
source='trgmst_set', # this is the model class name (and add set, this is how you call the reverse relation of bar)
view_name='trgmst-detail' # the name of the URL, required
)
class Meta:
model = Jobmst
resource_name = 'jobmst'
depth = 2
fields = ('jobmst_id', 'jobmst_type', 'jobmst_prntid', 'jobmst_active', 'evntmst_id',
'jobmst_evntoffset', 'jobmst_name', 'jobmst_mode', 'jobmst_owner', 'jobmst_desc',
'jobmst_crttm', 'jobdtl_id', 'jobmst_lstchgtm', 'runbook_url', 'jobcls_id', 'jobmst_prntname',
'jobmst_alias', 'jobmst_dirty', 'job_dependencies', 'job_events')
def get_url(self, obj):
if obj.jobmst_runbook == None:
pass
else:
return force_text(obj.jobmst_runbook[:-5])