Creating a django manager with a parameter - django

I have the following situation
I have a manager class that filters a queryset according to a field. The problem is that the field name is different according to the class but the value to which it filters comes from the same place (so i thought i don't need several managers). This is what i did so far.
class MyManager(models.Manager):
def __init__(self, field_name):
super(MyManager, self).__init__()
self.field_name = field_name
def get_queryset(self):
# getting some value
kwargs = { self.field_name: some_value }
return super(MyManager, self).get_queryset().filter(**kwargs)
class A:
# some datamembers
#property
def children(self):
return MyUtils.prepare(self.b_set.all())
class B:
objects = MyManager(field_name='my_field_name')
a = models.ForeignKey(A, null=False, blank=False)
When i run tests i that retrieve from the DB a B object, and try to read the children property i get the following error:
self = <django.db.models.fields.related_descriptors.RelatedManager object at 0x7f384d199290>, instance = <A: A object>
def __init__(self, instance):
> super(RelatedManager, self).__init__()
E TypeError: __init__() takes exactly 2 arguments (1 given)
I know its because of the constructor parameter because when i remove it (or give it a default value) all of the tests work.
How can i overcome this? Is this the right way of achieving this?
Tech stuff:
Django 1.9.5
test framework py.test 2.9.1
Thanks

Another option would be to generate the Manager class dynamically, such as:
def manager_factory(custom_field):
class MyManager(models.Manager):
my_field = custom_field
def get_queryset(self):
# getting some value
kwargs = {self.my_field: 'some-value'}
return super(MyManager, self).get_queryset().filter(**kwargs)
return MyManager()
class MyModel(models.Model):
objects = manager_factory('custom_field')
This way you can decouple the Manager from the Model class.

As you can see, that error is happening because Django instantiates a new Manager whenever you make a related objects call; that instantiation wouldn't get the extra parameter.
Rather than getting the value this way, you could try making it an attribute of the model and then referencing it via self.model.
class MyManager(models.Manager):
def get_queryset(self):
# getting some value
kwargs = { self.model.manager_field_name: some_value }
class B:
manager_field_name = 'my_field_name'
objects = MyManager()

Related

Add extra value from ImportForm to model instance before imported or saved

I have the following model that I want to import:
class Token(models.Model):
key = models.CharField(db_index=True,unique=True,primary_key=True, )
pool = models.ForeignKey(Pool, on_delete=models.CASCADE)
state = models.PositiveSmallIntegerField(default=State.VALID, choices=State.choices)
then a resource model:
class TokenResource(resources.ModelResource):
class Meta:
model = Token
import_id_fields = ("key",)
and a ImportForm for querying the pool:
class AccessTokenImportForm(ImportForm):
pool = forms.ModelChoiceField(queryset=Pool.objects.all(), required=True)
This value shouls be set for all the imported token objects.
The problem is, that I did not find a way to acomplish this yet.
How do I get the value from the form to the instance?
The before_save_instance and or similar methods I cannot access these values anymore. I have to pass this alot earlier I guess. Does someone ever done something similar?
Thanks and regards
Matt
It seems that you could pass the extra value in the Admin:
class TokenAdmin(ImportMixin, admin.ModelAdmin):
def get_import_data_kwargs(self, request, *args, **kwargs):
form = kwargs.get('form')
if form:
kwargs.pop('form')
if 'pool' in form.cleaned_data:
return {'pool_id' : form.cleaned_data['pool'].id }
return kwargs
return {}
And then use this data within your resources after_import_instance method to set this value
def after_import_instance(self, instance, new, row_number=None, **kwargs):
pool_id = kwargs.get('pool_id')
instance.pool_id = pool_id
instance.created_at = timezone.now()
Then the error from missing field (non null) is gone.

Display field other than __str__

I am trying to display the version field from the below model other than the default str which is field2_name:
Note: This SO link Displaying a specific field in a django form might be more than I need but I am not 100% sure. I tried to implement this but was not successful.
Also note that I tried the example at https://docs.djangoproject.com/en/1.10/ref/forms/fields/ but was not able to get it to work
Model (Generic names):
class CodeVersion(models.Model):
field1= models.ForeignKey(SomeOtherModel, on_delete=models.CASCADE)
field2_name = models.CharField(max_length=256)
field3_description = models.CharField(max_length=1000, blank=True)
version = models.PositiveIntegerField()
def __str__(self):
return self.field2_name
Form:
class VersionsForm(forms.Form):
code_versions = forms.ModelChoiceField(queryset=CodeVersion.objects.none())
def __init__(self, SomeOtherModel_id):
super(VersionsForm, self).__init__()
self.fields['infocode_versions'].queryset = CodeVersion.objects.filter(SomeOtherModel_id=SomeOtherModel_id)
This works - it returns field2_name as it is supposed to.
How do I return version instead - what is the simplest way?
Any help or guidance is appreciated.
From the ModelChoiceField docs:
The __str__ (__unicode__ on Python 2) method of the model will be called to generate string representations of the objects for use in the field’s choices; to provide customized representations, subclass ModelChoiceField and override label_from_instance. This method will receive a model object, and should return a string suitable for representing it. For example:
from django.forms import ModelChoiceField
class MyModelChoiceField(ModelChoiceField):
def label_from_instance(self, obj):
return "My Object #%i" % obj.id
If I got your question correctly you could change object string representation
def __str__(self):
return str(self.version)
You could then inherit ModelChoiceField and override label_from_instance method
or even monkey patch it like this
self.fields['field_name'].label_from_instance = self.label_from_instance
#staticmethod
def label_from_instance(self):
return str(self.value)
In one line :
self.fields['code_versions'].label_from_instance = lambda obj: f"{obj.version}"
Complete example
class VersionsForm(forms.Form):
code_versions = forms.ModelChoiceField(queryset=CodeVersion.objects.none())
def __init__(self, SomeOtherModel_id):
super().__init__()
self.fields['code_versions'].queryset = CodeVersion.objects.filter(SomeOtherModel_id=SomeOtherModel_id)
self.fields['code_versions'].label_from_instance = lambda obj: f"{obj.version}"
The simplest way in this case that I struggled myself:
under your line of code
self.fields['infocode_versions'].queryset = CodeVersion.objects.filter(SomeOtherModel_id=SomeOtherModel_id)
insert this:
self.fields["infocode_versions"].label_from_instance = lambda obj: "%s" % obj.version

Django - combining custom querysets used as managers with abstract class inheritance

Basically, I have to achieve two goals in my project that are currently conflicting with each other in terms of implementation.
The goals are:
Enabling chainable filtering for model instances (my_model.objects.custom_filter1().custom_filter2() etc)
For those tables with is_deleted field, and I want to exclude records marked as deleted, so that I don't have to explicitly filter for deleted records (use my_model.objects.all() instead of my_model.objects.filter(is_deleted = false))
Currently I have the following code:
class MixinManager(models.Manager):
def get_queryset(self):
try:
return self.model.MixinQuerySet(self.model).filter(is_deleted=False)
except FieldError:
return self.model.MixinQuerySet(self.model)
class BaseMixin(models.Model):
objects = MixinManager()
class MixinQuerySet(QuerySet):
pass
class Meta:
abstract = True
class DeleteMixin(BaseMixin):
is_deleted = models.BooleanField(default=False)
class Meta:
abstract = True
class Sms(DeleteMixin):
# core fielrds
# objects = SmsQuerySet.as_manager()
class Meta:
managed = False
db_table = 'sms'
However, my first goal becomes seemingly infeasible. Prior to using abstract classes for achieving the second goal, I had the following code
to solve the second goal:
class SmsQuerySet(models.query.QuerySet):
def filter_1(self, user):
return self.filter(...)
def filter_2(self, user):
return self.filter(...)
def filter_3(self, user):
return self.filter(...)
class Sms(models.Model):
# core fields
objects = SmsQuerySet.as_manager()
This syntax solved my first goal.
THE QUESTION:
The problem is that I can't combine these two structures into one logic, so that I can write:
sms.objects.filter_1().filter_2().filter_3()
so that deleted records are exluded from sms.objects.
If I uncomment the # objects = SmsQuerySet.as_manager() line in the first code snippet, then the objects field of BaseMixin will be ignored and I will end up with deleted records in the final queryset. On the other hand, if I comment the line, than my custom querysets become unreachable.
I hope I succeeded to concisely describe what I am trying to do. Will be very grateful for any hint towards the solution!
It should work
from django.db import models
class SmsQuerySet(models.query.QuerySet):
def filter_1(self, user):
return self.filter(...)
def filter_2(self, user):
return self.filter(...)
def filter_3(self, user):
return self.filter(...)
class MixinManager(models.Manager):
def get_queryset(self):
try:
return SmsQuerySet(self.model).filter(is_deleted=False)
except FieldError:
return SmsQuerySet(self.model)
class Sms(models.Model):
# core fields
objects = MixinManager()

Django Rest Framework - Incorrect source object on nested serialization when using proxy model

I have a question concerning using proxy models with the Django Rest Framework and nested serialization.
My proxy models are as follows:
class MyField(Field):
class Meta:
proxy = True
def field_type_name(self):
# logic that computes the field type name here
return "the result"
class MyForm(Form):
class Meta:
proxy = True
The Field model is defined in another app that I've included in my project. I wanted to add my own method to it without modifying the model so I made a proxy.
These are the serializers for the proxy models:
class MyFieldSerializer(serializers.HyperlinkedModelSerializer):
field_type = serializers.ChoiceField(source='field_type_name',
choices=form_fields.NAMES)
class Meta:
model = MyField
fields = ('url', 'field_type',)
class MyFormSerializer(serializers.HyperlinkedModelSerializer):
fields = MyFieldSerializer(many=True)
class Meta:
model = MyForm
fields = ('url', 'fields')
And the viewsets:
class MyFieldViewSet(viewsets.ModelViewSet):
queryset = MyField.objects.all()
serializer_class = MyFieldSerializer
class MyFormViewSet(viewsets.ModelViewSet):
queryset = MyForm.objects.all()
serializer_class = MyFormSerializer
urls.py:
router.register(r'fields', views.MyFieldViewSet)
router.register(r'forms', views.MyFormViewSet)
If I go to /fields/ it works fine. The method I added in the proxy model is executed correctly.
[
{
"url": "http://127.0.0.1:8000/fields/1/",
"field_type": "the result",
},
{ ...
But if I go to /forms/ I get the following error:
AttributeError at /forms/
'Field' object has no attribute 'field_type_name'
/Users/..../lib/python2.7/site-packages/rest_framework/fields.py in get_component
"""
Given an object, and an attribute name,
return that attribute on the object.
"""
if isinstance(obj, dict):
val = obj.get(attr_name)
else:
**val = getattr(obj, attr_name)**
if is_simple_callable(val):
return val()
return val
▼ Local vars
Variable Value
attr_name u'field_type_name'
obj <Field: Cools2>
As you can see the obj is Field instead of MyField which is why it's not able to call field_type_name. This only happens on the nested serialization. If anyone has a suggestion on how I can best fix this I'd greatly appreciate it.
EDIT:
Based on Kevin's response I'm editing the proxy models to try to fix this.
Here are the base models for reference:
class Form(AbstractForm):
pass
class Field(AbstractField):
form = models.ForeignKey("Form", related_name="fields")
Here is my attempt to fix the problem (using examples from Django proxy model and ForeignKey):
class MyField(Field):
class Meta:
proxy = True
def field_type_name(self):
# logic that computes the field type name here
return "the result"
# this works
#property
def form(self):
return MyForm.objects.get(id=self.form_id)
class MyForm(Form):
class Meta:
proxy = True
# this does not work
#property
def fields(self):
qs = super(MyForm, self).fields
qs.model = MyField
return qs
Now I can get MyForm from MyField but not MyField from MyForm (the reverse):
>>> MyField.objects.get(pk=1).form
<MyForm: Cool Form>
>>> MyForm.objects.get(pk=1).fields.all()
[]
I
This is because your model Form (or MyForm) isn't configured to return MyField objects when you access the field attribute on the form. It's not configured to substitute your proxied-version.
Try it yourself, open ./manage.py shell and try to read the fields related manager, it will return a collection of Field objects.
>>> form = MyForm.objects.all()[0].fields.all()
(Btw, I have to guess on the actual model structure since the original Field and Form models weren't included in your example).
If it's a read-only field, you could use serializers.SerializerMethodField to add a method to the serializer (your field_type_name(). If you want to be able to edit it, you're better off writing your own field sub-class that handles the conversion.

Custom QuerySet and Manager without breaking DRY?

I'm trying to find a way to implement both a custom QuerySet and a custom Manager without breaking DRY. This is what I have so far:
class MyInquiryManager(models.Manager):
def for_user(self, user):
return self.get_query_set().filter(
Q(assigned_to_user=user) |
Q(assigned_to_group__in=user.groups.all())
)
class Inquiry(models.Model):
ts = models.DateTimeField(auto_now_add=True)
status = models.ForeignKey(InquiryStatus)
assigned_to_user = models.ForeignKey(User, blank=True, null=True)
assigned_to_group = models.ForeignKey(Group, blank=True, null=True)
objects = MyInquiryManager()
This works fine, until I do something like this:
inquiries = Inquiry.objects.filter(status=some_status)
my_inquiry_count = inquiries.for_user(request.user).count()
This promptly breaks everything because the QuerySet doesn't have the same methods as the Manager. I've tried creating a custom QuerySet class, and implementing it in MyInquiryManager, but I end up replicating all of my method definitions.
I also found this snippet which works, but I need to pass in the extra argument to for_user so it breaks down because it relies heavily on redefining get_query_set.
Is there a way to do this without redefining all of my methods in both the QuerySet and the Manager subclasses?
The Django 1.7 released a new and simple way to create combined queryset and model manager:
class InquiryQuerySet(models.QuerySet):
def for_user(self, user):
return self.filter(
Q(assigned_to_user=user) |
Q(assigned_to_group__in=user.groups.all())
)
class Inquiry(models.Model):
objects = InqueryQuerySet.as_manager()
See Creating Manager with QuerySet methods for more details.
Django has changed! Before using the code in this answer, which was written in 2009, be sure to check out the rest of the answers and the Django documentation to see if there is a more appropriate solution.
The way I've implemented this is by adding the actual get_active_for_account as a method of a custom QuerySet. Then, to make it work off the manager, you can simply trap the __getattr__ and return it accordingly
To make this pattern re-usable, I've extracted out the Manager bits to a separate model manager:
custom_queryset/models.py
from django.db import models
from django.db.models.query import QuerySet
class CustomQuerySetManager(models.Manager):
"""A re-usable Manager to access a custom QuerySet"""
def __getattr__(self, attr, *args):
try:
return getattr(self.__class__, attr, *args)
except AttributeError:
# don't delegate internal methods to the queryset
if attr.startswith('__') and attr.endswith('__'):
raise
return getattr(self.get_query_set(), attr, *args)
def get_query_set(self):
return self.model.QuerySet(self.model, using=self._db)
Once you've got that, on your models all you need to do is define a QuerySet as a custom inner class and set the manager to your custom manager:
your_app/models.py
from custom_queryset.models import CustomQuerySetManager
from django.db.models.query import QuerySet
class Inquiry(models.Model):
objects = CustomQuerySetManager()
class QuerySet(QuerySet):
def active_for_account(self, account, *args, **kwargs):
return self.filter(account=account, deleted=False, *args, **kwargs)
With this pattern, any of these will work:
>>> Inquiry.objects.active_for_account(user)
>>> Inquiry.objects.all().active_for_account(user)
>>> Inquiry.objects.filter(first_name='John').active_for_account(user)
UPD if you are using it with custom user(AbstractUser), you need to change
from
class CustomQuerySetManager(models.Manager):
to
from django.contrib.auth.models import UserManager
class CustomQuerySetManager(UserManager):
***
You can provide the methods on the manager and queryset using a mixin.
This also avoids the use of a __getattr__() approach.
from django.db.models.query import QuerySet
class PostMixin(object):
def by_author(self, user):
return self.filter(user=user)
def published(self):
return self.filter(published__lte=datetime.now())
class PostQuerySet(QuerySet, PostMixin):
pass
class PostManager(models.Manager, PostMixin):
def get_query_set(self):
return PostQuerySet(self.model, using=self._db)
You can now use the from_queryset() method on you manager to change its base Queryset.
This allows you to define your Queryset methods and your manager methods only once
from the docs
For advanced usage you might want both a custom Manager and a custom QuerySet. You can do that by calling Manager.from_queryset() which returns a subclass of your base Manager with a copy of the custom QuerySet methods:
class InqueryQueryset(models.Queryset):
def custom_method(self):
""" available on all default querysets"""
class BaseMyInquiryManager(models.Manager):
def for_user(self, user):
return self.get_query_set().filter(
Q(assigned_to_user=user) |
Q(assigned_to_group__in=user.groups.all())
)
MyInquiryManager = BaseInquiryManager.from_queryset(InquiryQueryset)
class Inquiry(models.Model):
ts = models.DateTimeField(auto_now_add=True)
status = models.ForeignKey(InquiryStatus)
assigned_to_user = models.ForeignKey(User, blank=True, null=True)
assigned_to_group = models.ForeignKey(Group, blank=True, null=True)
objects = MyInquiryManager()
A slightly improved version of T. Stone’s approach:
def objects_extra(mixin_class):
class MixinManager(models.Manager, mixin_class):
class MixinQuerySet(QuerySet, mixin_class):
pass
def get_query_set(self):
return self.MixinQuerySet(self.model, using=self._db)
return MixinManager()
Class decorators make usage as simple as:
class SomeModel(models.Model):
...
#objects_extra
class objects:
def filter_by_something_complex(self, whatever parameters):
return self.extra(...)
...
Update: support for nonstandard Manager and QuerySet base classes, e. g. #objects_extra(django.contrib.gis.db.models.GeoManager, django.contrib.gis.db.models.query.GeoQuerySet):
def objects_extra(Manager=django.db.models.Manager, QuerySet=django.db.models.query.QuerySet):
def oe_inner(Mixin, Manager=django.db.models.Manager, QuerySet=django.db.models.query.QuerySet):
class MixinManager(Manager, Mixin):
class MixinQuerySet(QuerySet, Mixin):
pass
def get_query_set(self):
return self.MixinQuerySet(self.model, using=self._db)
return MixinManager()
if issubclass(Manager, django.db.models.Manager):
return lambda Mixin: oe_inner(Mixin, Manager, QuerySet)
else:
return oe_inner(Mixin=Manager)
based on django 3.1.3 source code, i found a simple solution
from django.db.models.manager import BaseManager
class MyQuerySet(models.query.QuerySet):
def my_custom_query(self):
return self.filter(...)
class MyManager(BaseManager.from_queryset(MyQuerySet)):
...
class MyModel(models.Model):
objects = MyManager()
There are use-cases where we need to call custom QuerySet methods from the manager instead of using the get_manager method of a QuerySet.
A mixin would suffice based on the solution posted in one of the accepted solution comments.
class CustomQuerySetManagerMixin:
"""
Allow Manager which uses custom queryset to access queryset methods directly.
"""
def __getattr__(self, name):
# don't delegate internal methods to queryset
# NOTE: without this, Manager._copy_to_model will end up calling
# __getstate__ on the *queryset* which causes the qs (as `all()`)
# to evaluate itself as if it was being pickled (`len(self)`)
if name.startswith('__'):
raise AttributeError
return getattr(self.get_queryset(), name)
For example,
class BookQuerySet(models.QuerySet):
def published(self):
return self.filter(published=True)
def fiction(self):
return self.filter(genre="fiction")
def non_fiction(self):
return self.filter(genre="non-fiction")
class BookManager(CustomQuerySetManagerMixin, models.Manager):
def get_queryset(self):
return BookQuerySet(self.model, using=self._db).published()
class Book(models.Model):
title = models.CharField(max_length=200)
genre = models.CharField(choices=[('fiction', _('Fiction')), ('non-fiction', _('Non-Fiction'))])
published = models.BooleanField(default=False)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
objects = BookManager()
class Author(models.Model):
name = models.CharField(max_length=200)
With the above, we can access related objects (Book) like below without defining new methods in the manager for each queryset method.
fiction_books = author.books.fiction()
The following works for me.
def get_active_for_account(self,account,*args,**kwargs):
"""Returns a queryset that is
Not deleted
For the specified account
"""
return self.filter(account = account,deleted=False,*args,**kwargs)
This is on the default manager; so I used to do something like:
Model.objects.get_active_for_account(account).filter()
But there is no reason it should not work for a secondary manager.