Search code examples
djangodjango-modelsdjango-viewsdjango-templatesdjango-urls

Categorize posts by months and years in Django


I am a noob in Django, and I am doing a project with lots of trials and errors. Here's a case. I want to implement this on my site:

arc asia

Each entry will have posts grouped by the months and the years. If I click, I will get to see bunch of posts on that month and year.

My site only has 4 posts now, all of which were on last September. The look that I managed to do is the following, which obviously is wrong because Sep 2022 should be a single entry. There has to be some way to do the group by but I can't seem to achieve that:

group

I wanted to do it with an archive view, as I failed at that attempt, I am doing in this way. I'd like to know both ways. Here are the relevant files:

blog/models.py

class News(models.Model):
    STATUS_CHOICES = (
        ('draft', 'Draft'),
        ('published', 'Published'),
    )
    news_title = models.CharField(max_length=250)
    null=True)
    slug = models.SlugField(max_length=300, unique_for_date='nw_publish')
    news_author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='news_posts')
    news_body = RichTextField()
    image_header = models.ImageField(upload_to='featured_image/%Y/%m/%d/', null=True, blank=True)  # this

    nw_publish = models.DateTimeField(default=timezone.now)
    nw_status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    tags = TaggableManager()

    def __unicode__(self):
        return '%name' % {'name': self.news_title}

    class Meta:
        ordering = ('nw_publish',)

    def __str__(self):
        return self.news_title

    def get_absolute_url(self):
        return reverse('news:news_detail', args=[self.slug])
blog/views.py

class NewsListView(TagMixin, ListView):
    model = News
    queryset = News.objects.all()
    context_object_name = 'newss'
    template_name = 'blog/news.html'


class NewsDetailView(TagMixin, DetailView):
    model = News
    date_field = "nw_publish"
    context_object_name = 'news'
    template_name = 'blog/news_detail.html'

    def get_context_data(self, *, object_list=None, **kwargs):
        data = super().get_context_data(**kwargs)
        data['news_details_view'] = News.objects.all()
        data['news_years'] = News.objects.annotate(year=ExtractYear('nw_publish')).values('year').annotate(total_entries=Count('year'))
        return data


class NewsTagView(TagMixin, ListView):
    model = News
    context_object_name = 'newss'
    template_name = 'blog/news.html'

    def get_queryset(self):
        return News.objects.filter(tags__slug=self.kwargs.get('tag_slug'))


class NewsYearArchiveView(YearArchiveView):
    context_object_name = 'news_years'
    date_field = "nw_publish"
    year_format = '%Y'
    make_object_list = True
    allow_future = True
    queryset = News.objects.filter(nw_status='published').order_by('nw_publish', 'news_title')

blog/urls.py

from django.urls import path, re_path

from . import views
from .views import Home, HomeDetail, Contact, ReportListView, ReportDetailView, NewsListView, NewsDetailView, \
    MemberListView, ProjectListView, ProjectDetailView, SearchView, ReportYearArchiveView, PhotoGallery, AboutDetail, \
    VideoGallery, FCListView, ReportTagView, NewsTagView, NewsYearArchiveView
from django.views.generic.base import RedirectView

# , Gallery

app_name = 'blog'
urlpatterns = [
    # post views

    path('', Home.as_view(), name='home'),
    path("search", SearchView.as_view(), name='search'),
    path('home/<slug:slug>/', HomeDetail.as_view(), name='details'),
    path('news/', NewsListView.as_view(), name='news_list'),
    path('news/<slug:slug>/', NewsDetailView.as_view(), name='news_detail'),
    path('news/taggit/tag/<slug:tag_slug>/', NewsTagView.as_view(), name='post_tag'),
    path('news/<int:year>/', NewsYearArchiveView.as_view(), name="news_year_archive"),
    
]

templates/news_details.html

{% extends 'base.html' %}

{% load static %}
{% block container %}
    <div class="stricky-header stricked-menu main-menu">
        <div class="sticky-header__content"></div><!-- /.sticky-header__content -->
    </div><!-- /.stricky-header -->
    <!--News Details Start-->
    ...
   
                        <div class="sidebar__single sidebar__category">
                            <h3 class="sidebar__title">Archives</h3>

                            <ul class="sidebar__category-list list-unstyled">
                                 {% for y in news_years %}
                                <li><a href={{ y.get_absolute_url }}><i class="fas fa-arrow-circle-right"></i>{{ y }}</a></li>
                               {% endfor %}
                            </ul>
                        </div>
                        <div class="sidebar__single sidebar__tags">
                            <h3 class="sidebar__title">Popular Tags</h3>

                            <div class="sidebar__tags-list">
                                {% for tag in tags %}
                                <a href="{% url 'blog:post_tag'  tag.slug %} ">{{ tag.name }}</a>
                               {% endfor %}
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </section>
    <!--News Details End-->
{% endblock %}

What I am trying to do:

  1. Combine all the same months and years posts together
  2. Making the list clickable
  3. Showing a list of the posts after the click (how to get this list?)

Sorry I am a newbie and I don't know Django that muuch; still learning hence I am stuck with this problem.

What I am trying to do:

Combine all the same months and years posts together Making the list clickable Showing a list of the posts after the click (how to get this list?) Sorry I am a newbie and I don't know Django that muuch; still learning hence I am stuck with this problem.


Solution

  • The only thing missing is a .order_by() [Django-doc] (yes, I know that is strange), this will force a GROUP BY statement:

    data['news_years'] = (
        News.objects.annotate(year=ExtractYear('nw_publish'))
        .values('year')
        .annotate(total_entries=Count('year'))
        .order_by()
    )

    as for using months, it might be better to truncate than extract, since then you still have a date object:

    from django.db.models.functions import TruncMonth
    
    data['news_years'] = (
        News.objects.values(month=TruncMonth('nw_publish'))
        .annotate(total_entries=Count('month'))
        .order_by()
    )

    Showing a list of the posts after the click (how to get this list?)

    You can make a path with the year and month with:

    path(
        'posts/<int:year>/<int:month>/', SomeListView.as_view(), name='posts-by-month'
    ),

    then in the template you link to this with:

    {% for news_year in news_years %}
        <a href="{% url 'posts-by-month' news_year.month.year news_year.month.year %}">{{ news_year.month }} ({{ news_year.total_entries }})</a>
    {% endfor %}

    in the SomeListView, you can then filter with:

    class SomeListView(ListView):
        model = News
    
        def get_queryset(self, *args, **kwargs):
            return (
                super()
                .get_queryset(*args, **kwargs)
                .filter(
                    nw_publish__year=self.kwargs['year'],
                    nw_publish__month=self.kwargs['month'],
                )
            )