Change Document 'objects' property to 'query' - django

I'm trying to change the document 'objects' property to 'query'. It's more intuitive since one is querying the database.
Like;
Collection.query.find()
Instead of;
Collection.objects.find()
I have tried setting a query attribute to my Collection model like;
class Collection(Document):
def __setattr__(self, key, objects):
self.__dict__['query'] = self.objects
But on checking the type it returns a class of the QueryManager instead of Queryset like;
>>>print(type(Collection.query))
<'class' mongoengine.queryset.queryset.QueryManager >
Instead of;
>>>print(type(Collection.query))
<'class' mongoengine.queryset.queryset.Queryset >
Could someone offer a solution ?

Define an abstract Document class and within it define a custom QuerySet manager using queryset_manager wrapper. Inherit the abstract class as a Base class for all other subsequent Document classes.
from mongoengine.document import Document
from mongoengine.queryset import queryset_manager
class BaseDocument(Document):
meta = {'abstract': True}
#queryset_manager
def query(self, queryset):
return queryset
class Foo(BaseDocument):
...
To query use Foo.query.* which is more intuitive
instead of the default Foo.objects.*.
Displaying the type will return <class 'mongoengine.queryset.queryset.Queryset'> as expected.

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"])

Correct way to replace Django Model class with an equivalent+decorated subclass

