Django: validating unique_together constraints in a ModelForm with excluded fields - django

I have a form:
class CourseStudentForm(forms.ModelForm):
class Meta:
model = CourseStudent
exclude = ['user']
for a model with some complicated requirements:
class CourseStudent(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL)
semester = models.ForeignKey(Semester)
block = models.ForeignKey(Block)
course = models.ForeignKey(Course)
grade = models.PositiveIntegerField()
class Meta:
unique_together = (
('semester', 'block', 'user'),
('user','course','grade'),
)
I want the new object to use the current logged in user for CourseStudent.user:
class CourseStudentCreate(CreateView):
model = CourseStudent
form_class = CourseStudentForm
success_url = reverse_lazy('quests:quests')
def form_valid(self, form):
form.instance.user = self.request.user
return super(CourseStudentCreate, self).form_valid(form)
This works, however, because the user is not part of the form, it misses the validation that Django would otherwise do with the unique_together constraints.
How can I get my form and view to use Django's validation on these constraints rather than having to write my own?
I though of passing the user in a hidden field in the form (rather than exclude it), but that appears to be unsafe (i.e. the user value could be changed)?

Setting form.instance.user in form_valid is too late, because the form has already been validated by then. Since that's the only custom thing your form_valid method does, you should remove it.
You could override get_form_kwargs, and pass in a CourseStudent instance with the user already set:
class CourseStudentCreate(CreateView):
model = CourseStudent
form_class = CourseStudentForm
success_url = reverse_lazy('quests:quests')
def get_form_kwargs(self):
kwargs = super(CreateView, self).get_form_kwargs()
kwargs['instance'] = CourseStudent(user=self.request.user)
return kwargs
That isn't enough to make it work, because the form validation skips the unique together constraints that refer to the user field. The solution is to override the model form's full_clean() method, and explicitly call validate_unique() on the model. Overriding the clean method (as you would normally do) doesn't work, because the instance hasn't been populated with values from the form at that point.
class CourseStudentForm(forms.ModelForm):
class Meta:
model = CourseStudent
exclude = ['user']
def full_clean(self):
super(CourseStudentForm, self).full_clean()
try:
self.instance.validate_unique()
except forms.ValidationError as e:
self._update_errors(e)

This worked for me, please check. Requesting feedback/suggestions.
(Based on this SO post.)
1) Modify POST request to send the excluded_field.
def post(self, request, *args, **kwargs):
obj = get_object_or_404(Model, id=id)
request.POST = request.POST.copy()
request.POST['excluded_field'] = obj
return super(Model, self).post(request, *args, **kwargs)
2) Update form's clean method with the required validation
def clean(self):
cleaned_data = self.cleaned_data
product = cleaned_data.get('included_field')
component = self.data['excluded_field']
if Model.objects.filter(included_field=included_field, excluded_field=excluded_field).count() > 0:
del cleaned_data['included_field']
self.add_error('included_field', 'Combination already exists.')
return cleaned_data

Related

django - view that creates or updates a model but requires a submit

I have class based modelview where I want the model/instance to be created or updated. If updating, I want the fields to show their current values. I have this working except that the instance is being created just by loading the view/template. As soon as the page loads, the object is saved. I think it's very important that the save is not done until the submit button is clicked.
I also want to override the model's save() method because I need to check what field is being updated and update a different model/object.
Model
class GradeBookSetup(DirtyFieldsMixin, models.Model):
user = models.OneToOneField(CustomUser, on_delete=CASCADE)
level1 = models.IntegerField(default=55)
scale_mode = models.CharField(max_length=7, blank=True, default="VSB")
teacher_show_sec_percent = models.BooleanField(default=True)
def save(self, *args, **kwargs):
super(GradeBookSetup, self).save(*args, **kwargs)
if 'scale_mode' in dirty_fields:
objs = Grade.objects.filter(user=self.user)
n = 0
for grade in objs:
objs[n].score = "BEG"
View
#method_decorator([login_required], name='dispatch')
class GradeBookSetupCreateView(UpdateView):
form_class = GradeBookSetupForm
model = GradeBookSetup
success_url = "/gradebook/"
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
def get_object(self, queryset=None):
obj, created = GradeBookSetup.objects.get_or_create(
user=self.request.user)
return obj
Form
class GradeBookSetupForm(forms.ModelForm):
class Meta:
model = GradeBookSetup
fields = ['level1', 'scale_mode', 'teacher_show_sec_percent']
labels = {
"level1": "What percent?",
"scale_mode": "Choose your proficiency scale.",
'teacher_show_sec_percent': "Teacher view (10-12), show percent",
}
EDIT
My next try is to move the get_or_create into the form_valid method as suggested by hendrikschneider. I still need to get the object to display the correct values after the initial save so I mean to do this with the try:. I had to change the get_or_create to update_or_create.
def form_valid(self, form):
form.instance.user = self.request.user
form.instance, created = GradeBookSetup.objects.update_or_create(
user=self.request.user, defaults={'scale_mode':form.instance.scale_mode})
return super().form_valid(form)
def get_object(self, queryset=None):
try:
obj = GradeBookSetup.objects.get(user=self.request.user)
return obj
except:
pass
The issue is your get_object method. The method is called when a get and when a post request is executed. As you use get_or_create there, it is always executed. Make sure that you only get_or_create in your form_valid method and not in get_object.

