I have a model like this:
class MyModel(models.Model):
details = models.JSONField()
# other fields
I want to annotate some fields from this model like this:
qs = MyModel.objects.filter(id__in=given_list).annotate(
first_name=F('details__first_name'),
last_name=F('details__last_name')
)
However the F() expression is not considering the json keys, its just returning the details field only.
I am using MySQL, so cannot use KeyTextTransform.
I tried using RawSQL like this:
qs = MyModel.objects.filter(id__in=given_list).annotate(
first_name=RawSQL("(details->%s)", ('first_name',)),
last_name=RawSQL("(details->%s)", ('last_name',))
)
But it was giving this error:
MySQLdb._exceptions.OperationalError: (3143, 'Invalid JSON path expression. The error is around character position 1.')
So what can I do to make everything work as expected?
You can use JSONExtract, it will be easier to write and understand:
from django_mysql.models.functions import JSONExtract
qs = MyModel.objects.filter(id__in=given_list).annotate(
first_name=JSONExtract('details', '$.first_name'),
last_name=JSONExtract('details', '$.last_name')
)
MySQL json extractions have a special syntax using jsonfield->"$.key". Try with this:
qs = MyModel.objects.filter(id__in=given_list).annotate(
first_name=RawSQL("(details->%s)", ('$.first_name',)),
last_name=RawSQL("(details->%s)", ('$.last_name',))
)
You can just add properties to your MyModel and have them return the corresponding information
class MyModel(models.Model):
details = models.JSONField()
#property
def first_name(self):
return self.details['first_name']
#property
def last_name(self):
return self.details['last_name']
Related
I'm looking for a way to filter two inputs(name and place) from two models (Photographer and Location); but, MultipleChoiceFilter seems like it doesn't work for me because I need to add more complex logic which performs in my custom filter as below.
from django.db.models import Q
from django_filters import rest_framework as filters
class SearchResultFilter(filters.FilterSet):
name = filters.CharFilter('Photographer')
place = filters.CharFilter(method='location_filter', distinct=True)
class Meta:
model = models.Photographer
fields = ('name')
def location_filter(self, queryset, name, value):
"""My complex logic"""
.
.
.
model = models.Location.filter(Q(location__icontains=value))
return model
The question is " Is there a way I can put name input field in my location_filter, so that I can filter two inputs (name and place) in this function and return a wanted querySet from here".
I tried to use self.name in location_filter; however, an error log produced. I guess "name" here is not an class variable
You could mimic the behavior of Django builtin multiple search using ?q=<value>
from django.db.models import Q
def multiple_search(queryset, name, value):
queryset = queryset.filter(Q(name__icontains=value) | Q(place__icontains=value))
return queryset
class SearchResultFilter(django_filters.FilterSet):
...
q = django_filters.CharFilter(label='name or place', method=multiple_search)
First time with Django. Trying to add an annotation to queryset:
class EnrollmentManager(models.Manager.from_queryset(EnrollmentCustomQuerySet)):
COURSE_DURATION = datetime.timedelta(days=183)
def get_queryset(self):
"""Overrides the models.Manager method"""
lookback = make_aware(datetime.datetime.today() - self.COURSE_DURATION)
qs = super(EnrollmentManager, self).get_queryset().annotate( \
is_expired=(Value(True)), output_field=models.BooleanField())
return qs
At the moment I am just trying to add an extra 'calculated' field on the returned queryset, which is hard-coded to True and the attribute/field should be called is_expired.
If I can get that to work, then Value(True) needs to be a derived value based on this expression:
F('enrolled') < lookback
But since 'enrolled' is a database field and lookback is calculated, how will I be able to do that?
Note
I tried this, which executes without throwing the error:
qs = super(EnrollmentManager, self).get_queryset().annotate( \
is_expired=(Value(True, output_field=models.BooleanField())))
and in the shell I can see it:
Enrollment.objects.all()[0].is_expired -> returns True
and I can add it to the serializer:
class EnrollmentSerializer(serializers.ModelSerializer):
is_active = serializers.SerializerMethodField()
is_current = serializers.SerializerMethodField()
is_expired = serializers.SerializerMethodField()
COURSE_DURATION = datetime.timedelta(days=183)
class Meta:
model = Enrollment
fields = ('id', 'is_active', 'is_current', 'is_expired')
def get_is_expired(self, obj):
return obj.is_expired
So it is possible...but how can I replace my hard-coded 'True" with a calculation?
UPDATE
Reading the documentation, it states:
"Annotates each object in the QuerySet with the provided list of query expressions. An expression may be a simple value, a reference to a field on the model (or any related models), or an aggregate expression (averages, sums, etc.) that has been computed over the objects that are related to the objects in the QuerySet."
A simple value - so, not a simple COMPUTED value then?
That makes me think this is not possible...
It seems like a pretty good use-case for a Case expression. I suggest getting as familiar as you can with these expression tools, they're very helpful!
I haven't tested this, but it should work. I'm assuming enrolled is a tz-aware datetime for when they first enrolled...
from django.db.models import Case, When, Value
def get_queryset(self):
"""Overrides the models.Manager method"""
lookback = make_aware(datetime.datetime.today() - self.COURSE_DURATION)
qs = super(EnrollmentManager, self).get_queryset().annotate(
is_expired=Case(
When(
enrolled__lt=lookback,
then=Value(True)
),
default=Value(False),
output_field=models.BooleanField()
)
)
You also don't have to pre-calculate the lookback variable. Check out ExpressionWrappers and this StackOverflow answer that addresses this.
ExpressionWrapper(
TruncDate(F('date1')) + datetime.timedelta(days=365),
output_field=DateField(),
)
Of course, I don't mean to do what prefetch_related does already.
I'd like to mimic what it does.
What I'd like to do is the following.
I have a list of MyModel instances.
A user can either follows or doesn't follow each instance.
my_models = MyModel.objects.filter(**kwargs)
for my_model in my_models:
my_model.is_following = Follow.objects.filter(user=user, target_id=my_model.id, target_content_type=MY_MODEL_CTYPE)
Here I have n+1 query problem, and I think I can borrow what prefetch_related does here. Description of prefetch_related says, it performs the query for all objects and when the related attribute is required, it gets from the pre-performed queryset.
That's exactly what I'm after, perform query for is_following for all objects that I'm interested in. and use the query instead of N individual query.
One additional aspect is that, I'd like to attach queryset rather than attach the actual value, so that I can defer evaluation until pagination.
If that's too ambiguous statement, I'd like to give the my_models queryset that has is_following information attached, to another function (DRF serializer for instance).
How does prefetch_related accomplish something like above?
A solution where you can get only the is_following bit is possible with a subquery via .extra.
class MyModelQuerySet(models.QuerySet):
def annotate_is_follwing(self, user):
return self.extra(
select = {'is_following': 'EXISTS( \
SELECT `id` FROM `follow` \
WHERE `follow`.`target_id` = `mymodel`.id \
AND `follow`.`user_id` = %s)' % user.id
}
)
class MyModel(models.Model):
objects = MyModelQuerySet.as_manager()
usage:
my_models = MyModel.objects.filter(**kwargs).annotate_is_follwing(request.user)
Now another solution where you can get a whole list of following objects.
Because you have a GFK in the Follow class you need to manually create a reverse relation via GenericRelation. Something like:
class MyModelQuerySet(models.QuerySet):
def with_user_following(self, user):
return self.prefetch_related(
Prefetch(
'following',
queryset=Follow.objects.filter(user=user) \
.select_related('user'),
to_attr='following_user'
)
)
class MyModel(models.Model):
following = GenericRelation(Follow,
content_type_field='target_content_type',
object_id_field='target_id'
related_query_name='mymodels'
)
objects = MyModelQuerySet.as_manager()
def get_first_following_object(self):
if hasattr(self, 'following_user') and len(self.following_user) > 0:
return self.following_user[0]
return None
usage:
my_models = MyModel.objects.filter(**kwargs).with_user_following(request.user)
Now you have access to following_user attribute - a list with all follow objects per mymodel, or you can use a method like get_first_following_object.
Not sure if this is the best approach, and I doubt this is what prefetch_related does because I'm joining here.
I found there's way to select extra columns in your query.
extra_select = """
EXISTS(SELECT * FROM follow_follow
WHERE follow_follow.target_object_id = myapp_mymodel.id AND
follow_follow.target_content_type_id = %s AND
follow_follow.user_id = %s)
"""
qs = self.extra(
select={'is_following': extra_select},
select_params=[CONTENT_TYPE_ID, user.id]
)
So you can do this with join.
prefetch_related way of doing it would be separate queryset and look it up in queryset for the attribute.
I created my own manager:
class DataManager(models.Manager):
def optfilter(self, options = dict()):
kwargs = dict()
if options.has_key('active'):
kwargs['active__id'] = options['active']
return self.filter(**kwargs)
active is ManyToMany field.
and it working exacly as I want. But what about when I want exclude object from filter?
Something like this:
kwargs['exclude_active_id'] = options['active']
Same idea as you're already implemented except use exclude instead of filter
I have these models:
def Foo(Models.model):
size = models.IntegerField()
# other fields
def is_active(self):
if check_condition:
return True
else:
return False
def Bar(Models.model):
foo = models.ForeignKey("Foo")
# other fields
Now I want to query Bars that are having active Foo's as such:
Bar.objects.filter(foo.is_active())
I am getting error such as
SyntaxError at /
('non-keyword arg after keyword arg'
How can I achieve this?
You cannot query against model methods or properties. Either use the criteria within it in the query, or filter in Python using a list comprehension or genex.
You could also use a custom manager. Then you could run something like this:
Bar.objects.foo_active()
And all you have to do is:
class BarManager(models.Manager):
def foo_active(self):
# use your method to filter results
return you_custom_queryset
Check out the docs.
I had similar problem: I am using class-based view object_list and I had to filter by model's method. (storing the information in database wasn't an option because the property was based on time and I would have to create a cronjob and/or... no way)
My answer is ineffective and I don't know how it's gonna scale on larger data; but, it works:
q = Model.objects.filter(...)...
# here is the trick
q_ids = [o.id for o in q if o.method()]
q = q.filter(id__in=q_ids)
You can't filter on methods, however if the is_active method on Foo checks an attribute on Foo, you can use the double-underscore syntax like Bar.objects.filter(foo__is_active_attribute=True)