urlpattern: first letter to generic class based view

Hi all. I tried googling. Did not help. I hope you can.
I have a small project with an Article model. It has article.title and article.body.
I have tried making a url pattern for catching “A/” and making it go to a template that only shows all the objects, that begins with the letter a
I can’t figure out how to capture the A in the url and send it to my ListView (which will be filtered).
I tried filtering in both the view and the template.

I can get it working by hardcoding the “A” in my view, but I would like it to read my url and pass the first letter instead. Please help :slight_smile:
Some of my code:
views.py

class ArticleListViewByLetter(LoginRequiredMixin, ListView):
    model = Article
    template_name = "article_list_filter.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["letter"] = "A"
        return context

the urlpatterns line for urls.py:

path(
        "<str:letter>/",
        ArticleListViewByLetter.as_view(),
        name="article_list_filter",
    ),

from my content block in my template file:

This is to test if I have access to the variable: {{ letter }}  (note: I do. It writes A)
{% for article in article_list %}

{% if article.title|first == letter %}

I sort of got it working doing this in views:

class ArticleListViewByLetter(LoginRequiredMixin, ListView):
    model = Article
    template_name = "article_list_filter.html"

    def get_queryset(self):
        queryset = super(ArticleListViewByLetter, self).get_queryset()
        return Article.objects.filter(title__startswith=self.kwargs["letter"])

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["letter"] = self.kwargs["letter"]
        return context

But at this point I am just copy pasting around without having a clear idea :rofl:

First, you want to do the filtering in get_context_data, not in the template.

The URL parameters are stored within the self.kwargs dict in the view.

You want to override the get_queryset method, to add your filter to the queryset.

Winging this, but it should be something like:

def get_queryset(self):
    queryset = super().get_queryset()
    queryset = queryset.filter(title__startswith=self.kwargs['letter'])
    return queryset

and remove the conditional from the template.

[Edit: I see you almost got it. If you’re going to create the complete queryset within your method, there’s no need to call super - you’re discarding the return from the function, so there’s no need to call it.]

Thank you Ken. I deleted my own try now and this is my views part now:

class ArticleListViewByLetter(LoginRequiredMixin, ListView):
    model = Article
    template_name = "article_list_filter.html"

    def get_queryset(self):
        queryset = super().get_queryset()
        queryset = queryset.filter(title__startswith=self.kwargs["letter"])
        return queryset

When I open http://127.0.0.1:8000/articles/A/ - nothing shows up. (I have two articles that begins with A)

And the line:
This is to test if I have access to the variable: {{ letter }}
Doesn’t output the variable letter

Probably the easiest thing to do is to throw a couple of print statements into get_queryset to see what’s happening with the query. (I’d print both self.kwargs[‘letter’] and queryset)

Also, can you post your template?

my template is here:

{% extends "base.html" %}

{% block title %}Articles{% endblock title %}

{% block content %}
This is to test if I have access to the variable: {{ letter }}
{% for article in article_list %}
{% if article.title|first == letter %}
<p><a href="{% url 'article_detail' article.pk %}">
                {{ article.title }}</a> &middot;
    <em>by {{ article.author }}</em> | {{ article.date }}
<p>{{ article.body }}</p>
<a href="{% url 'article_edit' article.pk %}">Edit</a> |
<a href="{% url 'article_delete' article.pk %}">Delete</a>
<br />
{% endif %}
{% endfor %}
{% endblock content %}

I tried your print statement suggestions:
I opened http://127.0.0.1:8000/articles/A/
and got:

queryset from super().get_queryset():  <QuerySet [<Article: Jesper nye?>, <Article: ny admin artikel>, <Article: And>, <Article: Ananas>]>
queryset from queryset.filter(title__startswith=self.kwargs['letter']):  <QuerySet [<Article: And>, <Article: Ananas>]>
kwargs from self.kwargs['letter']:  A

Ok, since you’ve already limited your queryset to the articles you’re interested in, now you need to remove the if from your template (along with the corresponding endif).

Yay! It works! Thank you. :slight_smile:

But now my view only uses the get_queryset, so I still have a bit of confusion about the

def get_queryset(self): 
def get_context_data(self, **kwargs):

Could you please explain the difference?

Before I can answer this appropriately, how familiar or comfortable are you with the Django generic Class-Based Views?
You’re obviously familiar enough to use them effectively - otherwise you wouldn’t have gotten to this point. So that leaves me a little unsure as to how I need to target my answer.