How to implement method from Model to form

I have in my models.py method of Model:
class Book(model.models):
...
def generate_tag(self):
return f'{self.inspection.series_of_tags}-{str(self.tag_number).zfill(7)}'
I want to implement this method in my form when I push on update view it generate new tag:
class TagForm(core.ModelForm):
def save(self, commit=True):
instance = super().save(commit=False)
if self.cleaned_data['tag_number']:
instance = Book.generate_tag(self.instance)
if commit:
instance.save()
return instance
class Meta:
model = Book
fields = (
'tag_number',
)
my views.py
class BookUpdateView(BookViewMixin, core.UpdateView):
form_class = TagForm
template_name = 'book.html'
I want that when I click update view - my field 'tag number' will give method from model 'generate_tag' when I save my Form!What I am doing wrong in my code?
So what you're doing in your form at the moment would work for a class method, Book.generate_tag(self.instance). That's calling a method on the Book class.
Your save() also won't work correctly because it may return a string, not a Book object;
def save(self, commit=True):
instance = super().save(commit=False)
if self.cleaned_data['tag_number']:
instance = Book.generate_tag(self.instance)
# Here instance is a string, because `generate_tag(self.instance)` returns a string.
if commit:
instance.save()
return instance
You probably want to exclude the field from the form as it doesn't get a value initially. Then you can generate the value in the view.
class TagForm(core.ModelForm):
class Meta:
model = Book
exclude = (
'tag_number',
)
class BookUpdateView(BookViewMixin, core.UpdateView):
form_class = TagForm
template_name = 'book.html'
def form_valid(self, form):
"""If the form is valid, save the associated model and generate the tag."""
self.object = form.save(commit=False)
tag = self.object.generate_tag()
# do something with your generated tag, like assign it to a field.
self.object.save()
return super().form_valid(form)

Create form to change relationship from related model's form

I have two models:
class Thing(forms.ModelForm):
class Owner(forms.ModelForm):
thing = models.OneToOneField(Thing)
I want to add a form to change the owner in Thing's UpdateView. I can do it like this:
class ThingForm(forms.ModelForm):
owner = forms.ModelChoiceField(
queryset=Owner.objects.all(),
)
class Meta:
model = Thing
fields = '__all__'
And then process the result inside form_valid() method. But isn't there a more direct approach for this, where i just add this to the fields of the form?
UPDATE
So I ended up doing it like this:
class ThingUpdateView(UpdateView):
model = Thing
form_class = ThingForm
def get_initial(self):
initial = super(ThingUpdateView, self).get_initial()
try:
initial['owner'] = self.object.owner
except Thing.owner.RelatedObjectDoesNotExist:
pass
return initial
def form_valid(self, form):
self.object = form.save(commit=False)
owner = form.cleaned_data['owner']
owner.thing = self.object
owner.save(update_fields=['thing'])
self.object.save()
return redirect(self.object.get_absolute_url())
Maybe there's a better way.

Django 1.3 CreateView/ModelForm: unique_together validation with one field excluded from form

