Django: parametric class-based views - django

I am trying to use generic CreateView class to handle forms for a set of models inherited from the same base class.
class BaseContent(models.Model):
...
class XContent(BaseContent):
...
class YContent(BaseContent):
...
To keep things DRY, I want to define one CreateView class that will handle all inherited classes from BaseContent.
The url pattern for that view is:
url(r'^content/add/(?P<model_name>\w+)/$', ContentCreateView.as_view(), name='content_add')
Something like this should work:
class ContentCreateView(CreateView):
template_name = 'content_form.html'
def get_model(self, request):
# 'content' is the name of the application; model_name is 'xcontent', 'ycontent', ...
return ContentType.objects.get_by_natural_key('content', self.model_name)
But I am getting this exception:
ContentCreateView is missing a queryset. Define ContentCreateView.model, ContentCreateView.queryset, or override ContentCreateView.get_object().
This suggestion does not seem to hold as I am not willing to set a class attribute like model or queryset to keep the model form generated dynamic. Overriding the get_object does not seem relevant for creating an object.
I tried overriding get_queryset() but this method does not accept the request parameter, nor have access to self.model_name which comes from the url pattern.
Long story short, how can I make a CreateView use a dynamic form based on a parameter passed from the url?
Thanks.

You could set the model attribute from your your urls.py, depending on the url being called:
url(r'^content/add/x/$',
ContentCreateView.as_view(model=XContent), name='x_content_add'),
url(r'^content/add/y/$',
ContentCreateView.as_view(model=YContent), name='y_content_add')
I admit it's not perfect as you are repeating yourself a bit, but therefore you have the advantage of having different names for the same view, depending on the model! Besides that you could also do something similar with overriding form_class...

Had this problem for some time, but found the solution. You need to override the dispatch method, defined in as_view() (django.views.generic.base), something like this:
class ContentCreateView(CreateView):
def dispatch(self, request, *args, **kwargs):
for app in ['foo', 'bar']:
model = models.get_model(app, kwargs['modelname'])
if model:
self.model = model
break
return super(GenericEdit, self).dispatch(request, *args, **kwargs)
...
...

Related

Django: how to re-create my function-based view as a (Generic Editing) Class Based View

