I have several "Listing" views which are very similar and I feel like I'm unecessarily repeating myself. Subclassing seems like the answer, but I've run into problems down the line before when subclassing things in Django so I'd like to ask before doing so.
If I have 2 views, like this, which use the same template, a variable message and different querysets:
class MyGiverView(LoginRequiredMixin, TemplateView):
template_name = "generic_listings.html"
message = ""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["list_of_stuff"] = MyModel.objects.filter(
giver=self.request.user,
status=1,
)
context["message"] = self.message
return context
class MyTakerView(LoginRequiredMixin, TemplateView):
template_name = "generic_listings.html" # this hasn't changed
message = ""
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["list_of_stuff"] = MyModel.objects.filter(
taker=self.request.user, # this has changed
status__in=(1,2,3), # this has changed
)
context["message"] = self.message # this hasn't changed
return context
Am I going to screw it up by creating a base class like:
class MyBaseView(LoginRequiredMixin, TemplateView):
template_name = "generic_listings.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["list_of_stuff"] = self.qs
context["message"] = self.message
return context
And using it in my views as such:
class MyGiverView(MyBaseView):
qs = MyModel.objects.filter(
giver=self.request.user,
status=1,
)
message = ""
class MyTakerView(MyBaseView):
qs = MyModel.objects.filter(
taker=self.request.user,
status__in=(1,2,3),
)
message = ""
It's DRYer, but I'm unsure of the implications regarding what goes on "under the hood".
Instead of creating a base class, it is better to create a mixin class for what you need.
Also, using ListView would be better for what you need.
This is my suggestion:
class InjectMessageMixin:
message = ""
# Always use this method to get the message. This allows user
# to override the message in case it is needed to do on a request
# basis (more dynamic) instead of using the class variable "message".
def get_message(self):
return self.message
def get_context_data(self, **kwargs):
# This line is super important so you can normally use
# this mixin with other CBV classes.
context = super().get_context_data(**kwargs)
context.update({'message': self.get_message()})
return context
class MyGiverView(LoginRequiredMixin, InjectMessageMixin, ListView):
# Even though this doesn't change I would keep it repeated. Normally,
# templates are not reused between views so I wouldn't say it's necessary
# to extract this piece of code.
template_name = "generic_listings.html"
message = "giver message"
def get_queryset(self):
return MyModel.objects.filter(
giver=self.request.user,
status=1,
)
class MyTakerView(LoginRequiredMixin, InjectMessageMixin, ListView):
# Even though this doesn't change I would keep it repeated. Normally,
# templates are not reused between views so I wouldn't say it's necessary
# to extract this piece of code.
template_name = "generic_listings.html"
message = "taker message"
def get_queryset(self, **kwargs):
return MyModel.objects.filter(
taker=self.request.user, # this has changed
status__in=(1,2,3), # this has changed
)
Related
class PublisherDetail(DetailView):
model = Publisher
def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super().get_context_data(**kwargs)
# Add in a QuerySet of all the books
context['book_list'] = Book.objects.all()
return context
According to the basic Python inheritance rules, super().get_context_data(...) would be DetailView.get_context_data(), but since it's not defined, it's inherited down from SingleObjectMixin.
I am writing a test for a View where I update context to pass additional information to the template.
Problem
In writing the test, I'm having trouble accessing context from the RequestFactory.
Code
View
class PlanListView(HasBillingRightsMixin, ListView):
"""Show the Plans for user to select."""
headline = "Select a Plan"
model = Plan
template_name = "billing/plan_list.html"
def get_context_data(self, *args, **kwargs):
context = super(PlanListView, self).get_context_data(**kwargs)
context.update({
"customer": self.get_customer()
})
return context
Test
class TestPlanListView(BaseTestBilling):
def setUp(self):
super(TestPlanListView, self).setUp()
request = self.factory.get('billing:plan_list')
request.user = self.user
request.company_uuid = self.user.company_uuid
self.view = PlanListView()
self.view.request = request
self.response = PlanListView.as_view()(request)
def test_get_context_data(self, **kwargs):
context = super(self.view, self).get_context_data(**kwargs)
context.update({"customer": self.view.get_customer()})
self.assertEqual(
self.view.get_context_data(),
context
)
Question
How can I test the view's get_context_data() method?
Using a test client gives you access to your context.
def test_context(self):
# GET response using the test client.
response = self.client.get('/list/ofitems/')
# response.context['your_context']
self.assertIsNone(response.context['page_obj'])
self.assertIsNone(response.context['customer']) # or whatever assertion.
.....
If you don't want to test the full browser behavior you could use the RequestFactory instead. This factory provides a request instance that you can pass to your view. The benefit in my case was that I can test a single view function as a black box, with exactly known inputs, testing for specific outputs. Just as described in the docs.
class TestView(TemplateView):
template_name = 'base.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = {'1': 11337}
return context
# ...
def test_context(self):
factory = RequestFactory()
request = factory.get('/customer/details')
response = TestView.as_view()(request)
self.assertIsInstance(response.context_data, dict)
self.assertEqual(response.context_data['1'], 1337)
I have been combing through the internet for quite some while without finding any solution to this problem.
What I am trying to do...
I have the following models:
class TrackingEventType(models.Model):
required_previous_event = models.ForeignKey(TrackingEventType)
class TrackingEvent(models.Model):
tracking = models.ForeignKey(Tracking)
class Tracking(models.Model):
last_event = models.ForeignKey(TrackingEvent)
Now the main model is Tracking, so my admin for Tracking looks like this:
class TrackingEventInline(admin.TabularInline):
model = TrackingEvent
extra = 0
class TrackingAdmin(admin.ModelAdmin):
inlines = [TrackingEventInline]
That's it for the current setup.
Now my quest:
In the TrackingAdmin, when I add new TrackingEvent inlines, I want to limit the options of TrackingEventType to onlye those, that are allowed to follow on the last TrackingEvent of the Tracking. (Tracking.last_event == TrackingEventType.required_previous_event).
For this, I would need to be able to access the related Tracking on the InlineTrackingEvent, to access the last_event and filter the options for TrackingEventType accordingly.
So I found this: Accessing parent model instance from modelform of admin inline, but when I set up TrackingEventInline accordingly:
class MyFormSet(forms.BaseInlineFormSet):
def _construct_form(self, i, **kwargs):
kwargs['parent_object'] = self.instance
print self.instance
return super(MyFormSet, self)._construct_form(i, **kwargs)
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
print kwargs
self.parent_object = kwargs.pop('parent_object')
super(MyForm, self).__init__(*args, **kwargs)
class TrackingEventInline(admin.TabularInline):
form = MyForm
formset = MyFormSet
model = TrackingEvent
extra = 0
I get a KeyError at /admin/.../tracking/2/change/ 'parent_object' from self.parent_object = kwargs.pop('parent_object')
Does anyone know how to solve this? Am I approaching the problem the wrong way? I guess this would be pretty easy in a custom form in the frontend, but I really want to use the admin, because the whole application is built to be used from the admin, and it would be a hell lot of work to build a custom admin interface just because of this problem :)
Ok, so posting on StackOverflow is always helping to get the problem straight. I was able to put together a solution that works for me.
It includes defining my own Form in a outer function, as well as defining two InlineAdmin objects for TrackingEvent (one for update / edit, one just for insert).
Here's the code:
def create_trackingevent_form(tracking):
"""
"""
class TrackingEventForm(forms.ModelForm):
"""
Form for Tracking Event Inline
"""
def clean(self):
"""
May not be needed anymore, since event type choices are limited when creating new event.
"""
next_eventtype = self.cleaned_data['event_type']
tracking = self.cleaned_data['tracking']
# get last event, this also ensures last_event gets updated everytime the change form for TrackingEvent is loaded
last_eventtype = tracking.set_last_event()
if last_eventtype:
last_eventtype = last_eventtype.event_type
pk = self.instance.pk
insert = pk == None
# check if the event is updated or newly created
if insert:
if next_eventtype.required_previous_event == last_eventtype:
pass
else:
raise forms.ValidationError('"{}" requires "{}" as last event, "{}" found. Possible next events: {}'.format(
next_eventtype,
next_eventtype.required_previous_event,
last_eventtype,
'"%s" ' % ', '.join(map(str, [x.name for x in tracking.next_tracking_eventtype_options()]))
)
)
else:
pass
return self.cleaned_data
def __init__(self, *args, **kwargs):
# You can use the outer function's 'tracking' here
self.parent_object = tracking
super(TrackingEventForm, self).__init__(*args, **kwargs)
self.fields['event_type'].queryset = tracking.next_tracking_eventtype_options()
#self.fields['event_type'].limit_choices_to = tracking.next_tracking_eventtype_options()
return TrackingEventForm
class TrackingEventInline(admin.TabularInline):
#form = MyForm
#formset = MyFormSet
model = TrackingEvent
extra = 0
#readonly_fields = ['datetime', 'event_type', 'note']
def has_add_permission(self, request):
return False
class AddTrackingEventInline(admin.TabularInline):
model = TrackingEvent
extra = 0
def has_change_permission(self, request, obj=None):
return False
def queryset(self, request):
return super(AddTrackingEventInline, self).queryset(request).none()
def get_formset(self, request, obj=None, **kwargs):
if obj:
self.form = create_trackingevent_form(obj)
return super(AddTrackingEventInline, self).get_formset(request, obj, **kwargs)
I hope this helps other people with the same problem.. Some credit to the Stack Overflow threads that helped me come up with this:
Prepopulating inlines based on the parent model in the Django Admin
Limit foreign key choices in select in an inline form in admin
https://docs.djangoproject.com/en/1.9/ref/models/instances/#django.db.models.Model.clean_fields
Please do not hesitate to ask questions if you have any
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 have a blog app that uses django_taggit. My HomePageView subclasses ArchiveIndexView and works well.
Now I'd like the following link to work: http://mysite.com/tag/yellow and I'd like to use the ArchiveIndexView generic class and pass in a modified queryset that filters on tag_slug. I want to do this because I want to use the same template as the homepage.
My urls.py is
url(r'^$', HomePageView.as_view(paginate_by=5, date_field='pub_date',template_name='homepage.html'),
),
url(r'^tag/(?P<tag_slug>[-\w]+)/$', 'tag_view'), # I know this is wrong
My views.py is
class HomePageView(ArchiveIndexView):
"""Extends the detail view to add Events to the context"""
model = Entry
def get_context_data(self, **kwargs):
context = super(HomePageView, self).get_context_data(**kwargs)
context['events'] = Event.objects.filter(end_time__gte=datetime.datetime.now()
).order_by('start_time')[:5]
context['comments'] = Comment.objects.filter(allow=True).order_by('created').reverse()[:4]
return context
I realize I'm lost here, and would like some help in finding out how to create a new class TagViewPage() that modifies the queryset by filtering on tag_slug.
The key thing is to override the get_queryset method, so that the queryset only includes returns entries with the chosen tag. I have made TagListView inherit from HomePageView, so that it includes the same context data - if that's not important, you could subclass ArchiveIndexView instead.
class TagListView(HomePageView):
"""
Archive view for a given tag
"""
# It probably makes more sense to set date_field here than in the url config
# Ideally, set it in the parent HomePageView class instead of here.
date_field = 'pub_date'
def get_queryset(self):
"""
Only include entries tagged with the selected tag
"""
return Entry.objects.filter(tags__name=self.kwargs['tag_slug'])
def get_context_data(self, **kwargs):
"""
Include the tag in the context
"""
context_data = super(TagListView, self).get_context_data(self, **kwargs)
context_data['tag'] = get_object_or_404(Tag, slug=self.kwargs['tag_slug'])
return context_data
# urls.py
url(r'^tag/(?P<tag_slug>[-\w]+)/$', TagListView.as_view(paginate_by=5, template_name='homepage.html')),