What is the best way to append or inject some extra data into a Django QuerySet?
Imagine a situation where I am displaying a list of Books, and I want to show the result of a special calculation on each one:
models.py
class Book(models.Model):
name = models.CharField(max_length=64)
book_list.html
{% for book in objects %}
{{ book.name }} - {{ book.special_result }}
{% endfor %}
views.py
class BookListView(ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
books = self.object_list
for book in books:
book.special_result = do_special_calculation(foo=bar)
context['books'] = books
return context
Imagine that do_special_calculation() method cannot be calculated in the template or as a model parameter, because it needs to have a variable foo passed in.
This code does not achieve the desired result of making each book's special_result value accessible from the template, because the book variable is overwritten with each iteration of the for loop. Every solution I've come up involves basically building out a new dictionary in parallel with the QuerySet, passing that into the template, and looping through them both in the template simultaneously, causing very ugly code.
I also don't want to save the result of do_special_calculations() back to the database for a host of reasons (efficiency, potential stale data, can't easily save an object).
What would be the best approach to make each entry's special calculation available in the template?
I finally solved this by making an empty list and using setattr() on each entry. Here is a fixed code example:
class BookListView(ListView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
books = self.object_list
book_list = []
for book in books:
special_result = do_special_calculation(foo=bar)
setattr(book, "special_result", special_result)
book_list.append(book)
context['books'] = book_list
return context
I have the following model:
class TestCase(models.Model):
tc_id = models.CharField(max_length=20)
tc_title = models.CharField(max_length=500)
class TestSteps(models.Model):
ts_case = models.ForeignKey(TestCase, on_delete=models.CASCADE)
ts_seq_no = models.IntegerField(default=1)
ts_instruction = models.CharField(max_length=200)
I want to display a test case together with its associated test steps in the template. For this I have written two views, one is not so nice but works:
def tc_steps(request, pk):
case = TestCase.objects.filter(id=pk)
steps = TestSteps.objects.filter(ts_case_id=pk).order_by('ts_seq_no')
context = {'case': case, 'steps': steps}
return render(request, 'testman/tc_steps.html', context)
Not very nice because I have to retrieve two querysets. Better to have this one:
def tc_steps(request, pk):
case = TestCase.objects.filter(id=pk)
return render(request, 'testman/tc_steps.html', {'case': case})
because this contains all the information I need in the template. Now the problem:
In the template for the second view I use the following tag to display the test steps:
{% for step in case.first.teststeps_set.all %}
Which works but the steps aren't in the right order. In the template for the first view I just use:
{% for step in steps %}
And get the correct order (sorted by ts_seq_no) because I did the sorting in the view already. I tried to use a filter but couldn't find one that does what I want. My question is, is there any way to do an order_by in the template tag?
You can use dictsort like this(use dictsortreversed for reversed order):
{% for step in case.first.teststeps_set.all|dictsort:"ts_seq_no" %}
I would add a method to the TestCase model to return its related steps in the required order.
class TestCase(models.Model):
...
def ordered_steps(self):
return self.teststeps_set.order_by('ts_seq_no')
Now in the template you can do {% for step in case.first.ordered_steps %}.
NoReverseMatch at /errorcodes/error_codes/2
Reverse for 'relatedpartsview' with no arguments not found. 1 pattern(s) tried: ['errorcodes\/error_codes\/relatedparts\/(?P[^/]+)$']
Currently I am trying to pass a PK into my link as an argument. However, I keep getting no reverse match. I have added a redirect statement, but that doesn't seem to be resolving the issue.
views.py
class RelatedPartsListView(ListView):
context_object_name = 'related_parts_list'
model = models.ErrorCodes
template_name = 'related_parts_list.html'
def get_context_data(self, **kwargs):
# xxx will be available in the template as the related objects
context = super(ErrorCodeView, self).get_context_data(**kwargs)
context['relatedparts'] = RelatedParts.objects.filter(related_error_code=self.get_object())
return context
return redirect('RelatedPartsListView', pk='pk')
urls.py
urlpatterns = [
path(r'error_codes/',views.ErrorCodeList.as_view(), name='errorcodelist'),
path(r'error_codes/<pk>',views.ErrorCodeView.as_view(), name='errorcodeview'),
path(r'error_codes/relatedparts/<pk>',views.RelatedPartsListView.as_view(), name='relatedpartsview')
]
Link
{% url 'errorcodes:relatedpartsview' %}
Update: I am getting a 'RelatedPartsListView' object has no attribute 'get_object'
class RelatedPartsListView(ListView):
context_object_name = 'related_parts_list'
model = models.ErrorCodes
template_name = 'related_parts_list.html'
def get_context_data(self, **kwargs):
# xxx will be available in the template as the related objects
obj = super(RelatedPartsListView, self).get_context_data(**kwargs)
obj = RelatedParts.objects.filter(related_error_code=self.get_object())
return redirect('related_parts_list', obj.pk)
Your url you are trying to use {% url %} without passing your pk which is wht that url relies on. You can reference the docs here https://docs.djangoproject.com/en/2.0/ref/templates/builtins/
So, you should be passing your object (the one with the pk) to your template through the context. Then you can access that object's pk value. So, you will have something like this.
{% url 'relatedpartsviewurl' obj.pk %}
Edit: Your redirect also does not make any sense and you have two return statements right after each other which makes absolutely no sense.
Edit: Okay, so you need to make your template name in your return redirect the exact same as what is shown in your urls where name= . Next, you need to get rid of your context, rather store your filter as a variable such as obj. Then for your pk in your redirect, just reference that variable like so redirect('template_name', obj.pk)
Edit: CLASS BASED VIEW (idk if works)
Your list view.
class RelatedPartsListView(ListView):
context_object_name = 'related_parts_list'
model = models.ErrorCodes
template_name = 'related_parts_list.html'
def get_context_data(self, **kwargs):
context = super(RelatedPartsListView, self).get_context_data(**kwargs)
context['relatedparts'] = RelatedParts.objects.filter(related_error_code=1)
return context
Replace the relatedparts url with this.
re_path(r'^error_codes/relatedparts/?P<parts_id>[0-9]+',views.RelatedPartsListView.as_view(), name='related_parts_view')
Make your link like this.
{% url 'errorcodes:related_parts_view' 1 %}
Edit: FUNCTION BASED VIEW
views.py
def related_parts_view(request, error_code):
parts = RelatedParts.objects.filter(related_error_code=error_code)
context = {
'relatedparts': parts
}
return render(request, 'path/to/my/template.html', context)
urls.py
re_path(r'^error_codes/relatedparts/?P<error_code>[0-9]+', errorcodes.views.related_parts_view, name='related_parts_view')
other_template.html
Go to Related Parts
template.html
{% for part in relatedparts %}
{{ part.pk }}
{% endfor %}
I'm trying to access the information in my gadb_action model based on the action_ids in my gadb_vote model. I'm initially only getting the information for a particular legislator and then attempting to get the bills associated with the actions that legislator has voted on.
Right now, my action_list is only storing action_ids, but not the related information from the gadb_action model that I want to use in my template.
What is the best way to store that information outside of the for loop to be accessed by the template? Is there a way to write to an empty QuerySet?
Thanks in advance for any and all help!
view
def each_member(request,legislator_id):
each_member = get_object_or_404(gadb_legislator, legislator_id=legislator_id)
each_vote = gadb_vote.objects.filter(legislator_id=legislator_id)
action_list = []
for i in each_vote:
action = gadb_action.objects.filter(action_id=i.action_id)
action_list.append(action)
context = {
'each_member': each_member,
'each_vote': each_vote,
'action_list': action_list
}
return render(request, "eachmember.html", context)
models
class gadb_action(models.Model):
action_id = models.IntegerField(unique=False, max_length=4, primary_key=True)
bill_id = models.IntegerField(unique=False, max_length=12)
class gadb_vote(models.Model):
vote_id = models.IntegerField(unique=False, max_length=11,primary_key=True)
legislator_id = models.IntegerField(unique=False, max_length=11)
action_id = models.IntegerField(unique=False, max_length=11)
template
{% for i in action_list %}
{{i.bill_id}}
{{i.action_id}}
{% endfor %}
Your models are broken.
For a start, although it's not directly related to the question, you need to define your primary keys as AutoFields so that they are autoincremented every time a new entity is added. Otherwise you'll get all sorts of errors when you save a new row. (Even better, don't define the PK at all, and let Django add it automatically.)
Secondly, as lalo says, you should have ForeignKeys from Action to Bill, and from Vote to Action and Vote to Legislator. That way you can get the relevant information with a single query, and follow the foreign keys as required in your template.
(Also, Django already includes the app name in the underlying table name: no need to prefix everything with 'gadb'.)
class Action(models.Model):
bill = models.ForeignKey(Bill)
class Vote(models.Model):
legislator = models.ForeignKey(Legislator)
action = models.ForeignKey(Action)
View:
def each_member(request,legislator_id):
actions = Action.objects.filter(vote__legislator_id=legislator_id)
return render(request, "eachmember.html", {'action_list': actions})
Template:
{% for action in actions %}
{{ action.bill.name }}
{{ action.someotherfield }}
{% endfor %}
To deal with the lack of nested inlines in django-admin, I've put special cases into two of the templates to create links between the admin change pages and inline admins of two models.
My question is: how do I create a link from the admin change page or inline admin of one model to the admin change page or inline admin of a related model cleanly, without nasty hacks in the template?
I would like a general solution that I can apply to the admin change page or inline admin of any model.
I have one model, post (not its real name) that is both an inline on the blog admin page, and also has its own admin page. The reason it can't just be inline is that it has models with foreign keys to it that only make sense when edited with it, and it only makes sense when edited with blog.
For the post admin page, I changed part of "fieldset.html" from:
{% if field.is_readonly %}
<p>{{ field.contents }}</p>
{% else %}
{{ field.field }}
{% endif %}
to
{% if field.is_readonly %}
<p>{{ field.contents }}</p>
{% else %}
{% ifequal field.field.name "blog" %}
<p>{{ field.field.form.instance.blog_link|safe }}</p>
{% else %}
{{ field.field }}
{% endifequal %}
{% endif %}
to create a link to the blog admin page, where blog_link is a method on the model:
def blog_link(self):
return '%s' % (reverse("admin:myblog_blog_change",
args=(self.blog.id,)), escape(self.blog))
I couldn't find the id of the blog instance anywhere outside field.field.form.instance.
On the blog admin page, where post is inline, I modified part of "stacked.html" from:
<h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>
<span class="inline_label">{% if inline_admin_form.original %}
{{ inline_admin_form.original }}
{% else %}#{{ forloop.counter }}{% endif %}</span>
to
<h3><b>{{ inline_admin_formset.opts.verbose_name|title }}:</b>
<span class="inline_label">{% if inline_admin_form.original %}
{% ifequal inline_admin_formset.opts.verbose_name "post" %}
<a href="/admin/myblog/post/{{ inline_admin_form.pk_field.field.value }}/">
{{ inline_admin_form.original }}</a>
{% else %}{{ inline_admin_form.original }}{% endifequal %}
{% else %}#{{ forloop.counter }}{% endif %}</span>
to create a link to the post admin page since here I was able to find the id stored in the foreign key field.
I'm sure there is a better, more general way to do add links to admin forms without repeating myself; what is it?
Use readonly_fields:
class MyInline(admin.TabularInline):
model = MyModel
readonly_fields = ['link']
def link(self, obj):
url = reverse(...)
return mark_safe("<a href='%s'>edit</a>" % url)
# the following is necessary if 'link' method is also used in list_display
link.allow_tags = True
New in Django 1.8 : show_change_link for inline admin.
Set show_change_link to True (False by default) in your inline model, so that inline objects have a link to their change form (where they can have their own inlines).
from django.contrib import admin
class PostInline(admin.StackedInline):
model = Post
show_change_link = True
...
class BlogAdmin(admin.ModelAdmin):
inlines = [PostInline]
...
class ImageInline(admin.StackedInline):
# Assume Image model has foreign key to Post
model = Image
show_change_link = True
...
class PostAdmin(admin.ModelAdmin):
inlines = [ImageInline]
...
admin.site.register(Blog, BlogAdmin)
admin.site.register(Post, PostAdmin)
This is my current solution, based on what was suggested by Pannu (in his edit) and Mikhail.
I have a couple of top-level admin change view I need to link to a top-level admin change view of a related object, and a couple of inline admin change views I need to link to the top-level admin change view of the same object. Because of that, I want to factor out the link method rather than repeating variations of it for every admin change view.
I use a class decorator to create the link callable, and add it to readonly_fields.
def add_link_field(target_model = None, field = '', link_text = unicode):
def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower()
def link(self, instance):
app_name = instance._meta.app_label
reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
link_obj = getattr(instance, field, None) or instance
url = reverse(reverse_path, args = (link_obj.id,))
return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
link.allow_tags = True
link.short_description = reverse_name + ' link'
cls.link = link
cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + ['link']
return cls
return add_link
You can also pass a custom callable if you need to get your link text in some way than just calling unicode on the object you're linking to.
I use it like this:
# the first 'blog' is the name of the model who's change page you want to link to
# the second is the name of the field on the model you're linking from
# so here, Post.blog is a foreign key to a Blog object.
#add_link_field('blog', 'blog')
class PostAdmin(admin.ModelAdmin):
inlines = [SubPostInline, DefinitionInline]
fieldsets = ((None, {'fields': (('link', 'enabled'),)}),)
list_display = ('__unicode__', 'enabled', 'link')
# can call without arguments when you want to link to the model change page
# for the model of an inline model admin.
#add_link_field()
class PostInline(admin.StackedInline):
model = Post
fieldsets = ((None, {'fields': (('link', 'enabled'),)}),)
extra = 0
Of course none of this would be necessary if I could nest the admin change views for SubPost and Definition inside the inline admin of Post on the Blog admin change page without patching Django.
I think that agf's solution is pretty awesome -- lots of kudos to him. But I needed a couple more features:
to be able to have multiple links for one admin
to be able to link to model in different app
Solution:
def add_link_field(target_model = None, field = '', app='', field_name='link',
link_text=unicode):
def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower()
def link(self, instance):
app_name = app or instance._meta.app_label
reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
link_obj = getattr(instance, field, None) or instance
url = reverse(reverse_path, args = (link_obj.id,))
return mark_safe("<a href='%s'>%s</a>" % (url, link_text(link_obj)))
link.allow_tags = True
link.short_description = reverse_name + ' link'
setattr(cls, field_name, link)
cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + \
[field_name]
return cls
return add_link
Usage:
# 'apple' is name of model to link to
# 'fruit_food' is field name in `instance`, so instance.fruit_food = Apple()
# 'link2' will be name of this field
#add_link_field('apple','fruit_food',field_name='link2')
# 'cheese' is name of model to link to
# 'milk_food' is field name in `instance`, so instance.milk_food = Cheese()
# 'milk' is the name of the app where Cheese lives
#add_link_field('cheese','milk_food', 'milk')
class FoodAdmin(admin.ModelAdmin):
list_display = ("id", "...", 'link', 'link2')
I am sorry that the example is so illogical, but I didn't want to use my data.
I agree that its hard to do template editing so, I create a custom widget to show an anchor on the admin change view page(can be used on both forms and inline forms).
So, I used the anchor widget, along with form overriding to get the link on the page.
forms.py:
class AnchorWidget(forms.Widget):
def _format_value(self,value):
if self.is_localized:
return formats.localize_input(value)
return value
def render(self, name, value, attrs=None):
if not value:
value = u''
text = unicode("")
if self.attrs.has_key('text'):
text = self.attrs.pop('text')
final_attrs = self.build_attrs(attrs,name=name)
return mark_safe(u"<a %s>%s</a>" %(flatatt(final_attrs),unicode(text)))
class PostAdminForm(forms.ModelForm):
.......
def __init__(self,*args,**kwargs):
super(PostAdminForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance',None)
if instance.blog:
href = reverse("admin:appname_Blog_change",args=(instance.blog))
self.fields["link"] = forms.CharField(label="View Blog",required=False,widget=AnchorWidget(attrs={'text':'go to blog','href':href}))
class BlogAdminForm(forms.ModelForm):
.......
link = forms..CharField(label="View Post",required=False,widget=AnchorWidget(attrs={'text':'go to post'}))
def __init__(self,*args,**kwargs):
super(BlogAdminForm, self).__init__(*args, **kwargs)
instance = kwargs.get('instance',None)
href = ""
if instance:
posts = Post.objects.filter(blog=instance.pk)
for idx,post in enumerate(posts):
href = reverse("admin:appname_Post_change",args=(post["id"]))
self.fields["link_%s" % idx] = forms..CharField(label=Post["name"],required=False,widget=AnchorWidget(attrs={'text':post["desc"],'href':href}))
now in your ModelAdmin override the form attribute and you should get the desired result. I assumed you have a OneToOne relationship between these tables, If you have one to many then the BlogAdmin side will not work.
update:
I've made some changes to dynamically add links and that also solves the OneToMany issue with the Blog to Post hope this solves the issue. :)
After Pastebin:
In Your PostAdmin I noticed blog_link, that means your trying to show the blog link on changelist_view which lists all the posts. If I'm correct then you should add a method to show the link on the page.
class PostAdmin(admin.ModelAdmin):
model = Post
inlines = [SubPostInline, DefinitionInline]
list_display = ('__unicode__', 'enabled', 'blog_on_site')
def blog_on_site(self, obj):
href = reverse("admin:appname_Blog_change",args=(obj.blog))
return mark_safe(u"<a href='%s'>%s</a>" %(href,obj.desc))
blog_on_site.allow_tags = True
blog_on_site.short_description = 'Blog'
As far as the showing post links on BlogAdmin changelist_view you can do the same as above. My earlier solution will show you the link one level lower at the change_view page where you can edit each instance.
If you want the BlogAdmin page to show the links to the post in the change_view page then you will have to include each in the fieldsets dynamically by overriding the get_form method for class BlogAdmin and adding the link's dynamically, in get_form set the self.fieldsets, but first don't use tuples to for fieldsets instead use a list.
Based on agfs and SummerBreeze's suggestions, I've improved the decorator to handle unicode better and to be able to link to backwards-foreignkey fields (ManyRelatedManager with one result). Also you can now add a short_description as a list header:
from django.core.urlresolvers import reverse
from django.core.exceptions import MultipleObjectsReturned
from django.utils.safestring import mark_safe
def add_link_field(target_model=None, field='', app='', field_name='link',
link_text=unicode, short_description=None):
"""
decorator that automatically links to a model instance in the admin;
inspired by http://stackoverflow.com/questions/9919780/how-do-i-add-a-link-from-the-django-admin-page-of-one-object-
to-the-admin-page-o
:param target_model: modelname.lower or model
:param field: fieldname
:param app: appname
:param field_name: resulting field name
:param link_text: callback to link text function
:param short_description: list header
:return:
"""
def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower()
def link(self, instance):
app_name = app or instance._meta.app_label
reverse_path = "admin:%s_%s_change" % (app_name, reverse_name)
link_obj = getattr(instance, field, None) or instance
# manyrelatedmanager with one result?
if link_obj.__class__.__name__ == "RelatedManager":
try:
link_obj = link_obj.get()
except MultipleObjectsReturned:
return u"multiple, can't link"
except link_obj.model.DoesNotExist:
return u""
url = reverse(reverse_path, args = (link_obj.id,))
return mark_safe(u"<a href='%s'>%s</a>" % (url, link_text(link_obj)))
link.allow_tags = True
link.short_description = short_description or (reverse_name + ' link')
setattr(cls, field_name, link)
cls.readonly_fields = list(getattr(cls, 'readonly_fields', [])) + \
[field_name]
return cls
return add_link
Edit: updated due to link being gone.
Looking through the source of the admin classes is enlightening: it shows that there is an object in context available to an admin view called "original".
Here is a similar situation, where I needed some info added to a change list view: Adding data to admin templates (on my blog).