I have a function-based view that is currently working successfully. However, I want to learn how to create the equivalent Class Based View version of this function, using the generic UpdateView class -- though I imagine the solution, whatever it is, will be the exact same for CreateView, as well.
I know how to create and use Class Based Views generally, but there is one line of my function-based view that I have not been able to work into the corresponding UpdateView -- as usual with the Generic Editing Class Based Views, it's not immediately clear which method I need to override to insert the desired functionality.
The specific task that I can't port-over to the CBV, so to speak, is a line that overrides the queryset that will be used for the display of a specific field, one that is defined as ForeignKey to another model in my database.
First, the working function-based view, with highlight at the specific bit of code I can't get working in the CVB version:
#login_required
def update_details(request, pk):
"""update details of an existing record"""
umd_object = UserMovieDetail.objects.select_related('movie').get(pk=pk)
movie = umd_object.movie
if umd_object.user != request.user:
raise Http404
if request.method != 'POST':
form = UserMovieDetailForm(instance=umd_object)
# this is the single line of code I can't get working in UpdateView version:
form.fields['user_guess'].queryset = User.objects.filter(related_game_rounds=movie.game_round)
else:
form = UserMovieDetailForm(instance=umd_object, data=request.POST)
if form.is_valid():
form.save()
return redirect(movie)
context = {'form': form, 'object': umd_object }
return render(request, 'movies/update_details.html', context)
I can recreate every part of this function-based view in UpdateView successfully except for this line (copied from above for clarity):
form.fields['user_guess'].queryset = User.objects.filter(related_game_rounds=movie.game_round)
What this line does: the default Form-field for a ForeignKey is ModelChoiceField, and it by default displays all objects of the related Model. My code above overrides that behavior, and says: I only want the form to display this filtered set of objects. It works fine, as is, so long as I'm using this function-based view.
Side-Note: I am aware that this result can be achieved by modifying the ModelForm itself in my forms.py file. The purpose of this question is to better understand how to work with the built-in Generic Class Based Views, enabling them to recreate the functionality I can already achieve with function-based views. So please, refrain from answering my question with "why don't you just do this in the form itself instead" -- I am already aware of this option, and it's not what I'm attempting to solve, specifically.
Now for the UpdateView (and again, I think it would be the same for CreateView). To start off, it would look essentially like this:
class UpdateDetailsView(LoginRequiredMixin, UpdateView):
model = UserMovieDetail
template_name = 'movies/update_details.html'
form_class = UserMovieDetailForm
login_url = 'login' # used by LoginRequiredMixin
# what method do I override here, to include that specific line of code, which needs
# to occur in the GET portion of the view?
def get_success_url(self):
return reverse('movies:movie', kwargs={'pk': self.object.movie.pk, 'slug': self.object.movie.slug })
The above is a working re-creation of my function-based view, replicating all the behavior except that one important line that filters the results of a specific field's ModelChoiceField display in the Form.
How do I get that line of code to function inside this UpdateView? I've reviewed the methods built-in to UpdateView on the classy class-based views website, and then attempted (by pure guess-work) to over-ride the get_form_class method, but I it didn't work, and I was basically shooting in the dark to begin with.
Note that since the functionality I want to re-create is about the display of items in ModelChoiceField of the form, the desired behavior applies to the GET portion of the view, rather than the POST. So I need to be able to override the form fields before the form is rendered for the first time, just like I did in my function based view. Where and how can I do this in UpdateView?
First, a note not related to form - from raise Http404 in functional view I understand that you want to allow user to access only his own movies. For that in class based view you can override get_queryset method:
class UpdateDetailsView(LoginRequiredMixin, UpdateView):
def get_queryset(self):
return UserMovieDetail.objects \
.filter(user=request.user) \
.select_related('movie')
Now let's move to customizing form.
Option 1 - .get_form()
You can override get_form method of the UpdateView:
class UpdateDetailsView(LoginRequiredMixin, UpdateView):
form_class = UserMovieDetailForm
def get_form(self, form_class=None):
form = super().get_form(form_class)
# add your customizations here
round = self.object.movie.game_round
form.fields['user_guess'].queryset = \
User.objects.filter(related_game_rounds=round)
return form
Option 2 - moving customizations to form class and .get_form_kwargs()
You might prefer to move customization logic from view to form. For that you can override form's __init__ method. If customization logic requires extra information (for example, queryset depends on current user), then you can also override get_form_kwargs method to pass extra parameters to the form:
# views.py
class UpdateDetailsView(LoginRequiredMixin, UpdateView):
form_class = UserMovieDetailForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs.update({'current_user': self.request.user})
return kwargs
# forms.py
class UserMovieDetailForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.current_user = kwargs.pop('current_user')
super().__init__(*args, **kwargs)
# add your customizations here
self.fields['user_guess'].queryset = ...
P.S. In general, great resource for understanding django class based views is https://ccbv.co.uk/

Set Custom Header Class Based View Django

I am learning Class Based Views in Django and have learnt to work with the base generic views like View and TemplateView.
I was playing with the generic view like ListView and DetailView, then I stumbled to a problem.
How can we add custom header in a Class Based View that inherits from one of the view classes like ListView and DetailView.
I searched it on the but all the answers of function based views.
I have been able to set headers in the Class Base Views that inherit from the View class.
class MyView(View):
http_method_names=['post','get']
message='<div id="myview">This is a class based view response.</div>'
content_type='text/html'
charset='utf-8'
template_name='base.html'
#method_decorator(gzip_page)
#method_decorator(condition(etag_func=None,last_modified_func=None))
def get(self,request,*args,**kwargs):
response=TemplateResponse(request,self.template_name,{'number':1,'number_2':2})
response.__setitem__('x-uuid',uuid.uuid4().hex) #set header explicitly
response.__setitem__('status',200)
response.__setitem__('page_id',str(uuid.uuid4().hex))
patch_vary_headers(response,['Etag','Cookie','User-Agent'])
patch_cache_control(response)
return response
#Override this method to tell what to do when one of the methods in http_method_names is called
def http_method_not_allowed(request, *args, **kwargs):
response=HttpResponse("Method not allowed",status=405)
response.__setitem__('x-uid',uuid.uuid4().hex) #set header explicitly
response.__setitem__('status',405)
response.__setitem__({'hello':'word'})
return response
#return any type of response here
# return JsonResponse(status=405,data={'not_allowed':True})
Can anybody tell me how to add a custom header in Class Based View that inherits from ListView or any other View like DetailView.
class GetParticularUserView(ListView):
http_method_names=['get']
template_name='one_user.html'
# context_object_name='user' # set context either by this or by get_context_data
'''queryset=User.objects.all()''' # one way to set the data in the context
def get_queryset(self):
# define the query set to be used here.
self.user=get_object_or_404(User,id=self.kwargs['id'])
return self.user
def get_context_data(self,**kwargs):
context=super(GetParticularUserView,self).get_context_data(**kwargs)
context['user']=self.user
return context
I would override the dispatch method. Call super() to get the response, set the headers, then return the response. Note you shouldn't need to call __setitem__ - just treat the response as a dictionary.
class GetParticularUserView(ListView):
#method_decorator(gzip_page)
#method_decorator(condition(etag_func=None,last_modified_func=None))
def dispatch(self, *args, **kwargs):
response = super(GetParticularUserView, self).dispatch(*args, **kwargs)
response['x-uid'] = uuid.uuid4().hex # set header
return response

