Django Generic UpdateView and Multitable Inheritance - django

Let's say I have the following models:
class Post(model):
...
class BlogPost(Post):
...
class OtherPost(Post):
...
Assume my url schema to edit a post is something like,
/site/post/\d+/edit
In other words, I don't have separate url paths for editing OtherPosts vs. BlogPost.
When using UpdateView, I need to set the model -- but of course, the actual model is a subclass of Post.
class Update(generics.UpdateView):
model = Post
What is the Djangoey/DRY way to handle this?
At the moment, looking over the UpdateView code, it looks like I could leave Update.model undefined, and override get_queryset, which would need to return a query with the right submodel. I would also need to override get_form to return the right form.
I'll post my solution when I get it working, but am looking for possibly better (DRYer) integrations.

It looks like the following method is working, which seems fairly minimal.
class Update(generic.edit.UpdateView):
model = Post
def get_form_class(self):
try:
if self.object.blogpost:
return BlogPostForm
except Post.DoesNotExist:
pass
try:
if self.object.otherpost:
return OtherPostForm
except Post.DoesNotExist:
pass
def get_object(self, queryset=None):
object = super(Update, self).get_object(queryset)
try:
return object.blogpost
except Post.DoesNotExist:
pass
try:
return object.otherpost
except Post.DoesNotExist:
pass
Or, if using a polymorphic mixin like InheritanceManager, then something like this:
class Update(generic.edit.UpdateView):
model = Post
form_class = {
BlogPost: BlogPostForm,
OtherPost: OtherPostForm,
}
def get_form_class(self):
return self.form_class[self.object.__class__]
def get_queryset(self):
return self.model.objects.select_subclasses()

Related

Trying to filter by user group using class based view with django-tables2, can't access self.user

