How to safely inherit from django models.Model class? - python

I have a following DB structure:
class Word(models.Model):
original = models.CharField(max_length=40)
translation = models.CharField(max_length=40)
class Verb(Word):
group = models.IntegerField(default=1)
In my view, I need to create a Word object first, and after determination of its group (depending on Word.original), create a Verb object, and save it.
What is the best way to inherit from the Word class and save the object as Verb ?
There are several solutions that I've tried:
1) Modification of the __init__ method in Verb :
class Verb(Word):
group = models.IntegerField(default=1)
def __init__(self, base_word):
self.original = base_word.original
self.translation = base_word.translation
This causes a lot of errors, since I'm overriding the django's built-in __init__ method.
2) Using super().__init__():
class Verb(Word):
group = models.IntegerField(default=1)
def __init__(self, base_word):
super().__init__()
self.original = base_word.original
self.translation = base_word.translation
Apparently, this works pretty well:
base_word = Word()
new_verb = Verb(base_word)
new_verb.save()
But there are two problems:
It causes an error when trying to see the objects in django admin page:
__init__() takes 2 positional arguments but 9 were given
This is still too much code, it doesn't feel right. I still need to write this:
self.original = base_word.original
self.translation = base_word.translation
in every subclass. And this is just an example. In real project, I have much more fields. I suppose there is a more elegant solution.

Overriding __init__ is not the right way to do this. Django models perform a lot of behind the scenes work, which overriding __init__ can conflict with, unless you do it in a safe way by following these rules:
Don't alter the signature of __init__ -- meaning you shouldn't change the arguments that the method accepts.
Perform the custom __init__ logic after calling the super().__init__(*args, **kwargs) method.
In this particular case, you might use django's proxy model inheritance features.
VERB = "V"
NOUN = "N"
# ...
WORD_TYPE_CHOICES = (
(VERB, "Verb"),
(NOUN, "Noun"),
# ...
)
class Word(models.Model):
original = models.CharField(max_length=40)
translation = models.CharField(max_length=40)
WORD_TYPE = "" # This is overridden in subclasses
word_type = models.CharField(
max_length=1,
blank=True,
editable=False, # So that the word type isn't editable through the admin.
choices=WORD_TYPE_CHOICES,
default=WORD_TYPE, # Defaults to an empty string
)
def __init__(self, *args, **kwargs):
# NOTE: I'm not 100% positive that this is required, but since we're not
# altering the signature of the __init__ method, performing the
# assignment of the word_type field is safe.
super().__init__(*args, **kwargs)
self.word_type = self.WORD_TYPE
def __str__(self):
return self.original
def save(self, *args, **kwargs):
# In the save method, we can force the subclasses to self-assign
# their word types.
if not self.word_type:
self.word_type = self.WORD_TYPE
super().save(*args, **kwargs)
class WordTypeManager(models.Manager):
""" This manager class filters the model's queryset so that only the
specific word_type is returned.
"""
def __init__(self, word_type, *args, **kwargs):
""" The manager is initialized with the `word_type` for the proxy model. """
self._word_type = word_type
super().__init__(*args, **kwargs)
def get_queryset(self):
return super().get_queryset().filter(word_type=self._word_type)
class Verb(Word):
# Here we can force the word_type for this proxy model, and set the default
# manager to filter for verbs only.
WORD_TYPE = VERB
objects = WordTypeManager(WORD_TYPE)
class Meta:
proxy = True
class Noun(Word):
WORD_TYPE = NOUN
objects = WordTypeManager(WORD_TYPE)
class Meta:
proxy = True
Now we can treat the different word types as if they were separate models, or access all of them together through the Word model.
>>> noun = Noun.objects.create(original="name", translation="nombre")
>>> verb = Verb(original="write", translation="escribir")
>>> verb.save()
# Select all Words regardless of their word_type
>>> Word.objects.values_list("word_type", "original")
<QuerySet [('N', 'name'), ('V', 'write')]>
# Select the word_type based on the model class used
>>> Noun.objects.all()
<QuerySet [<Noun: name>]>
>>> Verb.objects.all()
<QuerySet [<Verb: write>]>
This works with admin.ModelAdmin classes too.
#admin.register(Word)
class WordAdmin(admin.ModelAdmin):
""" This will show all words, regardless of their `word_type`. """
list_display = ["word_type", "original", "translation"]
#admin.register(Noun)
class NounAdmin(WordAdmin):
""" This will only show `Noun` instances, and inherit any other config from
WordAdmin.
"""

Related

DRF SerializerMethodField how to pass parameters