Specify a template name in class based View

I am new to Django and trying to work from django-jquery-file-upload
It has introduced me to class based Views.
I'm trying to replicate one of these class based views to contain a share link that will refer back to individual links in the gallery.
the class i'm trying to adapt is:
class PictureDeleteView(DeleteView):
model = Picture
def delete(self, request, *args, **kwargs):
"""
This does not actually delete the file, only the database record. But
that is easy to implement.
"""
self.object = self.get_object()
self.object.delete()
if request.is_ajax():
response = JSONResponse(True, {}, response_mimetype(self.request))
response['Content-Disposition'] = 'inline; filename=files.json'
return response
else:
return HttpResponseRedirect('/upload/new')
class JSONResponse(HttpResponse):
"""JSON response class."""
def __init__(self,obj='',json_opts={},mimetype="application/json",*args,**kwargs):
content = simplejson.dumps(obj,**json_opts)
super(JSONResponse,self).__init__(content,mimetype,*args,**kwargs)
This defaults to the template name picture_confirm_delete.html
How can i rewrite a class with the same functionality except that it points to a different template? or better still & in keeping with 'DRY' how can i reuse this class on another template?
I've had a look over the documentation here but can't seem to adapt it to my needs.
Thanks in advance.
In short, here's what's happening currently: a DeleteView in Django will render a default template based on the class name as explained in the docs:
The DeleteView page displayed to a GET request uses a template_name_suffix of '_confirm_delete'.
To change this, just overrule this behaviour by providing the template_name argument:
template_name
The full name of a template to use as defined by a string.
This works for any built-in class based view inheriting from a TemplateResponseMixin.
Apply it in the class definition or in the URL patterns (whichever you prefer):
In the class:
class MyOwnPictureDeleteView(PictureDeleteView):
template_name = "myown_picture_delete_template.html"
Yes, that's your complete new class based view.
or
In URLconf:
url(r'^picture_delete/(?P<pk>\d+)/', 'myapp.views.PictureDeleteView', \
{'template_name': 'myown_picture_delete_template.html'}),
Either way, you don't have to rewrite a single line of the original PictureDeleteView class, so this is as DRY as it gets.
Because DeleteView also inherits from SingleObjectTemplateResponseMixin it requires a template and therefore a template name.
But since you are not really using the functionality provided by Django's DeleteView but creating you own by using the HTTP method DELETE you can just change the parent class of your view from DeleteView to View.
Otherwise check which functionality from which of DeleteView's ancesctors you need, this is a good starting point for browsing class-based views (the official Django documentation isn't really yet).

Custom ModelAdmin: allow admin class validation to ignore fields in a fieldset?

