I have the following view which extends the base CreateView:
class PeopleImportCsv(FailedLoginMessageMixin, CreateView):
model = CsvFile
form_class = CustomerCsvImportForm
template_name = 'people/customer_uploadcsv_form.html'
def get_success_url(self):
url = reverse('customer_process_csv', args=[self.object.id])
return url
def form_valid(self, form):
instance = form.save(commit=False)
instance.uploaded_by = self.request.user
super(PeopleImportCsv, self).form_valid(form)
I am using the get_success_url() method so I can get the id of the newly created object in the database. However, when I attempt to submit my form I get the following ValueError message:
The view people.views.PeopleImportCsv didn't return an HttpResponse object.
If I place an assert False immediately after assigning the url in get_success_url() then I can see that it has the correct url I'm expecting so what can I do to sort this out?
You need to have a return from the form_valid method (if you are using a ModelForm):
def form_valid(self, form):
instance = form.save(commit=False)
instance.uploaded_by = self.request.user
return super(PeopleImportCsv, self).form_valid(form)
You can see the methods signature in the Django source
P.S There is a very useful site for referencing Djangos many class based views here: http://ccbv.co.uk/
Related
I have a form class view which creates an object (a product in a catalog) when the user fills the form. The object is created inside the form_valid method of the view. I want that the view redirects to the created object url (the product url) through the "success_url" atribute of the FormView.
The problem is that I do not know how to specify that url in the success_url method, since the object is still not created when the class itself is defined. I have tried with reverse_lazy, or the get_absolute_url() method of the object, but the same problem persists.
class ImageUpload(FormView):
[...]
success_url = reverse_lazy('images:product', kwargs={'id': product.id })
[...]
def form_valid(self, form):
[...]
self.product = Product.objects.create(
user=self.request.user, title=title)
Well at the class-level, there is no product, so you can not use product in the success_url.
What you can do is override the get_success_url, and thus determine the URL, like:
from django.urls import reverse
class ImageUpload(FormView):
def get_success_url(self):
return reverse('images:product', kwargs={'id': self.product.id })
def form_valid(self, form):
self.product = Product.objects.create(user=self.request.user, title=title)
return super(ImageUpload, self).form_valid(form)
In fact by default the get_success_url fetches the success_url attribute, and resolves it.
I use CreateView to let a user create a Piece. The Piece will automatically be assigned an id. After the user created the Piece I would like to redirect using get_success_url to another CreateView to add Versions of the Piece.
First of all, I do not know where the id of the Piece comes from (since it is generated automatically; I imagine this is the row number of the Piece in the model). How can I access this id to pass it to get_success_url?
The get_context_data method in CreateView seems not to be able to get the Piece id.
views.py
class PieceCreate(LoginRequiredMixin, CreateView):
model = Piece
fields = ['title', 'summary', 'created', 'piece_type']
initial = {'created': datetime.date.today()}
def form_valid(self, form):
form.instance.creator = Creator.objects.get(user=self.request.user)
return super(PieceCreate, self).form_valid(form)
def get_context_data(self, **kwargs):
context = super(PieceCreate, self).get_context_data(**kwargs)
return context['id']
def get_success_url(self):
return reverse_lazy('pieceinstance-create', kwargs={'pk': self.get_context_data()})
urls.py
path('pieceinstance/create/<int:pk>', views.PieceInstanceCreate.as_view(), name='pieceinstance-create')
The instance that is constructed in the CreateView can be accessed with self.object [Django-doc], so you can obtain the primary key with self.object.pk:
class PieceCreate(LoginRequiredMixin, CreateView):
model = Piece
fields = ['title', 'summary', 'created', 'piece_type']
initial = {'created': datetime.date.today()}
def form_valid(self, form):
form.instance.creator = Creator.objects.get(user=self.request.user)
return super(PieceCreate, self).form_valid(form)
def get_success_url(self):
return reverse_lazy('pieceinstance-create', kwargs={'pk': self.object.pk})
I would advice not to override the get_context_data function that way: first of all, the contract specifies that it should return a dictionary, so not an id, and multiple functions make use of this, and expect the contract to be satisfied.
I want to redirect the user to different pages (and different forms) , when the user fills out the first form (stating whether they have an online account or not) in a view and clicks submit.
I have tried this via the below but I get SupplyTypeForm has no attribute cleaned_data
class ServiceTypeView(FormView):
form_class = SupplyTypeForm
template_name = "supplier.html"
success_url = '/'
def post(self, request, *args, **kwargs):
super()
online_account = self.form_class.cleaned_data['online_account']
if online_account:
redirect('../online')
else:
redirect('../offline')
You should do this logic in the form_valid method, which recieves the form as an argument. Note you need to return the value of the call to render, and your call to super() on its own does nothing; you have to reference a method on that object.
def form_valid(self, form):
super().form_valid(form)
online_account = form.cleaned_data['online_account']
if online_account:
return render(request, "supplier_online.html")
else:
return render(request, 'supplier_offline.html')
Consider the FormView that I'm overriding, below. Upon successful creation of a new League record, how do I reference that record so that I can redirect the user to a more general edit page specific to that League?
class LeagueView(FormView):
template_name = 'leagueapp/addleague.html'
form_class = LeagueForm
def form_valid():
newleague = ??? #newly created league
success_url = '/league/editleague/' + str(newleague.id)
return HttpResponseRedirect(self.get_success_url())
this is really straight forward, given an url in the league namespace like
url(r'^league/editleague/(?P<id>\d+>)$', LeagueView.as_view(), name='edit')
you should edit the form_valid method in this way:
from django.shortcuts import redirect
class LeagueView(FormView):
template_name = 'leagueapp/addleague.html'
form_class = LeagueForm
def form_valid(self, form):
newleague = form.save()
return redirect('league:edit', id=newleague.id)
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))