I'm writing a decorator intended to add functionality to decorated Django Model classes. Something like this:
class NewFunctionality:
#classmethod
def fun1(cls):
...
#property
def prop1(self):
...
def add_functionality(Decorated):
class NewClass(Decorated, NewFunctionality):
class Meta(getattr(Decorated, 'Meta', object)):
app_label = Decorated._meta.app_label
NewClass.__name__ = Decorated.__name__
NewClass.__doc__ = Decorated.__doc__
return NewClass
#add_functionality
class MyModel(models.Model):
...
This seems to work until there are two decorated model classes, when I get an error Conflicting 'modelclass' models in application 'my_app'.
This is apparently due to the registry of models that Django keeps, which clearly has some automagic that doesn't appreciate new model classes being made, even if they are direct replacements of the existing one.
Is there anything I can do to accomplish this, other than by monkeypatching the decorated class, adding each needed method?
Edit
I've avoided the error by making the wrapper class into a proxy class of the decorated class:
def add_functionality(Decorated):
class NewClass(Decorated, NewFunctionality):
class Meta(getattr(Decorated, 'Meta', object)):
app_label = Decorated._meta.app_label
proxy = True
NewClass.__name__ = Decorated.__name__
NewClass.__doc__ = Decorated.__doc__
return NewClass
I also replace the use of the class keyword, using builtins.type, as in:
def add_functionality(Decorated):
return type(
Decorated.__name__,
(NewFunctionality, Decorated,),
{
'Meta': type('Meta', (object,), {
'app_label': Decorated._meta.app_label,
'proxy': True,
'__module__': Decorated.__module__,
}
)
I really like this as it seemingly completely replaces the decorated class. Now, however, I'm getting the following warning from Django:
.venv/lib/python3.9/site-packages/django/db/models/base.py:321
/home/tyler/orm-cache/django-ormsgpack/.venv/lib/python3.9/site-packages/django/db/models/base.py:321: RuntimeWarning: Model 'my_app.atestmodel' was already registered. Reloading models is not advised as it can lead to inconsistencies, most notably with related models.
new_class._meta.apps.register_model(new_class._meta.app_label, new_class)
If anyone can shed light onto how I might utilize the Django api to properly overwrite the registered model, I'd love to hear it!
thanks

Calling filters in django-graphene with GraphQL

I've been following the documentation for Graphene-Python in Django, and have just made it to the section on custom filters. While a tutorial is provided for how to write custom filters, there isn't a reference on how to call them in GraphiQL. If I have the following example code:
class AnimalNode(DjangoObjectType):
class Meta:
# Assume you have an Animal model defined with the following fields
model = Animal
filter_fields = ['name', 'genus', 'is_domesticated']
interfaces = (relay.Node, )
class AnimalFilter(django_filters.FilterSet):
# Do case-insensitive lookups on 'name'
name = django_filters.CharFilter(lookup_expr=['iexact'])
class Meta:
model = Animal
fields = ['name', 'genus', 'is_domesticated']
#property # make your own filter like this
def qs(self):
return super(EquityFilter, self).qs.filter(id=self.request.user)
class Query(ObjectType):
animal = relay.Node.Field(AnimalNode)
# We specify our custom AnimalFilter using the filterset_class param
all_animals = DjangoFilterConnectionField(AnimalNode,
filterset_class=AnimalFilter)
My question is, what would I need to type in GraphiQL to use this filter? Any help is greatly appreciated.
Inspect the schema in GraphiQL. It should show a root query similar to this one:
allAnimals(
before:String,
after:String,
firts:Int,
last:Int,
name:String,
genus:String,
isDomesticated:Boolean
):AnimalNodeConnection
The three filter criteria are exposed as query parameters, so you can use them with a query like this one:
query filteredAnimals{
allAnimals(
name:"Big Foot",
genus:"Unknown",
isDomesticated:false
) {
edges {
node {
name
genus
isDomesticated
}
}
}
}
Which will give you a connection with undomesticated animals named "Big Foot" ("big FOOT", "Big foot", etc.) with genus equal to "Unknown".
Note: Filters declared on the FilterSet Meta class are named after the type of filtering they do, like name_Icontains, name_Iexact. Filters declared as FilterSet fields (name filter in your case) keep their names unmodified, and extend or OVERRIDE filters declared in the FilterSet Meta class.

Pymongo query normally, but Mongoengine query nothing

Very strange: Pymongo query normally, but Mongoengine query nothing:
class VkWallPostListView(ListView):
model = VkWallPost
context_object_name = "vk_list"
def get_template_names(self):
return ["blog/vk_list.html"]
def get_queryset(self):
wallposts = VkWallPost.objects
if 'all_posts' not in self.request.GET:
#wallposts = wallposts.filter(text='S')
wallposts = VkWallPost._get_collection().find({"text":'S'})
tag = self.request.GET.get('tag', None)
if tag:
wallposts = wallposts.filter(tags=tag)
return wallposts
Option wallposts = VkWallPost._get_collection().find({"text":'S'}) return objects, but the same Mongoengine wallposts = wallposts.filter(text='S') is not working - empty results, no errors!
The more: i have the same class that query to another collection - there Mongoengine works normally.
It looks like your VkWallPostListView doesn't inherit directly from mongoengine.Document. I recently encountered this as well, where an identical model to the database schema didn't return anything when queried when it inherited from something other than the base Document class. It turns out, mongoengine adds a hidden field to child class documents called _cls and automatically checks for this when querying.
For example:
If I have a class TextHolder that is the base of my class Post
class TextHolder(mongoengine.Document):
text = mongoengine.StringField()
meta = { 'allow_inheritance' : True,
'abstract' : True }
class Post(TextHolder):
user = mongoengine.ReferenceField(User)
If I tried to query by Posts that already existed in the database, they will not have the _cls field defined in their documents. So, the way to query for documents that do not conform to this standard is to do this.
Post.objects(user=user_to_query_by, class_check=False)
This will give me all objects ignoring the _cls field and not checking for a match with the current model's class name.

Single custom Manager for Multiple models in Django

I have several models connected to each other with ForeignKeys relationships.
The main one in this sort of hierarchy contains a owner field.
I would like to create a single custom manager for all these models that changes the returned queryset depending on the models that is calling it.
I know that manager can access self.model to get the model that it is attached to.
Class Main(models.Model)
owner=models.ForeignKey (User)
owned = OwnedManager()
Class Second(models.Model)
main=models.ForeignKey('Main')
owned = OwnedManager()
Class Third(models.Model)
second=models.ForeignKey('Second')
owned = OwnedManager()
I would like my Custom Manager to have this sort of behavior:
class OwnedManager(models.Manager):
def get_owned_objs(self, owner):
if self.model == 'Main': # WRONG: How do I get the model name?
owned_main = self.filter(owner=owner)
return owned_main
elif self.model == 'Second':
owned_second = self.filter(main__owner=owner)
return owned_second
else:
owned_third = self.filter(second__main__owner=owner)
return owned_third
In order to have a consistent way to call it across different models, like so:
main_object.owned.get_owned_objs(owner=user1) # of the Model Main
second_object.owned.get_owned_objs(owner=user1) # of the Model Second
third_object.owned.get_owned_objs(owner=user1) # of the Model Third
QUESTION:
self.model == 'Main' is wrong. I don't get the model name like this. Is there a way to get it?
Is this efficient? Do you know a better way to implement this? Maybe Custom Managers Inheritance?
EDIT - MY SOLUTION:
The accepted answer below is a good solution but I also found a way to get the model name of the particular model calling the custom manager, that is:
if self.model.__name__ == 'Main':
The key here is the attribute __name__
1) Make abstract model
class AbstractModel(models.Model):
class Meta(models.Meta):
abstract = True
objects = OwnedManager()
2) Inherit your models from AbstractModel, put some key in meta
class Model(AbstractModel)
class Meta(AbstractModel.Meta):
filter_key = 'some_key'
3) Redesign your OwnedManager
class OwnedManager(models.Manager):
def get_owned_objs(self, owner):
if hasattr(self.model._meta, 'filter_key'):
return self.filter(**{self.model._meta.filter_key: owner})
Now you can use SomeModel.objects.get_owned_objs(owner=user1) in any inherited models, where filter_key is setted without getting models's name.