I'm trying to warp my head around Django's new class-based views from 1.3.
I've done a bit of reading:
http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html
http://www.caktusgroup.com/blog/2011/12/29/class-based-views-django-13/
But the one thing I haven't seen an example of, or how to do is, can several views subclass a common 'parent' class, reusing the data from there? (Pardon my thrashing of the common nomenclature)
An example of what I am trying to do:
class MyParentClass(TemplateView):
def get(self, request, *args, **kwargs):
session_data = request.session
other_variables = foovars
return self.render_to_response(context)
class MyChildClassOne(TemplateView):
template_name = "template_one.htm"
def get(self,request, *args, **kwargs):
resultant_data = foodata
return {'data' : resultant_data }
class MyChildClassTwo(TemplateView):
template_name = "template_two.htm"
def get(self,request, *args, **kwargs):
other_data = foootherdata
return {'data' : other_data }
So that the only difference between the two child classes is the templates they use and the 'data' they return. Both views would also return session_data and other_variables from the parent class, and therefore not repeating "return session_data, other_variables" in every child class.
First, don't override your view's get method. That's dangerous for several reasons I won't go into here.
Anmyway, what you need is the get_context_data method, which returns the context dict which is being passed to the template.
So, both your child views should look something like:
class ChildView(ParentView):
template_name = "foo"
def get_context_data(self, **kwargs):
context = super(ChildView, self).get_context_data(**kwargs)
context.update({
'foodata': 'bardata',
})
return context
But this is pretty much how views work out of the box; why do you think you need to subclass from an additional custom view class?
Here's a possible way: Your parent class will return a variable called data in the context which will be set by the child class.
Example:
class MyParentClass(TemplateView):
def get(self, request, *args, **kwargs):
session_data = request.session
other_variables = foovars
context['data'] = data
return self.render_to_response(context)
class MyChildClassOne(MyParentClass):
template_name = "template_one.htm"
def get(self,request, *args, **kwargs):
data = foodata
return super(MyChildClassOne, self).get(request, args, kwargs)
class MyChildClassTwo(MyParentClass):
template_name = "template_two.htm"
def get(self,request, *args, **kwargs):
data = foootherdata
return super(MyChildClassTwo, self).get(request, args, kwargs)
Both your child classes inherit from MyParentClass, whose get method automatically sets a variable named data into the context. The data values are provided by the child classes. Once done they call the parent's get method to perform the common operations, including rendering.
Related
I have a Django application that uses a JSON API as its data source.
Here's a simplified example of use in one of my views.py:
class GroupsList(LoginRequiredMixin):
def get(self, request, **kwargs):
# Get file list and totals
try:
group_list = group_adapter.list() # makes an API call and ALSO populates a meta info class
except APIAccessForbidden:
return HttpResponseRedirect(reverse('logout'))
return render(request, 'groups/index.html', {
# can I make a mixin to add data here gained from the API call?
'group_list': group_list,
})
This line:
The group_adapter.list() call populates some meta information into another class, that's not related to the group_list itself. I'd like to pass that data to the template. Ordinarily I'd use a context_processor, but when the context processor is called, the API call hasn't been made yet. I could manually check the information and add it to the render() method, but then I'd need to do that in dozens of different views.
Potential Solution #1: Create a Mixin For It
Can I use a mixin here that adds this information to context AFTER the view code runs but BEFORE render passes information to the template?
In other words is there a way to do this:
class GroupsList(LoginRequiredMixin, AddMetaInfoToContextMixin):
and then create a mixin something like this?
class AddMetaInfoToContextMixin(ContextMixin):
def get_context_data(self, **kwargs):
# self.request
context = super().get_context_data(**kwargs)
context['global_meta_information'] = get_global_meta_information()
return context
Potential Solution #2: Make an overridden templateview
Commenter Melvyn pointed out that I can potentially subclass TemplateView and override get_context_data(), so would something like this work?
class TemplateViewWithMeta(TemplateView):
def get_context_data(self, *args, **kwargs):
context = super(Home. self).get_context_data(*args, **kwargs)
context['global_meta_information'] = get_global_meta_information()
return context
class GroupsList(LoginRequiredMixin, TemplateViewWithMeta):
[...]
The typical workflow for a Django generic TemplateView is:
get()
get_context_data()
render_to_response()
So in your case keeping with the spirit of generic views, you could do it like this:
from django.views import generic
class BaseRemoteApiView(generic.TemplateView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.group_list = None
def get(self, request, *args, **kwargs):
try:
self.group_list = group_adapter.list() # makes an API call and ALSO populates a meta info class
except APIAccessForbidden:
return HttpResponseRedirect(reverse('logout'))
return super().get(request, *args, **kwargs)
class RemoteApiContextMixin(generic.base.ContextMixin):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["group_list"] = self.group_list
context["meta_information"] = get_global_meta_information()
return context
class ConcreteRemoteApiView(RemoteApiContextMixin, BaseRemoteApiView):
pass
Of course, you don't have to make 3 classes and can just combine the 3 into one - depends on how mixable you want to be.
I have the following structure in Django:
class EmailView(View, ABC):
def post(self, request):
pass
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
.........
class Base(AccessMixin, EmailView, ABC):
.....
class ADTView(ABC):
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
.......
class BaseMixin(Base, SubscribeNewsletterView, ADTView, ABC):
def get_context_data(self, *args, **kwargs):
..............
class ItemListView(BaseMixin, ListView):
...............
If EmailView is inherited by Base(as in example) the method get_context_data from ADTView is not called.
If EmailView is not inherited by Base : class Base(AccessMixin, ABC)
the method get_context_data from ADTView is called.
What is in the method it, doesn't matter (even if I get context , not modifying and returning it) the same thing happen.
What I want is the execution of the method order:
AccessMixin, EmailView, ADTView, ListView
I suppose is happen because ListView inherits from View, but in EmailView I used View, because I need as_view. Basically I'm calling the EmailView with an url, using Ajax.
In fact Python MRO use depth first approach,and you can check the MRO by this code
print(ItemListView.__mro__)
views.py
class PaginatorView(_LanguageMixin, ListView):
context_object_name = 'concepts'
#some custom functions like _filter_by_first_letter
def get_queryset(self):
# some logic here ...
all_concepts = self._filter_by_letter(self.concepts, letters, startswith)
#letters and startswith are obtained from the logic above
print all_concepts
return all_concepts
def get_context_data(self, **kwargs):
context = super(PaginatorView, self).get_context_data(**kwargs)
print context[self.context_object_name]
context.update({
'letters': [(l[0], self._letter_exists(context[self.context_object_name], l)) for l in self.all_letters],
'letter': self.letter_index,
'get_params': self.request.GET.urlencode(),
})
return context
The print all_concepts statement prints all my concepts correctly. So everything until here works just fine. Then, I return all_concepts.
Shouldn't at this point, all_concepts being added to the context, under the key specified by context_object_name? i.e., context['concepts'] should be populated with all_concepts?
If so,the print statement inside get_context_data prints nothing. Which suggests me that the context was not updated.
When I previously used a DetailView, the get_object function was updating the context referenced by context_object_name correctly.(i.e. context[context_object_name] was populated with the object returned by get_object) Shouldn't get_queryset do the same for the ListView?
_LanguageMixin is also defined in views.py, but it is not so relevant for my problem. Just included it here for you to see
class _LanguageMixin(object):
def dispatch(self, request, *args, **kwargs):
self.langcode = kwargs.pop("langcode")
self.language = get_object_or_404(Language, pk=self.langcode)
return super(_LanguageMixin, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(_LanguageMixin, self).get_context_data(**kwargs)
context.update({"language": self.language,
"languages": Language.objects.values_list('code',
flat=True)})
return context
[EDIT1]
if instead I do save all_concepts i.e. self.all_concepts=... and then I use self.all_concepts instead of context[self.contex_object_name], everything works fine.
[EDIT2]
I never instantiate the PaginatorView. It's only for extending purposes. Down here you can see how I extend it. self.concepts helps me to find all_concepts in the get_queryset of the parent class(PaginatorView)
class AlphabeticView(PaginatorView):
template_name = "alphabetic_listings.html"
model = Property
def get_queryset(self):
self.concepts = (
self.model.objects.filter(
name='prefLabel',
language__code=self.langcode,
)
.extra(select={'name': 'value',
'id': 'concept_id'},
order_by=['name'])
.values('id', 'name')
)
super(AlphabeticView, self).get_queryset()
The print statement in get_context_data is printing empty because the variable context_object_name is empty. You should try print context[self.context_object_name]
EDIT: In response to your correction, try
print context[self.get_context_object_name(self.get_queryset())]
get_context_object_name docs
EDIT 2: In response to your second edit, the reason its is printing 'None' is because you aren't returning from the get_queryset method of AlphabeticView. Change the last line in that method to
return super(AlphabeticView, self).get_queryset()
I've created a model, and I'm rendering the default/unmodified model form for it. This alone generates 64 SQL queries because it has quite a few foreign keys, and those in turn have more foreign keys.
Is it possible to force it to always (by default) perform a select_related every time one of these models are returned?
You can create a custom manager, and simply override get_queryset for it to apply everywhere. For example:
class MyManager(models.Manager):
def get_queryset(self):
return super(MyManager, self).get_queryset().select_related('foo', 'bar')
(Prior to Django 1.6, it was get_query_set).
Here's also a fun trick:
class DefaultSelectOrPrefetchManager(models.Manager):
def __init__(self, *args, **kwargs):
self._select_related = kwargs.pop('select_related', None)
self._prefetch_related = kwargs.pop('prefetch_related', None)
super(DefaultSelectOrPrefetchManager, self).__init__(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
qs = super(DefaultSelectOrPrefetchManager, self).get_queryset(*args, **kwargs)
if self._select_related:
qs = qs.select_related(*self._select_related)
if self._prefetch_related:
qs = qs.prefetch_related(*self._prefetch_related)
return qs
class Sandwich(models.Model):
bread = models.ForeignKey(Bread)
extras = models.ManyToManyField(Extra)
# ...
objects = DefaultSelectOrPrefetchManager(select_related=('bread',), prefetch_related=('extras',))
Then you can re-use the manager easily between model classes. As an example use case, this would be appropriate if you had a __unicode__ method on the model which rendered a string that included some information from a related model (or anything else that meant a related model was almost always required).
...and if you really want to get wacky, here's a more generalized version. It allows you to call any sequence of methods on the default queryset with any combination of args or kwargs. There might be some errors in the code, but you get the idea.
from django.db import models
class MethodCalls(object):
"""
A mock object which logs chained method calls.
"""
def __init__(self):
self._calls = []
def __getattr__(self, name):
c = Call(self, name)
self._calls.append(c)
return c
def __iter__(self):
for c in self._calls:
yield tuple(c)
class Call(object):
"""
Used by `MethodCalls` objects internally to represent chained method calls.
"""
def __init__(self, calls_obj, method_name):
self._calls = calls_obj
self.method_name = method_name
def __call__(self, *method_args, **method_kwargs):
self.method_args = method_args
self.method_kwargs = method_kwargs
return self._calls
def __iter__(self):
yield self.method_name
yield self.method_args
yield self.method_kwargs
class DefaultQuerysetMethodCallsManager(models.Manager):
"""
A model manager class which allows specification of a sequence of
method calls to be applied by default to base querysets.
`DefaultQuerysetMethodCallsManager` instances expose a property
`default_queryset_method_calls` to which chained method calls can be
applied to indicate which methods should be called on base querysets.
"""
def __init__(self, *args, **kwargs):
self.default_queryset_method_calls = MethodCalls()
super(DefaultQuerysetMethodCallsManager, self).__init__(*args, **kwargs)
def get_queryset(self, *args, **kwargs):
qs = super(DefaultQuerysetMethodCallsManager, self).get_queryset(*args, **kwargs)
for method_name, method_args, method_kwargs in self.default_queryset_method_calls:
qs = getattr(qs, method_name)(*method_args, **method_kwargs)
return qs
class Sandwich(models.Model):
bread = models.ForeignKey(Bread)
extras = models.ManyToManyField(Extra)
# Other field definitions...
objects = DefaultQuerysetMethodCallsManager()
objects.default_queryset_method_calls.filter(
bread__type='wheat',
).select_related(
'bread',
).prefetch_related(
'extras',
)
The python-mock-inspired MethodCalls object is an attempt at making the API more natural. Some might find that a bit confusing. If so, you could sub out that code for an __init__ arg or kwarg that just accepts a tuple of method call information.
Create a custom models.Manager and override all the methods (filter, get etc.) and append select_related onto every query. Then set this manager as the objects attribute on the model.
I would recommend just going through your code and adding the select_related where needed, because doing select_related on everything is going to cause some serious performance issues down the line (and it wouldn't be entirely clear where it's coming from).
I have a FormWizard where I need data from the first form to pass to the constructor of the second form so I can build a dynamic form.
I can get the first form's data via the process_step of the FormWizard.
I create the fields of the second form with a database call of the list of fields.
class ConditionWizardDynamicQuestions(forms.Form):
def __init__(self, DynamicQuestions=None, *args, **kwargs):
super(ConditionWizardDynamicQuestions, self).__init__(*args, **kwargs)
questions = Question.objects.filter(MYDATA = DATA_FROM_1STFORM)
for q in questions:
dynField = FieldFactory(q)
self.fields[q.label] = dynField
How can I pass over the DATA_FROM_1STFORM ?
my resultant code:
I abandoned the init of the form, and switched it to the CreateQuestions def. Then used the wizard's get_form override to alter the form after creation.
class ConditionWizard(SessionFormWizard):
def get_form(self, request, storage, step=None, data=None, files=None):
form = super(ConditionWizard, self).get_form(request, storage, step, data, files)
stepIndex = self.get_step_index(request, storage, step)
if stepIndex == 1:
form.CreateQuestions(request.session["WizardConditionId"])
if stepIndex == 3:
form.fields['hiddenConditionId'].initial = request.session["WizardConditionId"]
form.fields['medicationName'].queryset = Medication.objects.filter(condition = request.session["WizardConditionId"])
return form
I solved this by overriding get_form_kwargs for the WizardView. It normally just returns an empty dictionary that get_form populates, so by overriding it to return a dictionary with the data you need prepopulated, you can pass kwargs to your form init.
def get_form_kwargs(self, step=None):
kwargs = {}
if step == '1':
your_data = self.get_cleaned_data_for_step('0')['your_data']
kwargs.update({'your_data': your_data,})
return kwargs
Then, in your form init method you can just pop the kwarg off before calling super:
self.your_data = kwargs.pop('client', None)
FormWizard already passes the data from each previous form to the next form. If you want to get that data in order to instantiate a class (for example, if a form has special keyword arguments that it requires), one way of doing it is to grab the querydict by overriding get_form in your form wizard class. For example:
class SomeFormWizard(FormWizard):
def get_form(self, step, data=None):
if step == 1 and data: # change this to whatever step requires
# the extra data
extra_data = data.get('key_from_querydict')
if extra_data:
return self.form_list[step](data,
keyword_argument=extra_data,
prefix=self.prefix_for_step(step),
initial=self.initial.get(step, None))
# Fallback for the other forms.
return self.form_list[step](data,
prefix=self.prefix_for_step(step),
initial=self.initial.get(step, None))
Note that you can also override parse_params(self, request, *args, **kwargs) in FormWizard to access the url/request data, just like you would in a view, so if you have request data (request.user, for instance) that is going to be needed for all of the forms, it might be better to get the data from there.
Hope this helps.
Override the get_form_kwargs method of your form wizard in views
view.py
class FormWizard(SessionWizardView):
def get_form_kwargs(self, step=None):
kwargs = {}
if step == '1':
step0_form_field = self.get_cleaned_data_for_step('0')['previous_form_field_data']
kwargs.update({'step0_form_field': step0_form_field})
return kwargs
Override the init of your form by popping up the data you got from the previous field to create a dynamic field.
forms.py
class MyForm(forms.Form):
#some fields
class MyForm1(forms.Form):
def __init__(self, *args, **kwargs):
extra = kwargs.pop('step0_form_field')
super(MyForm1, self).__init__(*args, **kwargs)
for i in range(extra):
self.fields['name_%s' % i] = forms.CharField()
I was recently working with django form wizard, and i was solving the similar issue. I don't think you can pass data to init, however, what you can do, is override the init constructor:
next_form = self.form_list[1]
# let's change the __init__
# function which will set the choices :P
def __init__(self, *args, **kw):
super(next_form, self).__init__(*args, **kw)
self.fields['availability'].choices = ...
next_form.__init__ = __init__
It's quite annoying that in python you can't declare and assign a function in one go and have to put it in the namespace (unless you use lambdas), but oh well.