I'm trying to use a class based view using django-tables2 to define the table and template returned based on what group the logged in user belongs to.
This is my attempt at doing so:
class cases(LoginRequiredMixin, SingleTableView):
login_url = '/account/login/'
if User.objects.filter(pk=self.request.user.id, groups__name='teachers').exists():
model = Graduation
table_class = TeachersTable
template_name = 'mysite/teachers.html'
elif User.objects.filter(pk=self.request.user.id, groups__name='students').exists():
model = Graduation
table_class = StudentsTable
template_name = 'mysite/students.html'
I think the approach is more or less correct (I've only learned about class based views today), but I am unsure how to access the user id.
The relevant parts of this view should only be called when a user is logged in (at least I think) because I'm using the LoginRequiredMixin, so 'self' should exist.
The answers I have seen addressing this problem say override get_queryset, but I am reluctant to do that as I think that will break django-tables2.
What is the best approach in this case to do what I am trying to do?
There are a few things going on here.
First, all code in a class runs when the module is loaded, not when the view is run. So your code is running at the wrong time. Partially because of this, you don't have access to self.
self isn't that magic, it doesn't appear from nowhere, you can only use it in class methods:
class Foo:
self.spam = "eggs" # Wrong, no self here.
class Bar:
def set_spam(self): # self is the instance of the class.
self.spam = "eggs" # Works.
I'm not sure what table_class is, from a search it looks like it comes from django-tables, so there's a bit of guesswork here. It seems like you want something like this:
class GraduationCaseView(LoginRequiredMixin, SingleTableView):
model = Graduation
def get_template_names(self):
if self.request.user.groups.filter(name='teachers').exists():
return ['mysite/teachers.html']
return 'mysite/students.html'
def get_table_class(self):
if self.request.user.groups.filter(name='teachers').exists():
return TeachersTable
return StudentsTable
This should work. There is an issue with this: you'd be doing the same database query twice. There are some ways around this, but it requires knowing a bit about CBVs and their execution order. Since I'm not sure what SingleTableView is doing, this may or may not work:
class GraduationCaseView(LoginRequiredMixin, SingleTableView):
model = Graduation
def get_queryset(self):
qs = super().get_queryset()
if self.request.user.groups.filter(name='teachers').exists():
self.group = 'teachers'
else:
self.group = 'students'
return qs
def get_template_names(self):
if self.group == 'teachers':
return ['mysite/teachers.html']
return 'mysite/students.html'
def get_table_class(self):
if self.group == 'teachers':
return TeachersTable
return StudentsTable
You should probably read up on Python's documentation of classes, too, so you understand how they work.
One more thing, you don't need to set login_url on your view if settings.LOGIN_URL is set.

Django Class Based View With ModelChoiceField

I've been working with Django for about 3 months now and feel I'm getting a bit better, working my way up to class based views. On the surface they seem cleaner and easier to understand and in some cases they are. In others, not so much. I am trying to use a simple drop down view via ModelChoiceField and a form. I can get it to work with a function based view as shown below in my views.py file:
def book_by_name(request):
form = BookByName(request.POST or None)
if request.method == 'POST':
if form.is_valid():
book_byname = form.cleaned_data['dropdown']
return HttpResponseRedirect(book_byname.get_absolute_url1())
return render(request,'library/book_list.html',{'form':form})
Here is my form in forms.py:
class BookByName(forms.Form):
dropdown = forms.ModelChoiceField(queryset=Book.objects.none())
def __init__(self, *args, **kwargs):
super(BookByName, self).__init__(*args, **kwargs)
self.fields['dropdown'].widget.attrs['class'] = 'choices1'
self.fields['dropdown'].empty_label = ''
self.fields['dropdown'].queryset = Book.objects.order_by('publisher')
This code works. When I have tried to convert to a Class Based View, that's when the trouble begins. I tried to do something like this in views.py:
class BookByNameView(FormView, View):
form_class = BookByName
initial = { 'Book' : Book }
template_name = 'library/book_list.html'
def get(self, request, *args, **kwargs):
form = self.form_class(initial=self.initial)
return render(request, self.template_name, {'form': form})
def get_success_url(self, *args):
return reverse_lazy('library:book_detail', args = (self.object.id,))
When using this with the same form, I receive an attribute error,
'BookByNameView' object has no attribute 'object'.
I've tried ListView as well and received several other errors along the way. The get_success_url also needs to take in a primary key and I can't figure out how to get that passed in as well. Again, I'm a 3 month Django newbie so please be gentle and thanks in advance for your thoughts and suggestions! I feel like I'm in the ballpark...just can't find my seat! I'm very open to doing this differently, if there's a cleaner/better way to do this!
Based on the latest feedback, it would appear the Class Based View should look like:
class BookNameView(FormView):
form_class = BookName
template_name = 'library/book_list.html'
def get_success_url(self, *args):
return reverse_lazy('library:book_detail')
Is this correct? I ran a test version of this and in response to your question as to why I am using self.object.id at all, I am trying to get the pk from the modelchoicefield that I am using to return the view I am trying to get. This may be where I am getting a bit lost. I am trying to get the detail view from the modelchoicefield dropdown, and return the book that is selected. However, I can't seem to pass the pk to this view successfully.
I updated my code to...
class BookByNameView(FormView, ListView):
model = Book
form_class = BookByName
template_name = 'library/book_list.html'
def get_success_url(self, *args):
return reverse_lazy('library:book_detail')
But now it says error...Reverse for 'book_detail' with no arguments not found.
Why are you using self.object there at all? You used form.cleaned_data in the original view, that's what you should use in the class based version too. Note that the form is passed to form_valid.
Note that you've done lots of other weird things too. Your getmethod is pointless, as is your definition of the initial dict; you should delete them both. Also, FormView already inherits from View, there's no need to have View in your declaration explicitly.
You can override the form_valid() function in FormView to achieve what you want. If the form is valid then it is passed to the form_valid() function.
Try this:
class BookByNameView(FormView):
model = Book
form_class = BookByName
template_name = 'library/book_list.html'
def form_valid(self, form):
bookbyname = form.cleaned_data['dropdown']
return HttpResponseRedirect(bookbyname.get_absolute_url())

DeleteView with a dynamic success_url dependent on id

I have an app for posts, with a url for each post:
url(r'^post/(?P<id>\w+)/$', 'single_post', name='single_post'),
On each post, I have comments. I would like to be able to delete each comment from the post page and return to the post that I was on.
I have the following url for deleting comments:
url(r'^comment/(?P<pk>\d+)/delete/$', CommentDelete.as_view(),
name='comment_delete'),
And I know from previous research that I need override the get_success_url, but I'm not sure how to reference the post id that I was just on. I think I need to use kwargs, but not sure how. I have this currently, but it doesn't work...
class CommentDelete(PermissionMixin, DeleteView):
model = Comment
def get_success_url(self):
return reverse_lazy( 'single_post',
kwargs = {'post.id': self.kwargs.get('post.id', None)},)
Ideas appreciated!
This should work:
def get_success_url(self):
# Assuming there is a ForeignKey from Comment to Post in your model
post = self.object.post
return reverse_lazy( 'single_post', kwargs={'post.id': post.id})
Django's DeleteView inherits from SingleObjectMixin, which contains the get_object method.
I had a similar problem when using a custom delete view. It was fixed by adding a class variable (static variable). An extract:
# Using FormView since I need to customize more than I can do with the standard DeleteView
class MyDeleteView(generic.FormView):
person_id = 0
def get_success_url(self):
# I cannot access the 'pk' of the deleted object here
return reverse('person_identity', kwargs={'person_id': self.person_id})
def form_valid(self, form):
plan = get_object_or_404(Plan, pk=self.kwargs['pk'])
self.person_id = plan.person_id
if form.cleaned_data.get('delete', False):
Plan.objects.filter(person=plan.person, date__gte=plan.date)\
.filter(date__gte=datetime.date.today())\
.delete()
return super(MyDeleteView, self).form_valid(form)

How to write a basic try/except in a Django Generic Class View

I'd like to write an except clause that redirects the user if there isn't something in a queryset. Any suggestions welcome. I'm a Python noob, which I get is the issue here.
Here is my current code:
def get_queryset(self):
try:
var = Model.objects.filter(user=self.request.user, done=False)
except:
pass
return var
I want to do something like this:
def get_queryset(self):
try:
var = Model.objects.filter(user=self.request.user, done=False)
except:
redirect('add_view')
return var
A try except block in the get_queryset method isn't really appropriate. Firstly, Model.objects.filter() won't raise an exception if the queryset is empty - it just returns an empty queryset. Secondly, the get_queryset method is meant to return a queryset, not an HttpResponse, so if you try to redirect inside that method, you'll run into problems.
I think you might find it easier to write a function based view. A first attempt might look like this:
from django.shortcuts import render
def my_view(request):
"""
Display all the objects belonging to the user
that are not done, or redirect if there are not any,
"""
objects = Model.objects.filter(user=self.request.user, done=False)
if not objects:
return HttpResponseRedirect("/empty-queryset-url/")
return render(request, 'myapp/template.html', {"objects": objects})
The advantage is that the flow of your function is pretty straight forward. This doesn't have as many features as the ListView generic class based view (it's missing pagination for example), but it is pretty clear to anyone reading your code what the view is doing.
If you really want to use the class based view, you have to dig into the CBV documentation for multiple object mixins and the source code, and find a suitable method to override.
In this case, you'll find that the ListView behaviour is quite different to what you want, because it never redirects. It displays an empty page by default, or a 404 page if you set allow_empty = False. I think you would have to override the get method to look something like this (untested).
class MyView(ListView):
def get_queryset(self):
return Model.objects.filter(user=self.request.user, done=False)
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
if len(self.object_list == 0):
return HttpResponseRedirect("/empty-queryset-url/")
context = self.get_context_data(object_list=self.object_list)
return self.render_to_response(context)
This is purely supplemental to #Alasdair's answer. It should really be a comment, but couldn't be formatted properly that way. Instead of actually redefining get on the ListView, you could override simply with:
class MyView(ListView):
allow_empty = False # Causes 404 to be raised if queryset is empty
def get(self, request, *args, **kwargs):
try:
return super(MyView, self).get(request, *args, **kwargs)
except Http404:
return HttpResponseRedirect("/empty-queryset-url/")
That way, you're not responsible for the entire implementation of get. If Django changes it in the future, you're still good to go.

