When and how to use form methods - django

I am reading the Django documentation and ran across a couple examples in the forms sections that I do not understand why they did something 2 separate ways..
In the first example I found they send email from the FBV. This makes a lot of sense to a beginner:
from django.core.mail import send_mail
if form.is_valid():
subject = form.cleaned_data['subject']
message = form.cleaned_data['message']
sender = form.cleaned_data['sender']
cc_myself = form.cleaned_data['cc_myself']
recipients = ['info#example.com']
if cc_myself:
recipients.append(sender)
send_mail(subject, message, sender, recipients)
return HttpResponseRedirect('/thanks/')
In the second example I found they use CBVs and apply methods to the form and call this method in the view:
from django import forms
class ContactForm(forms.Form):
name = forms.CharField()
message = forms.CharField(widget=forms.Textarea)
def send_email(self):
# send email using the self.cleaned_data dictionary
pass
from myapp.forms import ContactForm
from django.views.generic.edit import FormView
class ContactView(FormView):
template_name = 'contact.html'
form_class = ContactForm
success_url = '/thanks/'
def form_valid(self, form):
# This method is called when valid form data has been POSTed.
# It should return an HttpResponse.
form.send_email()
return super().form_valid(form)
When should you apply form methods and call those methods in the view? Is there a benefit to writing the code this way?
Also, in the second example they used pass in the send_email() method. I wanted to investigate further so I applied this to simple invite form I had..
class InviteForm(forms.Form):
name = forms.CharField()
phone = PhoneNumberField(help_text='Format must be: +15595551234', required=False)
email = forms.EmailField()
subject = forms.CharField()
message = forms.CharField(widget=forms.Textarea)
def send_email(self):
send_mail(self.subject, self.message, 'someone#company.com', [self.email])
class InviteView(FormView):
template_name = 'invite/invite_form.html'
form_class = InviteForm
success_url = reverse_lazy('overview')
def form_valid(self, form):
form.send_email()
return super().form_valid(form)
Issue here is that I get:
AttributeError at /invite/
'InviteForm' object has no attribute 'subject'
Do I have to create parameters and pass them as arguments in the view after pulling the forms cleaned_data? If so, what is the point of creating the form method?

There isn't really a "correct" way of doing this. One could argue for doing this in the view but there are also good arguments to be made why send_mail should be part of the form.
Personally I like to keep my views as concise as possible. This is the first thing you read when you try to understand the behavior of the app. By just writing form.send_mail() it's clear that an email will be sent based on the data passed to the form. How this is done is nicely hidden and not really of concern to the view.
If you then later want to change what gets sent in the email, all the functionality is in one place, the form: the fields to populate and the send_mail functionality.

Related

Best way to save hidden fields from CreateView that have unique_together constraint?

I found a lot of questions and answers on this topic but all of them leave me confused as to what would be the best way to handle this and it seems a lot of aspects have to be considered (e. g. I read about putting the validate_unique() function into the ModelForm but some say it would be unsafe to pass the user to the form)
I want to build a form that users can use to register for an event. The event is derived from the url, the user from the request. Hence I don't actually need it in the form. The issue then is to conduct a check for the together_uniqueness as there can only be one registration per user per event.
Currently my code looks like this:
models.py:
class Participant(models.Model):
class Meta:
constraints = [models.UniqueConstraint(fields=["user", "event"], name="user_unique_per_event")]
user = models.ForeignKey(LocalUser, on_delete=models.PROTECT)
event = models.ForeignKey(Event, on_delete=models.CASCADE)
participating = models.BooleanField()
forms.py:
class EventRegistrationForm(forms.ModelForm):
class Meta:
model = Participant
fields= ['participating']
views.py:
class EventRegistrationView(CreateView):
template_name = 'events/event_registration.html'
form_class = EventRegistrationForm
queryset = Event.objects.all()
def form_valid(self, form):
form.instance.event = Event.objects.get(slug=self.kwargs['slug'])
form.instance.user = self.request.user
What's the best way to implement a check regarding the unique_together constraint?
What's the best way to handle an error that would be raised from that check?
I now figured out a way that seems to work. Maybe it helps somebody as I haven't yet found this exact solution on the web. Would be happy about feedback of course as well ;)
class EventRegistrationView(CreateView):
template_name = 'events/event_registration.html'
form_class = EventRegistrationForm
queryset = Event.objects.all()
def form_valid(self, form):
form.instance.event = Event.objects.get(slug=self.kwargs['slug'])
form.instance.user = self.request.user
try:
form.instance.validate_unique()
except ValidationError as e:
from django.forms.utils import ErrorList
form.add_error(field=None, error=e)
return super(EventRegistrationView, self).form_invalid(form)
return super().form_valid(form)

