Why would object.pk be None in get_success_url in CreateView? - django

I have a CreateView to which after creation I'd like the user to be directed to the DetailView for that newly created instance.
class ModelCreateView(LoginRequiredMixin, CreateView):
model = Model
template_name = 'models/model-create-template.html'
In my get_success_url method, I use self.object.pk, but it is None.
def get_success_url(self):
return f'/model/{self.object.pk}'
class ModelCreateView(LoginRequiredMixin, CreateView):
model = Model
template_name = 'models/model-create-template.html'
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
def get_success_url(self):
return f'/model/{self.object.pk}'
Model for reproduction:
class Model(models.Model):
id = models.IntegerField(primary_key=True)
Using this model with the code above will reproduce that self.object.pk or self.object.id is None within get_success_url.
I can see that I reach a successful save() since my post_save signals are firing, and form_valid() is called as well, since I update the user in this method.
When I look in my debugger at self.object, all of the other fields are populated, by id and pk are both None. Looking at the instance in admin, I can confirm these are actually being set correctly, but for some reason at the time of calling get_success_url I don't have access to this data.
Is the instance in self.object actually the created instance, or is it just representing the fields submitted by the CreateView form? If its the former, how could I access the instance in get_success_url?

Related

Django: access form argument in CreateView to pass to get_success_url

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.

Django UpdateView - get initial data from many-to-many field and add them when saving 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)

Class Based View, add data to the Form

I'm trying to achieve something that I tought would be pretty basic, but can't seem to find the solution for it.
I'm creating a pretty generic view for creating and updating users in a django app. I have a 'provider' model with associated permissions. I would like to add rights management in a very checkboxy simple way. When I show the form the checkbox should be check if user have permissions to add / delete / modify, and on the other hand, when the checkbox is checked, the permissions should be set in the database.
It goes like this :
class UserUpdate(UpdateView):
form_class = UserForm
model = User
def get_initial(self):
user = self.get_object()
if user is not None and user.has_perm('core.add_provider'):
return { 'right_provider' : True }
def form_valid(self, form):
user = form.save(commit=False)
if form.right_provider:
user.user_permissions.add('core.add_provider', 'core.change_provider', 'core.delete_provider')
else:
user.user_permissions.remove('core.add_provider', 'core.change_provider', 'core.delete_provider')
return super().form_valid(form)
Then a form :
class UserForm(ModelForm):
right_provider = BooleanField(label='Right Provider', required=False)
class Meta:
model = User
fields = ['username', 'email', 'first_name', 'last_name']
Apparently it's not the way to do it, since 'UserForm' object has no attribute 'right_provider'
Am i doing it right, and if so, what is the issue with this code?
Is there litterature on how to pass data back and forth between the ModelForm and the Model?
You can get the value from the form's cleaned_data.
def form_valid(self, form):
if form.cleaned_data['right_provider']:
...
else:
...
return super().form_valid(form)
def form_valid(self, form):
if form.cleaned_data['right_provider']:
...
else:
...
return super().form_valid(form)
Note I have removed the save() call - the form will be saved automatically when you call super().
If you do need to call save in your view, I would avoid calling super(). I think it's clearer, and it avoids a potential problem of save_m2m not being called. See the docs for the save method form more information.
def form_valid(self, form):
user = form.save(commit=False)
...
user.save() # save the user to the db
form.save_m2m() # required if the form has m2m fields
return HttpResponseRedirect(self.get_success_url())
Another option is to call super() first without returning, then access self.object, and finally return the response.
def form_valid(self, form):
response = super().form_valid(form)
user = self.object
if form.cleaned_data['right_provider']:
...
else:
...
return response
Model forms expect the form fields to match the model fields. So you could add it as a field to your model.
If you only want the field on the form, you want to add it in the innit method of the form.
class UserFrom(ModelForm)
def __init__(self, **kwargs)
super(UserForm, self).__init__(**kwargs)
# probably some code here to work out the initial state of the checkbox,
self.fields['right_provider'] = forms.BooleanField(label='Right Provider', required=False)

How do I set user field in form to the currently logged in user?

I'm making an election information app, and I want to allow the currently logged-in user to be able to declare himself and only himself as a candidate in an election.
I'm using Django's built-in ModelForm and CreateView. My problem is that the Run for Office form (in other words, the 'create candidate' form) allows the user to select any user in the database to make a candidate.
I want the user field in the Run for Office to be automatically set to the currently logged-in user, and for this value to be hidden, so the logged-in user cannot change the value of the field to someone else.
views.py
class CandidateCreateView(CreateView):
model = Candidate
form_class = CandidateForm
template_name = 'candidate_create.html'
def form_valid(self, form):
f = form.save(commit=False)
f.save()
return super(CandidateCreateView, self).form_valid(form)
forms.py
class CandidateForm(forms.ModelForm):
class Meta:
model = Candidate
models.py
class Candidate(models.Model):
user = models.ForeignKey(UserProfile)
office = models.ForeignKey(Office)
election = models.ForeignKey(Election)
description = models.TextField()
def __unicode__(self):
return unicode(self.user)
def get_absolute_url(self):
return reverse('candidate_detail', kwargs={'pk': str(self.id)})
Remove user field from rendered form (using exclude or fields, https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#selecting-the-fields-to-use )
class CandidateForm(forms.ModelForm):
class Meta:
model = Candidate
exclude = ["user"]
Find user profile and set user field in the create view.
class CandidateCreateView(CreateView):
...
def form_valid(self, form):
candidate = form.save(commit=False)
candidate.user = UserProfile.objects.get(user=self.request.user) # use your own profile here
candidate.save()
return HttpResponseRedirect(self.get_success_url())
Assumptions
We don't want to set null=True becaues we don't want to allow null users at the model and/or database level
We don't want to set blank=True to mess with the readability of model because the user actually will not be blank
#nbm.ten solution is a good one. It has an advantages over other 'solutions'to this problem that utilized model to set the user (like this one) in nbm.ten's doesn't undermine the assumptions above. We don't want to mess with the model to fix a problem in view!
But here I add two other solutions based on django documentation (Models and request.user):
Two other solutions
1. Using the generic CreateView
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView
from myapp.models import Candidate
class CandidateCreate(LoginRequiredMixin, CreateView):
model = Candidate
exclude = ['user']
def form_valid(self, form):
form.instance.user = self.request.user
return super().form_valid(form)
2. Using class-based views
class CandidateForm(ModelForm):
class Meta:
model = Candidate
exclude = [ 'user',]
class CandidateAddView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
form = CandidateForm()
context = {'form':form}
return render(request, 'myapp/addcandidateview.html', context)
def post(self, request, *args, **kwargs):
form = CandidateForm(request.POST)
form.instance.user = request.user
if form.is_valid():
form.save()
return redirect(reverse('myapp:index'))
NOTES
Note that LoginRequiredMixin prevents users who aren’t logged in from accessing the form. If we omit that, we'll need to handle unauthorized users in form_valid() or post().
Also exclude = ['user'] prevents the user field to be shown on the form.
We used form.instance.user to set the user not form.data or form.cleaned_data they don't work

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!