Is there a way to pass paremeters to a Django Rest Framework's SerializerMethodField?
Assume I have the models:
class Owner(models.Model):
name = models.CharField(max_length=10)
class Item(models.Model):
name = models.CharField(max_length=10)
owner = models.ForeignKey('Owner', related_name='items')
itemType = models.CharField(max_length=5) # either "type1" or "type2"
What I need is to return an Owner JSON object with the fields: name, type1items, type2items.
My current solution is this:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = models.Item
fields = ('name', 'itemType')
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getType1Items')
type2items = serializers.SerializerMethodField(method_name='getType2Items')
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getType1Items(self, ownerObj):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType="type1")
return ItemSerializer(queryset, many=True).data
def getType2Items(self, ownerObj):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType="type2")
return ItemSerializer(queryset, many=True).data
This works. But it would be much cleaner if I could pass a parameter to the method instead of using two methods with almost the exact code. Ideally it would look like this:
...
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getItems', "type1")
type2items = serializers.SerializerMethodField(method_name='getItems', "type2")
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getItems(self, ownerObj, itemType):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType=itemType)
return ItemSerializer(queryset, many=True).data
In the docs SerializerMethodField accepts only one parameter which is method_name.
Is there any way to achieve this behaviour using SerializerMethodField? (The example code here is overly simplified so there might be mistakes.)
There is no way to do this with the base field.
You need to write a custom serializer field to support it. Here is an example one, which you'll probably want to modify depending on how you use it.
This version uses the kwargs from the field to pass as args to the function. I'd recommend doing this rather than using *args since you'll get more sensible errors, and flexibility in how you write your function/field definitions.
class MethodField(SerializerMethodField):
def __init__(self, method_name=None, **kwargs):
# use kwargs for our function instead, not the base class
super().__init__(method_name)
self.func_kwargs = kwargs
def to_representation(self, value):
method = getattr(self.parent, self.method_name)
return method(value, **self.func_kwargs)
Using the field in a serializer:
class Simple(Serializer):
field = MethodField("get_val", name="sam")
def get_val(self, obj, name=""):
return "my name is " + name
>>> print(Simple(instance=object()).data)
{'field': 'my name is sam'}
You could just refactor what you have:
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getType1Items')
type2items = serializers.SerializerMethodField(method_name='getType2Items')
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getType1Items(self, ownerObj):
return getItems(ownerObj,"type1")
def getType2Items(self, ownerObj):
return getItems(ownerObj,"type2")
def getItems(self, ownerObj, itemType):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType=itemType)
return ItemSerializer(queryset, many=True).data

How to filter a nested serializer's field in Django DRF

