I have a FormView that generates a review of an object (which it is generically related to) and then links it to the object and saves it when the form is completed.
The issue I'm having is that I have no way to hold onto the data of the object I want to connect to. This means that I need to 'look it up' for context (template rendering) for valid processing (to do the linking) and for the success (to generate an appropriately reversed url.
Is there a better way to be binding the review to the object? Or better yet, is there a way to persist form data that I'm missing?
EDIT: Sorry the login decorator was on dispatch. I removed that method because SO was complaining about too much code and I didn't think it was relevant... I must have missed the decoratot
class ReviewCreate(FormView):
template_name = 'food/item_add_review.html'
form_class = ReviewForm
review_item = None
def get_context_data(self, **kwargs):
context = super(ReviewCreate, self).get_context_data(**kwargs)
item_modelname = self.kwargs.get('model')
item_model = apps.get_model('food',item_modelname)
review_item = get_object_or_404(item_model,pk=self.kwargs.get('pk'))
context['item'] = review_item
return context
def form_valid(self, form):
item_modelname = self.kwargs.get('model')
item_model = apps.get_model('food',item_modelname)
review_item = get_object_or_404(item_model,pk=self.kwargs.get('pk'))
r = form.save(commit=False)
r.content_object=review_item
r.save()
return super(ReviewCreate, self).form_valid(form)
def get_success_url(self):
item_modelname = self.kwargs.get('model')
item_model = apps.get_model('food',item_modelname)
review_item = get_object_or_404(item_model,pk=self.kwargs.get('pk'))
return reverse( 'pkitem', kwargs = {'pk': review_item.id, 'model':item_modelname},)
The view is an object right, so you just assign your values to instance variables, i e "to self" (this is thread-safe). Like this:
class ReviewCreate(FormView):
template_name = 'food/item_add_review.html'
form_class = ReviewForm
#method_decorator(login_required) # Use a class level mixin instead
def get_context_data(self, **kwargs):
return super(
ReviewCreate,
self
).get_context_data(
item=self.review_item,
**kwargs
)
def lookup_review_item(self):
self.item_modelname = self.kwargs.get('model')
item_model = apps.get_model('food', self.item_modelname)
self.review_item = get_object_or_404(
item_model,
pk=self.kwargs.get('pk')
)
def dispatch(self, request, *args, **kwargs):
# lookup performed here to be set for both GET and POST
self.lookup_review_item()
return super(ReviewCreate, self).dispatch(request, *args, **kwargs)
def form_valid(self, form):
r = form.save(commit=False)
r.content_object=self.review_item
r.save()
return super(ReviewCreate, self).form_valid(form)
def get_success_url(self):
return reverse(
'pkitem',
kwargs = {
'pk': self.review_item.id,
'model': self.item_modelname
},
)
The default form_valid() method for FormView redirects to the success url and reinitializes the form. You can make the form data persist by overriding form_valid():
def form_valid(self, form):
return super(YourFormView, self).get(form)
This will redirect to your success url with a (bounded) form having the posted data. The form is added to the context so you can use the data in your template or in your view as you wish.
(Django version 1.11.7)
The get_context_data should always return the context dictionary. It doesn't make sense to use the login_required decorator with it, because that means it might return a redirect response instead.
It would be better decorate the dispatch method instead. In your dispatch, you can set attributes on the instance.
class ReviewCreate(FormView):
#method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
self.item_modelname = self.kwargs.get('model')
self.item_model = apps.get_model('food',item_modelname)
self.review_item = get_object_or_404(item_model,pk=self.kwargs.get('pk'))
return super(ReviewCreate, self).dispatch(request, *args, **kwargs)
Then, in your other methods, you can access the attributes, for example:
def get_context_data(self, **kwargs):
context = super(ReviewCreate, self).get_context_data(**kwargs)
context['item'] = self.review_item
return context
Related
I want to use my own method, which will return JsonResponse, but this method isn't called in this View by default. So maybe i need to rewrite another method or?
views.py:
class CreatePostJS(LoginRequiredMixin, SelectRelatedMixin, generic.CreateView):
fields = ('post_message', 'post_image')
model = models.Post
select_related = ('user',)
template_name = 'posts/post_list.html'
template_name = 'posts/post_form.html'
response_data = {}
response_data['text'] = model.post_message
response_data['posted'] = model.posted_at
response_data['user'] = model.user
def create_post(request):
if request.method == 'POST':
return JsonResponse(serializers.serialize('json', response_data), safe = False)
def form_valid(self, form):
self.object = form.save(commit = False)
self.object.user = self.request.user
self.object.save()
return super().form_valid(form)
https://docs.djangoproject.com/en/3.1/topics/class-based-views/intro/
Basically if you want to execute the method on POST requests, you can override the post method with your own. It will look more or less like this:
def post(self, request, *args, **kwargs):
my_json_response = self.create_post(request)
return super().post(request, *args, **kwargs)
That way there is no need to check for request.method in create_post, unless you plan to use it elsewhere.
When you work with generic views and want to override the default request handler, you need to overwrite the default method handler for the request method. In your case, it's the post() method from CreateView class.
I strongly recommend OOP way, so it would look like:
def get_request_data(self):
return {
'text': self.model.post_message,
'posted': self.model.posted_at,
'user': self.model.user
}
def create_post(request):
return JsonResponse(serializers.serialize('json', self.get_response_data()), safe = False)
def post(self, request, *args, **kwargs):
return self.create_post(request)
Note the if request.method == 'POST': is not needed, as post() method will only execute when POST request is being made in the current view.
You tried to initialize response_data outside a method, so as in my code you'd need to create another method for this - which I included in my example code.
I have created two forms in forms.py one form has less fields than the other.
what I would like to now do is get the current users permissions and set the form class of the CBV based on those perms.
below is my current view:
class EditCircuit(UpdateView):
model = Circuits
# if user_passes_test(lambda u: u.has_perm('config.edit_circuit')))
form_class = CircuitForm
# else
# form_class = CircuitFormRestricted
template_name = "sites/circuit_form.html"
#method_decorator(user_passes_test(lambda u: u.has_perm('config.edit_circuit')))
def dispatch(self, *args, **kwargs):
self.site_id = self.kwargs['site_id']
self.site = get_object_or_404(SiteData, pk=self.site_id)
return super(EditCircuit, self).dispatch(*args, **kwargs)
def get_success_url(self, **kwargs):
return reverse_lazy("sites:site_detail_circuits", args=(self.site_id,))
def form_valid(self, form):
form.instance.site_data = self.object.site_data
return super(EditCircuit, self).form_valid(form)
def get_form_kwargs(self, *args, **kwargs):
kwargs = super().get_form_kwargs()
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['SiteID']=self.site_id
context['SiteName']=self.site.location
context['FormType']='Edit'
context['active_circuits']='class="active"'
return context
You can override the get_form_class method.
def get_form_class(self):
if self.request.user.has_perm('config.edit_circuit'):
return CircuitForm
return CircuitFormRestricted
Also, it looks as though you don't need the get_form_kwargs definition, as it's not doing anything at the moment.
you can create your own mixin like this
class AuthorOnlyMixin(object):
def has_permissions(self):
return self.get_object().created_by == self.request.user
then use it like this
class EditViewClass(AuthorOnlyMixin, EditView):
def get_form_class(self):
if self.has_permissions():
return FormWithPermission
else:
return FormWithoutPermission
I have a DemandDetailView(DetailView) and BidCreationView(CreateView).
On DemandDetailView page, there is a form (for creating Bids) which posts data to BidCreationView.
I can't figure out what to do in case form is invalid. I would like to render DemandDetailView again with form errors and preserve corresponding URL.
class DemandDetailView(DetailView):
model = Demand
template_name = 'demands/detail.html'
def dispatch(self, request, *args, **kwargs):
self.bid_creation_form = BidCreationForm(request.POST or None, request.FILES or None,request=request)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['bid_creation_form']=self.bid_creation_form
return context
class BidCreationView(CreateView):
http_method_names = ['post']
model = Bid
form_class = BidCreationForm
def get_success_url(self):
return reverse_lazy("demands:demands")
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({'request': self.request})
return kwargs
def form_valid(self, form):
form.instance.demand_id = self.kwargs.pop('demand_id')
return super().form_valid(form)
Do you have any ideas? My only idea is to use session which isn't probably the best way.
You could use is_valid() method from Form Objects. Something like:
class DemandDetailView(DetailView):
model = Demand
template_name = 'demands/detail.html'
def dispatch(self, request, *args, **kwargs):
form = BidCreationForm(request.POST or None, request.FILES or None,request=request)
if form.is_valid():
self.bid_creation_form = form
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['bid_creation_form']=self.bid_creation_form
return context
Option #2 (Personal Choice):
forms.py
from django import forms
from .models import Bid
class BidCreationForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(BidCreationForm, self).__init__(*args, **kwargs)
for field in self.fields.values():
field.widget.attrs = {"class": "form-control"}
class Meta:
model = Bid
fields = ('user', 'demands', 'amount', 'transaction')
Take a look at the Meta class within the Form. Its explicitly calling the Bid Model and the fields attribute is referring Bid fields from the Model Instance. Now you could call this form in any view without calling another view. If you want to add logic to this form, like calculating total amount or something like that then should do it within the form also. Write code once, Dont Repeat yourself.
I have a DetailView which displays a Post. I now want to add the ability to create a Comment for a Post. For that I need a CommentForm, within the DetailView, so that I can create comments while being on the same page with a Post.
Is this possible, or should I be looking for another approach, like doing the form handling 'manually'?
class Comment(models.Model):
body = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
author_name = models.CharField(max_length=255)
parent_post = models.ForeignKey('Post',related_name='comments')
class PostDetailView(BlogMixin,DetailView):
""" A view for displaying a single post """
template_name = 'post.html'
model = Post
#Add some code for the CommentForm here?
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
exclude = ("parent_post","created_at")
def create_view(request, **kwargs):
if request.method == "POST":
parent_fk = request.args['parent_fk'] #Im hoping to find how this will work soon
form = CommentForm(request.POST)
if form.is_valid():
new_comment = form.save(commit=False)
new_comment.parent_post = parent_fk
new_comment.save()
return HttpResponseRedirect(request.META.get('HTTP_REFERER'))
** Alternative **
I have been trying to apply the solution - A better alternative - but I get
Exception Value: __init__() takes exactly 1 argument (3 given)
Exception Location: .../sitepackages/django/core/handlers/base.py in get_response, line 112
and have not been able to trace it yet.
class PostView(BlogMixin,DetailView):
""" A view for displaying a single post """
template_name = 'post.html'
model = Post
def get_context_data(self, **kwargs):
context = super(PostView, self).get_context_data(**kwargs)
context['form'] = CommentForm()
return context
class PostDetailView(View):
def get(self, request, *args, **kwargs):
view = PostView.as_view()
return view(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
view = PostComment.as_view()
return view(request, *args, **kwargs)
class PostComment( SingleObjectMixin , FormView):
template_name = 'post.html'
form_class = CommentForm
model = Post
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return super(PostComment, self).post(request, *args, **kwargs)
def get_success_url(self):
return reverse('post-detail', kwargs={'pk': self.object.pk})
class BlogMixin(object):
"""
Basic mixin for all the views. Update the context with additional
information that is required across the whole site, typically
to render base.html properly
"""
def get_context_data(self, *args, **kwargs):
context = super(BlogMixin, self).get_context_data(*args, **kwargs)
blog = Blog.get_unique()
context.update({
'blog': blog,
'active_user': users.get_current_user(),
'is_admin': users.is_current_user_admin()
})
return context
urls.py:
url(r'^post/(?P[\d]+)/$', views.PostDetailView., name="post-detail"),
If you want to use your first method, you can make the FK a hidden field. In your view, you can save the FK before committing the comment to the database. Like this:
if form.is_valid():
comment = form.save(commit=False)
comment.parent_post = parent_post
comment.save()
Edit: If you want to fetch the comments, then you can use filter by post to get a QuerySet of the comments.
Why dont you send your form in the context in detail view:
class YourDetailView(DetailView):
#Your stuff here
def get_context_date(self, **kwargs):
context = super(YOurDetailView, self).get_context_data(**kwargs)
context['form'] = YourForm
return context
PS. Look for the parameters in get_context_date..
In the end I could not make it work with the redirect, but the following is working:
class PostDetailView(BlogMixin,CreateView):
""" A view for displaying a single post """
template_name = 'post.html'
model = Comment
fields = ['body','author_name']
def get_context_data(self, **kwargs):
context = super(PostDetailView, self).get_context_data(**kwargs)
context['post'] = Post.objects.get(pk=self.kwargs['pk'])
return context
def form_valid(self, form):
# self.object = form.save()
obj = form.save(commit=False)
obj.parent_post = Post.objects.get(pk=self.kwargs['pk'])
obj.save()
return redirect('post-detail', self.kwargs['pk'])
my view is like this
class RecordView(View):
record_form = RecordForm
record_files = {}
templare = 'acquisition.html'
def get(self, request, *args, **kwargs):
fil = urllib.urlopen('/home/student/wwww.jpg')
self.record_files = {'small_cover': SimpleUploadedFile('hehe.jpg', fil.read())}
rr_form = self.record_form()
return render(request, self.template_name, {'rr_form': rr_form,
})
def post(self, request, *args, **kwargs):
record = RecordForm(request.POST, self.record_files)
record.save()
HttpResponseRedirect('/')
Here i have populated self.record_files in get method.. but after i post data i see self.record_files as empty dictionary. I get confused here. What can i do to do so.
The state of your view instance is not maintained between a get and a post, so setting record_files on the instance will not keep it for the next request. You would need to put that logic in the dispatch method, or store information in the user's session.
class RecordView(View):
record_form = RecordForm
record_files = {}
templare = 'acquisition.html'
# dispatch is called before deciding whether to use get() or post()
# so any instance-level properties that require the request can go here.
# This could even go in __init__().
def dispatch(self, request, *args, **kwargs):
fil = urllib.urlopen('/home/student/wwww.jpg')
self.record_files = {'small_cover': SimpleUploadedFile('hehe.jpg', fil.read())}
return super(RecordView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
rr_form = self.record_form()
return render(request, self.template_name, {'rr_form': rr_form,
})
# self.record_files will be available in a get, or a post (or any valid
# method for that matter).
def post(self, request, *args, **kwargs):
record = RecordForm(request.POST, self.record_files)
record.save()
return HttpResponseRedirect('/')
That is exactly how it suppose to work. Whenever you have a GET request, it will call method get, and if it is a POST request, naturally it will call post method, but not the get method. So here is how you can solve your problem:
from django.views.generic import TemplateView
class RecordView(TemplateView):
record_form = RecordForm
record_files = {}
template_name = 'acquisition.html'
def get_context_data(self, **context):
fil = urllib.urlopen('/home/student/wwww.jpg')
self.record_files = {
'small_cover': SimpleUploadedFile('hehe.jpg', fil.read())
}
context.update({
'record_files': self.record_files,
'rr_form': self.record_form()
})
fil.close()
return super(RecordView, self).get_context_data(**context)
def post(self, request, *args, **kwargs):
context = self.get_context_data()
record_form = self.record_form(request.POST, self.record_files)
if record_form.is_valid():
record_form.save()
## or do a redirect instead, like you had before:
# return HttpResponseRedirect('/')
context['rr_form'] = record_form
return self.render_to_response(context)