django: Passing arguments to a custom view class (inheritance edition)

again, apologies for what is probably a straightforward question!
Ok, so!
my problem is i have a saveModel function, where it saves a model. If the model is all good (is_valid), it will save the model and redirect to pageA
if the model is bad, or the request is a GET, then i'd like to redirect to pageB
all well and good, but i do this several times, how annoying! I don't want to cut and paste all the time, so i came up with this:
class SaveModel(View):
def as_view(self):
if request.method == "POST":
form = SaveModel.getPostForm(self.request)
if form.is_valid():
processedForm = SaveModel.processForm(self.request)
processedForm.save()
if (self.success_template):
return render_to_response(self.success_template)
else:
return render_to_response('pageA.html')
else:
form = SaveModel.getForm()
if (self.context_object_name):
contextName = context_object_name
else:
contextName = 'form'
if (self.template_name):
return render_to_response(template_name,{contextName:form})
else :
return render_to_response('pageB.html',{contextName:form})
def getForm(self):
return None
def getPostForm(self,request):
return None
def processForm(self,form,request):
return None
THEN, i define other classes to handle particular models, like, for example, so:
class StoryModelView(SaveModel):
def getForm(self,request):
return StoryForm()
def getPostForm(self,request):
return StoryForm(request.POST)
def processForm(self,form,request):
theStory = form.save(commit=False)
theStory.user = request.user
return theStory
and then, finally, in my urls.py i will refer to (as above) the model to use like so:
url(r'^addStory/$',
StoryModelView.as_view(
context_object_name='form',
template_name='accounts/addStory.html',
success_template='accounts/addStorySuccess.html'
)
),
This doesn't seem to work though - pycharm assures me that my references to self.context_object_name and so on are invalid. I'm v. new to python and django (which is why i thought i'd build a website with them! clever andrew!), so i am sure that i've missed a whole bunch of things (abstract methods and stuff... python does that, right?)
what do i need to do to get this all working? Is this how i should be doing things?
ANSWER BY ME!
Ok, so the comments everyone has written about the CreateView are probably correct. "Probably" because i never ended up using it, because i ended up sticking with my code instead.
In case anybody is, like me, new to python and django and wants to see how the whole thing works, here we are!
class SaveModel(View):
success_template = None
context_object_name = None
template_name = None
def post(self, request):
form = self.getPostForm(self.request)
if form.is_valid():
processedForm = self.processForm(form,self.request)
processedForm.save()
if self.success_template:
return render_to_response(self.success_template)
else:
return render_to_response('accounts/addStorySuccess.html')
else:
self.renderValidations(form)
def get(self,request):
form = self.getForm()
self.renderValidations(form)
def renderValidations(self,form):
if self.context_object_name:
contextName = self.context_object_name
else:
contextName = 'form'
if self.template_name:
return render_to_response(self.template_name,{contextName:form})
else :
return render_to_response('accounts/addStory.html',{contextName:form})
def getForm(self):
return None
def getPostForm(self,request):
return None
def processForm(self,form,request):
return None
and that is the main class, then i can override it like so:
class StoryModelView(SaveModel):
def getForm(self):
return StoryForm()
def getPostForm(self,request):
return StoryForm(request.POST)
def processForm(self,form,request):
theStory = form.save(commit=False)
theStory.user = request.user
return theStory
i tripped myself up with how "self" works in python a few times. it seems to be magically sent across with all method calls, but you need it as the first arg in the method declaration (but you never need to use it when calling/using the method)
i think there's only post or get for methods when overriding the View class. i don't have a good idea of the "process" of the call, or what the order is, dispatch was mentioned as something to override, but i suspect that is only where i need to change when/how to deal with differing request types (GET, POST, HEAD etc)
oh! the urls.py!
url(r'^addStory/$',
StoryModelView.as_view(
context_object_name='form',
template_name = 'accounts/addStory.html',
success_template= 'accounts/addStorySuccess.html'
)
),
i can just chuck whatever i want into that "as_view" call, and then, as long as those parameters are defined in the overriding class it's all good.
so yay! my classes all work and women want me. use my code, and this can happen to you too!*
*results atypical and fictional. your results may differ.