Parameterized property in Django - python

For example, I have two models:
class Page(models.Model):
# Some fields
...
#property
def title(self):
return PageTranslation.objects.get(page=self, language=language).title # I can not pass property to the parameter
class PageTranslation(models.Model):
page = models.ForeignKey(Page)
title = models.CharField()
And some DRF view, which get_queryset method looks like this:
def get_queryset(self):
return Page.objects.all()
And serializer:
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = (..., 'title',) # title = property
I want to return QuerySet with Page model instances, and use title property in serializer, but I can not pass language (that is set somewhere in the request — headers, query param, etc) there.
What is the correct way to do this?

from django.utils.translation import get_language
from django.config import settings
class Page(models.Model):
#property
def title(self):
language = get_language() or settings.LANGUAGE_CODE
return PageTranslation.objects.get(page=self, language=language).title
get_language() gives you the current active language, if i18n is disabled it gives you None, and for that we have the settings.LANGUAGE_CODE fallback.
For the serializer part, I think you are supposed to explicitly say that your property is a field, ModelSerializer only finds the actual database fields for you, nothing else.
class PageSerializer(serializers.ModelSerializer):
title = serializers.Field()
class Meta:
model = Page
fields = (..., 'title',)

Register django.middleware.locale.LocaleMiddleware in your project settings. This makes LANGUAGE_CODE property available in the request
In DRF view where there is the request context, filter QuerySet by the language.
def get_queryset(self):
return Page.objects.filter(language=self.request.LANGUAGE_CODE)
Then computed title property declared in the Page model is not necessary and inefficient.
This is why:
N queries are executed per record in the original QuerySet to serialise a value for the title field. There is great likelihood for an N+1 problem to occur when another model has a many-to-many relation with Page.
Also, serialised results can be inconsistent because title value can be null in cases where the record doesn't exist for the language.

Related

Set a custom queryset (select_related) for a list field in the admin change page?