Django set creator/owner for the object when created

Let's say I have a simple model:
class Contact(models.Model):
owner = models.ForeignKey(User, editable=False)
first_name = models.CharField(max_length=255,)
last_name = models.CharField(max_length=255,)
email = models.EmailField()
I would like to set owner (request.user, logged in user) for the object automatically when it is created. I've searched a lot of different options but most of them are related to how you do it in admin side and other ones just don't work for me. I tried this for example http://blog.jvc26.org/2011/07/09/django-automatically-populate-request-user and then I've tried many ways to override save method or some kind of pre_save signal stuff. Nothing seems to do the trick, I just get an error
IntegrityError at /new
null value in column "owner_id" violates not-null constraint
What is the right way to do that? I know that this is simple think to do but I just can't find the way to do it.
...EDIT...
My create view looks like this:
class CreateContactView(LoginRequiredMixin, ContactOwnerMixin, CreateWithInlinesView):
model = models.Contact
template_name = 'contacts/edit_contact.html'
form_class = forms.ContactForm
inlines = [forms.ContactAddressFormSet]
def form_valid(self, form):
obj = form.save(commit=False)
obj.owner = self.request.user
obj.save()
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
return reverse('contacts-list')
def get_context_data(self, **kwargs):
context = super(CreateContactView, self).get_context_data(**kwargs)
context['action'] = reverse('contacts-new')
return context
That is just one way I tried to solve that problem so far. I found that solution from http://blog.jvc26.org/2011/07/09/django-automatically-populate-request-user
Assuming you are using ContactForm ModelForm:
def contact(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
contact = form.save(commit=False)
contact.owner = request.user
contact.save()
return HttpResponseRedirect('/thanks/')
else:
# do stuff
please post the exact code of what you tried.
If your view requires that a user is logged in, make sure it is enforced. This can be done by using the #login_required decorator
If you are in a view, and using a ModelForm to create the Contact, pass the commit=False kwarg to save method (like the example in the link you posted). This will keep the contact from being created until you assign the owner = request.user.
Since a logged in user is only available within the context of a request, just make sure that you are setting owner attribute it the views when creating a new Contact
The problem is that the default implementation of the form_valid method sets self.object, which is then used by get_success_url to determine where to redirect to.
If you replace your local obj variable with self.object, you should be fine:
def form_valid(self, form):
self.object = form.save(commit=False)
self.object.owner = self.request.user
self.object.save()
return HttpResponseRedirect(self.get_success_url())
I find a quick check of the original implementation for side-effects on the Classy Class-Based Views Web site, or the Django source-code on GitHub useful for spotting any side effects I need to reproduce in a subclass implementation.

How do I use UpdateView?

I have two, presumably related, problems with UpdateView. First, it is not updating the user but creating a new user object. Second, I cannot restrict the fields displayed in the form.
Here is my views.py:
class RegistrationView(FormView):
form_class = RegistrationForm
template_name = "register.html"
success_url = "/accounts/profile/"
def form_valid(self, form):
if form.is_valid:
user = form.save()
user = authenticate(username=user.username, password=form.cleaned_data['password1'])
login(self.request, user)
return super(RegistrationView, self).form_valid(form) #I still have no idea what this is
class UserUpdate(UpdateView):
model = User
form_class = RegistrationForm
fields = ['username', 'first_name']
template_name = "update.html"
success_url = "/accounts/profile/"
and urls.py
url(r'^create/$', RegistrationView.as_view(), name="create-user"),
url(r'^profile/(?P<pk>\d+)/edit/$', UserUpdate.as_view(), name="user-update"),
How do I properly use UpdateView?
Problem 1.
The user is not being updated because you are using the same form
(RegistrationForm) to do your updates and to create new users.
Problem 2. Forms belong in a file of their own called forms.py.
My suggested refactoring:
#forms.py
#place(forms.py) this in the same directory as views.py
class UpdateForm(forms.ModelForm):
#form for updating users
#the field you want to use should already be defined in the model
#so no need to add them here again DRY
class Meta:
model = User
fields = ('field1', 'field2', 'field3',)
#views.py
#import your forms
from .forms import UpdateForm
#also import your CBVs
from django.views.generic import UpdateView
class UserUpdate(UpdateView):
context_object_name = 'variable_used_in `update.html`'
form_class = UpdateForm
template_name = 'update.html'
success_url = 'success_url'
#get object
def get_object(self, queryset=None):
return self.request.user
#override form_valid method
def form_valid(self, form):
#save cleaned post data
clean = form.cleaned_data
context = {}
self.object = context.save(clean)
return super(UserUpdate, self).form_valid(form)
slightly elegant urls.py
#urls.py
#i'm assuming login is required to perform edit function
#in that case, we don't need to pass the 'id' in the url.
#we can just get the user instance
url(
regex=r'^profile/edit$',
view= UserUpdate.as_view(),
name='user-update'
),
You left out a lot of info so not really sure what your setup is.My solution is based on the assumption that you have Django 1.5. You can learn more about handling forms with CBVs
first: user = form.save() saves in the db the form. since there's no pk in the form it creates a new one.
what you have to do is probably to check if a user with that username exists and if not create it (for this part check google).
second: to restrict field you have to specify them in the Meta class of the Form (which you didn't show here) check this https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#modelform.
If you are getting new objects in the database instead of updating existing ones then it is likely that you copied and pasted the template for new objects and forgot to change the form's action attribute. This should point to view that does the update either in the form of a hard-coded path or a URL tag ({% url '<name of URLconf>' object.id %).

What is the best way to pass the request variable to the model using CreateView

I am working on a messaging system where I want to set the originator of the message based on the currently logged in user.
class Message(models.Model):
originator = models.ForeignKey(User, related_name='+')
destination = models.ForeignKey(User, related_name='+')
subject = models.CharField(max_length=100)
content = models.TextField()
I use a ModelForm and CreateView to represent this:
class MessageForm(forms.ModelForm):
class Meta:
model = Message
fields = ('destination', 'subject', 'content')
So prior to saving this form, originator needs to be set to be the currently logged-in user. I don't think overriding the save method of the model is appropriate here, so i was going to do it in the form's save method, however I don't have access to the request variable. In another CreateView post the recommendation was to override the get_form_kwargs method:
class MyFormView(FormView):
def get_form_kwargs(self):
# pass "user" keyword argument with the current user to your form
kwargs = super(MyFormView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
However that doesn't work, since you can't actually pass the kwarg user to the ModelForm, since ModelForm doesn't accept custom arguments.
What is the best (cleanest and most practical) way to update the originator variable with the user information?
In a discussion with yuji-tomita-tomita he suggested the following, which I wanted to share:
The first method relies on his original code, sending a user kwarg to the form with get_form_kwargs, you must then modify your form model similar to this:
class MyModelForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.user = self.kwargs.pop('user', None)
super(MyModelForm, self).__init__(*args, **kwargs)
This way the original __init__ function gets the arguments it is expecting.
But the preferred method he suggested was:
class MyView(CreateView):
def form_valid(self, form):
obj = form.save(commit=False)
obj.user = self.request.user
obj.save()
return super(MyView, self).form_valid(form)
This works really well. When the form.save(commit=False) method executes it populates self.instance in the form object with the same instance of the model it is returning to our variable obj. When you update obj by executing obj.user = self.request.user and save it, the form object has a reference to this exact same object, and therefore the form object here already is complete. When you pass it to the original form_valid method of CreateView, it has all of the data and will be successfully inserted into the database.
If anyone has a different way of doing this I'd love to hear about it!

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))