Multiple object types references in Django - django

We are currently running with the following configuration to avoid other issues.
So for the question: let's assume that this is a must and we can not change the Models part.
At the beginning we had the following models:
class A(Model):
b = ForeignKey(B)
... set of fields ...
class B(Model):
...
Then we added something like this:
class AVer2(Model):
b = ForeignKey(B)
... ANOTHER set of fields ...
Assuming an object of type B can only be referenced by either A or AVer2 but never both:
Is there a way to run a query on B that will return, at runtime, the correct object type that references it, in the query result (and the query has both types in it)?
You can assume that an object of type B holds the information regarding who's referencing it.
I am trying to avoid costly whole-system code changes for this.
EDIT:
Apparently, my question was not clear. So I will try to explain it better. The answers I got were great but apparently I missed a key point in my question so here it is. Assuming I have the model B from above, and I get some objects:
b_filter = B.objects.filter(some_of_them_have_this_true=True)
Now, I want to get a field that is in both A and AVer2 with one filter into one values list. So for example, I want to get a field named "MyVal" (both A and AVer2 have it) I don't care what is the actual type. So I want to write something like:
b_filter.values(['a__myval', 'aver2__myval'])
and get something like the following in return: [{'myval': }]
Instead, I currently get [{'a__myval': , 'aver2__myval': None}]
I hope it is clearer.
Thanks!

Short answer: You can not make your exact need.
Long answer: The first thing that came to my mind when I read your question is Content Types, Generic Foreign Keys and Generic Relations
Whether you will use "normal" foreign keys or "generic foreign keys" (combined with Generic Relation), Your B instances will have both A field and AVer2 field and this natural thing make life easier and make your goal (B instance has a single Field that may be A or Avr2) unreachable. And here you should also override the B model save method to force it to have only the A field and the Avr2 to be None or A to be None and Avr2 to be used. And if you do so, don't forget to add null=True, blank=True to A and Avr2 foreign key fields.
On the other hand, the opposite of your schema makes your goal reachable:
B model references A and Avr2 that means that B model has ONE generic foreign key to both A and Avr2 like this: (this code is with Django 1.8, for Django 1.9 or higher the import of GenericRelation, GenericForeignKey has changed)
from django.db import models
from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
from django.contrib.contenttypes.models import ContentType
class B(models.Model):
# Some of your fields here...
content_type = models.ForeignKey(ContentType, null=True, blank=True)
object_id = models.PositiveIntegerField(null=True, blank=True)
# Generic relational field will be associed to diffrent models like A or Avr2
content_object = GenericForeignKey('content_type', 'object_id')
class A(models.Model):
# Some of your fields here...
the_common_field = models.BooleanField()
bbb = GenericRelation(B, related_query_name="a") # since it is a foreign key, this may be one or many objects refernced (One-To-Many)
class Avr2(models.Model):
# Some of your fields here...
the_common_field = models.BooleanField()
bbb = GenericRelation(B, related_query_name="avr2") # since it is a foreign key, this may be one or many objects refernced (One-To-Many)
Now both A and Avr2 have "bbb" field which is a B instance.
a = A(some fields initializations)
a.save()
b = B(some fields initializations)
b.save()
a.bbb = [b]
a.save()
Now you can do a.bbb and you get the B instances
And get the A or Avr2 out of b like this:
b.content_object # which will return an `A object` or an `Avr2 object`
Now let's return to your goals:
Is there a way to run a query on B that will return, at runtime, the correct object type that references it, in the query result (and the query has both types in it)?
Yes: like this:
B.objects.get(id=1).content_type # will return A or Avr2
You wanna perform something like this: b_filter = B.objects.filter(some_of_them_have_this_true=True) :
from django.db.models import Q
filter = Q(a__common_field=True) | Q(avr2__common_field=True)
B.objects.filter(filter)
Getting [{'a__myval': , 'aver2__myval': None}] is 100% normal since values is asked to provide two fields values. One way to overcome this, is by getting two clean queries and then chain them together like so:
from itertools import chain
c1 = B.objects.filter(content_type__model='a').values('a__common_field')
c2 = B.objects.filter(content_type__model='avr2').values('avr2__common_field')
result_list = list(chain(c1, c2))
Please notice that when we added related_query_name to the generic relation, a and avr2 has become accessible from B instances, which is not the default case.
And voilà ! I hope this helps !

