Merge branch 'admin-date-hierarchy' of git://github.com/powersurge360/django-admin2 into powersurge360-admin-date-hierarchy

This commit is contained in:
Daniel Greenfeld 2013-08-01 12:31:38 +02:00
commit 68cc955fce
10 changed files with 256 additions and 11 deletions

View file

@ -4,6 +4,7 @@ from __future__ import division, absolute_import, unicode_literals
import collections
from itertools import chain
from django import forms
from django.forms.util import flatatt
from django.utils.html import format_html
from django.utils.encoding import force_text
@ -16,6 +17,10 @@ import django_filters
LINK_TEMPLATE = '<a href=?{0}={1} {2}>{3}</a>'
class NumericDateFilter(django_filters.DateFilter):
field_class = forms.IntegerField
class ChoicesAsLinksWidget(django_widgets.Select):
"""Select form widget taht renders links for choices
instead of select element with options.
@ -95,6 +100,29 @@ def build_list_filter(request, model_admin, queryset):
)(request.GET, queryset=queryset)
def build_date_filter(request, model_admin, queryset):
filterset_dict = {
"year": NumericDateFilter(
name="published_date",
lookup_type="year",
),
"month": NumericDateFilter(
name="published_date",
lookup_type="month",
),
"day": NumericDateFilter(
name="published_date",
lookup_type="day",
)
}
return type(
b'%sDateFilterSet' % queryset.model.__name__,
(django_filters.FilterSet,),
filterset_dict,
)(request.GET, queryset=queryset)
def get_filter_for_field_name(model, field_name):
"""Returns filter for model field by field name.
"""

View file

@ -8,12 +8,11 @@ from django.conf import settings
# views. This is a security feature.
# See the docstring on djadmin2.types.ModelAdmin2 for more detail.
MODEL_ADMIN_ATTRS = (
'actions_selection_counter', 'list_display', 'list_display_links',
'list_filter', 'admin', 'search_fields', 'field_renderers',
'index_view', 'detail_view', 'create_view', 'update_view', 'delete_view',
'get_default_view_kwargs', 'get_list_actions',
'actions_on_bottom', 'actions_on_top',
'save_on_top', 'save_on_bottom',
'readonly_fields', )
'actions_selection_counter', "date_hierarchy", 'list_display',
'list_display_links', 'list_filter', 'admin', 'search_fields',
'field_renderers', 'index_view', 'detail_view', 'create_view',
'update_view', 'delete_view', 'get_default_view_kwargs',
'get_list_actions', 'actions_on_bottom', 'actions_on_top',
'save_on_top', 'save_on_bottom', 'readonly_fields', )
ADMIN2_THEME_DIRECTORY = getattr(settings, "ADMIN2_THEME_DIRECTORY", "djadmin2theme_default")

View file

@ -7,3 +7,12 @@
.sort_link:hover {
color: black;
}
.previous-link a {
color: gray;
}
.date-drilldown {
padding-bottom: 0;
padding-top: 0;
}

View file

@ -36,13 +36,29 @@
</div>
{% endif %}
<div id="model-list" class="row">
<form id="model-list-form" class="form-inline" method="post">
{% csrf_token %}
<div class="span12">
{% if dates %}
<ul class="date-drilldown nav well nav-pills">
{% if previous_date %}
<li class="previous-link">
<a href="{{ previous_date.link|safe }}">
{{ previous_date.text|safe }}
</a>
</li>
{% endif %}
{% for link, date in dates %}
<li class="{% ifequal active_day date %}active{% endifequal %}">
<a href="{{ link|safe }}">{{ date }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
{% if view.model_admin.actions_on_top %}
{% include 'djadmin2theme_default/includes/list_actions.html' with position='top' %}
{% endif %}

View file

@ -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 = ()

View file

@ -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)

View file

@ -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)

View file

@ -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):

View file

@ -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

View file

@ -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, '<a href="?year=2012">2012</a>')
self.assertContains(response, "<tr>", 4)
response = self.client.get(
"%s?%s" % (
reverse('admin2:blog_post_index'),
"year=2012",
)
)
self.assertContains(
response,
'<a href="?year=2012&month=05">May 2012</a>',
)
self.assertContains(
response,
'All dates',
)
self.assertContains(response, "<tr>", 3)
response = self.client.get(
"%s?%s" % (
reverse('admin2:blog_post_index'),
"year=2012&month=5",
)
)
self.assertContains(response, "<tr>", 2)
self.assertContains(
response,
'<a href="?year=2012&month=05&day=20">May 20</a>',
)
self.assertContains(response, '<a href="?year=2012">')
response = self.client.get(
"%s?%s" % (
reverse('admin2:blog_post_index'),
"year=2012&month=05&day=20",
)
)
self.assertContains(response, "<tr>", 1)
self.assertContains(
response,
'<a href="?year=2012&month=05&day=20">May 20</a>',
)
self.assertContains(
response,
'<li class="active">'
)
self.assertContains(
response,
'May 2012'
)
class PostListTestCustomAction(BaseIntegrationTest):