Django: Faking a field in the admin interface? - python

I have a model, Foo. It has several database properties, and several properties that are calculated based on a combination of factors. I would like to present these calculated properties to the user as if they were database properties. (The backing factors would be changed to reflect user input.) Is there a way to do this with the Django admin interface?

I would suggest you subclass a modelform for Foo (FooAdminForm) to add your own fields not backed by the database. Your custom validation can reside in the clean_* methods of ModelForm.
Inside the save_model method of FooAdmin you get the request, an instance of Foo and the form data, so you could do all processing of the data before/after saving the instance.
Here is an example for a model with a custom form registered with django admin:
from django import forms
from django.db import models
from django.contrib import admin
class Foo(models.Model):
name = models.CharField(max_length=30)
class FooAdminForm(forms.ModelForm):
# custom field not backed by database
calculated = forms.IntegerField()
class Meta:
model = Foo
class FooAdmin(admin.ModelAdmin):
# use the custom form instead of a generic modelform
form = FooAdminForm
# your own processing
def save_model(self, request, obj, form, change):
# for example:
obj.name = 'Foo #%d' % form.cleaned_data['calculated']
obj.save()
admin.site.register(Foo, FooAdmin)
Providing initial values for custom fields based on instance data
(I'm not sure if this is the best solution, but it should work.)
When a modelform for a existing model instance in the database is constructed, it gets passed this instance. So in FooAdminForm's __init__ one can change the fields attributes based on instance data.
def __init__(self, *args, **kwargs):
super(FooAdminForm, self).__init__(*args, **kwargs)
# only change attributes if an instance is passed
instance = kwargs.get('instance')
if instance:
self.fields['calculated'].initial = (instance.bar == 42)

It's easy enough to get arbitrary data to show up in change list or make a field show up in the form: list_display arbitrarily takes either actual model properties, or methods defined on the model or the modeladmin, and you can subclass forms.ModelForm to add any field type you'd like to the change form.
What's far more difficult/impossible is combining the two, i.e. having an arbitrary piece of data on the change list that you can edit in-place by specifying list_editable. Django seems to only accept a true model property that corresponds to a database field. (even using #property on the method in the model definition is not enough).
Has anyone found a way to edit a field not actually present on the model right from the change list page?

In the edit form, put the property name into readonly_fields (1.2 upwards only).
In the changelist, put it into list_display.

You can use the #property decorator in your model (Python >= 2.4):
class Product(models.Model):
#property
def ranking(self):
return 1
"ranking" can then be used in list_display:
class ProductAdmin(admin.ModelAdmin):
list_display = ('ranking', 'asin', 'title')

Related

django Admin - Filter foreign key select depending on other choice in edit form (without jQuery)

I am working on a project which is administered by a super admin who puts in data for different companies.
Lets say, I have these models:
class Company(models.Model):
name = models.CharField(max_length=100)
class ContactPerson(models.Model):
name = models.CharField(max_length=100)
company = models.ForeignKey(Company)
class Item(models.Model):
company = models.ForeignKey(Company)
contact_person = models.ForeignKey(ContactPerson)
I need to ensure that I (in django admin) in the edit mode I only see contact persons which belong to the selected company.
Being not in the year 2005 anymore I want to avoid writing loads of super ugly jQuery code.
I guess I could overwrite the admin form for Item. But still I had to make the contact_person optional, so when I create a new Item, the list of contact persons need to be empty. Then I'd select a company, save it and go back to edit. Now the contact_person list would be filled and I could add somebody. But if I now change the comany, I'd have to remove all selected contact persons. Sure, I could to this in the form... but it looks SO hacky and not like a nice django solution.
Anybody got some fancy ideas?
Actually, django provided me with a neat solution.
When you look at the UserAdmin class within the django code, you'll find a built-in way to handle a two-step creation process.
#admin.register(User)
class UserAdmin(admin.ModelAdmin):
...
add_form = UserCreationForm
...
def get_form(self, request, obj=None, **kwargs):
"""
Use special form during user creation
"""
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super().get_form(request, obj, **defaults)
When the attribute add_form is set and the object has no id yet (= we are creating it), it takes a different form than usual.
I wrapped this idea in an admin mixin like this:
class AdminCreateFormMixin:
"""
Mixin to easily use a different form for the create case (in comparison to "edit") in the django admin
Logic copied from `django.contrib.auth.admin.UserAdmin`
"""
add_form = None
def get_form(self, request, obj=None, **kwargs):
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super().get_form(request, obj, **defaults)
Now, when I have dependent fields, I create a small form, containing all values independent of - in my case - company and a regular form containing everything.
#admin.register(Item)
class ItemAdmin(AdminCreateFormMixin, admin.ModelAdmin):
form = ItemEditForm
add_form = ItemAddForm
...
Now I can customise the querysets of the dependent field in my edit form:
class ItemEditForm(forms.ModelForm):
class Meta:
model = Item
exclude = ()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['contact_person'].queryset = ContactPerson.objects.filter(company=self.instance.company)
The only drawback is, that all dependent fields need to be nullable for the database. Otherwise you wouldn't be able to save it in the creation process.
Luckily, you can tell django that a field is required in the form but not on database level with blank=False, null=True in the model declaration.
Hope this helps somebody else as well!

Where should i do the django validations for objects and fields?

I'm creating a django application which uses both the Django Rest Framework and the plain django-views as entrypoint for users.
I want to do validation both independant fields of my models, and on objects on a whole. For example:
Field: is the entered licence-plate a correct one based on a regex function. No relation to other fields.
Object: Is the entered zipcode valid for the given country. Relates to zipcode and country in the model.
For the DRF-API i use ModelSerializers which automatically call all the validators i have placed in my Model, for example:
class MyModel(models.Model):
licence_plate = CharField(max_length=20, validators=[LicencePlateValidator])
Since the validator is given in the model, the API POSTS (because i use a ModelSerializer), as well as the objects created in the django admin backend are validated.
But when i want to introduce object level validation i need to do that in the serializer's validate()-method, which means objects are only validated in the API.
I'll have to override the model's save method too, to validate the objects created in the Django admin page.
Question: This seems a bit messy to me, is there a single point where i can put the object-level validators so that they are run at the API and in the admin-page, like i did with the field-level validation (I only have to put them in my model-declaration and everything is handled)
For model-level validation, there is the Model.clean method.
It is called if you are using ModelForm (which is used by default in admin), so this solves django views and admin parts.
On the other hand, DRF does not call models' clean automatically, so you will have to do it yourself in Serializer.validate (as the doc suggests). You can do it via a serializer mixin:
class ValidateModelMixin(object)
def validate(self, attrs):
attrs = super().validate(attrs)
obj = self.Meta.model(**attrs)
obj.clean()
return attrs
class SomeModelSerializer(ValidateModelMixin, serializers.ModelSerializer):
#...
class Meta:
model = SomeModel
or write a validator:
class DelegateToModelValidator(object):
def set_context(self, serializer):
self.model = serializer.Meta.model
def __call__(self, attrs):
obj = self.model(**attrs)
obj.clean()
class SomeModelSerializer(serializers.ModelSerializer):
#...
class Meta:
model = SomeModel
validators = (
DelegateToModelValidator(),
)
Caveats:
an extra instantiation of your models just to call clean
you will still have to add the mixin/validator to your serializers
You can create a separate function validate_zipcode_with_country(zipcode, country) which will take 2 arguments zipcode and country.
Then, we will call this method in the serializer's validate() and in our model's clean().
from django.core.exceptions import ValidationError
def validate_zipcode_with_country(zipcode, country):
# check zipcode is valid for the given country
if not valid_zipcode:
raise ValidationError("Zipcode is not valid for this country.")
Then in your serializers.py, you need to call this function in your validate() function.
class MySerializer(serializers.ModelSerializer):
def validate(self, attrs):
zipcode = attrs.get('zipcode')
country = attrs.get('country')
validate_zipcode_with_country(zipcode, country) # call the function
...
Similarly, you need to override the model's clean() and call this function.
class MyModel(models.Model):
def clean(self):
validate_zipcode_with_country(self.zipcode, self.country) # call this function
...

Django REST Framework - Serializing optional fields

I have an object that has optional fields. I have defined my serializer this way:
class ProductSerializer(serializers.Serializer):
code = serializers.Field(source="Code")
classification = serializers.CharField(source="Classification", required=False)
I thought required=False would do the job of bypassing the field if it doesn't exist. However, it is mentioned in the documentation that this affects deserialization rather than serialization.
I'm getting the following error:
'Product' object has no attribute 'Classification'
Which is happening when I try to access .data of the serialized instance. (Doesn't this mean it's deserialization that's raising this?)
This happens for instances that do not have Classification. If I omit Classification from the serializer class it works just fine.
How do I correctly do this? Serialize an object with optional fields, that is.
Django REST Framework 3.0+
Dynamic fields now supported, see http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields -- this approach defines all of the fields in the serializer, and then allows you to selectively remove the ones you don't want.
Or you could also do something like this for a Model Serializer, where you mess around with Meta.fields in the serializer init:
class ProductSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('code',)
def __init__(self, *args, **kwargs):
if SHOW_CLASSIFICATION: # add logic here for optional viewing
self.Meta.fields = list(self.Meta.fields)
self.Meta.fields.append('classification')
super(ProductSerializer, self).__init__(*args, **kwargs)
You'd have to ask Tom though if this is the "correct way" since it may not fit in with the long term plan.
Django REST Framework < 3.0
Try something like this:
class ProductSerializer(serializers.Serializer):
...
classification = serializers.SerializerMethodField('get_classification')
def get_classification(self, obj):
return getattr(obj, 'classification', None)
Multiple Serializers
Another approach would be to create multiple serializers with different sets of fields. One serializer inherits from another and adds additional fields. Then you can choose the appropriate serializer in the view with the get_serializer_class method. Here's an actual example of how I use this approach to call different serializers to present different user data if the user object is the same as the request user.
def get_serializer_class(self):
""" An authenticated user looking at their own user object gets more data """
if self.get_object() == self.request.user:
return SelfUserSerializer
return UserSerializer
Removing fields from representation
Another approach that I've used in security contexts is to remove fields in the to_representation method. Define a method like
def remove_fields_from_representation(self, representation, remove_fields):
""" Removes fields from representation of instance. Call from
.to_representation() to apply field-level security.
* remove_fields: a list of fields to remove
"""
for remove_field in remove_fields:
try:
representation.pop(remove_field)
except KeyError:
# Ignore missing key -- a child serializer could inherit a "to_representation" method
# from its parent serializer that applies security to a field not present on
# the child serializer.
pass
and then in your serializer, call that method like
def to_representation(self, instance):
""" Apply field level security by removing fields for unauthorized users"""
representation = super(ProductSerializer, self).to_representation(instance)
if not permission_granted: # REPLACE WITH PERMISSION LOGIC
remove_fields = ('classification', )
self.remove_fields_from_representation(representation, remove_fields)
return representation
This approach is straightforward and flexible, but it comes at the cost of serializing fields that are sometimes not displayed. But that's probably okay.
The method describe below did the work for me.
Pretty simple,easy and worked for me.
DRF version used = djangorestframework (3.1.0)
class test(serializers.Serializer):
id= serializers.IntegerField()
name=serializers.CharField(required=False,default='some_default_value')
The serializers are deliberately designed to use a fixed set of fields so you wouldn't easily be able to optionally drop out one of the keys.
You could use a SerializerMethodField to either return the field value or None if the field doesn't exist, or you could not use serializers at all and simply write a view that returns the response directly.
Update for REST framework 3.0 serializer.fields can be modified on an instantiated serializer. When dynamic serializer classes are required I'd probably suggest altering the fields in a custom Serializer.__init__() method.
The serializers Charfield method has a property allow_blank
By default it is set to False.
Setting it to True will allow you to mark the field as optional during "serialization".
This is the code that you should write
classification = serializers.CharField(source="Classification", allow_blank=True)
Note: required property is used for deserialization.
DynamicSerializer for DRF 3, which allows dynamicly specifying which fields will be used in serializer, which will be excluded, and optionally which will become required!
Create Mixin
class DynamicSerializerMixin:
"""
A Serializer that takes an additional `fields` argument that
controls which fields should be used.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop("fields", None)
excluded_fields = kwargs.pop("excluded_fields", None)
required_fields = kwargs.pop("required_fields", None)
# Instantiate the superclass normally
super().__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
if isinstance(fields, dict):
for field, config in fields.items():
set_attrs(self.fields[field], config)
if excluded_fields is not None:
# Drop any fields that are not specified in the `fields` argument.
for field_name in excluded_fields:
self.fields.pop(field_name)
if required_fields is not None:
for field_name in required_fields:
self.fields[field_name].required = True
Initialize/adjust your serializer by adding DynamicSerializerMixin to inheritence
class UserProfileSerializer(DynamicSerializerMixin, serializers.ModelSerializer):
class Meta:
model = User
fields = (
"id",
'first_name', 'last_name'
"email",
"is_staff",
)
Use it :)
class RoleInvitationSerializer(serializers.ModelSerializer):
invited_by = UserProfileSerializer(fields=['id', 'first_name', 'last_name'])
or in action apis
#action(detail=True, serializer_class=YourSerialzierClass)
def teams_roles(self, request, pk=None):
user = self.get_object()
queryset = user.roles.all()
serializer = self.get_serializer(queryset, many=True, excluded_fields=['user'])
return Response(data=serializer.data)
For this purpose the serializers have the partial argument. If when the serializer is initialized you can pass partial=True. If you are using generics or mixins you can overrider the get_serializer function as follows:
def get_serializer(self, *args, **kwargs):
kwargs['partial'] = True
return super(YOUR_CLASS, self).get_serializer(*args, **kwargs)
And that will do the trick.
Note: This allows all fields to be optional and not only a specific one. If you want only specifics, you can override the method (i.e. update) and add validations of existence for various fields.
What has worked well for me is to set the serializer like so:
classification = serializers.CharField(max_length=20, allow_blank=True, default=None)
From the "it's a terrible hack relying on specific implementation details of both DRF and Django, but it works (at least for now)" files, here's the approach I used to include some additional debugging data in the response from a "create" method implementation on a serializer:
def create(self, validated_data)
# Actual model instance creation happens here...
self.fields["debug_info"] = serializers.DictField(read_only=True)
my_model.debug_info = extra_data
return my_model
This is a temporary approach that lets me use the browsable API to display some of the raw response data received from a particular remote service during the creation process. In the future, I'm inclined to keep this capability, but hide it behind a "report debugging info" flag in the creation request rather than returning the lower level info by default.

How can I rename a column label in Django Admin for a field that is a method//property?

I am trying to redefine my admin page for the auth.User model.
Everything is working properly, except for one thing. Check the code below:
from django.contrib import admin
from django.contrib.auth.models import User
from access.models import UserProfile
class UserProfileInline(admin.StackedInline):
model = UserProfile
class UserAdmim(admin.ModelAdmin):
inlines = [UserProfileInline,]
list_display = ['id', 'username', 'get_full_name', 'email']
admin.site.unregister(User)
admin.site.register(User, UserAdmim)
As you can see, one of the fields I want to be displayed in the model page listing -- defined by list_display -- is get_full_name. The problem is that the column label in the admin is displayed as Get full name.
My question is simple: can I override this? If so, how?
Thanks for your help.
Set an attribute in your function called short_description to your desired label in your model definition.
# note, this must be done in the class definition;
# not User.get_full_name.short_description
get_full_name.short_description = 'my label'
Alternatively, if you don't want to pollute your model with admin specific code, you can set list_display to a method on the ModelAdmin which takes one argument: the instance. You'll also have to set readonly_fields so that the admin doesn't try to look up this field in your model. I prefix admin fields with _ to differentiate.
class MyAdmin(...):
list_display = ('_my_field',)
readonly_fields = ('_my_field', )
def _my_field(self, obj):
return obj.get_full_name()
_my_field.short_description = 'my custom label'
Update:
Note that this will break default admin ordering. Your admin will no longer sort fields by clicking the label. To enable this functionality again, define an admin_order_field.
def _date_created(self, obj):
return obj.date_created.strftime('%m/%d/%Y')
_date_created.short_description = "Date Created"
_date_created.admin_order_field = 'date_created'
Update 2:
I've written an admin method decorator that simplifies this process, because once I started using highly descriptive verbose method names, setting attributes on the function became massively repetitive and cluttering.
def admin_method_attributes(**outer_kwargs):
""" Wrap an admin method with passed arguments as attributes and values.
DRY way of extremely common admin manipulation such as setting short_description, allow_tags, etc.
"""
def method_decorator(func):
for kw, arg in outer_kwargs.items():
setattr(func, kw, arg)
return func
return method_decorator
# usage
class ModelAdmin(admin.ModelAdmin):
#admin_method_attributes(short_description='Some Short Description', allow_tags=True)
def my_admin_method(self, obj):
return '''<em>obj.id</em>'''

Creating modelformset from a modelform

I have a model MyModel which contains a PK - locid, that is an AutoField.
I want to construct a model formset from this, with some caveats:
The queryset for the formset should be a custom one (say, order_by('field')) rather than all()
Since locid for MyModel is an AutoField and thus hidden by default, I want to be able to show it to the user.
I'm not sure how to do this. I've tried multiple approaches,
MyModelFormSet = modelformset_factory(MyModel, fields=('locid', 'name', 'dupof'))
The above gives me the 3 fields, but locid is hidden.
class MyModelForm(ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.fields['locid'].widget.attrs["type"] = 'visible'
locid = forms.IntegerField(min_value = 1, required=True)
class Meta:
model = MyModel
fields = ('locid', 'name', 'dupof')
The above gives me a ManyToMany error.
Has anyone done something like this before?
Edit 2
I can now use a custom query when I instantiate the formset - but I still need to show the locid field to the user, because the id is important for the application's use. How would I do this? Is there a way to override the default behavior of hiding a PK if its an autofield?
It makes no sense to show an autofield to the user, as it's an autoincremented primary key -- the user can not change it and it will not be available before saving the record to the database (where the DBMS selectes the next available id).
This is how you set a custom queryset for a formset:
from django.forms.models import BaseModelFormSet
class OrderedFormSet(BaseModelFormSet):
def __init__(self, *args, **kwargs):
self.queryset = MyModel.objects.order_by("field")
super(OrderedFormSet, self).__init__(*args, **kwargs)
and then you use that formset in the factory function:
MyModelFormSet = modelformset_factory(MyModel, formset=OrderedFormSet)
I ended up using a template side variable to do this, as I mentioned here:
How to show hidden autofield in django formset
If you like cheap workarounds, why not mangle the locid into the __unicode__ method? The user is guaranteed to see it, and no special knowledge of django-admin is required.
But, to be fair, all my answers to django-admin related questions tend along the lines of "don't strain to hard to make django-admin into an all-purpose CRUD interface".

Categories

Resources