The quick answer is that the fundamental purpose of get_queryset in a ListView is to create the queryset being used to build the list to be rendered. The get_context_data is available to add other data to the context being rendered by the template.
Yes, you can create the queryset-to-be-rendered-in-the-list in get_context_data, but then your CBV is doing double work. The get_queryset method will still get called, and the base queryset will be created before being overwritten by the queryset created in get_context_data.

I’ll also refer you to two third-party sites that I found have helped my understanding tremendously -

Thank you so much! Perfect answer. :clap:

I thought about it for a while and now I have one more question. :slight_smile:

If I use the get_queryset to filter some data and still want to pass extra data via get_context_data I could do it like:

def get_queryset:
     some stuff to filter...
def get_context_data:
     some stuff to add to context 
     I wont use get_queryset here, as it will get called via get_queryset anyway

Did I get that right?

Yes, that’s basically it. (Ignoring the illegal syntax because I know you’re just expressing an idea and not demonstrating code.)

1 Like

Now I went back and fiddled some more. And something is still a little off. I will post my code below. But just to recap I’m trying to catch a urlpattern like “Articles/A” and it will go to a generic ListView page and show all the articles that starts with A.
I’m hoping to save the A to the context under the key “letter” vis str:letter in the urls.py url pattern and to test if it comes through - can be shown on the page in the template.
I am getting all the right articles from the dtb, but the variable wont show.
Snips from my code:
from the template file:

{% block title %}Articles{% endblock title %}
{% block content %}
This is to test if I have access to the variable: {{ letter }}
{% if letter %}
{{ letter }}
{% endif %}
<br>
This is to demonstrate get_context_data in the view. The time is now: {{ now }}
<br>
{% for article in article_list %}
<!-- Articles show up here - which works on my page-->

my line from urls.py (which I think is where I dont fully understand how to pass the string correctly):

urlpatterns = [
    path("<str:letter>/", ArticleListViewByLetter.as_view(),name="article_list_filter",),
]

views.py:

class ArticleListViewByLetter(LoginRequiredMixin, ListView):
    model = Article
    template_name = "article_list_filter.html"

    def get_queryset(self):
        queryset = super().get_queryset()
        queryset = queryset.filter(title__startswith=self.kwargs["letter"])
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["now"] = timezone.now()
        return context

I hope you still want to help. :slight_smile:
(And I hope people can learn from me exposing my ignorance.) :grinning:

You still need to pass the letter through the context to the template to be rendered. You’re adding “now” to the context, you also need to add “letter”.

I thought I allready did that in this part of the url path in urlpatterns?:

path("<str:letter>/",

That passes it from the url to the view. Now the view needs to provide it to the rendering engine to make it available to the template - that’s the purpose of the context.

Yay! Now it works! You are awesome!
I added it, and now it works perfectly. (I hope).
My full view for documenting. :slight_smile:

class ArticleListViewByLetter(LoginRequiredMixin, ListView):
    model = Article
    template_name = "article_list_filter.html"

    def get_queryset(self):
        queryset = super().get_queryset()
        queryset = queryset.filter(title__startswith=self.kwargs["letter"])
        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["now"] = timezone.now()
        context["letter"] = self.kwargs["letter"]
        return context

Since I am still practicing and I get som much good help in here I will share this too.
I was thinking about class based vs. function based and I tried (and somewhat succeeded) in writing both.
Comments are welcome. :slight_smile: (I refer back to my own posts and your answers here as my personale stack-overflow :smiley:
Function-based:

def LetterPagesViewFunctionBased(request, letter):
    pages = Page.objects.filter(title__startswith=letter)
    context = {"letter": letter, 
               "page_list": pages}
    return render(request, "pages/pages_index.html", context)

Class-based:

class LetterPagesView(ListView):
    model = Page
    template_name = "pages/pages_index.html"
    def get_queryset(self):
        queryset = super().get_queryset()
        queryset = queryset.filter(title__startswith=self.kwargs["letter"])
        return queryset
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["letter"] = self.kwargs["letter"]
        return context

And for my testing purposes I had them both active with this little change in my
urls.py:

urlpatterns = [
 ...snip...
    path("pages/<str:letter>", LetterPagesView.as_view(), name="LetterPagesView"),
    path("pages/func/<str:letter>", LetterPagesViewFunctionBased, name="LetterPagesViewFunctionBased",
    ),
]

Hi qvisty,

I’m also learing Django, in my little experience at this stage I’m almost always use function based view as there is less ‘magic’ in it. I do write more code but what’s happening in the view is more ‘explicit’.

I guess that with experience I will switch to Class based view as they are certainly more rapid to write.

1 Like