I'm not sure what do you want to get in query set.
I assumed that you want set of "correct object types" that "has both types in it", so in fact you want set of related class types (like [<class 'main.models.A'>, <class 'main.models.A2'>]). If that is not the case, I can change answer after more specific details in comments.
This is solution for that "class list", you can use it to get what you precisely want.
# Our custom QuerySet that with function that returns list of classes related to given B objects
class CustomQuerySet(models.QuerySet):
def get_types(self, *args, **kwargs):
all_queryset = self.all()
return [b.get_a() for b in all_queryset]
# Our custom manager - we make sure we get CustomQuerySet, not QuerySet
class TypesManager(models.Manager):
def get_queryset(self, *args, **kwargs):
return CustomQuerySet(self.model)
class B(models.Model):
# some fields
# Managers
objects = models.Manager()
a_types_objects = TypesManager()
# Get proper A "type"
def get_a(self):
if self.a_set.all() and self.a2_set.all():
raise Exception('B object is related to A and A2 at the same time!')
elif self.a_set.all():
return A
elif self.a2_set.all():
return A2
return None
class A(models.Model):
b = models.ForeignKey(
B
)
class A2(models.Model):
b = models.ForeignKey(
B
)
And now you can use it like this:
>>> from main.models import *
>>> B.a_types_objects.all()
<CustomQuerySet [<B: B object>, <B: B object>]>
>>> B.a_types_objects.all().get_types()
[<class 'main.models.A'>, <class 'main.models.A2'>]
>>> B.a_types_objects.filter(id=1)
<CustomQuerySet [<B: B object>]>
>>> B.a_types_objects.filter(id=1).get_types()
[<class 'main.models.A'>]
Using a_types_objects works like normal objects, but it returns CustomQuerySet, which has extra function returning list of class.
EDIT:
If you worrying about changing a lot of B.objects.(...) into B.a_types_objects.(...) you could just set your main manager to TypesManager like that:
class B(models.Model):
# some fields
# Override manager
objects = TypesManager()
Rest of your code will remain intact, but from now on you will use CustomQuerySet instead of QuerySet - still, nothing really changes.

Related

Filter by URL Kwargs while using Django FilterSets

I have an endpoint that can follow this format:
www.example.com/ModelA/2/ModelB/5/ModelC?word=hello
Model C has a FK to B, which has a FK to A. I should only ever see C's that correspond to the same A and B at one time. In the above example, we should...
filter C by those with an FK to B id = 5 and A id = 2
also filter C by the field 'word' that contains hello.
I know how to use the filter_queryset() method to accomplish #1:
class BaseModelCViewSet(GenericViewSet):
queryset = ModelC.objects.all()
class ModelCViewSet(BaseModelCViewSet, mixins.RetrieveModelMixin, mixins.ListModelMixin):
def filter_queryset(self, queryset):
return queryset.filter(ModelB=self.kwargs["ModelB"], ModelB__ModelA=self.kwargs["ModelA"])
I also know how to use a Filterset class to filter by fields on ModelC to accomplish #2
class ModelCFilterSet(GeoFilterSet):
word = CharFilter(field_name='word', lookup_expr='icontains')
But I can only get one or the other to work. If I add filterset_class = ModelCFilterSet to ModelCViewSet then it no longer does #1, and without it, it does not do #2.
How do I accomplish both? Ideally I want all of this in the ModelCFilterSet
Note - As hinted by the use of GeoFilterSet I will (later on) be using DRF to add a GIS query, this is just a simplified example. So I think that restricts me to using FilterSet classes in some manner.
I'm not sure this would be of help in your situation, but I often use nested urls in DRF in a way that would be convenient to perform the task. I use a library called drf-nested-routers that does part of the job, namely keeps track of the relations by the provided ids. Let me show an example:
# views.py
from rest_framework import exceptions, viewsets
class ModelBViewSet(viewsets.ModelViewSet):
# This is a viewset for the nested part that depends on ModelA
queryset = ModelB.objects.order_by('id').select_related('model_a_fk_field')
serializer_class = ModelBSerializer
filterset_class = ModelBFilterSet # more about it below
def get_queryset(self, *args, **kwargs):
model_a_entry_id = self.kwargs.get('model_a_pk')
model_a_entry = ModelA.objects.filter(id=model_a_entry_id).first()
if not model_a_entry:
raise exceptions.NotFound("MAYDAY")
return self.queryset.filter(model_c_fk_field=model_a_entry)
class ModelAViewSet(viewsets.ModelViewSet):
queryset = ModelA.objects.order_by('id')
serializer_class = ModelASerializer
# urls.py
from rest_framework_nested import routers
router = routers.SimpleRouter()
router.register('model-a', ModelAViewSet, basename='model_a')
model_a_router = routers.NestedSimpleRouter(router, 'model-a', lookup='model_a')
model_a_router.register('model-b', ModelBViewSet, basename='model_b')
...
In this case I can make a query like www.example.com/ModelA/2/ModelB/ that will only return the entries of ModelB that point to the object of ModelA with id 2. Likewise, www.example.com/ModelA/2/ModelB/5 will return only the corresponding object of ModelB in case it is related to ModelA-id2. A further level for ModelC would act correspondingly.
Sticking to the example, by now we have filtered the entries of ModelB related to a particular object of ModelA, that is we have received the relevant queryset. Next, we have to search for a particular subset within this queryset, and here's where FilterSet comes in play. The easiest way to customise its behaviour is by writing specific methods.
# filters.py
import django_filters
class ModelBFilterSet(django_filters.FilterSet):
word = django_filters.CharFilter(
method="get_word",
)
def get_word(self, queryset, name, value):
return queryset.filter(word__icontains=value)
In fact, you don't even have to use a method here; the way you pasted would work as well (word = CharFilter(field_name='word', lookup_expr='icontains')), I just wanted to point out that there is such an option too.
The filter starts its job with the queryset that has already been processed by the viewset and now it will just narrow down our sample using the given parameter.
I haven't tried this with a three-level nested URL, only checked on the example of two levels, but I think the third level should act in the same way.
Figured it out. So creating a filter_queryset() method overwrites the one that is in the GenericAPIView class (which I already knew).
However - that overwritten class is also responsible for using the FilterSet class that I defined. So by overwriting it, I also "broke" the FilterSet.
Solution was adding a super() to call the original class before the one I wrote:
def filter_queryset(self, queryset):
queryset = super().filter_queryset(queryset)
return queryset.filter(asset=self.kwargs["asset"], asset__program=self.kwargs["program"])

