How to override field value display in Django admin change form - django

How can I override the value that is displayed for a field in the Django admin? The field contains XML and when viewing it in the admin I want to pretty-format it for easy readability. I know how to do reformatting on read and write of the field itself, but this is not what I want to do. I want the XML stored with whitespace stripped and I only want to reformat it when it is viewed in the admin change form.
How can I control the value displayed in the textarea of the admin change form for this field?

class MyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
self.initial['some_field'] = some_encoding_method(self.instance.some_field)
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm
...
Where, some_encoding_method would be something you've set up to determine the spacing/indentation or some other 3rd-party functionality you're borrowing on. However, if you write your own method, it would be better to put it on the model, itself, and then call it through the instance:
class MyModel(models.Model):
...
def encode_some_field(self):
# do something with self.some_field
return encoded_some_field
Then:
self.instance.encode_some_field()

Related

Django ModelForm- How to make a form generated uneditable

I am learning django form and want to know how to make a model form generated display only.
models.py
class Person(models.Model):
first_name = models.CharField(max_length=40, null=True)
last_name = models.CharField(max_length=40, null=True)
#more fields
forms.py
class PersonForm(ModelForm):
class Meta:
model = Person
To generate a form with some existing data in the database:
person=Person.objects.get(id=someid)
person_form = PersonForm(instance = person)
All the fields in the form are editable in the page. However, I just want to display the data.
After some searching in StackOverflow I found a similar solution how to show a django ModelForm field as uneditable , which teaches how to set individual field uneidtable.
But I want to make the whole form uneditable. Is there any better way to do so instead of setting all the fields as uneditable one by one?
Thank you very much for your help.
Updates: I find the flowing code helps make the form uneditable, but still not sure whether this is the correct way to do it.
for field in person_form.fields:
person_form.fields[field].widget.attrs['readonly'] = True
Thank you for giving your advice.
There is no attribute called editable or something similar on the form which can act on all the fields. So, you can't do this at form level.
Also, there is no such attribute on Field class used by django forms as well, so it wouldn't be possible to set such attribute and make the field read only. So, you will have to operate on on the fields of the form in __init__ of your form.
class PersonForm(ModelForm):
class Meta:
model = Person
def __init__(self, *args, **kwargs):
super(PersonForm, self).__init__(*args, **kwargs)
for name, field in self.fields.iteritems():
field.widget.attrs['readonly'] = 'true'
In case, you only want to make some fields uneditable, change the __init__.
def __init__(self, *args, **kwargs):
super(PersonForm, self).__init__(*args, **kwargs)
uneditable_fields = ['first_name', 'last_name']
for field in uneditable_fields:
self.fields[field].widget.attrs['readonly'] = 'true'
Another solution perhaps, do not have to do any processing, just display like this..
<table border='1'>
{% for field in form%}
<tr>
<td>{{field.label}}</td>
<td>{{field.value}}</td>
</tr>
{% endfor%}
</table>
I know, old question, but since I had the same question this week it might help other people.
This technique only works if you want the whole form to be readonly. It overrides any posted data (see def clean(self)) and sets the widget attributes to readonly.
Note: Setting the widget attributes to readonly does not prevent altering the model object instance.
class MyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(MyModelForm, self).__init__(*args, **kwargs)
if self.is_readonly():
for k,f in self.fields.iteritems():
f.widget.attrs['readonly'] = True
def clean(self):
if self.is_readonly():
return {}
return super(CompanyQuestionUpdateForm, self).clean()
def is_readonly(self, question):
if your_condition:
return True
return False
class Meta:
model = MyModel
It is possible to implement field widget to render bound ModelForm field values wrapped into div or td, sample implementation is there
https://github.com/Dmitri-Sintsov/django-jinja-knockout/blob/master/django_jinja_knockout/widgets.py
# Read-only widget for existing models.
class DisplayText(Widget):
Then a form metaclass can be implemented which will set field widget to DisplayText for all ModelForm fields automatically like that:
https://github.com/Dmitri-Sintsov/djk-sample/search?utf8=%E2%9C%93&q=DisplayModelMetaclass
class ClubDisplayForm(BootstrapModelForm, metaclass=DisplayModelMetaclass):
class Meta(ClubForm.Meta):
widgets = {
'category': DisplayText()
}
Feel free to use or to develop your own versions of widget / form metaclass.
There was discussion about read-only ModelForms at django bug ticket:
https://code.djangoproject.com/ticket/17031
closed as "Froms are for processing data, not rendering it."
But I believe that is mistake for these reasons:
ModelForms are not just processing data, they also map forms to models. Read-only mapping is the subset of mapping.
There are inline formsets and having read-only inline formsets is even more convenient, it leaves a lot of burden from rendering relations manually.
Class-based views can share common templates to display and to edit ModelForms. Thus read-only display ModelForms increase DRY (one of the key Django principles).

