Perhaps just another stupid beginner question:
I am having trouble overwriting a django model form which is currently using the following code in views.py.
The code below works well !
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
fields = ['title', 'content']
### want this instead of line 1 and 2 above --> form_class = PostCreateForm()
view_args = collections.namedtuple('view_args', ['page_title'])
view_args = view_args(page_title="Create Post")
def get_context_data(self, *args, **kwargs):
self.tgt_url_args = MyHelper.parse_tgt_url(self)
context = super(PostCreateView, self).get_context_data(*args, **kwargs)
context.update(MyHelper.get_context_metadata(self, self.tgt_url_args, PostCreateView.view_args))
return context
def form_valid(self, form):
form.instance.author = self.request.user
tgt_url_args = MyHelper.parse_tgt_url(self)
blog_article = Article.objects.get(pk=tgt_url_args.get('Article', '0'))
form.instance.article_field = blog_article
return super().form_valid(form)
However, I would like to overwrite the form in order to add placeholder to the fields. So I replace line 1 and 2 by:
form_class = PostCreateForm()
I also added the follwing in forms.py:
from django import forms
from . models import Post
class PostCreateForm(forms.ModelForm):
class Meta:
model = Post
fields = ['title', 'content']
print('imported PostCreateForm')
When I import and run this however, I get the following error:
'PostCreateForm' object is not callable
What am I doing wrong here?
You should not pass a constructed form, only a reference to the form class:
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostCreateForm # no parenthesis
# ...
Django will thus each time construct a new form (depending on the situation with request.POST, etc.).
If you need to pass extra parameters to the form, you can specify a dictionary in get_form_kwargs [Django-doc].
Related
I have a form attached to a DetailedView and its working fine when saved. I would like the form field(position) to be prepopulated with the value coming from the slug of the detailed view(e.g jobs/human-resource-manager). The Model of the form field has a Foreignkey to the JobPost model. Need help. Part of my view looks like this
class JobsDetailView(DetailView):
model = JobPost
template_name = 'job_post-detail.html'
def get_context_data(self, **kwargs):
context = super(JobsDetailView, self).get_context_data(**kwargs)
context['position'] = JobPost.objects.order_by('position')
context['job_app_form'] = JobsForm()
return context
foms.py
from django import forms
from job_post.models import JobsApplied
class JobsForm(forms.ModelForm):
class Meta:
model = JobsApplied
fields = '__all__'
def form_valid(self, form):
form.instance.customuser = self.request.user
return super().form_valid(form)
I'm assuming you do not want your users to be able to interact with or change these prefilled values.
I'm making a comments/review model and I want it to automatically link reviews to the people they are about
models.py
class Review(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
...
I hide the person field in the ReviewsForm to prevent user input by either omitting it from the 'fields' or adding it to an 'exclude'.
forms.py
class ReviewsForm(forms.ModelForm):
class Meta:
model = Review
fields = ('rating', 'summary', 'review_text')
Then, when processing the form in the view, I use commit=False so I can manipulate field values before saving to the database.
Include prefilled values, save and then redirect the user wherever is ideal
views.py
def person(request, area_slug, category_slug, person_id):
...
if form.is_valid():
pending_review = form.save(commit=False)
pending_review.person = Person.objects.get(pk = person_id)
pending_review.save()
return HttpResponseRedirect(...)
django fill form field automatically from context data for django form and django formsets
For formsets in forms.py
StoreRequestAccessoryUpdateFormSet = forms.modelformset_factory(StoreRequestAccessory, form=StoreRequestAccessoryUpdateForm, exclude=["storeRequestId"], can_delete=True)
In get_context_data you can add it as you like for django
class StoreRequestUpdateView(LoginRequiredMixin, UpdateView):
template_name = "Inventory/Stock/StoreRequest/StoreRequestUpdateView.html"
model = StoreRequest
fields = ["fromStoreId", "toStoreId", "reference", "status", "remark"]
def get_context_data(self, **kwargs):
context = super(StoreRequestUpdateView, self).get_context_data(**kwargs)
print(self.object.pk)
context.update({
# "StoreRequestForm": context.get("form"),
"StoreRequestForm": StoreRequestUpdateForm(instance=StoreRequest.objects.get(id=self.object.pk)),
"StoreRequestAccessoryForm": StoreRequestAccessoryUpdateFormSet(
queryset=StoreRequestAccessory.objects.filter(storeRequestId=self.object.pk),
prefix="storereq_accessory_form"),
})
return context
I have a url /<subject_id>/comments/new/ which renders a Django ModelForm. I am using a view class derived from FormView to process the form. I wish to do the following:
subject_id should not appear on the rendered form.
subject_id should be added to the form prior to is_valid() being called, or if this is not possible should be added to the Comment instance.
forms/comment_form.py:
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ['text']
views.py:
class CommentCreate(FormView):
form_class = CommentForm
def form_valid(self, form):
# Do some stuff to the validated Comment instance
# Maybe save the comment, maybe not
return super().form_valid(form)
How do I do this? If I add subject_id as a field in CommentForm then it appears on the rendered form. If I don't then the form is instantiated with subject_id present from `self.kwargs['subject_id'] and complains of an "unexpected keyword argument".
After some hunting around in the docs I have discovered that the correct answer is to use the get_form() method to pre-populate the form with the data that I don't want to appear on the form, but that needs to be present for validation.
class CommentCreate(FormView):
form_class = CommentForm
def get_form(self):
self.subject= get_object_or_404(Subject, id=self.kwargs['subject_id'])
partial_comment = Comment(user=self.request.user, subject=self.subject)
form = CommentForm(**self.get_form_kwargs(), instance=partial_comment)
return form
You can remove subject_id from form fields:
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ['text']
And add it to new comment object in form_valid method like this:
class OrderCreate(FormView):
form_class = CommentForm
def form_valid(self, form):
subject_id = self.kwargs['subject_id']
subject = Subject.objects.get(id=subject_id)
form.instance.subject_id = subject
return super().form_valid(form)
I'm using Django class based generic view. In my models.py I have a model called MyModel with many-to-many field called m2m. I have multiple groups of users they can edit the m2m field. Each group of users can only see and add their portion to the field - using get_form to set what they can see in the m2m field. The problem I'm having is that when one user enter his record it will delete the initial records in the m2m field. I need to somehow get the initial values from the m2m field save them and then add them to the new ones when the form is submitted. Here is my views.py:
class MyModelUpdate(UpdateView):
model = MyModel
fields = ['m2m']
def get_initial(self):
return initials
def get_form(self, form_class=None):
form = super(MyModelUpdate, self).get_form(form_class)
form.fields["m2m"].queryset = DiffModel.objects.filter(user = self.request.user)
return form
def form_valid(self, form):
form.instance.m2m.add( ??? add the initial values)
return super(MyModelUpdate, self).form_valid(form)
def get_success_url(self):
...
I'm adding this answer to offer a simplified explanation of this kind of problem, and also because the OP switches from an UpdateView to a function based view in his solution, which might not be what some users are looking for.
If you are using UpdateView for a model that has a ManyToMany field, but you are not displaying it to the user because you just want this data to be left alone, after saving the form all the m2m values will be erased.
That's obviously because Django expects this field to be included in the form, and not including it is the same as just sending it empty, therefore, to tell Django to delete all ManyToMany relationships.
In that simple case, you don't need to define the form_valid and then retrieve the original values and so on, you just need to tell Django not to expect this field.
So, if that's you view:
class ProjectFormView(generic.UpdateView):
model = Project
form_class = ProjectForm
template_name = 'project.html'
In your form, exclude the m2m field:
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
fields = '__all__'
exclude = ['many_to_many_field']
after few days of searching and coding I've found a solution.
views.py:
from itertools import chain
from .forms import MyForm,
def MyModelUpdate(request, pk):
template_name = 'mytemplate.html'
instance = MyModel.objects.get(pk = pk)
instance_m2m = instance.m2m.exclude(user=request.user)
if request.method == "GET":
form = MyForm(instance=instance, user=request.user)
return render(request, template_name, {'form':form})
else:
form = MyForm(request.POST or None, instance=instance, user=request.user)
if form.is_valid():
post = form.save(commit=False)
post.m2m = chain(form.cleaned_data['m2m'], instance_m2m)
post.save()
return redirect(...)
forms.py:
from django import forms
from .models import MyModel
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ['m2m']
def __init__(self, *args, **kwargs):
current_user = kwargs.pop('user')
super(MyForm, self).__init__(*args, **kwargs)
self.fields['m2m'].queryset = self.fields['m2m'].queryset.filter(user=current_user)
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())