I am writing a little app to allow an AddThis share field in the django admin change list to allow the user share the object they are currently editing (as well as seeing the share count):
Taking a simple BlogEntry as an example, I have created a custom ModelAdmin:
class AddThisAdmin(admin.ModelAdmin):
addthis_config = {
'title_field' : None,
'description_field' : None,
'url_field' : None,
'image_field' : None,
}
def get_form(self, request, obj=None, *args, **kwargs):
metaform = super(AddThisAdmin, self).get_form(request, obj, **kwargs)
if obj:
# Grab users config and find the fields they specified ...
metaform.base_fields['add_this'] = AddThisField(self.add_this)
return metaform
Which is inherited in the users BlogEntryAdmin like so:
class BlogEntryAdmin(admin.ModelAdmin, AddThisAdmin):
addthis_config = {
'title_field' : 'blog_title',
'description_field' : 'blurb',
}
where the addthis_config allows the user to specify the fields in their BlogEntry object from where to pull the title/description/url and image used in AddThis. This all works really nicely until I decide to use a custom fieldset in the BlogEntryAdmin:
class BlogEntryAdmin(admin.ModelAdmin, AddThisAdmin):
addthis_config = {
'title_field' : 'blog_title',
'description_field' : 'blurb',
}
fieldsets = [{ ... }]
'BlogEntry.fieldsets0['fields']' refers to field 'add_this' that is missing from the form.
I understand that this is happening because the django admin runs a validation on the fieldsets (django.contrib.admin.validation) on the BlogEntryAdmin class before it is actually instantiated (and my custom field is inserted).
tldr : Is there a way I can tell the django.contrib.admin.validation to ignore the field in the fieldset?
The typical approach is to provide base form like AddThisAdminForm which has the required field(s), and the make other ModelAdmin's forms inherit from that. It looks like you're trying to avoid that and auto insert the fields into whatever form is being used. If you insist on that approach, something like the following should work much better:
def get_form(self, request, obj=None, **kwargs):
ModelForm = super(AddThisAdmin, self).get_form(request, obj, **kwargs)
class AddThisForm(ModelForm):
add_this = AddThisField(self.add_this)
return AddThisForm
It's not documented, but you could use the get_fieldsets method to define your fieldsets. As an example, look at how Django changes the fieldsets in the UserAdmin when adding new users.
I've not tested this, but I believe it will avoid the fieldset validation.

adding new form fields dynamically in admin

I am trying to add dynamically new form fields (I used this blog post), for a form used in admin interface :
class ServiceRoleAssignmentForm(forms.ModelForm):
class Meta:
model = ServiceRoleAssignment
def __init__(self, *args, **kwargs):
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
self.fields['test'] = forms.CharField(label='test')
class ServiceRoleAssignmentAdmin(admin.ModelAdmin):
form = ServiceRoleAssignmentForm
admin.site.register(ServiceRoleAssignment, ServiceRoleAssignmentAdmin)
However, no matter what I try, the field doesn't appear on my admin form ! Could it be a problem related to the way admin works ? Or to ModelForm ?
Thank for any help !
Sébastien
PS : I am using django 1.3
When rendering your form in template, fields enumerating from fieldsets variable, not from fields. Sure you can redefine fieldsets in your AdminForm, but then validations will fail as original form class doesn't have such field. One workaround I can propose is to define this field in form definition statically and then redefine that field in form's init method dynamically. Here is an example:
class ServiceRoleAssignmentForm(forms.ModelForm):
test = forms.Field()
class Meta:
model = ServiceRoleAssignment
def __init__(self, *args, **kwargs):
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)
# Here we will redefine our test field.
self.fields['test'] = forms.CharField(label='test2')
I actually have a the same issue which I'm working through at the moment.
While not ideal, I have found a temporary workaround that works for my use case. It might be of use to you?
In my case I have a static name for the field, so I just declared it in my ModelForm. as normal, I then override the init() as normal to override some options.
ie:
def statemachine_form(for_model=None):
"""
Factory function to create a special case form
"""
class _StateMachineBaseModelForm(forms.ModelForm):
_sm_action = forms.ChoiceField(choices=[], label="Take Action")
class Meta:
model = for_model
def __init__(self, *args, **kwargs):
super(_StateMachineBaseModelForm, self).__init__(*args, **kwargs)
actions = (('', '-----------'),)
for action in self.instance.sm_state_actions():
actions += ((action, action),)
self.fields['_sm_action'] = forms.ChoiceField(choices=actions,
label="Take Action")
if for_model: return _StateMachineBaseModelForm
class ContentItemAdmin(admin.ModelAdmin):
form = statemachine_form(for_model=ContentItem)
Now as I mentioned before, this is not entirely 'dynamic', but this will do for me for the time being.
I have the exact same problem that, if I add the field dynamically, without declaring it first, then it doesn't actually exist. I think this does in fact have something to do with the way that ModelForm creates the fields.
I'm hoping someone else can give us some more info.
Django - Overriding get_form to customize admin forms based on request
Try to add the field before calling the super.init:
def __init__(self, *args, **kwargs):
self.fields['test'] = forms.CharField(label='test')
super(ServiceRoleAssignmentForm, self).__init__(*args, **kwargs)