I have two models named 'School' and 'Student'. I've created each's serializers and the nested serializer for School having a student serializer as a nested field.
Here I want to apply filters on the fields of the serializers using 'django-filters' and it is almost working, BUT ...the problem is that when I filter the nested field, i-e 'students's field' , It doesn't show me the required result.
My models are :
class School(models.Model):
name = models.CharField(max_length=256)
principal = models.CharField(max_length=256)
location = models.CharField(max_length=256)
is_government = models.BooleanField(default=True)
def __str__(self):
return self.name
class Student(models.Model):
name = models.CharField(max_length=256)
age = models.PositiveIntegerField()
school = models.ForeignKey(School,related_name='students',on_delete = models.CASCADE)
is_adult = models.BooleanField(default=True)
def __str__(self):
return self.name
and my serializers are:
class SchoolSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
# Instantiate the superclass normally
super(SchoolSerializer, self).__init__(*args, **kwargs)
allow_students = self.context.get("allow_students",None)
if allow_students:
self.fields['students'] = StudentSerializer(many=True, context=kwargs['context'], fields=['name','age','is_adult'])
class Meta():
model = School
fields = '__all__'
class StudentSerializer(DynamicFieldsModelSerializer):
class Meta():
model = Student
fields = '__all__'
and these are the filters that i am using in my views:
from django_filters.rest_framework import DjangoFilterBackend
from django_filters import FilterSet
from django_filters import rest_framework as filters
class SchoolStudentAPIView(generics.ListAPIView, mixins.CreateModelMixin):
queryset = School.objects.all()
serializer_class = SchoolSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields = ('is_government','students__is_adult')
Here, the issue is that when i search for "students__is_adult", which is a nested field, It filters out the list of students that are adult ALONG WITH THE students that are not.
Can someone add something extra or give another solutuion? thank you
The problem
First of all, Django Rest Framework is not doing the query you'd expect. Let's see how to check.
One way to debug the actual query is adding a custom list() method to the SchoolStudentAPIView class, as follows:
def list(self, request, *args, **kwargs):
resp = super().list(request, *args, **kwargs)
from django.db import connection
print(connection.queries) # or set a breakpoint here
return resp
This method does nothing more than dumping all the executed queries to the console.
The last element of connection.queries is what we should focus on. It'll be a dict() with its "sql" key looking something like:
SELECT `school`.`id`, `school`.`name`, `school`.`location`, `school`.`is_government`
FROM `school` INNER JOIN `student` ON (`school`.`id` = `student`.`school_id`)
WHERE `student`.`is_adult` = 1
This query means that the SchoolSerializer will be passed all the Schools that have at least one adult Student.
By the way, the same school can appear multiple times, since the above query produces one row per adult student.
The SchoolSerializer, in the end, shows all the Students in the School regardless of any filtering option: this is what this line achieves.
if allow_students:
self.fields['students'] = StudentSerializer(many=True, ...)
Suggested solution
No simple solution is to be found with serializers. Maybe the more straightforward way is to write a custom list() method in the SchoolStudentAPIView class.
The method will:
look for the query string argument student__is_adult: if it's there, the method will create a custom field on each School in the queryset (I named it filtered_students), and make that field point to the correct Student queryset.
pass a context argument to the SchoolSerializer, to tell it that students are filtered
The SchoolSerializer class, in turn, will populate its students field in two different ways, depending on the presence or absence of the context argument. Specifically, the StudentSerializer field will have the source kwarg if the students__is_adult key is present in the passed context.
In code:
class SchoolStudentAPIView(generics.ListAPIView, mixins.CreateModelMixin):
# ...
def list(self, request, *args, **kwargs):
schools = self.get_queryset()
ctx = {}
if 'students__is_adult' in request.query_params:
filter_by_adult = bool(request.query_params['students__is_adult'])
ctx = {
'students__is_adult': filter_by_adult,
'allow_students': True,
}
for s in schools:
s.filtered_students = s.students.filter(is_adult=filter_by_adult)
ser = SchoolSerializer(data=schools, many=True, context=ctx)
ser.is_valid()
return Response(ser.data)
class SchoolSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super(SchoolSerializer, self).__init__(*args, **kwargs)
allow_students = self.context.get("allow_students", None)
if allow_students:
# Change 'source' to custom field if students are filtered
filter_active = self.context.get("posts__is_active", None)
if filter_active is not None:
stud = StudentSerializer(
source='filtered_students', many=True,
context=kwargs['context'],
fields=['name', 'age', 'is_adult'])
else:
stud = StudentSerializer(
many=True, context=kwargs['context'],
fields=['name', 'age', 'is_adult'])
self.fields['students'] = stud

Django Form field initial value when updating an instance

