I have two objects that are connected together by a ForeignKey.
class Question(models.Model):
text = models.Charfield()
class AnswerOption(models.Model):
text = models.Charfield()
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name="options")
When Question is created in the admin interface I'm using a Inline form for AnswerOptions so that they can be created at the same time. I'd like to perform some validation on the Question and it needs access to the new AnswerOptions to make the decision.
I've added a clean method to Question but the options property is empty.
What is the correct way to validate Question?
[EDIT]
Made it clear that Question needs access to the AnswerOptions to be able to validate everything.
[EDIT]
Added explicit reference to using an InlineForm for AnswerOptions in the admin interface.
I'd do this through a Django form, which have a more robust interface for
validation. The clean method on your form is the place for this type
of validation.
# forms.py
from django import forms
from .models import Question
class QuestionForm(forms.Form):
text = models.Charfield()
class Meta:
model = Question
def clean(self):
options = self.cleaned_data['options']
if not option.are_ok:
raise forms.ValidationError
# admin.py
from django import admin
from .forms import QuestionForm
class QuestionAdmin(admin.ModelAdmin):
form = QuestionForm
...
From the docs:
The form subclass’s clean() method can perform validation that requires access to multiple form fields. This is where you might put in checks such as “if field A is supplied, field B must contain a valid email address”. This method can return a completely different dictionary if it wishes, which will be used as the cleaned_data.
This is what I've discovered:
When creating Inline forms in the admin interface Django creates a Formset to handle the multiple forms. (The example here is the same as my use case)
Formsets have a clean() method like other forms and they have a forms property to access the child forms.
Just like normal forms they have an instance property that refers to the 'base' class and the individual forms have an instance property that gets you a instance of the newly submitted data.
Putting it all together:
# models.py
class Question(models.Model):
text = models.Charfield()
class AnswerOption(models.Model):
text = models.Charfield()
question = models.ForeignKey(Question, on_delete=models.CASCADE, related_name="options")
# admin.py
from django.contrib import admin
from django.forms.models import BaseInlineFormSet
class AnswerOptionFormset(BaseInlineFormset):
def clean(self):
super().clean() # See note in docs about calling this to check unique constraints
#self.instance -> Question, with all the newly submitted, and validated, data.
#self.forms -> iterator over all the submitted AnswerOption forms
#for f in self.forms:
# f.instance -> instance of AnswerOption containing the new validated data
#Note: self.instance.options will refer to the previous AnswerOptions
#raise ValidationError for anything that is wrong.
#It is also possible to modify the data in self.instance or form.instance instead.
class AnswerOptionInline(admin.TabularInline):
formset = AnswerOptionFormset # note formset on AnswerOption NOT QuestionAdmin
class QuestionAdmin(admin.ModelAdmin):
inlines = [AnswerOptionInline]
Related
Could anyone explain to me similarities and differences of Django's forms.Form & forms.ModelForm?
Forms created from forms.Form are manually configured by you. You're better off using these for forms that do not directly interact with models. For example a contact form, or a newsletter subscription form, where you might not necessarily be interacting with the database.
Where as a form created from forms.ModelForm will be automatically created and then can later be tweaked by you. The best examples really are from the superb documentation provided on the Django website.
forms.Form:
Documentation: Form objects
Example of a normal form created with forms.Form:
from django import forms
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
message = forms.CharField()
sender = forms.EmailField()
cc_myself = forms.BooleanField(required=False)
forms.ModelForm:
Documentation: Creating forms from models
Straight from the docs:
If your form is going to be used to
directly add or edit a Django model,
you can use a ModelForm to avoid
duplicating your model description.
Example of a model form created with forms.Modelform:
from django.forms import ModelForm
from . import models
# Create the form class.
class ArticleForm(ModelForm):
class Meta:
model = models.Article
This form automatically has all the same field types as the Article model it was created from.
The similarities are that they both generate sets of form inputs using widgets, and both validate data sent by the browser. The differences are that ModelForm gets its field definition from a specified model class, and also has methods that deal with saving of the underlying model to the database.
Here's how I'm extending the builtin UserCreationForm myapp/forms.py:
from django import forms
from django.contrib.auth.forms import UserCreationForm
class RegisterForm(UserCreationForm):
first_name = forms.CharField(max_length=30)
last_name = forms.CharField(max_length=30)
email = forms.CharField(max_length=75)
class Meta(UserCreationForm.Meta):
fields = ('username','first_name','last_name', 'email')
The difference is simple, ModelForm serves to create the form of a Model.
meaning that Model is designed to create kind of schema of your table where you will save data from form submission and ModelForm simply creates a form of the model (from the schema of the table)
# This creates a form from model Article
class ArticleForm(ModelForm):
class Meta:
model = Article
fields = ['pub_date', 'headline', 'content', 'reporter']
Form is a common form that is unrelated to your database (model ).
# A simple form to display Subject and Message field
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
message = forms.CharField(widget=forms.Textarea)
To say in other words,
If you have a model in your app and you want to create a Form to enter data in that model (and by it to a db) use forms.ModelForm
If you simple want to create a form using django use form.Form
But you can also use this together:
from django import forms
# A simple form to display Subject and Message field
class ContactForm(forms.ModelForm):
subject = forms.CharField(max_length=100)
message = forms.CharField(widget=forms.Textarea)
class Meta:
model = Contact #when you have this model
fields = [
'subject',
'message',
]
https://docs.djangoproject.com/en/1.10/ref/forms/validation/
States that run_validators() is run before the form subclass’s clean().
My model looks like:
def validate_ascii(value):
try:
value.encode('ascii')
except UnicodeEncodeError:
raise ValidationError("Contains non-ascii characters")
class Keyword(models.Model):
name = models.CharField(max_length=50, unique=True, validators=[validate_ascii])
In my form's clean() method
class KeywordAdminForm(ModelForm):
class Meta:
model = Keyword
def clean(self):
import pdb; pdb.set_trace()
cleaned_data = super(KeywordAdminForm, self).clean()
import pdb; pdb.set_trace()
return super(KeywordAdminForm, self).clean()
After that, the validators for each field in the form is run. This is causing issues because my clean method assumes each field has had the validator run first and crashes.
Why is my form's clean() method being run before the validators on the field?
Change your forms clean() method to call cleaned_data = super(KeywordAdminForm, self).clean() first before performing the rest of your validation. This is how the docs recommend you do it
This section of the docs has an explanation for your issue.
Model validation (Model.full_clean()) is triggered from within the
form validation step, right after the form’s clean() method is called.
This would suggest that you cannot rely on any model validation in your clean method
According to "Validation on a ModelForm" paragraph of the docs:
There are two main steps involved in validating a ModelForm:
Validating the form
Validating the model instance
This defines that there are two completely different layers of validation, one in the model level and one in the form level.
Therefore it is clearly wrong to expect that these layers of validation are somehow related.
However, there is a sound solution as described in the "Overriding the Default Fields" paragraph of the same chapter:
If you want to specify a field’s validators, you can do so by defining
the field declaratively and setting its validators parameter.
Your example could become:
from django.forms import CharField, ModelForm
from myapp.models import Keyword
class KeywordAdminForm(ModelForm):
slug = CharField(max_length=50, validators=[validate_ascii])
class Meta:
model = Keyword
fields = '__all__'
Bear in mind though to read the green "Note" that follows this example which states that:
Similarly, fields defined declaratively do not draw their attributes
like max_length or required from the corresponding model. If you want
to maintain the behavior specified in the model, you must set the
relevant arguments explicitly when declaring the form field.
Alternatively, you can do something like this:
from django.forms import CharField, ModelForm
from myapp.models import Keyword, validate_ascii
class KeywordAdminForm(ModelForm):
def clean_slug(self):
slug = self.cleaned_data.get('slug')
validate_ascii(slug)
return slug
def clean(self):
cleaned_data = super().clean()
if self.errors:
return cleaned_data
...
return cleaned_data
class Meta:
model = Keyword
fields = '__all__'
The above code works because it potentially raises a ValidationError inside clean_<field>() that is called before clean().
all. I'm working on the admin for my django site, and I've run into an obstacle.
I've got an Entry model and a Related model. The Related model has two foreign key fields: one to the Entry model (entry) and one to django's User model (author). The Related model is considered a "sub-model" of the Entry model, and each user can only have one Related per Entry.
In the admin, Related is edited inline with Entry. As I have it, the admin shows only one extra Related at a time, and it automatically fills the author field with the current user:
from django.contrib import models
from django.contrib.auth.models import User
class Entry(models.Model):
pass
class Related(models.Model):
entry = models.ForeignKey(Entry)
author = models.ForeignKey(User)
class Meta:
unique_together = ('entry', 'author')
from django.contrib import admin
class RelatedInline(admin.StackedInline):
model = Related
exclude = ('author',)
max_num = 1
class EntryAdmin(admin.ModelAdmin):
inlines = (RelatedInline,)
def save_formset(self, request, form, formset, change):
instances = formset.save(commit=False)
for instance in filter(lambda obj: isinstance(obj, Related), instances):
if instance.__dict__.get('author', None) is None:
instance.author = request.user
instance.save()
formset.save_m2m()
The problem is that if a user wants to edit an entry which already has a Related by anyone, then only that one related field will be shown.
If possible, I wonder if anyone has any ideas about how I could keep a setup similar to this, but have the admin automatically display the user's related if it exists and an empty form if it doesn't. Barring that, I would just get rid of the line max_num = 1 and replace it with extra = 1. Of course, this would mean that a "new related" form would show even if the user already had one for the current entry, so I wonder if anyone has any idea about how I would catch a possible IntegrityError and let the user know that an error had occurred.
It turns out this is pretty simple. You just need to add a queryset function to your RelatedInline class, specifying which inline to show. If the returned queryset has at least one member, the first will be shown. If the queryset is empty, a single blank inline will be shown!
class RelatedInline(admin.StackedInline):
model = Related
exclude = ('author',)
max_num = 1
def queryset(request):
return Related.objects.filter(author = request.user)
I am having trouble getting my model manager to behave correctly when using the Admin interface. Basically, I have two models:
class Employee(models.Model):
objects = models.EmployeeManager()
username = models.CharField(max_length=45, primary_key=True)
. . .
class Eotm(models.Model): #Employee of the Month
date = models.DateField()
employee = models.ForeignKey(Employee)
. . .
And I have an EmployeeManager class that overrides the get() method, something like this:
class EmployeeManager(models.Manager):
use_for_related_fields = True
def get(self, *arguments, **keywords):
try:
return super(EmployeeManager, self).get(*arguments, **keywords)
except self.model.DoesNotExist:
#If there is no Employee matching query, try an LDAP lookup and create
#a model instance for the result, if there is one.
Basically, the idea is to have Employee objects automatically created from the information in Active Directory if they don't already exist in the database. This works well from my application code, but when I tried to create a Django admin page for the Eotm model, things weren't so nice. I replaced the default widget for ForeignKey fields with a TextInput widget so users could type a username (since username is the primary key). In theory, this should call EmployeeManager.get(username='whatever'), which would either return an Employee just like the default manager or create one and return it if one didn't already exist. The problem is, my manager is not being used.
I can't find anything in the Django documentation about using custom Manager classes and the Admin site, aside from the generic manager documentation. I did find a blog entry that talked about specifying a custom manager for ModelAdmin classes, but that doesn't really help because I don't want to change the model represented by a ModelAdmin class, but one to which it is related.
I may not be understanding what you're trying to do here, but you could use a custom Form for your Eotm model:
#admin.py
from forms import EotmAdminForm
class EotmAdmin(models.ModelAdmin):
form = EotmAdminForm
#forms.py
from django import forms
from models import Eotm, Employee
class EotmAdminForm(forms.ModelForm)
class Meta:
model = Eotm
def clean_employee(self):
username = self.cleaned_data['employee']
return Employee.get(username=username)
That, in theory, should work. I haven't tested it.
I just want to know that is it possible to limit the number of objects of a model in admin panel?
It is that, for example, I have a model named 'Homepage' and in the admin panel I don't want a user can create more than one instance of Homepage.
Is there a way I can do this?
If it's just the admin that you want to affect (and don't want to affect the database model), you can create a custom ModelAdmin subclass:
class HomePageAdmin(admin.ModelAdmin):
def add_view(self, request):
if request.method == "POST":
# Assuming you want a single, global HomePage object
if HomePage.objects.count() > 1:
# redirect to a page saying
# you can't create more than one
return HttpResponseRedirect("foo")
return super(HomePageAdmin, self).add_view(request)
# ...
admin.site.register(HomePage, HomePageAdmin)
An alternative strategy to do the same thing is to create a custom ModelForm for HomePage, with a clean method that enforces the single HomePage requirement. This will make your requirement appear as a validation error, rather than as a redirect (or as a database error):
from django import forms
from django.forms.util import ErrorList
class HomePageModelForm(forms.ModelForm):
def clean(self):
if HomePage.objects.count() > 1:
self._errors.setdefault('__all__', ErrorList()).append("You can only create one HomePage object.")
return self.cleaned_data
# ...
class HomePageAdmin(admin.ModelAdmin):
form = HomePageModelForm
# ...
admin.site.register(HomePage, HomePageAdmin)
If it's "one HomePage per user", you will need HomePage to have a ForeignKey to User and adapt the above. You may also need to store the current User object in threadlocals in order to access it from HomePageModelForm.clean
If you want to limit Homepage to one for every user, then you could use one-to-one relation, with OneToOneField. As for limiting to N - a pre_save signal might be useful.
Try
class HomePage(models.Model):
user = models.ForeignKey(User, unique=True)
homepage = models.CharField(max_length=100, unique=True)
class Meta:
unique_together = (("user", "homepage"),)