I am looking for a simple answer by example to this common problem. The answers I found so far leave out critical points for us beginners.
I have an app where almost every model has a ForeignKey to User, and there is a unique_together constraint, where one of the fields is always 'user'.
For example:
class SubscriberList(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=70)
date_created = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = (
('user', 'name',),
)
def __unicode__(self):
return self.name
A SubscriberList is always created by a logged in User, and thus in the form to create a Subscriber List, I exclude the user field and give it a value of self.request.user when saving the form, like so:
class SubscriberListCreateView(AuthCreateView):
model = SubscriberList
template_name = "forms/app.html"
form_class = SubscriberListForm
success_url = "/app/lists/"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
return super(SubscriberListCreateView, self).form_valid(form)
And here is the accompanying form:
class SubscriberListForm(ModelForm):
class Meta:
model = SubscriberList
exclude = ('user')
With this code, valid data is fine. When I submit data that is not unique_together, I get an Integrity Error from the database. The reason is clear to me - Django doesn't validate the unique_together because the 'user' field is excluded.
How do I change my existing code, still using CreateView, so that submitted data that is not unique_together throws a form validation error, and not an Integrity Error from the db.
Yehonatan's example got me there, but I had to call the messages from within the ValidationError of form_valid, rather than a separate form_invalid function.
This works:
class SubscriberCreateView(AuthCreateView):
model = Subscriber
template_name = "forms/app.html"
form_class = SubscriberForm
success_url = "/app/subscribers/"
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
try:
self.object.full_clean()
except ValidationError:
#raise ValidationError("No can do, you have used this name before!")
#return self.form_invalid(form)
from django.forms.util import ErrorList
form._errors["email"] = ErrorList([u"You already have an email with that name man."])
return super(SubscriberCreateView, self).form_invalid(form)
return super(SubscriberCreateView, self).form_valid(form)
Taking from the docs at:
https://docs.djangoproject.com/en/dev/ref/models/instances/?from=olddocs#validating-objects
You should only need to call a model’s full_clean() method if you plan to handle validation errors yourself, or if you have excluded fields from the ModelForm that require validation.
Taking from the docs at:
https://docs.djangoproject.com/en/dev/ref/class-based-views/#formmixin
Views mixing FormMixin must provide an implementation of form_valid() and form_invalid().
This means that in order to view the error (which isn't form related) you'll need to implement your own form_invalid, add the special error message there, and return it.
So, running a full_clean() on your object should raise the unique_together error, so your code could look like this:
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.user = self.request.user
# validate unique_together constraint
try:
self.object.full_clean()
except ValidationError:
# here you can return the same view with error messages
# e.g.
return self.form_invalid(form)
return super(SubscriberListCreateView, self).form_valid(form)
def form_invalid(self, form):
# using messages
# from django.contrib import messages
# messages.error('You already have a list with that name')
# or adding a custom error
from django.forms.util import ErrorList
form._errors["name"] = ErrorList([u"You already have a list with that name"])
return super(SubscriberListCreateView, self).form_invalid(form)
HTH
adding another example that might be a bit easier for noobs.
forms.py
class GroupItemForm(ModelForm):
def form_valid(self):
self.object = self.save(commit=False)
try:
self.object.full_clean()
except ValidationError:
# here you can return the same view with error messages
# e.g. field level error or...
self._errors["sku"] = self.error_class([u"You already have an email with that name."])
# ... form level error
self.errors['__all__'] = self.error_class(["error msg"]
return False
return True
views.py
def add_stock_item_detail(request, item_id, form_class=GroupItemForm, template_name="myapp/mytemplate.html"):
item = get_object_or_404(Item, pk=item_id)
product = Product(item=item)
if request.method == 'POST':
form = form_class(request.POST, instance=product)
if form.is_valid() and form.form_valid():
form.save()
return HttpResponseRedirect('someurl')
else:
form = form_class(instance=product)
ctx.update({
"form" : form,
})
return render_to_response(template_name, RequestContext(request, ctx))

Django, adding excluded properties to the submitted modelform

I've a modelform and I excluded two fields, the create_date and the created_by fields. Now I get the "Not Null" error when using the save() method because the created_by is empty.
I've tried to add the user id to the form before the save() method like this: form.cleaned_data['created_by'] = 1 and form.cleaned_data['created_by_id'] = 1. But none of this works.
Can someone explain to me how I can 'add' additional stuff to the submitted modelform so that it will save?
class Location(models.Model):
name = models.CharField(max_length = 100)
created_by = models.ForeignKey(User)
create_date = models.DateTimeField(auto_now=True)
class LocationForm(forms.ModelForm):
class Meta:
model = Location
exclude = ('created_by', 'create_date', )
Since you have excluded the fields created_by and create_date in your form, trying to assign them through form.cleaned_data does not make any sense.
Here is what you can do:
If you have a view, you can simply use form.save(commit=False) and then set the value of created_by
def my_view(request):
if request.method == "POST":
form = LocationForm(request.POST)
if form.is_valid():
obj = form.save(commit=False)
obj.created_by = request.user
obj.save()
...
...
`
If you are using the Admin, you can override the save_model() method to get the desired result.
class LocationAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
obj.created_by = request.user
obj.save()
Pass a user as a parameter to form constructor, then use it to set created_by field of a model instance:
def add_location(request):
...
form = LocationForm(user=request.user)
...
class LocationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
super(forms.ModelForm, self).__init__(*args, **kwargs)
self.instance.created_by = user
The correct solution is to pass an instance of the object with pre-filled fields to the model form's constructor. That way the fields will be populated at validation time. Assigning values after form.save() may result in validation errors if fields are required.
LocationForm(request.POST or None, instance=Location(
created_by=request.user,
create_date=datetime.now(),
))
Notice that instance is an unsaved object, so the id will not be assigned until form saves it.
One way to do this is by using form.save(commit=False) (doc)
That will return an object instance of the model class without committing it to the database.
So, your processing might look something like this:
form = some_form(request.POST)
location = form.save(commit=False)
user = User(pk=1)
location.created_by = user
location.create_date = datetime.now()
location.save()