I have a custom Django ModelForm that I use to update a model instance.
This is the example model:
class MyModel(models.Model):
number = models.CharField(_("Number"), max_length=30, unique=True)
sent_date = models.DateField(_('Sent date'), null=True, blank=True)
When creating an instance I will pass only the number field, that is why I don't want the sent_date to be required.
Then I have a view that updates the sent_date field, using this custom form:
# Generic form updater
class MyModelUpdateForm(forms.ModelForm):
class Meta:
model = MyModel
fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make fields mandatory
if hasattr(self, 'required_fields'):
for field_name in self.required_fields:
self.fields[field_name].required = True
# Set initial values
if hasattr(self, 'initial_values'):
for field_name, value in self.initial_values.items():
self.initial[field_name] = value
class SentForm(MyModelUpdateForm):
required_fields = ['sent_date']
initial_values = {'sent_date': datetime.date.today()}
class Meta(MyModelUpdateForm.Meta):
fields = ['sent_date']
field_classes = {'sent_date': MyCustomDateField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
MyModelUpdateForm is a generic ancestor for concrete forms like SentForm.
In my view whenever there is a GET I manually instantiate the form with:
my_form = SentForm({instance: my_model_instance})
So in this case I would expect the sent_date field to have an initial value set to today's date even tough the real model instance field is None.
If I inspect my_form object it does indeed have these attributes:
initial: {'sent_date': datetime.date(2018, 3, 1)}
instance: my_model_instance
fields: {'sent_date':
...: ...,
'initial': None # Why this is None?
...: ...
}
So apparently it should work but it doesn't: the field is always empty.
So I suspect that the value is coming from my_model_instance.sent_date that is in fact None.
The initial['sent_date'] = datetime.date(2018, 3, 1) is correct.
On the other side fields['sent_date']['initial'] = None it's not.
How can I always show the initial value when my_model_instance.sent_date is None?
Apparently I've solved with:
class MyModelUpdateForm(forms.ModelForm):
class Meta:
model = MyModel
fields = []
def __init__(self, *args, **kwargs):
initial = kwargs.get('initial', {})
if hasattr(self, 'initial_values') and not kwargs.get('data'):
for field_name, value in self.initial_values.items():
if not getattr(kwargs.get('instance', None), field_name, None):
initial[field_name] = value
kwargs.update({'initial': initial})
super().__init__(*args, **kwargs)
# Make fields mandatory
if hasattr(self, 'required_fields'):
for field_name in self.required_fields:
self.fields[field_name].required = True
Even tough it works I wouldn't mind a less hackish solution if anyone has any :)
I have this case in many places in my app without having any problem. However, I use a different way to set up initial value of some fields of an existing instance. Instead of:
self.initial[field_name] = value
I write, after having called super():
self.fields[field_name].initial = value
Can you try and tell the result ?

How to specify the database for Factory Boy?

FactoryBoy seem to always create the instances in the default database. But I have the following problem.
cpses = CanonPerson.objects.filter(persons__vpd=6,
persons__country="United States").using("global")
The code is pointing to the global database. I haven't found a way to specify the database within the factory:
class CanonPersonFactory(django_factory.DjangoModelFactory):
class Meta:
model = CanonPerson
django_get_or_create = ('name_first', 'p_id')
p_id = 1
name_first = factory.Sequence(lambda n: "name_first #%s" % n)
#factory.post_generation
def persons(self, create, extracted, **kwargs):
if not create:
# Simple build, do nothing.
return
if extracted:
# A list of groups were passed in, use them
for person in extracted:
self.persons.add(person)
Looks like Factory Boy does not provide this feature from box, but you can easily add it manually:
class CanonPersonFactory(django_factory.DjangoModelFactory):
class Meta:
model = CanonPerson
...
#classmethod
def _get_manager(cls, model_class):
manager = super(CanonPersonFactory, cls)._get_manager(model_class)
return manager.using('global')
...
This is now directly supported by adding the database attribute on Meta:
class CanonPersonFactory(django_factory.DjangoModelFactory):
class Meta:
model = CanonPerson
database = 'global'
...

django rest framework and model inheritance

I have an "abstract" model class MyField:
class MyField(models.Model):
name = models.CharField(db_index = True, max_length=100)
user = models.ForeignKey("AppUser", null=False)
I have a few other subclasses of MyField each defining a value of a specific type.
for example:
class MyBooleanField(MyField):
value = models.BooleanField(db_index = True, default=False)
In MyField I have a method get_value() that returns the value based on the specific subclass.
In django rest I want to fetch all the fields of a user
class AppUserSerializer(serializers.ModelSerializer):
appuserfield_set = MyFieldSerializer(many=True)
class Meta:
model = AppUser
fields = ('appuser_id', 'appuserfield_set')
On the client side I want the user to be able to add new fields and set values to them and then on the server I want to be able to create the correct field based on the value.
What is the correct way to achieve this behavior?
After some digging, here is what I ended up doing. Aside from the code below I had to implement get_or_create and create the relevant subclass of MyField based on the passed value.
class ValueField(serializers.WritableField):
#called when serializing a field to a string. (for example when calling seralizer.data)
def to_native(self, obj):
return obj;
"""
Called when deserializing a field from a string
(for example when calling is_valid which calles restore_object)
"""
def from_native(self, data):
return data
class MyFieldSerializer(serializers.ModelSerializer):
value = ValueField(source='get_value', required=False)
def restore_object(self, attrs, instance=None):
"""
Called by is_valid (before calling save)
Create or update a new instance, given a dictionary
of deserialized field values.
Note that if we don't define this method, then deserializing
data will simply return a dictionary of items.
"""
if instance:
# Update existing instance
instance.user = attrs.get('user', instance.user)
instance.name = attrs.get('name', instance.name)
else:
# Create new instance
instance = MyField.get_or_create(end_user=attrs['user'],
name=attrs['name'],
value=attrs['get_value'])[0]
instance.value = attrs['get_value']
return instance
def save_object(self, obj, **kwargs):
#called when saving the instance to the DB
instance = MyField.get_or_create(end_user=obj.user,
name=obj.name,
value=obj.value)[0]
class Meta:
model = MyField
fields = ('id', 'user', 'name', 'value')

Categories

Resources