How can I use a related field in a SlugRelatedField?

I have the following structures
class State(models.Model):
label = models.CharField(max_length=128)
....
class ReviewState(models.Model):
state = models.ForeignKey(State, on_delete=models.CASCADE)
...
class MySerializer(serializers.HyperlinkedModelSerializer):
state = serializers.SlugRelatedField(queryset=ReviewState.objects.all(), slug_field='state__label', required=False)
class Meta:
model = MyModel
fields = [
'id',
'state', # this points to a ReviewState object
....
]
What I'm trying to do is using the State object's label as the field instead. But it doesn't seem like djangorestframework likes the idea of using __ to lookup slug fields. Would it be possible to do this? If it was:
class MySerializer(serializers.HyperlinkedModelSerializer):
state = serializers.SlugRelatedField(queryset=State.objects.all(), slug_field='label', required=False)
that would be no problem, but I'm trying to use the ReviewState instead. I'm also trying to avoid having a ReviewStateSerializer as the resulting json would look like this
{...
'state': {'state': 'Pending'}}
}
Interesting question, and well put.
Using SlugRelatedField('state__label', queryset=...) works fine, with 1 caveat: its just calling queryset.get(state__label="x") which errors if there isn't exactly 1 match.
1) Write a custom field?
Inherit from SlugRelatedField and override to_internal_value(), maybe by calling .first() instead of .get(), or whatever other logic you need.
2) Re-evaluate this relationship, maybe its 1-to-1? a choice field?
I'm a bit confused on how this would all work, since you can have a "1 to many" with State => ReviewState. The default lookup (if you don't do #1) will throw an error when multiple matches occur.
Maybe this is a 1-to-1 situation with the model? Perhaps the ReviewState can use a ChoiceField instead of a table of states?
Perhaps the 'label' can be the PK of the State table, and also a SlugField rather than a non-unique CharField?
3) Write different serializers for the List and Create cases
DRF doesn't give us a built-in way to do this, but this reliance on "one serializer to do it all" is the cause of a lot of problems I see on SO. Its just really hard to get what you want without having different serializers for different cases. It's not hard to roll-your-own mixin to do it, but here's an example which uses an override:
from rest_framework import serializers as s
class MyCreateSerializer(s.ModelSerializer):
state = s.SlugRelatedField(...)
...
class MyListSerializer(s.ModelSerializer):
# use dotted notation, serializers read *object* attributes
state = s.CharField(source="state.state.label")
...
class MyViewSet(ModelViewSet):
queryset = MyModel.objects.select_related('state__state')
...
def get_serializer_class(self):
if self.action == "create":
return MyCreateSerializer
else:
return MyListSerializer

Filter objects by self relation

