+ {% if dates %}
+
+ {% endif %}
+
{% if view.model_admin.actions_on_top %}
{% include 'djadmin2theme_default/includes/list_actions.html' with position='top' %}
{% endif %}
diff --git a/djadmin2/types.py b/djadmin2/types.py
index c1c9cd6..108d43a 100644
--- a/djadmin2/types.py
+++ b/djadmin2/types.py
@@ -59,6 +59,7 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)):
bypass the blocking features of the ImmutableAdmin.
"""
actions_selection_counter = True
+ date_hierarchy = False
list_display = ('__str__',)
list_display_links = ()
list_filter = ()
diff --git a/djadmin2/views.py b/djadmin2/views.py
index 2883f22..c5a627d 100644
--- a/djadmin2/views.py
+++ b/djadmin2/views.py
@@ -2,6 +2,7 @@
from __future__ import division, absolute_import, unicode_literals
import operator
+from datetime import datetime
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import (PasswordChangeForm,
@@ -26,7 +27,7 @@ from . import permissions, utils
from .forms import AdminAuthenticationForm
from .models import LogEntry
from .viewmixins import Admin2Mixin, AdminModel2Mixin, Admin2ModelFormMixin
-from .filters import build_list_filter
+from .filters import build_list_filter, build_date_filter
class AdminView(object):
@@ -155,6 +156,9 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
if self.model_admin.list_filter:
queryset = self.build_list_filter(queryset).qs
+ if self.model_admin.date_hierarchy:
+ queryset = self.build_date_filter(queryset).qs
+
queryset = self._modify_queryset_for_sort(queryset)
if search_use_distinct:
@@ -194,6 +198,18 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
)
return self._list_filter
+ def build_date_filter(self, queryset=None):
+ if not hasattr(self, "_date_filter"):
+ if queryset is None:
+ queryset = self.get_queryset()
+ self._date_filter = build_date_filter(
+ self.request,
+ self.model_admin,
+ queryset,
+ )
+
+ return self._date_filter
+
def get_context_data(self, **kwargs):
context = super(ModelListView, self).get_context_data(**kwargs)
context['model'] = self.get_model()
@@ -202,8 +218,79 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
context['search_term'] = self.request.GET.get('q', '')
context['list_filter'] = self.build_list_filter()
context['sort_term'] = self.request.GET.get('sort', '')
+
+ if self.model_admin.date_hierarchy:
+ year = self.request.GET.get("year", False)
+ month = self.request.GET.get("month", False)
+ day = self.request.GET.get("day", False)
+
+ if year and month and day:
+ new_date = datetime.strptime(
+ "%s %s %s" % (month, day, year),
+ "%m %d %Y",
+ )
+ context["previous_date"] = {
+ "link": "?year=%s&month=%s" % (year, month),
+ "text": "‹ %s" % new_date.strftime("%B %Y")
+ }
+
+ context["active_day"] = new_date.strftime("%B %d")
+
+ context["dates"] = self._format_days(context)
+ elif year and month:
+ context["previous_date"] = {
+ "link": "?year=%s" % (year),
+ "text": "‹ %s" % year,
+ }
+
+ context["dates"] = self._format_days(context)
+ elif year:
+ context["previous_date"] = {
+ "link": "?",
+ "text": ugettext_lazy("‹ All dates"),
+ }
+
+ context["dates"] = self._format_months(context)
+ else:
+ context["dates"] = self._format_years(context)
+
return context
+ def _format_years(self, context):
+ years = context['object_list'].dates('published_date', 'year')
+ if len(years) == 1:
+ return self._format_months(context)
+ else:
+ return [
+ (("?year=%s" % year.strftime("%Y")), year.strftime("%Y"))
+ for year in
+ context['object_list'].dates('published_date', 'year')
+ ]
+
+ def _format_months(self, context):
+ return [
+ (
+ "?year=%s&month=%s" % (
+ date.strftime("%Y"), date.strftime("%m")
+ ),
+ date.strftime("%B %Y")
+ ) for date in
+ context["object_list"].dates('published_date', 'month')
+ ]
+
+ def _format_days(self, context):
+ return [
+ (
+ "?year=%s&month=%s&day=%s" % (
+ date.strftime("%Y"),
+ date.strftime("%m"),
+ date.strftime("%d"),
+ ),
+ date.strftime("%B %d")
+ ) for date in
+ context["object_list"].dates('published_date', 'day')
+ ]
+
def get_success_url(self):
view_name = 'admin2:{}_{}_index'.format(
self.app_label, self.model_name)
diff --git a/example/blog/admin.py b/example/blog/admin.py
index d5e95bd..545806c 100644
--- a/example/blog/admin.py
+++ b/example/blog/admin.py
@@ -12,8 +12,9 @@ class CommentInline(admin.TabularInline):
class PostAdmin(admin.ModelAdmin):
inlines = [CommentInline, ]
- search_fields = ('title', 'body')
+ search_fields = ('title', 'body', "published_date")
list_filter = ['published', 'title']
+ date_hierarchy = "published_date"
admin.site.register(Post, PostAdmin)
admin.site.register(Comment)
diff --git a/example/blog/admin2.py b/example/blog/admin2.py
index 5b9739d..fb0d222 100644
--- a/example/blog/admin2.py
+++ b/example/blog/admin2.py
@@ -29,11 +29,12 @@ class PostAdmin(djadmin2.ModelAdmin2):
list_actions = [DeleteSelectedAction, CustomPublishAction, unpublish_items]
inlines = [CommentInline]
search_fields = ('title', '^body')
- list_display = ('title', 'body', 'published')
+ list_display = ('title', 'body', 'published', "published_date",)
field_renderers = {
'title': renderers.title_renderer,
}
save_on_top = True
+ date_hierarchy = "published_date"
class CommentAdmin(djadmin2.ModelAdmin2):
diff --git a/example/blog/models.py b/example/blog/models.py
index 7222041..e3503f0 100644
--- a/example/blog/models.py
+++ b/example/blog/models.py
@@ -11,6 +11,7 @@ class Post(models.Model):
title = models.CharField(max_length=255, verbose_name=_('title'))
body = models.TextField(verbose_name=_('body'))
published = models.BooleanField(default=False, verbose_name=_('published'))
+ published_date = models.DateField(blank=True, null=True)
def __unicode__(self):
return self.title
diff --git a/example/blog/tests/test_views.py b/example/blog/tests/test_views.py
index b9827fd..5eec156 100644
--- a/example/blog/tests/test_views.py
+++ b/example/blog/tests/test_views.py
@@ -1,3 +1,6 @@
+# -*- coding: utf-8 -*-
+from datetime import datetime
+
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse
@@ -59,6 +62,46 @@ class CommentListTest(BaseIntegrationTest):
class PostListTest(BaseIntegrationTest):
+ def _create_posts(self):
+ Post.objects.bulk_create([
+ Post(
+ title="post_1_title",
+ body="body",
+ published_date=datetime(
+ month=7,
+ day=22,
+ year=2013
+ )
+ ),
+ Post(
+ title="post_2_title",
+ body="body",
+ published_date=datetime(
+ month=5,
+ day=20,
+ year=2012,
+ )
+ ),
+ Post(
+ title="post_3_title",
+ body="body",
+ published_date=datetime(
+ month=5,
+ day=30,
+ year=2012,
+ ),
+ ),
+ Post(
+ title="post_4_title",
+ body="body",
+ published_date=datetime(
+ month=6,
+ day=20,
+ year=2012,
+ )
+ )
+ ])
+
def test_view_ok(self):
post = Post.objects.create(title="A Post Title", body="body")
response = self.client.get(reverse("admin2:blog_post_index"))
@@ -134,6 +177,65 @@ class PostListTest(BaseIntegrationTest):
response = self.client.get(reverse('admin2:blog_post_index'))
self.assertContains(response, 'icon-ok-sign')
+ def test_drilldowns(self):
+ self._create_posts()
+
+ response = self.client.get(reverse('admin2:blog_post_index'))
+ self.assertContains(response, '
2012')
+ self.assertContains(response, "
", 4)
+
+ response = self.client.get(
+ "%s?%s" % (
+ reverse('admin2:blog_post_index'),
+ "year=2012",
+ )
+ )
+
+ self.assertContains(
+ response,
+ 'May 2012',
+ )
+ self.assertContains(
+ response,
+ 'All dates',
+ )
+ self.assertContains(response, "
", 3)
+
+ response = self.client.get(
+ "%s?%s" % (
+ reverse('admin2:blog_post_index'),
+ "year=2012&month=5",
+ )
+ )
+
+ self.assertContains(response, "
", 2)
+ self.assertContains(
+ response,
+ 'May 20',
+ )
+ self.assertContains(response, '')
+
+ response = self.client.get(
+ "%s?%s" % (
+ reverse('admin2:blog_post_index'),
+ "year=2012&month=05&day=20",
+ )
+ )
+
+ self.assertContains(response, "", 1)
+ self.assertContains(
+ response,
+ 'May 20',
+ )
+ self.assertContains(
+ response,
+ ''
+ )
+ self.assertContains(
+ response,
+ 'May 2012'
+ )
+
class PostListTestCustomAction(BaseIntegrationTest):