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.
Related
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.
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)
I have two models as shown below
class college(models.Model):
name = models.CharField(max_length=256)
class education(models.Model):
author = models.ForeignKey('auth.User')
school = ForeignKey(college)
field = models.CharField(max_length=200)
startyear = models.IntegerField(blank =True,null = True)
endyear = models.IntegerField(blank =True,null = True)
Views as shown below
class EducationListView(ListView):
template_name = 'education.html'
def get_queryset(self):
return education.objects.filter(author__username=self.request.user.username).order_by('-startyear')
class EducationCreate(CreateView):
model = dupeducation
fields = ('school','field','startyear','endyear')
template_name = 'education_form.html'
def form_valid(self, form):
form.instance.author = self.request.user
obj,created = college.objects.get_or_create(name=form['school'])
obj.save()
form.instance.school = obj
return super(EducationCreate, self).form_valid(form)
class EducationUpdate(UpdateView):
model = education
fields = ('school','field','startyear','endyear')
template_name = 'education_form.html'
def form_valid(self, form):
form.instance.author = self.request.user
return super(EducationUpdate, self).form_valid(form)
class EducationDelete(DeleteView):
model = education
success_url = reverse_lazy('education')
I am unable to save the form. It's throwing an error to the school field like this "Select a valid choice. That choice is not one of the available choices.".
My goal is to take input for the school field and check that field with get_object_or_create . If that object does not exist, create it and attach it to the school field.
If you debug, you'll see that save() is not being reached. Your problem is in the Field validation.
What you need to do is to override the clean_<field>() method called before any object is saved.
You can read more about it here: Django how to override clean() method in a subclass of custom form?
While overriding clean_school(), you will be able to add the value to the database, and later, in save(), simply make the attribution.
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
I'm using django extra views:
# views.py
from django.forms.models import inlineformset_factory
from extra_views import (CreateWithInlinesView, UpdateWithInlinesView,
InlineFormSet, )
class LinkInline(InlineFormSet):
model = Link
form = LinkForm
extra = 1
def get_form(self):
return LinkForm({})
def get_formset(self):
return inlineformset_factory(self.model, self.get_inline_model(), form=LinkForm, **self.get_factory_kwargs())
class TargetCreateView(BaseSingleClient, CreateWithInlinesView):
model = Target
form_class = TargetForm
inlines = [LinkInline, ]
template_name = 'clients/target_form.html'
I want this 'keywords' field to change based on the pk I pass to the view through the url.
# forms.py
class LinkForm(forms.ModelForm):
keywords = forms.ModelMultipleChoiceField(queryset=ClientKeyword.objects.filter(client__pk=1))
class Meta:
model = Link
I could manage to overwrite the form's init, however:
I don't have access to self.kwargs inside LinkInline
Even if I did, I'm not sure I can pass an instantiated form to inlineformset_factory()
Ok, if any poor soul needs an answer to how to accomplish this... I managed to do it by overwriting construct_inlines() (which is part of extra_views.advanced.ModelFormWithInlinesMixin) and modifying the field's queryset there.
class TargetCreateView(BaseSingleClient, CreateWithInlinesView):
model = Target
form_class = TargetForm
inlines = [LinkInline, ]
template_name = 'clients/target_form.html'
def construct_inlines(self):
'''I need to overwrite this method in order to change
the queryset for the "keywords" field'''
inline_formsets = super(TargetCreateView, self).construct_inlines()
inline_formsets[0].forms[0].fields[
'keywords'].queryset = ClientKeyword.objects.filter(
client__pk=self.kwargs['pk'])
return inline_formsets
def forms_valid(self, form, inlines):
context_data = self.get_context_data()
# We need the client instance
client = context_data['client_obj']
# And the cleaned_data from the form
data = form.cleaned_data
self.object = self.model(
client=client,
budget=data['budget'],
month=data['month']
)
self.object.save()
for formset in inlines:
f_cd = formset.cleaned_data[0]
print self.object.pk
link = Link(client=client,
target=self.object,
link_type=f_cd['link_type'],
month=self.object.month,
status='PEN',
)
# save the object so we can add the M2M fields
link.save()
for kw in f_cd['keywords'].all():
link.keywords.add(kw)
return HttpResponseRedirect(self.get_success_url())