I have an using the Django admin interface to manage a lot of objects, and one of the page is giving me issue, this page has a field to a related object (Foreign Key) that has a __str__ that also goes to its related objects, this make a lot of queries and is barely useable (Around 3000 queries to show the page as there are a LOT of objects).
I would like to know if there is a way to set a custom queryset ? I would like to add a select_related or prefetch_related to this element.
The part causing issue is this certificate requests list :
The page model (Certificate has the following attribute:
class Certificate(models.Model):
certificate_request = models.OneToOneField(
"CertificateRequest",
verbose_name=_("Certificate request"),
related_name="certificate",
blank=True,
null=True
)
And the related model has this :
class CertificateRequest(models.Model):
domain = models.ForeignKey(
"Domain",
verbose_name=_("Domain"),
related_name="certificate_requests"
)
def __str__(self):
return "{state} certificate request for {domain} from {creation_date}".format(
state=dict(self.STATUS).get(self.status),
domain=self.domain.fqdn,
creation_date=self.creation_date
)
What would be the way to fix this ? How can I set a queryset on this part ?
EDIT: I added more informations.
I tried using a custom form, but this didn't do any change :
class CertificateForm(forms.ModelForm):
certificate_request = forms.ModelChoiceField(queryset=CertificateRequest.objects.select_related("domain"))
class Meta:
model = Certificate
fields = "__all__"
#admin.register(Certificate)
class CertificateAdmin(CompareVersionAdmin):
model = Certificate
class Meta:
form = CertificateForm
You can create a custom ModelForm for your admin where you specify a ModelChoiceField for the ForeignKey. Here you can specify the queryset parameter:
# forms.py
class MyForm(forms.ModelForm):
certificate_request = forms.ModelChoiceField(queryset=CertReq.objects.foo().bar())
# select/prefetch-------^^^^^^^^^^^
class Meta:
model = Foo
# admin.py
class YourAdmin(ModelAdmin):
form = MyForm
The get_object method on the ModelAdmin class is what is responsible for retrieving the object to edit. You could certainly extend that method in your subclass to use select_related as necessary.

DRF: Simple foreign key assignment with nested serializers?

With Django REST Framework, a standard ModelSerializer will allow ForeignKey model relationships to be assigned or changed by POSTing an ID as an Integer.
What's the simplest way to get this behavior out of a nested serializer?
Note, I am only talking about assigning existing database objects, not nested creation.
I have hacked away around this in the past with additional 'id' fields in the serializer and with custom create and update methods, but this is such a seemingly simple and frequent issue for me that I'm curious to know the best way.
class Child(models.Model):
name = CharField(max_length=20)
class Parent(models.Model):
name = CharField(max_length=20)
phone_number = models.ForeignKey(PhoneNumber)
child = models.ForeignKey(Child)
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
# phone_number relation is automatic and will accept ID integers
children = ChildSerializer() # this one will not
class Meta:
model = Parent
Updated on July 05 2020
This post is getting more attention and it indicates more people have a similar situation. So I decided to add a generic way to handle this problem. This generic way is best suitable for you if you have more serializers that need to change to this format
Since DRF doesn't provide this functionality out of the box, we need to create a serializer field first.
from rest_framework import serializers
class RelatedFieldAlternative(serializers.PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.serializer = kwargs.pop('serializer', None)
if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
raise TypeError('"serializer" is not a valid serializer class')
super().__init__(**kwargs)
def use_pk_only_optimization(self):
return False if self.serializer else True
def to_representation(self, instance):
if self.serializer:
return self.serializer(instance, context=self.context).data
return super().to_representation(instance)
I am not well impressed with this class name, RelatedFieldAlternative, you can use anything you want.
Then use this new serializer field in your parent serializer as,
class ParentSerializer(ModelSerializer):
child = RelatedFieldAlternative(queryset=Child.objects.all(), serializer=ChildSerializer)
class Meta:
model = Parent
fields = '__all__'
Original Post
Using two different fields would be ok (as #Kevin Brown and #joslarson mentioned), but I think it's not perfect (to me). Because getting data from one key (child) and sending data to another key (child_id) might be a little bit ambiguous for front-end developers. (no offense at all)
So, what I suggest here is, override the to_representation() method of ParentSerializer will do the job.
def to_representation(self, instance):
response = super().to_representation(instance)
response['child'] = ChildSerializer(instance.child).data
return response
Complete representation of Serializer
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
fields = '__all__'
class ParentSerializer(ModelSerializer):
class Meta:
model = Parent
fields = '__all__'
def to_representation(self, instance):
response = super().to_representation(instance)
response['child'] = ChildSerializer(instance.child).data
return response
Advantage of this method?
By using this method, we don't need two separate fields for creation and reading. Here both creation and reading can be done by using child key.
Sample payload to create parent instance
{
"name": "TestPOSTMAN_name",
"phone_number": 1,
"child": 1
}
Screenshot
The best solution here is to use two different fields: one for reading and the other for writing. Without doing some heavy lifting, it is difficult to get what you are looking for in a single field.
The read-only field would be your nested serializer (ChildSerializer in this case) and it will allow you to get the same nested representation that you are expecting. Most people define this as just child, because they already have their front-end written by this point and changing it would cause problems.
The write-only field would be a PrimaryKeyRelatedField, which is what you would typically use for assigning objects based on their primary key. This does not have to be write-only, especially if you are trying to go for symmetry between what is received and what is sent, but it sounds like that might suit you best. This field should have a source set to the foreign key field (child in this example) so it assigns it properly on creation and updating.
This has been brought up on the discussion group a few times, and I think this is still the best solution. Thanks to Sven Maurer for pointing it out.
Here's an example of what Kevin's answer is talking about, if you want to take that approach and use 2 separate fields.
In your models.py...
class Child(models.Model):
name = CharField(max_length=20)
class Parent(models.Model):
name = CharField(max_length=20)
phone_number = models.ForeignKey(PhoneNumber)
child = models.ForeignKey(Child)
then serializers.py...
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
# if child is required
child = ChildSerializer(read_only=True)
# if child is a required field and you want write to child properties through parent
# child = ChildSerializer(required=False)
# otherwise the following should work (untested)
# child = ChildSerializer()
child_id = serializers.PrimaryKeyRelatedField(
queryset=Child.objects.all(), source='child', write_only=True)
class Meta:
model = Parent
Setting source=child lets child_id act as child would by default had it not be overridden (our desired behavior). write_only=True makes child_id available to write to, but keeps it from showing up in the response since the id already shows up in the ChildSerializer.
There is a way to substitute a field on create/update operation:
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
child = ChildSerializer()
# called on create/update operations
def to_internal_value(self, data):
self.fields['child'] = serializers.PrimaryKeyRelatedField(
queryset=Child.objects.all())
return super(ParentSerializer, self).to_internal_value(data)
class Meta:
model = Parent
A few people here have placed a way to keep one field but still be able to get the details when retrieving the object and create it with only the ID. I made a little more generic implementation if people are interested:
First off the tests:
from rest_framework.relations import PrimaryKeyRelatedField
from django.test import TestCase
from .serializers import ModelRepresentationPrimaryKeyRelatedField, ProductSerializer
from .factories import SomethingElseFactory
from .models import SomethingElse
class TestModelRepresentationPrimaryKeyRelatedField(TestCase):
def setUp(self):
self.serializer = ModelRepresentationPrimaryKeyRelatedField(
model_serializer_class=SomethingElseSerializer,
queryset=SomethingElse.objects.all(),
)
def test_inherits_from_primary_key_related_field(self):
assert issubclass(ModelRepresentationPrimaryKeyRelatedField, PrimaryKeyRelatedField)
def test_use_pk_only_optimization_returns_false(self):
self.assertFalse(self.serializer.use_pk_only_optimization())
def test_to_representation_returns_serialized_object(self):
obj = SomethingElseFactory()
ret = self.serializer.to_representation(obj)
self.assertEqual(ret, SomethingElseSerializer(instance=obj).data)
Then the class itself:
from rest_framework.relations import PrimaryKeyRelatedField
class ModelRepresentationPrimaryKeyRelatedField(PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.model_serializer_class = kwargs.pop('model_serializer_class')
super().__init__(**kwargs)
def use_pk_only_optimization(self):
return False
def to_representation(self, value):
return self.model_serializer_class(instance=value).data
The usage is like so, if you have a serializer somewhere:
class YourSerializer(ModelSerializer):
something_else = ModelRepresentationPrimaryKeyRelatedField(queryset=SomethingElse.objects.all(), model_serializer_class=SomethingElseSerializer)
This will allow you to create an object with a foreign key still only with the PK, but will return the full serialized nested model when retrieving the object you created (or whenever really).
There is a package for that! Check out PresentablePrimaryKeyRelatedField in Drf Extra Fields package.
https://github.com/Hipo/drf-extra-fields
I think the approach outlined by Kevin probably would be the best solution, but I couldn't ever get it to work. DRF kept throwing errors when I had both a nested serializer and a primary key field set. Removing one or the other would function, but obviously didn't give me the result I needed. The best I could come up with is creating two different serializers for reading and writing, Like so...
serializers.py:
class ChildSerializer(serializers.ModelSerializer):
class Meta:
model = Child
class ParentSerializer(serializers.ModelSerializer):
class Meta:
abstract = True
model = Parent
fields = ('id', 'child', 'foo', 'bar', 'etc')
class ParentReadSerializer(ParentSerializer):
child = ChildSerializer()
views.py
class ParentViewSet(viewsets.ModelViewSet):
serializer_class = ParentSerializer
queryset = Parent.objects.all()
def get_serializer_class(self):
if self.request.method == 'GET':
return ParentReadSerializer
else:
return self.serializer_class
Here's how I've solved this problem.
serializers.py
class ChildSerializer(ModelSerializer):
def to_internal_value(self, data):
if data.get('id'):
return get_object_or_404(Child.objects.all(), pk=data.get('id'))
return super(ChildSerializer, self).to_internal_value(data)
You'll just pass your nested child serializer just as you get it from the serializer ie child as a json/dictionary. in to_internal_value we instantiate the child object if it has a valid ID so that DRF can further work with the object.
I started by implementing something similar to JPG's solution before I found this answer, and noticed that it breaks the built-in Django Rest Framework's templates. Now, that isn't such a big deal (as their solution works wonderfully via requests/postman/AJAX/curl/etc.), but if someone's new (like me) and wants the built-in DRF form to help them along the way, here's my solution (after cleaning it up and integrating some of JPG's ideas):
class NestedKeyField(serializers.PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.serializer = kwargs.pop('serializer', None)
if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
raise TypeError('You need to pass a instance of serialzers.Serializer or atleast something that inherits from it.')
super().__init__(**kwargs)
def use_pk_only_optimization(self):
return not self.serializer
def to_representation(self, value):
if self.serializer:
return dict(self.serializer(value, context=self.context).data)
else:
return super().to_representation(value)
def get_choices(self, cutoff=None):
queryset = self.get_queryset()
if queryset is None:
return {}
if cutoff is not None:
queryset = queryset[:cutoff]
return OrderedDict([
(
self.to_representation(item)['id'] if self.serializer else self.to_representation(item), # If you end up using another column-name for your primary key, you'll have to change this extraction-key here so it maps the select-element properly.
self.display_value(item)
)
for item in queryset
])
and an example below,
Child Serializer class:
class ChildSerializer(serializers.ModelSerializer):
class Meta:
model = ChildModel
fields = '__all__'
Parent Serializer Class:
class ParentSerializer(serializers.ModelSerializer):
same_field_name_as_model_foreign_key = NestedKeyField(queryset=ChildModel.objects.all(), serializer=ChildSerializer)
class Meta:
model = ParentModel
fields = '__all__'
Based on the answers of both JPG and Bono, I came up with a solution that handles the OpenAPI Schema generator of DRF as well.
The actual field class is:
from rest_framework import serializers
class ModelRepresentationPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.response_serializer_class = kwargs.pop('response_serializer_class', None)
if self.response_serializer_class is not None \
and not issubclass(self.response_serializer_class, serializers.Serializer):
raise TypeError('"serializer" is not a valid serializer class')
super(ModelRepresentationPrimaryKeyRelatedField, self).__init__(**kwargs)
def use_pk_only_optimization(self):
return False if self.response_serializer_class else True
def to_representation(self, instance):
if self.response_serializer_class is not None:
return self.response_serializer_class(instance, context=self.context).data
return super(ModelRepresentationPrimaryKeyRelatedField, self).to_representation(instance)
The extended AutoSchema class is:
import inspect
from rest_framework.schemas.openapi import AutoSchema
from .fields import ModelRepresentationPrimaryKeyRelatedField
class CustomSchema(AutoSchema):
def _map_field(self, field):
if isinstance(field, ModelRepresentationPrimaryKeyRelatedField) \
and hasattr(field, 'response_serializer_class'):
frame = inspect.currentframe().f_back
while frame is not None:
method_name = frame.f_code.co_name
if method_name == '_get_request_body':
break
elif method_name == '_get_responses':
field = field.response_serializer_class()
return super(CustomSchema, self)._map_field(field)
frame = frame.f_back
return super(CustomSchema, self)._map_field(field)
Then on your Dganjo's project settings you can define this new Schema class to be used globally like:
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': '<path_to_custom_schema>.CustomSchema',
}
Lastly from within your models you can use the new field type like:
class ExampleSerializer(serializers.ModelSerializer):
test_field = ModelRepresentationPrimaryKeyRelatedField(queryset=Test.objects.all(), response_serializer_class=TestListSerializer)
I have been also stuck in the same situation. But what i have done that i have created two serializers for the following models as follow:
class Base_Location(models.Model):
Base_Location_id = models.AutoField(primary_key = True)
Base_Location_Name = models.CharField(max_length=50, db_column="Base_Location_Name")
class Location(models.Model):
Location_id = models.AutoField(primary_key = True)
Location_Name = models.CharField(max_length=50, db_column="Location_Name")
Base_Location_id = models.ForeignKey(Base_Location, db_column="Base_Location_id", related_name="Location_Base_Location", on_delete=models.CASCADE)
This is my parent serializer
class BaseLocationSerializer(serializers.ModelSerializer):
class Meta:
model = Base_Location
fields = "__all__"
I'm using this serializer only for get request so in response i got data with foreign key also because of nested serializer
class LocationSerializerList(serializers.ModelSerializer): <-- using for get request
Base_Location_id = BaseLocationSerializer()
class Meta:
model = Location
fields = "__all__"
Screenshot of get method request and response in postman
I'm using this serializer only for post request so while sending post request i do not need to include any additional information rather than primary key field value
class LocationSerializerInsert(serializers.ModelSerializer): <-- using for post request
class Meta:
model = Location
fields = "__all__"
Screenshot of post method request and response in postman
Here's what I'm using all over. This may be the simplest, most straight forward method which needs no hacks etc, and is directly using DRF without jumping thru hoops. Happy to hear disagreements with this approach.
In the view's perform_create (or equivalent), fetch the FK model database object corresponding to the field sent in the POST request, and then send that into the Serializer. The field in the POST request can be anything that can be used to filter and locate the DB object, need not be an ID.
This is documented here: https://www.django-rest-framework.org/api-guide/generic-views/#genericapiview
These hooks are particularly useful for setting attributes that are
implicit in the request, but are not part of the request data. For
instance, you might set an attribute on the object based on the
request user, or based on a URL keyword argument.
def perform_create(self, serializer):
serializer.save(user=self.request.user)
This method also has the advantage of maintaining parity between the read and write side, by not sending a nested representation for child in the response to the GET or POST.
Given the example posted by the OP:
class Child(models.Model):
name = CharField(max_length=20)
class Parent(models.Model):
name = CharField(max_length=20)
phone_number = models.ForeignKey(PhoneNumber)
child = models.ForeignKey(Child)
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
# Note this is different from the OP's example. This will send the
# child name in the response
child = serializers.ReadOnlyField(source='child.name')
class Meta:
model = Parent
fields = ('name', 'phone_number', 'child')
In the View's perform_create:
class SomethingView(generics.ListCreateAPIView):
serializer_class = ParentSerializer
def perform_create(self, serializer):
child_name = self.request.data.get('child_name', None)
child_obj = get_object_or_404(Child.objects, name=child_name)
serializer.save(child=child_obj)
PS: Please note that I've not tested this above snippet, however its based on a pattern I'm using in many places so it should work as is.

Override queryset filters while generating django admin URLs

My models.py looks like this:
class Person(models.Model):
Name = models.CharField(max_length=100)
class Lecture(models.Model):
Speaker = model.ForeignKey(Person)
Topic = models.CharField(max_length=100)
Choices = ((1,"Upcoming"),(2,"In Progress",),(3,"Completed"))
Status = models.SmallIntegerField(choices=Choices, default=1, max_length=1)
My admin.py looks like this:
class LectureAdmin(admin.ModelAdmin):
def get_queryset(self):
return Lecture.objects.exclude(Status='Completed')
So my change list view in the django admin for the Lecture model shows only Lectures in "Upcoming" and "In Progress" status. This works fine.
Now I need to get the URL for the list of all lectures to be passed as a view somewhere else.The standard way of doing this in the django admin is by reversing the URL, so I do this:
urlresolvers.reverse('admin:%s_%s_changelist' % (app_label, model_name))
However, when I do this,I get the the filtered Queryset with Lectures in "Completed" state missing.How do I construct a url reverse function to get entire Lecture queryset and not the filtered queryset?
Here's a workaround, looks ugly, I understand.
Add all GET parameter to the changelist url:
url = urlresolvers.reverse('admin:%s_%s_changelist' % (app_label, model_name))
url += '?all'
Call get_queryset() on super(), exclude Completed status only if there is no all in request.GET:
class LectureAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super(LectureAdmin, self).get_queryset(request)
if 'all' not in request.GET:
qs = qs.exclude(Status='Completed')
return qs
UPD (applying other filters from request.GET):
from xadmin.plugin.related import RELATE_PREFIX # or just set RELATE_PREFIX = '_rel_'
qs = qs.filter(**{key[len(RELATE_PREFIX):]: value
for key, value in request.GET.iteritems()
if key.startswith(RELATE_PREFIX)})
** unpacks the dictionary into keyword arguments.
Hope it works for you.
get_queryset() is the basic queryset used in admin listing, thus you wo'nt be able to get all the records if you override it this way.
Possible solutions:
use filters ( https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.filter_vertical ) to exclude unwanted records (these with Status='Completed'
or
create proxy model for Lecture, register it in admin and use modified get_queryset() in given listing. Proxy model is required because each model can have registered only single AdminModel class
models.py
class IncompletedLecture(Lecture):
class Meta:
proxy = True
admin.py
class IncompletedAdmin(admin.ModelAdmin):
def get_queryset():
return Lecture.query.exclude(Status='Completed')
admin.site.register(IncompletedLecture, IncompletedAdmin)

how to add annotate data in django-rest-framework queryset responses?

I am generating aggregates for each item in a QuerySet:
def get_queryset(self):
from django.db.models import Count
queryset = Book.objects.annotate(Count('authors'))
return queryset
But I am not getting the count in the JSON response.
thank you in advance.
The accepted solution will hit the database as many times as results are returned. For each result, a count query to the database will be made.
The question is about adding annotations to the serializer, which is way more effective than doing a count query for each item in the response.
A solution for that:
models.py
class Author(models.Model):
name = models.CharField(...)
other_stuff = models...
...
class Book(models.Model):
author = models.ForeignKey(Author)
title = models.CharField(...)
publication_year = models...
...
serializers.py
class BookSerializer(serializers.ModelSerializer):
authors = serializers.IntegerField()
class Meta:
model = Book
fields = ('id', 'title', 'authors')
views.py
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.annotate(authors=Count('author'))
serializer_class = BookSerializer
...
That will make the counting at database level, avoiding to hit database to retrieve authors count for each one of the returned Book items.
The queryset returned from get_queryset provides the list of things that will go through the serializer, which controls how the objects will be represented. Try adding an additional field in your Book serializer, like:
author_count = serializers.IntegerField(
source='author_set.count',
read_only=True
)
Edit: As others have stated, this is not the most efficient way to add counts for cases where many results are returned, as it will hit the database for each instance. See the answer by #José for a more efficient solution.
Fiver's solution will hit the db for every instance in the queryset so if you have a large queryset, his solution will create a lot of queries.
I would override the to_representation of your Book serializer, it reuses the result from the annotation. It will look something like:
class BookSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
return {'id': instance.pk, 'num_authors': instance.authors__count}
class Meta:
model = Book
So, if you make an annotation like
Model.objects.annotate(
some_new_col=Case(
When(some_field=some_value, then=Value(something)),
# etc...
default=Value(something_default),
output_field=SomeTypeOfField(),
)
).filter()#etccc
and the interpreter throws in an error that something is not a model field for the related serializer, there is a workaround. It's not nice but if you add a method some_new_col, it recognizes the value from the query above.
The following will do just fine.
def some_new_col(self):
pass;

Add custom form fields that are not part of the model (Django)

I have a model registered on the admin site. One of its fields is a long string expression. I'd like to add custom form fields to the add/update pages of this model in the admin. Based on the values of these fields I will build the long string expression and save it in the relevant model field.
How can I do this?
I'm building a mathematical or string expression from symbols. The user chooses symbols (these are the custom fields that are not part of the model) and when they click save then I create a string expression representation from the list of symbols and store it in the DB. I don't want the symbols to be part of the model and DB, only the final expression.
Either in your admin.py or in a separate forms.py you can add a ModelForm class and then declare your extra fields inside that as you normally would. I've also given an example of how you might use these values in form.save():
from django import forms
from yourapp.models import YourModel
class YourModelForm(forms.ModelForm):
extra_field = forms.CharField()
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
# ...do something with extra_field here...
return super(YourModelForm, self).save(commit=commit)
class Meta:
model = YourModel
To have the extra fields appearing in the admin just:
Edit your admin.py and set the form property to refer to the form you created above.
Include your new fields in your fields or fieldsets declaration.
Like this:
class YourModelAdmin(admin.ModelAdmin):
form = YourModelForm
fieldsets = (
(None, {
'fields': ('name', 'description', 'extra_field',),
}),
)
UPDATE:
In Django 1.8 you need to add fields = '__all__' to the metaclass of YourModelForm.
It it possible to do in the admin, but there is not a very straightforward way to it. Also, I would like to advice to keep most business logic in your models, so you won't be dependent on the Django Admin.
Maybe it would be easier (and maybe even better) if you have the two seperate fields on your model. Then add a method on your model that combines them.
For example:
class MyModel(models.model):
field1 = models.CharField(max_length=10)
field2 = models.CharField(max_length=10)
def combined_fields(self):
return '{} {}'.format(self.field1, self.field2)
Then in the admin you can add the combined_fields() as a readonly field:
class MyModelAdmin(models.ModelAdmin):
list_display = ('field1', 'field2', 'combined_fields')
readonly_fields = ('combined_fields',)
def combined_fields(self, obj):
return obj.combined_fields()
If you want to store the combined_fields in the database you could also save it when you save the model:
def save(self, *args, **kwargs):
self.field3 = self.combined_fields()
super(MyModel, self).save(*args, **kwargs)
Django 2.1.1
The primary answer got me halfway to answering my question. It did not help me save the result to a field in my actual model. In my case I wanted a textfield that a user could enter data into, then when a save occurred the data would be processed and the result put into a field in the model and saved. While the original answer showed how to get the value from the extra field, it did not show how to save it back to the model at least in Django 2.1.1
This takes the value from an unbound custom field, processes, and saves it into my real description field:
class WidgetForm(forms.ModelForm):
extra_field = forms.CharField(required=False)
def processData(self, input):
# example of error handling
if False:
raise forms.ValidationError('Processing failed!')
return input + " has been processed"
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
# self.description = "my result" note that this does not work
# Get the form instance so I can write to its fields
instance = super(WidgetForm, self).save(commit=commit)
# this writes the processed data to the description field
instance.description = self.processData(extra_field)
if commit:
instance.save()
return instance
class Meta:
model = Widget
fields = "__all__"
You can always create new admin template, and do what you need in your admin_view (override the admin add URL to your admin_view):
url(r'^admin/mymodel/mymodel/add/$','admin_views.add_my_special_model')
If you absolutely only want to store the combined field on the model and not the two seperate fields, you could do something like this:
Create a custom form using the form attribute on your ModelAdmin. ModelAdmin.form
Parse the custom fields in the save_formset method on your ModelAdmin. ModelAdmin.save_model(request, obj, form, change)
I never done something like this so I'm not completely sure how it will work out.
The first (highest score) solution (https://stackoverflow.com/a/23337009/10843740) was accurate, but I have more.
If you declare fields by code, that solution works perfectly, but what if you want to build those dynamically?
In this case, creating fields in the __init__ function for the ModelForm won't work. You will need to pass a custom metaclass and override the declared_fields in the __new__ function!
Here is a sample:
class YourCustomMetaClass(forms.models.ModelFormMetaclass):
"""
For dynamically creating fields in ModelForm to be shown on the admin panel,
you must override the `declared_fields` property of the metaclass.
"""
def __new__(mcs, name, bases, attrs):
new_class = super(NamedTimingMetaClass, mcs).__new__(
mcs, name, bases, attrs)
# Adding fields dynamically.
new_class.declared_fields.update(...)
return new_class
# don't forget to pass the metaclass
class YourModelForm(forms.ModelForm, metaclass=YourCustomMetaClass):
"""
`metaclass=YourCustomMetaClass` is where the magic happens!
"""
# delcare static fields here
class Meta:
model = YourModel
fields = '__all__'
This is what I did to add the custom form field "extra_field" which is not the part of the model "MyModel" as shown below:
# "admin.py"
from django.contrib import admin
from django import forms
from .models import MyModel
class MyModelForm(forms.ModelForm):
extra_field = forms.CharField()
def save(self, commit=True):
extra_field = self.cleaned_data.get('extra_field', None)
# Do something with extra_field here
return super().save(commit=commit)
#admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
You might get help from my answer at :
my response previous on multicheckchoice custom field
You can also extend multiple forms having different custom fields and then assigning them to your inlines class like stackedinline or tabularinline:
form =
This way you can avoid formset complication where you need to add multiple custom fields from multiple models.
so your modeladmin looks like:
inlines = [form1inline, form2inline,...]
In my previous response to the link here, you will find init and save methods.
init will load when you view the page and save will send it to database.
in these two methods you can do your logic to add strings and then save thereafter view it back in Django admin change_form or change_list depending where you want.
list_display will show your fields on change_list.
Let me know if it helps ...
....
class CohortDetailInline3(admin.StackedInline):
model = CohortDetails
form = DisabilityTypesForm
...
class CohortDetailInline2(admin.StackedInline):
model = CohortDetails
form = StudentRPLForm
...
...
#admin.register(Cohort)
class CohortAdmin(admin.ModelAdmin):
form = CityInlineForm
inlines = [uploadInline, cohortDetailInline1,
CohortDetailInline2, CohortDetailInline3]
list_select_related = True
list_display = ['rto_student_code', 'first_name', 'family_name',]
...

Categories