I have plenty of Hardware models which have a HardwareType with various characteristics. Like so:
# models.py
from django.db import models
class HardwareType(model.Models):
name = models.CharField(max_length=32, unique=True)
# some characteristics of this particular piece of hardware
weight = models.DecimalField(max_digits=12, decimal_places=3)
# and more [...]
class Hardware(models.Model):
type = models.ForeignKey(HardwareType)
# some attributes
is_installed = models.BooleanField()
location_installed = models.TextField()
# and more [...]
If I wish to add a new Hardware object, I first have to retrieve the HardwareType every time, which is not very DRY:
tmp_hd_type = HardwareType.objects.get(name='NG35001')
new_hd = Hardware.objects.create(type=tmp_hd_type, is_installed=True, ...)
Therefore, I have tried to override the HardwareManager.create() method to automatically import the type when creating new Hardware like so:
# models.py
from django.db import models
class HardwareType(model.Models):
name = models.CharField(max_length=32, unique=True)
# some characteristics of this particular piece of hardware
weight = models.DecimalField(max_digits=12, decimal_places=3)
# and more [...]
class HardwareManager(models.Manager):
def create(self, *args, **kwargs):
if 'type' in kwargs and kwargs['type'] is str:
kwargs['type'] = HardwareType.objects.get(name=kwargs['type'])
super(HardwareManager, self).create(*args, **kwargs)
class Hardware(models.Model):
objects = HardwareManager()
type = models.ForeignKey(HardwareType)
# some attributes
is_installed = models.BooleanField()
location_installed = models.TextField()
# and more [...]
# so then I should be able to do:
new_hd = Hardware.objects.create(type='ND35001', is_installed=True, ...)
But I keep getting errors and really strange behaviors from the ORM (I don't have them right here, but I can post them if needed). I've searched in the Django documentation and the SO threads, but mostly I end up on solutions where:
the Hardware.save() method is overridden (should I get the HardwareType there ?) or,
the manager defines a new create_something method which calls self.create().
I also started digging into the code and saw that the Manager is some special kind of QuerySet but I don't know how to continue from there. I'd really like to replace the create method in place and I can't seem to manage this. What is preventing me from doing what I want to do?
The insight from Alasdair's answer helped a lot to catch both strings and unicode strings, but what was actually missing was a return statement before the call to super(HardwareManager, self).create(*args, **kwargs) in the HardwareManager.create() method.
The errors I was getting in my tests yesterday evening (being tired when coding is not a good idea :P) were ValueError: Cannot assign None: [...] does not allow null values. because the subsequent usage of new_hd that I had create()d was None because my create() method didn't have a return. What a stupid mistake !
Final corrected code:
class HardwareManager(models.Manager):
def create(self, *args, **kwargs):
if 'type' in kwargs and isinstance(kwargs['type'], basestring):
kwargs['type'] = HardwareType.objects.get(name=kwargs['type'])
return super(HardwareManager, self).create(*args, **kwargs)
Without seeing the traceback, I think the problem is on this line.
if 'type' in kwargs and kwargs['type'] is str:
This is checking whether kwargs['type'] is the same object as str, which will always be false.
In Python 3, to check whether `kwargs['type'] is a string, you should do:
if 'type' in kwargs and isinstance(kwargs['type'], str):
If you are using Python 2, you should use basestring, to catch byte strings and unicode strings.
if 'type' in kwargs and isinstance(kwargs['type'], basestring):
I was researching the same problem as you and decided not to use an override.
In my case making just another method made more sense given my constraints.
class HardwareManager(models.Manager):
def create_hardware(self, type):
_type = HardwareType.objects.get_or_create(name=type)
return self.create(type = _type ....)
Related
I have this manager:
class ConfigValueManager(models.Manager):
def get(self, key):
config_value = self.filter(key=key).first()
if config_value:
type_caster = locate(config_value.type)
return type_caster(config_value.value)
return config_value
def set(self, key, value):
self.filter(key=key).update(value=value)
def set2(self, key, value):
qs = self.filter(key=key)
if qs:
qs.update(value=value, type=type(value).__name__, company=self.instance)
else:
self.create(key=key, value=value, type=type(value).__name__, company=self.instance)
the problem is that I can't overwrite set. The method is still coming from the parent, even though I've created set on the child. Funny thing is that get and set2 are fine. Even add which isn't in my example can't be overridden.
My question is how can I overwrite set and why this happens?
I add some details on why it's not easily possible, because I struggled on the same issue.
set, like add or create, are overridden in the dynamically created RelatedManager, as we can see in the django source code. This RelatedManager actually uses our manager as a super class that's why your get and set2 methods can used, but it does not help for overridden methods.
This manager is created in the ReverseManyToOneDescriptor.related_manager_cls cached property. In the example on your github snippet, Company.config_values is an instance of this ReverseManyToOneDescriptor.
I'll show an example on how to override the set method, by making some assumptions on your code, because it misses some definitions (like the Company model, the ForeignKey field inside FooConfigValue.)
I don't advise to use it, as it's absolutely not robust against django changes, and I didn't do any test, it just serves as a proof on how RelatedManager instances are created
Add this at the end of the example code and it should work
def modify_related_manager_set(model_cls):
# model_cls = Company here, and config_values is the related field name
reverse_descriptor = model_cls.config_values
base_set = reverse_descriptor.related_manager_cls.set
def custom_set(*args, **kwargs):
print("in my custom set")
return base_set(*args, **kwargs)
reverse_descriptor.related_manager_cls.set = custom_set
# do this call after all the models have been created
# e.g. after defining FooConfigValue
modify_related_manager_set(Company)
And you should now see the in my custom set being printed.
I know this doesn't help much, but at least it helped understand how related fields work
models.py
from django.db import models
from django.db.models.query import QuerySet
class PersonQuerySet(QuerySet):
def set(self, slug, **kwargs):
return self.filter(slug=slug).update(**kwargs)
class Person(models.Model):
name = models.CharField(max_length=100, null=True)
slug = models.CharField(max_length=10, null=True)
objects = PersonQuerySet.as_manager()
tests.py
from django.test import TestCase
from core.models import Person
class TestSet(TestCase):
def test_just_update_records_with_the_same_slug(self):
Person.objects.create(slug='batman', name='John')
Person.objects.create(slug='batman', name='Connor')
Person.objects.create(slug='bruce', name='Ill be back')
Person.objects.set('batman', name='###')
expected_value = 2
result = Person.objects.filter(name='###').count()
self.assertEqual(result, expected_value)
github example
https://github.com/luivilella/django-test-manager
I'm using Django with the REST Framework. In a serializer, I would like to assign a field value based on a view or request (request.data['type']) parameter, so I need the view/request in the context.
I succeeded, but only in a cumbersome way, and I am looking into ways to simplify the code. Here's the successful approach (omitting irrelevant fields):
class TypeDefault(object):
def set_context(self, serializer_field):
view = serializer_field.context['view'] # or context['request']
self.type = view.kwargs['type'].upper()
def __call__(self):
return self.type
class RRsetSerializer(serializers.ModelSerializer):
type = serializers.CharField(read_only=True, default=serializers.CreateOnlyDefault(TypeDefault()))
class Meta:
model = RRset
fields = ('type',)
read_only_fields = ('type',)
To simplify things, I tried removing the TypeDefault class, and replacing the type serializer field by
type = serializers.SerializerMethodField()
def get_type(self, obj):
return self.context.get('view').kwargs['type'].upper() # also tried self._context
However, context.get('view') returns None. I am unsure why the view context is not available here. My impression is that it should be possible to get the desired functionality without resorting to an extra class.
As a bonus, it would be nice to specify the default in the field declaration itself, like
type = serializers.CharField(default=self.context.get('view').kwargs['type'].upper())
However, self is not defined here, and I'm not sure what the right approach would be.
Also, I am interested if there is any difference in retrieving information from the view or from the request data. While the context approach should work for both, maybe there's a simpler way to get the CreateOnlyDefault functionality when the value is obtained from request data, as the serializers deals with the request data anyways.
Edit: Per Geotob's request, here is the code of the view that calls the serializer:
class RRsetsDetail(generics.ListCreateAPIView):
serializer_class = RRsetSerializer
# permission_classes = ... # some permission constraints
def get_queryset(self):
name = self.kwargs['name']
type = self.kwargs.get('type')
# Note in the following that the RRset model has a `domain` foreign-key field which is referenced here. It is irrelevant for the current problem though.
if type is not None:
return RRset.objects.filter(domain__name=name, domain__owner=self.request.user.pk, type=type)
else:
return RRset.objects.filter(domain__name=name, domain__owner=self.request.user.pk)
In urls.py, I have (among others):
url(r'^domains/(?P<name>[a-zA-Z\.\-_0-9]+)/rrsets/$', RRsetsDetail.as_view(), name='rrsets'),
url(r'^domains/(?P<name>[a-zA-Z\.\-_0-9]+)/rrsets/(?P<type>[A-Z]+)/$', RRsetsDetail.as_view(), name='rrsets-type'),
SerializerMethodField is a read-only field so I do not think it will work unless you set a default value... and you are back to the same problem as with CharField.
To simply things you could get rid of serializers.CreateOnlyDefault:
class RRsetSerializer(serializers.ModelSerializer):
type = serializers.CharField(read_only=True, default=TypeDefault())
If you want something more dynamic, I can only think of something like this:
class FromContext(object):
def __init__(self, value_fn):
self.value_fn = value_fn
def set_context(self, serializer_field):
self.value = self.value_fn(serializer_field.context)
def __call__(self):
return self.value
class RRsetSerializer(serializers.ModelSerializer):
type = serializers.CharField(read_only=True,
default=FromContext(lambda context: context.get('view').kwargs['type'].upper()))
FromContext takes a function during instantiation that will be used to retrieve the value you want from context.
All in all, your second approach above is the correct one:
Use serializers.SerializerMethodField and access self.context from the serializer method:
class SomeSerializer(serializers.ModelSerializer):
type = serializers.SerializerMethodField()
def get_type(self, obj):
return self.context['view'].kwargs['type'].upper()
The view, request and format keys are automatically added to your serializer context by all of the DRF generic views (http://www.django-rest-framework.org/api-guide/generic-views/#methods at the end of the section). This works just fine.
If you are creating a serializer instance manually, you will have to pass context=contextDict as an argument, where contextDict is whatever you need it to be (http://www.django-rest-framework.org/api-guide/serializers/#including-extra-context).
As #Michael has pointed out in another answer, the SerializerMethodField will be read only. But going by your first example (type = serializers.CharField(read_only=True.....) this seems to be what you want.
I've created a series of custom ModelFields that are simply restricted ForeignKeys. Below you'll find CompanyField. When instantiated, you may provide a type (e.g., Client, Vendor). With a type provided, the field ensures that only values that have the appropriate type are allowed.
The crm app, the one that defines the custom fields, compiles and runs just fine. Eventually I added references to the fields to a different app (incidents) using "from crm import fields". Now I'm seeing a whole bunch of errors like this:
incidents.incident: 'group' has a relation with model Company, which has either not been installed or is abstract.
Here are all the gory details. Please let me know if there's any more information I could provide which may be helpful.
## crm/fields.py
import models as crmmods
class CompanyField(models.ForeignKey):
def __init__(self, *args, **kwargs):
# This is a hack to get South working. In either case, we just need to
# make sure the FK refers to Company.
try:
# kwargs['to'] == crmmods.company doesn't work for some reason I
# still haven't figured out
if str(kwargs['to']) != str(crmmods.Company):
raise AttributeError("Only crm.models.Company is accepted " + \
"for keyword argument 'to'")
except:
kwargs['to'] = 'Company'
# See if a CompanyType was provided and, if so, store it as self.type
if len(args) > 0:
company_type = args[0]
# Type is expected to be a string or CompanyType
if isinstance(company_type, str):
company_type = company_type.upper()
if hasattr(crmmods.CompanyType, company_type):
company_type = getattr(crmmods.CompanyType, company_type)
else:
raise AttributeError(
"%s is not a valid CompanyType." % company_type)
elif not isinstance(company_type, crmmods.CompanyType):
raise AttributeError(
"Expected str or CompanyType for first argument.")
self.type = company_type
else:
self.type = None
super(CompanyField, self).__init__(**kwargs)
def formfield(self, **kwargs):
# Restrict the formfield so it only displays Companies with the correct
# type.
if self.type:
kwargs['queryset'] = \
crmmods.Company.objects.filter(companytype__role=self.type)
return super(CompanyField, self).formfield(**kwargs)
def validate(self, value, model_instance):
super(CompanyField, self).validate(value, model_instance)
# No type set, nothing to check.
if not value or not self.type:
return
# Ensure that value is correct type.
if not \
value.companytype_set.filter(role=self.type).exists():
raise ValidationError("Company does not have the " + \
"required roles.")
## crm/models.py
import fields
class CompanyType(models.Model):
name = models.CharField(max_length=25)
class Company(models.Model):
type = models.ForeignKey(CompanyType)
class Person(models.Model):
name = models.CharField(max_length=50)
company = fields.CompanyField("Client")
## incidents/models.py
from crm import fields as crmfields
class Incident(models.Model):
company = crmfields.CompanyField("Client")
You have a circular package dependency. fields imports models which imports fields which imports models which imports fields . . .
Circular package dependecies are A BAD IDEA(tm). Although it may work in some cases, it doesn't work in your case, for a complicated reason involving metaclasses, which I will spare myself.
EDIT
The reason is that the Django ORM module uses metaclasses to turn its class variables (the fields of the Model) into property descriptors on the object. This is done at class definition time by the metaclass. A class is defined when the module is loaded. For this reason its attributes must also be defined at class loading. This is unlike the code of a method, where references to names are resolved the moment a class is executed.
Now, since you refer to a field object in your class definition from models and back again, this will not work.
If you place all three in the same package, your problem will be solved.
The was fixed, after much toil, simply by changing
kwargs['to'] = 'Company'
to
kwargs['to'] = 'crm.Company'
It seems that when the 'to' argument was evaluated outside of the crm app, it was evaluating it in the context of the incidents app. That is, it was looking for 'incidents.Company' which, as the error message suggested, didn't exist.
I have been using ActiveState Komodo for a while and while most of the code-completion is spot on it lacks the code completion from Django's model manager.
I have included the Django directory in my PYTHONPATH and get most of the code completion, the notable exception being the models.
Assuming I have a model users I would expect the code users.objects. to show autocomplete options such as all(),count(),filter() etc. however these are added by the model's manager which does so in a seemingly abnormal way.
I am wondering if I can 'force' Komodo to pick up the models.
The model manager looks to be included from the following code (taken from manager.py)
def ensure_default_manager(sender, **kwargs):
"""
Ensures that a Model subclass contains a default manager and sets the
_default_manager attribute on the class. Also sets up the _base_manager
points to a plain Manager instance (which could be the same as
_default_manager if it's not a subclass of Manager).
"""
cls = sender
if cls._meta.abstract:
return
if not getattr(cls, '_default_manager', None):
# Create the default manager, if needed.
try:
cls._meta.get_field('objects')
raise ValueError("Model %s must specify a custom Manager, because it has a field named 'objects'" % cls.__name__)
except FieldDoesNotExist:
pass
cls.add_to_class('objects', Manager())
cls._base_manager = cls.objects
...
Specifically the last two lines. Is there any way to tell Komodo that <model>.objects = Manager() so the proper code completion is shown?
Probably the easiest way to get this to work seems to be to add the following to the top of models.py:
from django.db.models import manager
and then under each model add
objects = manager.Manager()
so that, for example, the following:
class Site(models.Model):
name = models.CharField(max_length=200)
prefix = models.CharField(max_length=1)
secret = models.CharField(max_length=255)
def __unicode__(self):
return self.name
becomes
class Site(models.Model):
name = models.CharField(max_length=200)
prefix = models.CharField(max_length=1)
secret = models.CharField(max_length=255)
objects = manager.Manager()
def __unicode__(self):
return self.name
This is how you would (explicitly) set your own model manager, and by explicitly setting the model manager (to the default) Kommodo picks up the code completion perfectly.
Hopefully this will help someone :-)
My application uses class inheritance to minimize repetition across my models. My models.py looks kind of like this:
class BaseModel(models.Model):
title = models.CharField(max_length=100)
pub_date = models.DateField()
class Child(BaseModel):
foo = models.CharField(max_length=20)
class SecondChild(BaseModel):
bar = models.CharField(max_length=20)
Now most of the time, my views and templates only deal with instances of Child or SecondChild. Once in a while, however, I have a situation where I have an instance of BaseModel, and need to figure out which class is inheriting from that instance.
Given an instance of BaseModel, let's call it base, Django's ORM offers base.child and base.secondchild. Currently, I have a method that loops through all of them to figure it out. It would look something like this:
class BaseModel(models.Model):
...
def get_absolute_url(self):
url = None
try:
self.child
url = self.child.get_absolute_url()
except Child.DoesNotExist:
pass
if not url:
try:
self.secondchild
url = self.secondchild.get_absolute_url()
except SecondChild.DoesNotExist:
pass
if not url:
url = '/base/%i' % self.id
return url
That is hopelessly ugly, and gets uglier with every additional child class I have. Does anybody have any ideas on a better, more pythonic way to go about this?
Various forms of this question pop up here regularly. This answer demonstrates a generic way to "cast" a parent type to its proper subtype without having to query every subtype table. That way you wouldn't need to define a monster get_absolute_url on the parent which covers all the cases, you'd just convert to the child type and call get_absolute_url normally.
I haven't messed with Django inheitance much, so I suppose you can't override get_absolute_url() in the model classes?
Perhaps the visitor pattern could help if there are lot of functions that need this in many different places.
I haven't tested this, but it might be worth tinkering with:
def get_absolute_url(self):
subclasses = ('child', 'secondchild', )
for subclass in subclasses:
if hasattr(self, subclass):
return getattr(self, subclass).get_absolute_url()
return '/base/%i' % self.id