Here is my Django model:
class Message(models.Model):
text = models.TextField()
parent = models.ForeignKey('Message', null=True, on_delete=models.SET_NULL)
threads = ThreadManager()
class ThreadManager(models.Manager):
def get_queryset(self):
return super(ThreadManager, self).get_queryset().filter()
As you see, it uses a custom manager. In ThreadManager I want to filter messages which has a self relation. I mean parent refers to its record. (x.parent = x)
How to write that filter? Is it possible at all?
Self reference
You can use a filter where you address the pk column:
from django.db.models import F
Message.objects.filter(parent=F('pk'))
So here we specify that the parent_id should be the same as the primary key, hence in that case the relation is cyclic.
Note that this filter will not detect cycles that have a length longer than 1. For example if we have two messages A and B, and A.parent = B and B.parent = A, then this filter will not detect this. It only detects Messages where A.parent == A.
Furthermore Django does not perform a semantical check: if you later let parent refer to another model, this filter does not makes much sense anymore, but Django will not produce a warning.
Non self-referencing
In case we want to opposite, we can use .exclude(..) or wrap it into a Q object:
# using exclude
Message.objects.exclude(parent=F('pk'))
# using Q
from django.db.models import Q
Message.objects.filter(~Q(parent=F('pk')))
So in that case the Message either has no parent, or a parent that is not the same message.

How to filter multiple fields with list of objects

I want to build an webapp like Quora or Medium, where a user can follow users or some topics.
eg: userA is following (userB, userC, tag-Health, tag-Finance).
These are the models:
class Relationship(models.Model):
user = AutoOneToOneField('auth.user')
follows_user = models.ManyToManyField('Relationship', related_name='followed_by')
follows_tag = models.ManyToManyField(Tag)
class Activity(models.Model):
actor_type = models.ForeignKey(ContentType, related_name='actor_type_activities')
actor_id = models.PositiveIntegerField()
actor = GenericForeignKey('actor_type', 'actor_id')
verb = models.CharField(max_length=10)
target_type = models.ForeignKey(ContentType, related_name='target_type_activities')
target_id = models.PositiveIntegerField()
target = GenericForeignKey('target_type', 'target_id')
tags = models.ManyToManyField(Tag)
Now, this would give the following list:
following_user = userA.relationship.follows_user.all()
following_user
[<Relationship: userB>, <Relationship: userC>]
following_tag = userA.relationship.follows_tag.all()
following_tag
[<Tag: tag-job>, <Tag: tag-finance>]
To filter I tried this way:
Activity.objects.filter(Q(actor__in=following_user) | Q(tags__in=following_tag))
But since actor is a GenericForeignKey I am getting an error:
FieldError: Field 'actor' does not generate an automatic reverse relation and therefore cannot be used for reverse querying. If it is a GenericForeignKey, consider adding a GenericRelation.
How can I filter the activities that will be unique, with the list of users and list of tags that the user is following? To be specific, how will I filter GenericForeignKey with the list of the objects to get the activities of the following users.
You should just filter by ids.
First get ids of objects you want to filter on
following_user = userA.relationship.follows_user.all().values_list('id', flat=True)
following_tag = userA.relationship.follows_tag.all()
Also you will need to filter on actor_type. It can be done like this for example.
actor_type = ContentType.objects.get_for_model(userA.__class__)
Or as #Todor suggested in comments. Because get_for_model accepts both model class and model instance
actor_type = ContentType.objects.get_for_model(userA)
And than you can just filter like this.
Activity.objects.filter(Q(actor_id__in=following_user, actor_type=actor_type) | Q(tags__in=following_tag))
What the docs are suggesting is not a bad thing.
The problem is that when you are creating Activities you are using auth.User as an actor, therefore you can't add GenericRelation to auth.User (well maybe you can by monkey-patching it, but that's not a good idea).
So what you can do?
#Sardorbek Imomaliev solution is very good, and you can make it even better if you put all this logic into a custom QuerySet class. (the idea is to achieve DRY-ness and reausability)
class ActivityQuerySet(models.QuerySet):
def for_user(self, user):
return self.filter(
models.Q(
actor_type=ContentType.objects.get_for_model(user),
actor_id__in=user.relationship.follows_user.values_list('id', flat=True)
)|models.Q(
tags__in=user.relationship.follows_tag.all()
)
)
class Activity(models.Model):
#..
objects = ActivityQuerySet.as_manager()
#usage
user_feed = Activity.objects.for_user(request.user)
but is there anything else?
1. Do you really need GenericForeignKey for actor? I don't know your business logic, so probably you do, but using just a regular FK for actor (just like for the tags) will make it possible to do staff like actor__in=users_following.
2. Did you check if there isn't an app for that? One example for a package already solving your problem is django-activity-steam check on it.
3. IF you don't use auth.User as an actor you can do exactly what the docs suggest -> adding a GenericRelation field. In fact, your Relationship class is suitable for this purpose, but I would really rename it to something like UserProfile or at least UserRelation. Consider we have renamed Relation to UserProfile and we create new Activities using userprofile instead. The idea is:
class UserProfile(models.Model):
user = AutoOneToOneField('auth.user')
follows_user = models.ManyToManyField('UserProfile', related_name='followed_by')
follows_tag = models.ManyToManyField(Tag)
activies_as_actor = GenericRelation('Activity',
content_type_field='actor_type',
object_id_field='actor_id',
related_query_name='userprofile'
)
class ActivityQuerySet(models.QuerySet):
def for_userprofile(self, userprofile):
return self.filter(
models.Q(
userprofile__in=userprofile.follows_user.all()
)|models.Q(
tags__in=userprofile.relationship.follows_tag.all()
)
)
class Activity(models.Model):
#..
objects = ActivityQuerySet.as_manager()
#usage
#1st when you create activity use UserProfile
Activity.objects.create(actor=request.user.userprofile, ...)
#2nd when you fetch.
#Check how `for_userprofile` is implemented this time
Activity.objects.for_userprofile(request.user.userprofile)
As stated in the documentation:
Due to the way GenericForeignKey is implemented, you cannot use such fields directly with filters (filter() and exclude(), for example) via the database API. Because a GenericForeignKey isn’t a normal field object, these examples will not work:
You could follow what the error message is telling you, I think you'll have to add a GenericRelation relation to do that. I do not have experience doing that, and I'd have to study it but...
Personally I think this solution is too complex to what you're trying to achieve. If only the user model can follow a tag or authors, why not include a ManyToManyField on it. It would be something like this:
class Person(models.Model):
user = models.ForeignKey(User)
follow_tag = models.ManyToManyField('Tag')
follow_author = models.ManyToManyField('Author')
You could query all followed tag activities per Person like this:
Activity.objects.filter(tags__in=person.follow_tag.all())
And you could search 'persons' following a tag like this:
Person.objects.filter(follow_tag__in=[<tag_ids>])
The same would apply to authors and you could use querysets to do OR, AND, etc.. on your queries.
If you want more models to be able to follow a tag or author, say a System, maybe you could create a Following model that does the same thing Person is doing and then you could add a ForeignKey to Follow both in Person and System
Note that I'm using this Person to meet this recomendation.
You can query seperately for both usrs and tags and then combine them both to get what you are looking for. Please do something like below and let me know if this works..
usrs = Activity.objects.filter(actor__in=following_user)
tags = Activity.objects.filter(tags__in=following_tag)
result = usrs | tags
You can use annotate to join the two primary keys as a single string then use that to filter your queryset.
from django.db.models import Value, TextField
from django.db.models.functions import Concat
following_actor = [
# actor_type, actor
(1, 100),
(2, 102),
]
searchable_keys = [str(at) + "__" + str(actor) for at, actor in following_actor]
result = MultiKey.objects.annotate(key=Concat('actor_type', Value('__'), 'actor_id',
output_field=TextField()))\
.filter(Q(key__in=searchable_keys) | Q(tags__in=following_tag))