Can I create a custom django modelchoicefield with a default queryset

I have an order model with a followed_by field:
class order(models.Model):
followed_by = models.ForeignKey(User, limit_choices_to={'groups__name': "Managers"})
I have several such models and forms for those models. By default the form displays a modelchoicefield listing users that are mangers. This is fine. But the display isn't nice: it gives the username, and I want first+last name. This would work nicely: Change Django ModelChoiceField to show users' full names rather than usernames
except that now in everyform I must declare the queryset to limit users to managers. Can I use the above method so that the custom modelchoicefield defaults to my filtered queryset. so then from a form I can just say:
followed_by = ManagerUserModelChoiceField()
Can you define the queryset on your ModelChoiceField child class?
class UserModelChoiceField(ModelChoiceField):
# Query that returns set of valid choices
queryset = User.objects.filter(group__name='Managers')
def label_from_instance(self, obj):
return obj.get_full_name()
Try passing in the queryset as an argument to the ManagerUserModelChoiceField class.
followed_by = ModelChoiceField(queryset = User.objects.filter(groups__name="Managers")
After my comment to #Enrico this thought occurred to me: I overwrote the "init" class on my custom field like so:
class UserModelChoiceField(forms.ModelChoiceField):
def __init__(self, *args, **kwargs):
super(UserModelChoiceField, self).__init__(queryset=User.objects.filter(groups__name="Managers"), *args, **kwargs)
I've seen stuff like this done in python before but I'm new to python so I'm not sure if this is a bad thing to do or if I should make this better somehow? I'd appreciate some feedback. That being said, it seems to be working correctly.

When subclassing a model field, __init__'s arguments are not processed

I have a subclass of models.ForeignKey, the only purpose of which is to use a custom widget:
from django.db import models
from .models import Images
class GAEImageField(models.ForeignKey):
def __init__(self, **kwargs):
super(GAEImageField, self).__init__(Images, **kwargs)
def formfield(self, *args, **kwargs):
field = forms.ModelChoiceField(self, widget=ImageUploadWidget, *args, **kwargs)
field.queryset = Images.objects.all()
return field
The problem is, when I try to use this field, any parameters to __init__ are ignored. For instance, if I try this model:
class SomethingWithImage(models.Model):
name = models.CharField('Awesome name', max_length=64)
image = GAEImageField(verbose_name='Awesome image', blank=True)
...despite the fact I specified verbose_name, the label on a generated form will be "Image" and trying to specify empty value will raise an error even though I use blank=True
Well, the problem is with your formfield method. If you look at the implementation used in the default ForeignKey for example, you'll see it calls super. I'd recommend something like this:
def formfield(self, **kwargs):
defaults = {
'widget': ImageUploadWidget,
}
defaults.update(kwargs)
return super(GAEImageField, self).formfield(**defaults)
The problem is, form fields don't extract any information from model fields. Form fields can be used completely independently from models, they don't have to be necessarily backed by a model field. That means all settings, like whether the field is required or not, its label etc. have to be passed as parameters to their constructors. It is the responsibility of the model field to create an appropriate instance of a form field, all settings included. The default implementation of django.db.models.fields.Field.formfield takes care of that which is why you usually want to call the parent's method.
As for the blank issue, try also setting null=True, otherwise even though the form will accept a blank value, the database will reject it. Also, note that modifying the value of null after syncdb has been run requires a database migration.

Django: Read only field

How do I allow fields to be populated by the user at the time of object creation ("add" page) and then made read-only when accessed at "change" page?
The simplest solution I found was to override the get_readonly_fields function of ModelAdmin:
class TestAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
'''
Override to make certain fields readonly if this is a change request
'''
if obj is not None:
return self.readonly_fields + ('title',)
return self.readonly_fields
admin.site.register(TestModel, TestAdmin)
Object will be none for the add page, and an instance of your model for the change page.
Edit: Please note this was tested on Django==1.2
There's two thing to address in your question.
1. Read-only form fields
Doesn't exist as is in Django, but you can implement it yourself, and this blog post can help.
2. Different form for add/change
I guess you're looking for a solution in the admin site context (otherwise, just use 2 different forms in your views).
You could eventually override add_view or change_view in your ModelAdmin and use a different form in one of the view, but I'm afraid you will end up with an awful load of duplicated code.
Another solution I can think of, is a form that will modify its fields upon instantiation, when passed an instance parameter (ie: an edit case). Assuming you have a ReadOnlyField class, that would give you something like:
class MyModelAdminForm(forms.ModelForm):
class Meta:
model = Stuff
def __init__(self, *args, **kwargs):
super(MyModelAdminForm, self).__init__(*args, **kwargs)
if kwargs.get('instance') is not None:
self.fields['title'] = ReadOnlyField()
In here, the field title in the model Stuff will be read-only on the change page of the admin site, but editable on the creation form.
Hope that helps.
You can edit that model's save method to handle such a requirement. For example, you can check if the field already contains some value, if it does, ignore the new value.
One option is to override or replace the change_form template for that specific model.

How to prevent self (recursive) selection for FK / MTM fields in the Django Admin

Given a model with ForeignKeyField (FKF) or ManyToManyField (MTMF) fields with a foreignkey to 'self' how can I prevent self (recursive) selection within the Django Admin (admin).
In short, it should be possible to prevent self (recursive) selection of a model instance in the admin. This applies when editing existing instances of a model, not creating new instances.
For example, take the following model for an article in a news app;
class Article(models.Model):
title = models.CharField(max_length=100)
slug = models.SlugField()
related_articles = models.ManyToManyField('self')
If there are 3 Article instances (title: a1-3), when editing an existing Article instance via the admin the related_articles field is represented by default by a html (multiple)select box which provides a list of ALL articles (Article.objects.all()). The user should only see and be able to select Article instances other than itself, e.g. When editing Article a1, related_articles available to select = a2, a3.
I can currently see 3 potential to ways to do this, in order of decreasing preference;
Provide a way to set the queryset providing available choices in the admin form field for the related_articles (via an exclude query filter, e.g. Article.objects.filter(~Q(id__iexact=self.id)) to exclude the current instance being edited from the list of related_articles a user can see and select from. Creation/setting of the queryset to use could occur within the constructor (__init__) of a custom Article ModelForm, or, via some kind of dynamic limit_choices_to Model option. This would require a way to grab the instance being edited to use for filtering.
Override the save_model function of the Article Model or ModelAdmin class to check for and remove itself from the related_articles before saving the instance. This still means that admin users can see and select all articles including the instance being edited (for existing articles).
Filter out self references when required for use outside the admin, e.g. templates.
The ideal solution (1) is currently possible to do via custom model forms outside of the admin as it's possible to pass in a filtered queryset variable for the instance being edited to the model form constructor. Question is, can you get at the Article instance, i.e. 'self' being edited the admin before the form is created to do the same thing.
It could be I am going about this the wrong way, but if your allowed to define a FKF / MTMF to the same model then there should be a way to have the admin - do the right thing - and prevent a user from selecting itself by excluding it in the list of available choices.
Note: Solution 2 and 3 are possible to do now and are provided to try and avoid getting these as answers, ideally i'd like to get an answer to solution 1.
Carl is correct, here's a cut and paste code sample that would go in admin.py
I find navigating the Django relationships can be tricky if you don't have a solid grasp, and a living example can be worth 1000 time more than a "go read this" (not that you don't need to understand what is happening).
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self, *args, **kwargs):
super(MyForm, self).__init__(*args, **kwargs)
self.fields['myManyToManyField'].queryset = MyModel.objects.exclude(
id__exact=self.instance.id)
You can use a custom ModelForm in the admin (by setting the "form" attribute of your ModelAdmin subclass). So you do it the same way in the admin as you would anywhere else.
You can also override the get_form method of the ModelAdmin like so:
def get_form(self, request, obj=None, **kwargs):
"""
Modify the fields in the form that are self-referential by
removing self instance from queryset
"""
form = super().get_form(request, obj=None, **kwargs)
# obj won't exist yet for create page
if obj:
# Finds fieldnames of related fields whose model is self
rmself_fields = [f.name for f in self.model._meta.get_fields() if (
f.concrete and f.is_relation and f.related_model is self.model)]
for fieldname in rmself_fields:
form.base_fields[fieldname]._queryset =
form.base_fields[fieldname]._queryset.exclude(id=obj.id)
return form
Note that this is a on-size-fits-all solution that automatically finds self-referencing model fields and removes self from all of them :-)
I like the solution of checking at save() time:
def save(self, *args, **kwargs):
# call full_clean() that in turn will call clean()
self.full_clean()
return super().save(*args, **kwargs)
def clean(self):
obj = self
parents = set()
while obj is not None:
if obj in parents:
raise ValidationError('Loop error', code='infinite_loop')
parents.add(obj)
obj = obj.parent