I have following models:
class BankAccount(models.Model):
owner = models.ForeignKey(User)
class MoneyTransfer(models.Model):
sender = models.ForeignKey(BankAccount)
and url:
url(r'^accounts/(?P<pk>\w+)/send_transfer$', SendTransfer.as_view(), name='SendTransfer')
that means "I want to send money from Account with id=pk"
This is my view:
class SendTransfer(View):
form_class = SendTransferForm
template_name = 'dashboard/send_transfer.html'
def get(self, request, *args, **kwargs):
instance = BankAccount.objects.get(id=self.kwargs['pk'])
if instance.is_legal():
if instance.organization.owners.all().filter(user__id=self.request.user.id).count() == 0:
return None
else:
if instance.citizen.user.id != self.request.user.id:
return None
return render(self.request, self.template_name, self.get_context_data())
def post(self, request, *args, **kwargs):
sender = BankAccount.objects.get(id=kwargs['pk'])
form = self.form_class(sender, self.request.user, request.POST)
if form.is_valid():
MoneyTransfer.objects.create(sender=sender,
receiver=BankAccount.objects.get(id=self.request.POST['receiver']),
total=float(self.request.POST['total']),
when=timezone.localtime(timezone.now()),
comment=self.request.POST['comment'])
return redirect('AccountDetail', kwargs['pk'])
data = self.get_context_data()
data['form'] = form
return render(request, self.template_name, data)
def get_context_data(self):
account = BankAccount.objects.get(id=self.kwargs['pk'])
return {'form': SendTransferForm(account, self.request.user),
'user': self.request.user,
'account': account}
I think there's a lot of redudant code for CBV. What can I do for shorting it?
UPD
my current code:
class SendTransfer(SingleObjectMixin, FormView):
model = BankAccount
form_class = SendTransferForm
template_name = 'dashboard/send_transfer.html'
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
return super(SendTransfer, self).dispatch(request, *args, **kwargs)
def get_object(self, queryset=None):
obj = super(SendTransfer, self).get_object(queryset)
if obj.is_legal():
if not obj.organization.owners.filter(user=self.request.user).exists():
raise Http404
else:
if obj.citizen.user != self.request.user:
raise Http404
return obj
def form_valid(self, form):
data = form.cleaned_data
MoneyTransfer.objects.create(sender=self.object,
receiver=data['receiver'], # ModelChoiceField in the form
total=data['total'], # FloatField in the form, etc.
when=timezone.localtime(timezone.now()),
comment=data['comment'])
return redirect('AccountDetail', self.object.pk)
last line of dispatch() method raises TypeError: init() takes at least 3 arguments (1 given)
CBVs are designed for code reuse. If you don't yet have another class that could benefit of code you posted, the actual amount of code is almost identical, be that a CBVs or a plain function.
But the more pythonic and Django-ish (from my biased POV) way would be to:
Inherit your class from the FormView instead of the View. That eases the form management a bit.
Add a SingleObjectMixin to get the object from url kwargs for free.
Move your object validation to the get_object() method. It's a good practive to raise 404 if your object doesn't validate.
Refactor out the get_context_data() as you already have all that data in your context (request, form and object)
Instead of relying on the self.request.POST, clean your data through the form.
class SendTransfer(SingleObjectMixin, FormView):
model = BankAccount
form_class = SendTransferForm
template_name = 'dashboard/send_transfer.html'
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
return super(SendTransfer).dispatch(request, *args, **kwargs)
def get_object(self, queryset=None):
obj = super(SendTransfer, self).get_object(queryset)
if obj.is_legal():
if not obj.organization.owners.filter(user=self.request.user).exists():
raise Http404
else:
if obj.citizen.user != self.request.user:
raise Http404
return obj
def form_valid(self, form):
data = form.cleaned_data
MoneyTransfer.objects.create(sender=self.object,
receiver=data['receiver'], # ModelChoiceField in the form
total=data['total'], # FloatField in the form, etc.
when=timezone.localtime(timezone.now()),
comment=data['comment'])
return redirect('AccountDetail', self.object.pk)
Some of your code has gone thanks to the CBV magic, some just has moved to another methods. Take a look, I'd welcome your comments.
Related
I am using a generic view to render my blog post item:
class PostUpdateView(UpdateView, LoginRequiredMixin):
model = Post
# etc
I have a model method on the Post model that results in a boolean True or False:
#property
def can_edit(self):
return self.displays_set.count() == 0
If can_edit is False for the Post object, how can I refactor the view to redirect from my UpdateView to a different DetailView?
Override the dispatch method, and check obj.can_edit there. That way the object will be checked for get and post requests.
class PostUpdateView(LoginRequiredMixin, UpdateView):
model = Post
def dispatch(self, *args, **kwargs):
obj = self.get_object()
if not obj.can_edit:
return redirect('/readonly-view/')
return super().dispatch(*args, **kwargs)
With this solution, get_object() is called twice so there is a duplicate SQL query. However this is probably worth it to keep the code simple.
I would say that override dispatch method is best solution,
but if you want to avoid extra database hit, then you need to override get and post methods
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if not obj.can_edit:
return redirect('/readonly-view/')
return self.render_to_response(self.get_context_data())
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if not obj.can_edit:
return redirect('/readonly-view/')
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
I have this URL
path('private/productores/<pk>', views.Productor_Private.as_view()),
Views.py
class Productor_Private(generic.DetailView):
model = Productor
template_name = 'firstpage/productor_private.html'
def get(self, request, pk):
form = RepartoForm()
return render(request, self.template_name, {'form': form})
def post(self, request, pk):
form = RepartoForm(request.POST)
if form.is_valid():
return render(request, self.template_name, args)
I want to retrieve the pk from the URL to use it as a filter inside the forms.py, to do something like this:
class RepartoForm(forms.Form):
productos = forms.ModelMultipleChoiceField(queryset=Producto.objects.filter(productor=pk))
So in other words, I need to check what the current user's "productor" id is in order to only retrieve the "productos" that belong to this "productor"
You will need to "patch" the form constructor, and manually set the queryset in the __init__ function:
class RepartoForm(forms.Form):
productos = forms.ModelMultipleChoiceField(queryset=Producto.objects.all())
def __init__(self, *args, productor_pk=None, **kwargs):
super(forms.Form, self).__init__(*args, **kwargs)
if productor_pk is not None:
self.fields['productos'].queryset = Producto.objects.filter(
productor=productor_pk
)
Or for older versions of Python that does not implement more advanced parameter unpacking, one can implement it like:
class RepartoForm(forms.Form):
productos = forms.ModelMultipleChoiceField(queryset=Producto.objects.all())
def __init__(self, *args, **kwargs):
productor_pk = kwargs.pop('productor_pk', None)
super(forms.Form, self).__init__(*args, **kwargs)
if productor_pk is not None:
self.fields['productos'].queryset = Producto.objects.filter(
productor=productor_pk
)
In case no product_pk is given, the queryset are all the Productos (in case you do not want that, you can alter the form, and for example by default use an empty QuerySet like Producto.objects.none()).
Then in the view, you can construct the form with a named productor_pk parameter:
class Productor_Private(generic.DetailView):
model = Productor
template_name = 'firstpage/productor_private.html'
def get(self, request, pk):
form = RepartoForm(productor_pk=pk)
return render(request, self.template_name, {'form': form})
def post(self, request, pk):
form = RepartoForm(request.POST, productor_pk=pk)
if form.is_valid():
return render(request, self.template_name, args)
Note: you also need to cover the case where the form is invalid: right now post will return None for that, but you should return a HTTP response for all codepaths.
There is my view:
class SendTransfer(SingleObjectMixin, FormView):
model = BankAccount
form_class = SendTransferForm
template_name = 'dashboard/send_transfer.html'
def dispatch(self, request, *args, **kwargs):
self.object = self.get_object()
return super(SendTransfer, self).dispatch(request, *args, **kwargs)
def get_object(self, queryset=None):
obj = super(SendTransfer, self).get_object(queryset)
if not obj.is_owner(self.request.user.citizen):
raise Http404
return obj
def form_valid(self, form):
data = form.cleaned_data
MoneyTransfer.objects.create(sender=self.object,
receiver=data['receiver'], # ModelChoiceField in the form
total=data['total'], # FloatField in the form, etc.
when=timezone.localtime(timezone.now()),
comment=data['comment'])
return redirect('AccountDetail', self.object.pk)
def form_invalid(self, form):
return render(self.request, self.template_name, self.get_context_data())
def get_form_kwargs(self):
return {'sender': BankAccount.objects.get(id=self.kwargs['pk']), 'user': self.request.user}
when form is submitting - I'm getting the same result as after get. Debugger says that clean() is not called but form_invalid is works. What is the problem?
You have overridden get_form_kwargs, and now you are no longer passing data to the form. Without data, the form is unbound, so will never be valid.
It would be better to call super() first, update the kwargs, then return them.
def get_form_kwargs(self):
kwargs = super(SendTransfer, self).get_form_kwargs()
kwargs['sender'] = BankAccount.objects.get(id=self.kwargs['pk']),
kwargs['user'] = self.request.user
return kwargs
In my forms.py I raise an validation error when the user is already a member of the project. If i try to add a user who is already a member the validation error gets perfectly raised, but then I get redirected to the template and I have no context any more.
Any Best Practices in raising a form validation error? What am I doing wrong?
class AddUserForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.project = kwargs.pop('project')
self.user = kwargs.pop('user')
super(AddUserForm, self).__init__(*args, **kwargs)
self._user_cache = None
def clean_user(self):
"""
Check if the user is already a member of the project.
"""
user = self.cleaned_data['user']
if ProjectMember.objects.filter(project=self.project, user=user).exists():
raise forms.ValidationError(_("User is already a member of this project."))
# store user instance we queried for here to prevent additional lookups.
self._user_cache = user
return user
views.py without the ProjectUpdate view because it does not matter in this case. The views are a little bit complicated, because I have 2 forms in one template. If you know any better way to accomplish this, let me know.
class ProjectDetailView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
view = ProjectDisplay.as_view()
return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
if 'update_form' in request.POST:
view = ProjectUpdate.as_view()
elif 'add_user_form' in request.POST:
view = ProjectAddUser.as_view()
return view(request, *args, **kwargs)
class ProjectDisplay(DetailView):
model = Project
def get_context_data(self, **kwargs):
context = super(ProjectDisplay, self).get_context_data(**kwargs)
context['update_form'] = ProjectUpdateForm(initial={
'name': self.object.name,
'description': self.object.description
})
context['add_user_form'] = AddUserForm(project=self.object, user=self.request.user)
context['project'] = self.object
context['is_member'] = self.object.user_is_member(self.request.user)
return context
class ProjectAddUser(CreateView):
model = ProjectMember
form_class = AddUserForm
template_name = 'projects/project_detail.html'
def get_success_url(self):
return reverse('project_detail', kwargs={'slug': self.get_object().slug})
def get_object(self, queryset=None):
return Project.objects.get(slug=self.kwargs['slug'])
def get_form_kwargs(self):
kwargs = super(ProjectAddUser, self).get_form_kwargs()
kwargs.update({'project': self.get_object()})
kwargs.update({'user': self.request.user})
return kwargs
I have a CreateView in which I need to output data from an object. It works fine with this code below
class MyCreateView(CreateView):
model = ModelName
def dispatch(self, request, *args, **kwargs):
self.project = get_object_or_404(Project, slug=kwargs['project'])
return super(MyCreateView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, *args, **kwargs):
context_data = super(MyCreateView, self).get_context_data(*args, **kwargs)
context_data.update({'object': self.project})
return context_data
def form_valid(self, form):
obj = form.save(commit=False)
obj.project = self.project
obj.save()
return HttpResponseRedirect(self.get_success_url())
but I was wondering if it's possible to replace the dispatch() and get_context_data() with a get_object(). I've tried the code below, but it does not give me the project as object.
class MyCreateView(CreateView):
model = ModelName
def get_object(self, queryset=None):
return = Project.objects.filter(slug=self.kwargs['project'])
def form_valid(self, form):
obj = form.save(commit=False)
obj.project = self.object
obj.save()
return HttpResponseRedirect(self.get_success_url())
As their respective names imply, the get_object method returns a single object to be manipulated by the view, while get_context_data returns a dict to be displayed in the template. So no, you cannot replace one with the other, and your original code is perfectly fine.