filter with select_related on Django

I have a problem with the usage of select_related feature of Django with the filter operation, here's my problem, I have three classes :
class A:
# various fields here
class B(models.model):
related_A = models.ForeignKey(A)
related_C = models.ForeignKey(C)
attribute1 = models.CharField(..)
# Other attributes
class C(models.model):
# Attributes
What I'm trying to do is, getting class A by filtering on class B on the key related_C according to another parameter attribute1 (from class B).
To illustrate it properly, I have a function get_class_A(self) in my class C
get_class_A(self,param):
classes_B = B.objects.filter(related_C = self,attribute1 = param)
It returns a QuerySet of classes B. What I want to do is to follow the ForeignKey pointing to A in order to transform this QuerySet of B into a list of objects A.
I tried various things such as :
classes_A = B.objects.select_related('A').filter(related_C = self, attribute1 = param)
and some variations but nothing worked. Does anyone knows how to do this ?
Thanks
def get_class_A(self, param):
return A.objects.filter(b__related_c=self, b__attribute1=param).distinct()
What you've described looks a lot like a ManyToMany relationship between A and C. If you declare it as such, and include your extra attributes by specifying B as a through model, Django will create the relationship between A and C for you.
Also, select_related() has nothing to do with filtering results, it's just a tool that can allow you to reduce the number of database queries. From the docs:
This is a performance booster which results in a single more complex query but means later use of foreign-key relationships won’t require database queries.