Using Django, I'm looking for a way to use one url patern (with slug) to query one model and if nothing is found query a second model. I'm using Class Based Views.
I am following this answer, and the next View is being called. But then I get the following error:
"Generic detail view must be called with either an object pk or a slug."
I can't figure out how to pass the slug to the next View.
My url:
url(r'^(?P<slug>[-\w]+)/$', SingleView.as_view(), name='singleview'),
My CBV's:
class SingleView(DetailView):
def dispatch(self, request, *args, **kwargs):
post_or_page_slug = kwargs.pop('slug')
if Page.objects.filter(slug=post_or_page_slug).count() != 0:
return PageDetailView.as_view()(request, *args, **kwargs)
elif Post.objects.filter(slug=post_or_page_slug).count() != 0:
return PostDetailView.as_view()(request, *args, **kwargs)
else:
raise Http404
class PageDetailView(DetailView):
model = Page
template_name = 'page-detail.html'
class PostDetailView(DetailView):
model = Post
template_name = 'post-detail.html'
The problem is that you are popping the slug, which removes it from kwargs. This means that the slug is not getting passed to the view.
You can change it to:
post_or_page_slug = kwargs.pop['slug']
I would usually discourage calling MyView.as_view(request, *args, **kwargs) inside another view. Class based views are intended to be extended by subclassing, not by calling them inside other views.
For the two views in your example, you could combine them into a single view by overriding get_object and get_template_names.
from django.http import Http404
class PageOrPostDetailView(DetailView):
def get_object(self):
for Model in [Page, Post]:
try:
object = Model.objects.get(slug=self.kwargs['slug'])
return object
except Model.DoesNotExist:
pass
raise Http404
def get_template_names(self):
if isinstance(self.object, Page):
return ['page-detail.html']
else:
return ['post-detail.html']
Related
I created the FormView below that will dynamically return a form class based on what step in the process that the user is in. I'm having trouble with the get_form method. It returns the correct form class in a get request, but the post request isn't working.
tournament_form_dict = {
'1':TournamentCreationForm,
'2':TournamentDateForm,
'3':TournamentTimeForm,
'4':TournamentLocationForm,
'5':TournamentRestrictionForm,
'6':TournamentSectionForm,
'7':TournamentSectionRestrictionForm,
'8':TournamentSectionRoundForm,}
class CreateTournament(FormView):
template_name = 'events/create_tournament_step.html'
def __init__(self, *args, **kwargs):
form_class = self.get_form()
success_url = self.get_success_url()
super(CreateTournament, self).__init__(*args, **kwargs)
def get_form(self, **kwargs):
if 'step' not in kwargs:
step = '1'
else:
step = kwargs['step']
return tournament_form_dict[step]
def get_success_url(self, **kwargs):
if 'step' not in kwargs:
step = 1
else:
step = int(kwargs['step'])
step += 1
if 'record_id' not in kwargs:
record_id = 0
else:
record_id = int(kwargs['record_id'])
return 'events/tournaments/create/%d/%d/' % (record_id, step)
The post request fails at the django\views\generic\edit.py at the get_form line, which I realize is because I've overwritten it in my FormView:
def post(self, request, *args, **kwargs):
"""
Handle POST requests: instantiate a form instance with the passed
POST variables and then check if it's valid.
"""
form = self.get_form()
if form.is_valid(): …
return self.form_valid(form)
else:
return self.form_invalid(form)
However, when I change the name of my custom get_form method to say gen_form, like so:
def __init__(self, *args, **kwargs):
form_class = self.gen_form()
success_url = self.get_success_url()
super(CreateTournament, self).__init__(*args, **kwargs)
def gen_form(self, **kwargs):
if 'step' not in kwargs:
step = '1'
else:
step = kwargs['step']
return tournament_form_dict[step]
my form class doesn't get processed in the get request and evaluates to None. I'm scratching my head as to why when I override the get_form method, it works, but my own named method doesn't? Does anyone know what the flaw might be?
Django's FormMixin [Django-doc] defines a get_form function [Django-doc]. You here thus basically subclassed the FormView and "patched" the get_form method.
Your attempt with the gen_form does not work, since you only defined local variables, and thus do not make much difference anyway, only the super(..) call will have some side effects. The other commands will keep the CPU busy for some time, but at the end, will only assign a reference to a Form calls to the form_class variable, but since it is local, you will throw it away.
That being said, your function contains some errors. For example the **kwargs will usually contain at most one parameter: form_class. So the steps will not do much. You can access the URL parameters through self.args and self.kwargs, and the querystring parameters through self.request.GET. Furthermore you probably want to patch the get_form_class function anyway, since you return a reference to a class, not, as far as I understand it, a reference to an initilized form.
Constructing URLs through string processing is probably not a good idea either, since if you would (slightly) change the URL pattern, then it is likely you will forget to replace the success_url, and hence you will refer to a path that no longer exists. Using the reverse function is a safer way, since you pass the name of the view, and parameters, and then this function will "calculate" the correct URL. This is basically the mechanism behind the {% url ... %} template tag in Django templates.
A better approach is thus:
from django.urls import reverse
class CreateTournament(FormView):
template_name = 'events/create_tournament_step.html'
def get_form_class(self):
return tournament_form_dict[self.kwargs.get('step', '1')]
def get_success_url(self):
new_step = int(self.kwargs.get('step', 1)) + 1
# use a reverse
return reverse('name_of_view', kwargs={'step': new_step})
Django 1.10strong text
Could you help me understand why this results in:
The view wiki.views.WikiCreate didn't return an HttpResponse object. It returned None instead.
post: I didn't interfere with the chain of inheritance when redefining post method. I just intercepted the parameters from url.
form_valid: I returned the HttpResponse. But seemingly somewhere later it disappeared.
urlpatterns = [
url(r'^(?P<model>[-\w]+)/(?P<pk>\d+)/wiki/create/$', WikiCreate.as_view(), name='wiki_create'),
]
class WikiCreate(CreateView):
model = Wiki
fields = ['article']
def post(self, request, *args, **kwargs):
#For wiki history.
self.author = request.user
# Parameters from url reflect fields not shown to user.
self.related_model = kwargs.get('model')
self.related_object_id = kwargs.get('pk')
super(WikiCreate, self).post(request, *args, **kwargs)
def form_valid(self, form):
"""
Only "article" field was shown to tue user.
Now we save fields that were not explicitly shown (but were in the url).
"""
self.object = form.save(commit=False)
self.object.related_model = self.related_model
self.object.related_id = self.related_object_id
self.object.save()
return HttpResponseRedirect(self.get_success_url())
You didn't return the response from the super call in post.
Note it's rarely a good idea to override that method, and you certainly didn't need to here. You can access those kwargs directly in form_valid.
Let's say I have two models, Book and Page:
class Book(models.Model):
pass
class Page(models.Model):
book = models.ForeignKey(Book)
I need to delete a page and being redirected to the specific book that the page belonged to. To achieve this, I make a class based view to delete the page:
class PageDeleteView(DeleteView):
model = Page
def get_success_url(self, **kwargs):
return reverse_lazy('book_detail', self.book.pk)
The problem is that, since the object is deleted before get_success_url is called, this method fails, and I get a 404 error.
How could I do it?
Update:
Following the idea of #DrTyrsa, I have achieved it overriding the delete method, so the class would be as follows:
reverse_lazy = lambda name=None, *args : lazy(reverse, str)(name, args=args)
class PageDeleteView(DeleteView):
model = Page
def get_success_url(self, **kwargs):
return reverse_lazy('book_detail', self.book.pk)
def delete(self, request, *args, **kwargs):
self.book_pk = self.get_object().book.pk
return super(PageDeleteView, self).delete(request, *args, **kwargs)
Save object's pk before deleting. In __init__, for example. And name your URL patterns.
I'm want to create one page with a form, and every time I submit the form it adds an item to the list below the form.
I can make it work using 2 pages:
one page using the mixin CreateView to add items
one page ListView to have the list.
But I'm trying to have the form and the list on the same page. So I tried to create a class with both mixin:
class FormAndListView(ListView, CreateView):
pass
Then I've used this class:
FormAndListView.as_view(
queryset=PdfFile.objects.order_by('id'),
context_object_name='all_PDF',
success_url = 'listview',
form_class = UploadFileForm,
template_name='textfrompdf/index.html',)),
But when I try to load the page, I get the error: Exception Value: 'FormAndListView' object has no attribute 'object'
Traceback:
File "C:\Program Files\Python_2.7\lib\site-packages\django\core\handlers\base.py" in get_response
111. response = callback(request, *callback_args, **callback_kwargs)
File "C:\Program Files\Python_2.7\lib\site-packages\django\views\generic\base.py" in view
47. return self.dispatch(request, *args, **kwargs)
File "C:\Program Files\Python_2.7\lib\site-packages\django\views\generic\base.py" in dispatch
68. return handler(request, *args, **kwargs)
File "C:\Program Files\Python_2.7\lib\site-packages\django\views\generic\list.py" in get
122. return self.render_to_response(context)
File "C:\Program Files\Python_2.7\lib\site-packages\django\views\generic\base.py" in render_to_response
94. template = self.get_template_names(),
File "C:\Program Files\Python_2.7\lib\site-packages\django\views\generic\list.py" in get_template_names
134. names = super(MultipleObjectTemplateResponseMixin, self).get_template_names()
File "C:\Program Files\Python_2.7\lib\site-packages\django\views\generic\detail.py" in get_template_names
122. if self.object and self.template_name_field:
Exception Type: AttributeError at /PDF/
Exception Value: 'FormAndListView' object has no attribute 'object'
I've no idea how to debug that. Where to start?
I use a lot of views that involve a form and a list of objects. Rather than trying to mixin things I just add the queryset into the context data as below.
class UploadFileView(CreateView):
form_class = UploadFileForm
success_url = 'listview'
template_name = 'textfrompdf/index.html'
def get_context_data(self, **kwargs):
kwargs['object_list'] = PdfFile.objects.order_by('id')
return super(UploadFileView, self).get_context_data(**kwargs)
Do not mix list and update views.
Instead, create two separate views for these tasks:
List view displays the list and a web form with action URL pointing to the create view.
Create view accepts POST data and
displays form with error message in case of failure;
redirects to the list view in case of success.
Also I've tried to use class-based views and found that they are too complex.
I think it is much easier to use old-style function views.
I found the answer, there is 2 problems:
ListView and CreateView are "high level" mixin which aggregate "lower
level" mixins. But these lower level mixins are not compatible together.
The View class calls directly the render_to_response(), but in my scenario, there is 2 view class and render_to_response() should only be called once at the end.
I was able "solve" this issue using the following steps:
Instead of calling ListView and CreateView, I used lower level mixins. Moreover I called explicitly BaseCreateView and BaseListView from which I "extracted" the form and object_list
class FormAndListView(BaseCreateView, BaseListView, TemplateResponseMixin):
def get(self, request, *args, **kwargs):
formView = BaseCreateView.get(self, request, *args, **kwargs)
listView = BaseListView.get(self, request, *args, **kwargs)
formData = formView.context_data['form']
listData = listView.context_data['object_list']
return render_to_response('textfrompdf/index.html', {'form' : formData, 'all_PDF' : listData},
context_instance=RequestContext(request))
It's not clean but it works!
I have made my own class to solve this problem. I don't know if it's better or worse, but it works too. I have tried to use the generic mixins and have tested that validation and pagination work.
The code in GitHub
class ListAppendView(MultipleObjectMixin,
MultipleObjectTemplateResponseMixin,
ModelFormMixin,
ProcessFormView):
""" A View that displays a list of objects and a form to create a new object.
The View processes this form. """
template_name_suffix = '_append'
allow_empty = True
def get(self, request, *args, **kwargs):
self.object_list = self.get_queryset()
allow_empty = self.get_allow_empty()
if not allow_empty and len(self.object_list) == 0:
raise Http404(_(u"Empty list and '%(class_name)s.allow_empty' is False.")
% {'class_name': self.__class__.__name__})
self.object = None
form_class = self.get_form_class()
form = self.get_form(form_class)
context = self.get_context_data(object_list=self.object_list, form=form)
return self.render_to_response(context)
def post(self, request, *args, **kwargs):
self.object = None
return super(ListAppendView, self).post(request, *args, **kwargs)
def form_invalid(self, form):
self.object_list = self.get_queryset()
return self.render_to_response(self.get_context_data(object_list=self.object_list, form=form))
If you try it and find any errors, please tell me here or in GitHub.
I got into this problem and solve it with the following code, the answer by #jondykeman does not have pagination and other utilities for base classes. the other approaches that are proposed are a little complicated than the following:
class ObjectCreateView(LoginRequiredMixin, MultipleObjectMixin, View):
queryset = Wallet.objects.all()
def get(self, request):
self.object_list = super(ObjectCreateView, self).get_queryset().filter(user=request.user)
allow_empty = super(ObjectCreateView, self).get_allow_empty()
if not allow_empty:
if super(ObjectCreateView, self).get_paginate_by(self.object_list) is not None and hasattr(self.object_list, 'exists'):
is_empty = not self.object_list.exists()
else:
is_empty = not self.object_list
if is_empty:
raise Http404()
context = super(ObjectCreateView, self).get_context_data()
form = CreateObjectForm()
context['form'] = form
return render(request, 'objects/object-list.html', context=context)
def post(self, request):
form = CreateWalletForm(request.POST)
if form.is_valid():
Object.objects.create(name=form.cleaned_data['name'], user=request.user)
messages.success(request, 'Object is created')
else:
messages.error(request, utility.get_form_errors_as_string(form))
return redirect('objects:create')
I've just created a forms.models.BaseInlineFormSet to override the default formset for a TabularInline model. I need to evaluate the user's group in formset validation (clean) because some groups must write a number inside a range (0,20).
I'm using django admin to autogenerate the interface.
I've tried getting the request and the user from the kwargs in the init method, but I couldn't get the reference.
This is what I have now:
class OrderInlineFormset(forms.models.BaseInlineFormSet):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(OrderInlineFormset, self).__init__(*args, **kwargs)
def clean(self):
# get forms that actually have valid data
count = 0
for form in self.forms:
try:
if form.cleaned_data:
count += 1
if self.user.groups.filter(name='Seller').count() == 1:
if form.cleaned_data['discount'] > 20:
raise forms.ValidationError('Not authorized to specify a discount greater than 20%')
except AttributeError:
# annoyingly, if a subform is invalid Django explicity raises
# an AttributeError for cleaned_data
pass
if count < 1:
raise forms.ValidationError('You need to specify at least one item')
class OrderItemInline(admin.TabularInline):
model = OrderItem
formset = OrderInlineFormset
Then I use it as inlines = [OrderItemInline,] in my ModelAdmin.
Unfortunatly self.user is always None so I cannot compare the user group and the filter is not applied. I need to filter it because other groups should be able to specify any discount percent.
How can I do? If you also need the ModelAdmin code I'll publish it (I just avoided to copy the whole code to avoid confusions).
Well, I recognise my code there in your question, so I guess I'd better try and answer it. But I would say first of all that that snippet is really only for validating a minimum number of forms within the formset. Your use case is different - you want to check something within each form. That should be done with validation at the level of the form, not the formset.
That said, the trouble is not actually with the code you've posted, but with the fact that that's only part of it. Obviously, if you want to get the user from the kwargs when the form or formset is initialized, you need to ensure that the user is actually passed into that initialization - which it isn't, by default.
Unfortunately, Django's admin doesn't really give you a proper hook to intercept the initialization itself. But you can cheat by overriding the get_form function and using functools.partial to wrap the form class with the request argument (this code is reasonably untested, but should work):
from functools import partial
class OrderForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
super(OrderForm, self).__init__(*args, **kwargs)
def clean(self)
if self.user.groups.filter(name='Seller').count() == 1:
if self.cleaned_data['discount'] > 20:
raise forms.ValidationError('Not authorized to specify a discount greater than 20%')
return self.cleaned_data
class MyAdmin(admin.ModelAdmin):
form = OrderForm
def get_form(self, request, obj=None, **kwargs):
form_class = super(MyAdmin, self).get_form(request, obj, **kwargs)
return functools.partial(form_class, user=request.user)
Here's another option without using partials. First override the get_formset method in your TabularInline class.
Assign request.user or what ever extra varaibles you need to be available in the formset as in example below:
class OrderItemInline(admin.TabularInline):
model = OrderItem
formset = OrderInlineFormset
def get_formset(self, request, obj=None, **kwargs):
formset = super(OrderProductsInline, self).get_formset(request, obj, **kwargs)
formset.user = request.user
return formset
Now the user is available in the formset as self.user
class OrderInlineFormset(forms.models.BaseInlineFormSet):
def clean(self):
print(self.user) # is available here