mirror of
https://github.com/jazzband/django-admin2.git
synced 2026-03-26 11:00:27 +00:00
Merge pull request #299 from dbrgn/iss287
Implemented custom value renderers
This commit is contained in:
commit
89e6cd8db0
15 changed files with 414 additions and 69 deletions
83
djadmin2/renderers.py
Normal file
83
djadmin2/renderers.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
There are currently a few renderers that come directly with django-admin2. They
|
||||
are used by default for some field types.
|
||||
"""
|
||||
from __future__ import division, absolute_import, unicode_literals
|
||||
|
||||
import os.path
|
||||
from decimal import Decimal
|
||||
from datetime import date, time, datetime
|
||||
|
||||
from django.db import models
|
||||
from django.utils import formats, timezone
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from djadmin2 import settings
|
||||
|
||||
|
||||
def boolean_renderer(value, field):
|
||||
"""
|
||||
Render a boolean value as icon.
|
||||
|
||||
This uses the template ``renderers/boolean.html``.
|
||||
|
||||
:param value: The value to process.
|
||||
:type value: boolean
|
||||
:param field: The model field instance
|
||||
:type field: django.db.models.fields.Field
|
||||
:rtype: unicode
|
||||
|
||||
"""
|
||||
# TODO caching of template
|
||||
tpl = os.path.join(settings.ADMIN2_THEME_DIRECTORY, 'renderers/boolean.html')
|
||||
return render_to_string(tpl, {'value': value})
|
||||
|
||||
|
||||
def datetime_renderer(value, field):
|
||||
"""
|
||||
Localize and format the specified date.
|
||||
|
||||
:param value: The value to process.
|
||||
:type value: datetime.date or datetime.time or datetime.datetime
|
||||
:param field: The model field instance
|
||||
:type field: django.db.models.fields.Field
|
||||
:rtype: unicode
|
||||
|
||||
"""
|
||||
if isinstance(value, datetime):
|
||||
return formats.localize(timezone.template_localtime(value))
|
||||
elif isinstance(value, (date, time)):
|
||||
return formats.localize(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
def title_renderer(value, field):
|
||||
"""
|
||||
Render a string in title case (capitalize every word).
|
||||
|
||||
:param value: The value to process.
|
||||
:type value: str or unicode
|
||||
:param field: The model field instance
|
||||
:type field: django.db.models.fields.Field
|
||||
:rtype: unicode
|
||||
|
||||
"""
|
||||
return unicode(value).title()
|
||||
|
||||
|
||||
def number_renderer(value, field):
|
||||
"""
|
||||
Format a number.
|
||||
|
||||
:param value: The value to process.
|
||||
:type value: float or long
|
||||
:param field: The model field instance
|
||||
:type field: django.db.models.fields.Field
|
||||
:rtype: unicode
|
||||
|
||||
"""
|
||||
if isinstance(field, models.DecimalField):
|
||||
return formats.number_format(value, field.decimal_places)
|
||||
return formats.number_format(value)
|
||||
|
|
@ -6,6 +6,7 @@ from django.conf import settings
|
|||
MODEL_ADMIN_ATTRS = (
|
||||
'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',
|
||||
|
|
|
|||
|
|
@ -84,12 +84,12 @@
|
|||
{% for attr in view.model_admin.list_display %}
|
||||
<td>
|
||||
{% if permissions.has_change_permission %}
|
||||
<a href="{% url view|admin2_urlname:'update' pk=obj.pk %}">{% get_attr obj attr %}</a>
|
||||
<a href="{% url view|admin2_urlname:'update' pk=obj.pk %}">{% render obj attr %}</a>
|
||||
{% else %}
|
||||
{% if permissions.has_view_permission %}
|
||||
<a href="{% url view|admin2_urlname:'detail' pk=obj.pk %}">{% get_attr obj attr %}</a>
|
||||
<a href="{% url view|admin2_urlname:'detail' pk=obj.pk %}">{% render obj attr %}</a>
|
||||
{% else %}
|
||||
{% get_attr obj attr %}
|
||||
{% render obj attr %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
{% if value %}
|
||||
<i class="icon-ok-sign" title="True"></i>
|
||||
{% else %}
|
||||
<i class="icon-minus-sign" title="False"></i>
|
||||
{% endif %}
|
||||
|
|
@ -1,9 +1,11 @@
|
|||
from numbers import Number
|
||||
from datetime import date, time, datetime
|
||||
from django import template
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
|
||||
register = template.Library()
|
||||
|
||||
from .. import utils
|
||||
from .. import utils, renderers
|
||||
|
||||
|
||||
@register.filter
|
||||
|
|
@ -97,12 +99,35 @@ def for_object(permissions, obj):
|
|||
return permissions.bind_object(obj)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def get_attr(record, attribute_name):
|
||||
""" Allows dynamic fetching of model attributes in templates """
|
||||
if attribute_name == "__str__":
|
||||
return record.__unicode__()
|
||||
attribute = getattr(record, attribute_name)
|
||||
if callable(attribute):
|
||||
return attribute()
|
||||
return attribute
|
||||
@register.simple_tag(takes_context=True)
|
||||
def render(context, model_instance, attribute_name):
|
||||
"""
|
||||
This filter applies all renderers specified in admin2.py to the field.
|
||||
"""
|
||||
value = utils.get_attr(model_instance, attribute_name)
|
||||
|
||||
# Get renderer
|
||||
admin = context['view'].model_admin
|
||||
renderer = admin.field_renderers.get(attribute_name, False)
|
||||
if renderer is None:
|
||||
# Renderer has explicitly been overridden
|
||||
return value
|
||||
if not renderer:
|
||||
# Try to automatically pick best renderer
|
||||
if isinstance(value, bool):
|
||||
renderer = renderers.boolean_renderer
|
||||
elif isinstance(value, (date, time, datetime)):
|
||||
renderer = renderers.datetime_renderer
|
||||
elif isinstance(value, Number):
|
||||
renderer = renderers.number_renderer
|
||||
else:
|
||||
return value
|
||||
|
||||
# Apply renderer and return value
|
||||
try:
|
||||
field = model_instance._meta.get_field_by_name(attribute_name)[0]
|
||||
except FieldDoesNotExist:
|
||||
# There is no field with the specified name.
|
||||
# It must be a method instead.
|
||||
field = None
|
||||
return renderer(value, field)
|
||||
|
|
|
|||
|
|
@ -5,3 +5,4 @@ from test_views import *
|
|||
from test_core import *
|
||||
from test_actions import *
|
||||
from test_auth_admin import *
|
||||
from test_renderers import *
|
||||
|
|
|
|||
|
|
@ -90,35 +90,3 @@ class TagsTests(TestCase):
|
|||
admin2_tags.formset_visible_fieldlist(formset),
|
||||
[u'Visible 1', u'Visible 2']
|
||||
)
|
||||
|
||||
def test_get_attr_callable(self):
|
||||
class Klass(object):
|
||||
def hello(self):
|
||||
return "hello"
|
||||
|
||||
self.assertEquals(
|
||||
admin2_tags.get_attr(Klass(), "hello"),
|
||||
"hello"
|
||||
)
|
||||
|
||||
def test_get_attr_str(self):
|
||||
class Klass(object):
|
||||
def __str__(self):
|
||||
return "str"
|
||||
|
||||
def __unicode__(self):
|
||||
return "unicode"
|
||||
|
||||
self.assertEquals(
|
||||
admin2_tags.get_attr(Klass(), "__str__"),
|
||||
"unicode"
|
||||
)
|
||||
|
||||
def test_get_attr(self):
|
||||
class Klass(object):
|
||||
attr = "value"
|
||||
|
||||
self.assertEquals(
|
||||
admin2_tags.get_attr(Klass(), "attr"),
|
||||
"value"
|
||||
)
|
||||
|
|
|
|||
118
djadmin2/tests/test_renderers.py
Normal file
118
djadmin2/tests/test_renderers.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import division, absolute_import, unicode_literals
|
||||
|
||||
import datetime as dt
|
||||
from decimal import Decimal
|
||||
|
||||
from django.test import TestCase
|
||||
from django.db import models
|
||||
from django.utils.translation import activate
|
||||
|
||||
from .. import renderers
|
||||
|
||||
|
||||
class RendererTestModel(models.Model):
|
||||
decimal = models.DecimalField(decimal_places=5)
|
||||
|
||||
|
||||
class BooleanRendererTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.renderer = renderers.boolean_renderer
|
||||
|
||||
def test_boolean(self):
|
||||
out1 = self.renderer(True, None)
|
||||
self.assertIn('icon-ok-sign', out1)
|
||||
out2 = self.renderer(False, None)
|
||||
self.assertIn('icon-minus-sign', out2)
|
||||
|
||||
def test_string(self):
|
||||
out1 = self.renderer('yeah', None)
|
||||
self.assertIn('icon-ok-sign', out1)
|
||||
out2 = self.renderer('', None)
|
||||
self.assertIn('icon-minus-sign', out2)
|
||||
|
||||
|
||||
class DatetimeRendererTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.renderer = renderers.datetime_renderer
|
||||
|
||||
def tearDown(self):
|
||||
activate('en_US')
|
||||
|
||||
def test_date_german(self):
|
||||
activate('de')
|
||||
out = self.renderer(dt.date(2013, 7, 6), None)
|
||||
self.assertEqual('6. Juli 2013', out)
|
||||
|
||||
def test_date_spanish(self):
|
||||
activate('es')
|
||||
out = self.renderer(dt.date(2013, 7, 6), None)
|
||||
self.assertEqual('6 de Julio de 2013', out)
|
||||
|
||||
def test_date_default(self):
|
||||
out = self.renderer(dt.date(2013, 7, 6), None)
|
||||
self.assertEqual('July 6, 2013', out)
|
||||
|
||||
def test_time_german(self):
|
||||
activate('de')
|
||||
out = self.renderer(dt.time(13, 37, 01), None)
|
||||
self.assertEqual('13:37:01', out)
|
||||
|
||||
def test_time_chinese(self):
|
||||
activate('zh')
|
||||
out = self.renderer(dt.time(13, 37, 01), None)
|
||||
self.assertEqual('1:37 p.m.', out)
|
||||
|
||||
def test_datetime(self):
|
||||
out = self.renderer(dt.datetime(2013, 7, 6, 13, 37, 01), None)
|
||||
self.assertEqual('July 6, 2013, 1:37 p.m.', out)
|
||||
|
||||
# TODO test timezone localization
|
||||
|
||||
|
||||
class TitleRendererTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.renderer = renderers.title_renderer
|
||||
|
||||
def testLowercase(self):
|
||||
out = self.renderer('oh hello there!', None)
|
||||
self.assertEqual('Oh Hello There!', out)
|
||||
|
||||
def testTitlecase(self):
|
||||
out = self.renderer('Oh Hello There!', None)
|
||||
self.assertEqual('Oh Hello There!', out)
|
||||
|
||||
def testUppercase(self):
|
||||
out = self.renderer('OH HELLO THERE!', None)
|
||||
self.assertEqual('Oh Hello There!', out)
|
||||
|
||||
|
||||
class NumberRendererTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.renderer = renderers.number_renderer
|
||||
|
||||
def testInteger(self):
|
||||
out = self.renderer(42, None)
|
||||
self.assertEqual('42', out)
|
||||
|
||||
def testFloat(self):
|
||||
out = self.renderer(42.5, None)
|
||||
self.assertEqual('42.5', out)
|
||||
|
||||
def testEndlessFloat(self):
|
||||
out = self.renderer(1.0/3, None)
|
||||
self.assertEqual('0.333333333333', out)
|
||||
|
||||
def testPlainDecimal(self):
|
||||
number = '0.123456789123456789123456789'
|
||||
out = self.renderer(Decimal(number), None)
|
||||
self.assertEqual(number, out)
|
||||
|
||||
def testFieldDecimal(self):
|
||||
field = RendererTestModel._meta.get_field_by_name('decimal')[0]
|
||||
out = self.renderer(Decimal('0.123456789'), field)
|
||||
self.assertEqual('0.12345', out)
|
||||
|
|
@ -135,3 +135,35 @@ class UtilsTest(TestCase):
|
|||
self.instance._meta.app_label,
|
||||
utils.model_app_label(self.instance)
|
||||
)
|
||||
|
||||
def test_get_attr_callable(self):
|
||||
class Klass(object):
|
||||
def hello(self):
|
||||
return "hello"
|
||||
|
||||
self.assertEquals(
|
||||
utils.get_attr(Klass(), "hello"),
|
||||
"hello"
|
||||
)
|
||||
|
||||
def test_get_attr_str(self):
|
||||
class Klass(object):
|
||||
def __str__(self):
|
||||
return "str"
|
||||
|
||||
def __unicode__(self):
|
||||
return "unicode"
|
||||
|
||||
self.assertEquals(
|
||||
utils.get_attr(Klass(), "__str__"),
|
||||
"unicode"
|
||||
)
|
||||
|
||||
def test_get_attr(self):
|
||||
class Klass(object):
|
||||
attr = "value"
|
||||
|
||||
self.assertEquals(
|
||||
utils.get_attr(Klass(), "attr"),
|
||||
"value"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -63,15 +63,18 @@ class ModelAdmin2(object):
|
|||
# TODO: Confirm that this is what the Django admin uses
|
||||
list_fields = []
|
||||
|
||||
#This shows up on the DocumentListView of the Posts
|
||||
# This shows up on the DocumentListView of the Posts
|
||||
list_actions = [actions.DeleteSelectedAction]
|
||||
|
||||
# This shows up in the DocumentDetailView of the Posts.
|
||||
document_actions = []
|
||||
|
||||
# shows up on a particular field
|
||||
# Shows up on a particular field
|
||||
field_actions = {}
|
||||
|
||||
# Defines custom field renderers
|
||||
field_renderers = {}
|
||||
|
||||
fields = None
|
||||
exclude = None
|
||||
fieldsets = None
|
||||
|
|
|
|||
|
|
@ -83,6 +83,19 @@ def model_app_label(obj):
|
|||
return model_options(obj).app_label
|
||||
|
||||
|
||||
def get_attr(obj, attr):
|
||||
"""
|
||||
Get the right value for the attribute. Handle special cases like callables
|
||||
and the __str__ attribute.
|
||||
"""
|
||||
if attr == '__str__':
|
||||
value = unicode(obj)
|
||||
else:
|
||||
attribute = getattr(obj, attr)
|
||||
value = attribute() if callable(attribute) else attribute
|
||||
return value
|
||||
|
||||
|
||||
class NestedObjects(Collector):
|
||||
"""
|
||||
This is adopted from the Django core. django-admin2 mandates that code
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ Reference
|
|||
ref/permissions
|
||||
ref/views
|
||||
ref/built-in-views
|
||||
ref/renderers
|
||||
ref/meta
|
||||
|
||||
Indices and tables
|
||||
|
|
|
|||
62
docs/ref/renderers.rst
Normal file
62
docs/ref/renderers.rst
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
================
|
||||
Custom Renderers
|
||||
================
|
||||
|
||||
It is possible to create custom renderers for specific fields. Currently they
|
||||
are only used in the object list view, for example to render boolean values
|
||||
using icons. Another example would be to customize the rendering of dates.
|
||||
|
||||
|
||||
Renderers
|
||||
---------
|
||||
|
||||
A renderer is a function that accepts a value and the field and returns a HTML
|
||||
representation of it. For example, the very simple builtin datetime renderer
|
||||
works like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def title_renderer(value, field):
|
||||
"""Render a string in title case (capitalize every word)."""
|
||||
return unicode(value).title()
|
||||
|
||||
In this case the ``field`` argument is not used. Sometimes it useful though:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def number_renderer(value, field):
|
||||
"""Format a number."""
|
||||
if isinstance(field, models.DecimalField):
|
||||
return formats.number_format(value, field.decimal_places)
|
||||
return formats.number_format(value)
|
||||
|
||||
You can create your renderers anywhere in your code, but it is recommended to
|
||||
put them in a file called ``renderers.py`` in your project.
|
||||
|
||||
|
||||
Using Renderers
|
||||
---------------
|
||||
|
||||
The renderers can be specified in the Admin2 class using the
|
||||
``field_renderers`` attribute. The attribute contains a dictionary that maps a
|
||||
field name to a renderer function.
|
||||
|
||||
By default, some renderers are automatically applied, for example the boolean
|
||||
renderer when processing boolean values. If you want to suppress that renderer,
|
||||
you can assign ``None`` to the field in the ``field_renderers`` dictionary.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class PostAdmin(djadmin2.ModelAdmin2):
|
||||
list_display = ('title', 'body', 'published')
|
||||
field_renderers = {
|
||||
'title': renderers.title_renderer,
|
||||
'published': None,
|
||||
}
|
||||
|
||||
|
||||
Builtin Renderers
|
||||
-----------------
|
||||
|
||||
.. automodule:: djadmin2.renderers
|
||||
:members:
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Import your custom models
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
import djadmin2
|
||||
from djadmin2 import renderers
|
||||
from djadmin2.actions import DeleteSelectedAction
|
||||
|
||||
# Import your custom models
|
||||
from .actions import CustomPublishAction
|
||||
from .models import Post, Comment
|
||||
from .models import Post, Comment, Event, EventGuide
|
||||
|
||||
|
||||
class CommentInline(djadmin2.Admin2Inline):
|
||||
|
|
@ -25,7 +29,10 @@ class PostAdmin(djadmin2.ModelAdmin2):
|
|||
list_actions = [DeleteSelectedAction, CustomPublishAction, unpublish_items]
|
||||
inlines = [CommentInline]
|
||||
search_fields = ('title', '^body')
|
||||
list_filter = ['published', 'title']
|
||||
list_display = ('title', 'body', 'published')
|
||||
field_renderers = {
|
||||
'title': renderers.title_renderer,
|
||||
}
|
||||
|
||||
|
||||
class CommentAdmin(djadmin2.ModelAdmin2):
|
||||
|
|
@ -33,6 +40,12 @@ class CommentAdmin(djadmin2.ModelAdmin2):
|
|||
list_filter = ['post', ]
|
||||
|
||||
|
||||
class EventAdmin(djadmin2.ModelAdmin2):
|
||||
list_display = ('date',)
|
||||
|
||||
|
||||
# Register each model with the admin
|
||||
djadmin2.default.register(Post, PostAdmin)
|
||||
djadmin2.default.register(Comment, CommentAdmin)
|
||||
djadmin2.default.register(Event, EventAdmin)
|
||||
djadmin2.default.register(EventGuide)
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class CommentListTest(BaseIntegrationTest):
|
|||
|
||||
class PostListTest(BaseIntegrationTest):
|
||||
def test_view_ok(self):
|
||||
post = Post.objects.create(title="a_post_title", body="body")
|
||||
post = Post.objects.create(title="A Post Title", body="body")
|
||||
response = self.client.get(reverse("admin2:blog_post_index"))
|
||||
self.assertContains(response, post.title)
|
||||
|
||||
|
|
@ -67,33 +67,53 @@ class PostListTest(BaseIntegrationTest):
|
|||
self.assertInHTML('<a tabindex="-1" href="#" data-name="action" data-value="DeleteSelectedAction">Delete selected items</a>', response.content)
|
||||
|
||||
def test_delete_selected_post(self):
|
||||
post = Post.objects.create(title="a_post_title", body="body")
|
||||
post = Post.objects.create(title="A Post Title", body="body")
|
||||
params = {'action': 'DeleteSelectedAction', 'selected_model_pk': str(post.pk)}
|
||||
response = self.client.post(reverse("admin2:blog_post_index"), params)
|
||||
# caution : uses pluralization
|
||||
self.assertInHTML('<p>Are you sure you want to delete the selected post? The following item will be deleted:</p>', response.content)
|
||||
|
||||
def test_delete_selected_post_confirmation(self):
|
||||
post = Post.objects.create(title="a_post_title", body="body")
|
||||
post = Post.objects.create(title="A Post Title", body="body")
|
||||
params = {'action': 'DeleteSelectedAction', 'selected_model_pk': str(post.pk), 'confirmed': 'yes'}
|
||||
response = self.client.post(reverse("admin2:blog_post_index"), params)
|
||||
self.assertRedirects(response, reverse("admin2:blog_post_index"))
|
||||
|
||||
def test_delete_selected_post_none_selected(self):
|
||||
Post.objects.create(title="a_post_title", body="body")
|
||||
Post.objects.create(title="A Post Title", body="body")
|
||||
params = {'action': 'DeleteSelectedAction'}
|
||||
response = self.client.post(reverse("admin2:blog_post_index"), params, follow=True)
|
||||
self.assertContains(response, "Items must be selected in order to perform actions on them. No items have been changed.")
|
||||
|
||||
def test_search_posts(self):
|
||||
Post.objects.create(title="a_post_title", body="body")
|
||||
Post.objects.create(title="another_post_title", body="body")
|
||||
Post.objects.create(title="post_with_keyword_in_body", body="another post body")
|
||||
Post.objects.create(title="A Post Title", body="body")
|
||||
Post.objects.create(title="Another Post Title", body="body")
|
||||
Post.objects.create(title="Post With Keyword In Body", body="another post body")
|
||||
params = {"q":"another"}
|
||||
response = self.client.get(reverse("admin2:blog_post_index"), params)
|
||||
self.assertContains(response, "another_post_title")
|
||||
self.assertContains(response, "post_with_keyword_in_body")
|
||||
self.assertNotContains(response, "a_post_title")
|
||||
self.assertContains(response, "Another Post Title")
|
||||
self.assertContains(response, "Post With Keyword In Body")
|
||||
self.assertNotContains(response, "A Post Title")
|
||||
|
||||
def test_renderer_title(self):
|
||||
Post.objects.create(title='a lowercase title', body='body', published=False)
|
||||
response = self.client.get(reverse('admin2:blog_post_index'))
|
||||
self.assertContains(response, 'A Lowercase Title')
|
||||
|
||||
def test_renderer_body(self):
|
||||
Post.objects.create(title='title', body='a lowercase body', published=False)
|
||||
response = self.client.get(reverse('admin2:blog_post_index'))
|
||||
self.assertContains(response, 'a lowercase body')
|
||||
|
||||
def test_renderer_unpublished(self):
|
||||
Post.objects.create(title='title', body='body', published=False)
|
||||
response = self.client.get(reverse('admin2:blog_post_index'))
|
||||
self.assertContains(response, 'icon-minus-sign')
|
||||
|
||||
def test_renderer_published(self):
|
||||
Post.objects.create(title='title', body='body', published=True)
|
||||
response = self.client.get(reverse('admin2:blog_post_index'))
|
||||
self.assertContains(response, 'icon-ok-sign')
|
||||
|
||||
|
||||
class PostListTestCustomAction(BaseIntegrationTest):
|
||||
|
|
@ -103,7 +123,7 @@ class PostListTestCustomAction(BaseIntegrationTest):
|
|||
self.assertInHTML('<a tabindex="-1" href="#" data-name="action" data-value="CustomPublishAction">Publish selected items</a>', response.content)
|
||||
|
||||
def test_publish_selected_items(self):
|
||||
post = Post.objects.create(title="a_post_title",
|
||||
post = Post.objects.create(title="A Post Title",
|
||||
body="body",
|
||||
published=False)
|
||||
self.assertEqual(Post.objects.filter(published=True).count(), 0)
|
||||
|
|
@ -121,7 +141,7 @@ class PostListTestCustomAction(BaseIntegrationTest):
|
|||
self.assertInHTML('<a tabindex="-1" href="#" data-name="action" data-value="unpublish_items">Unpublish selected items</a>', response.content)
|
||||
|
||||
def test_unpublish_selected_items(self):
|
||||
post = Post.objects.create(title="a_post_title",
|
||||
post = Post.objects.create(title="A Post Title",
|
||||
body="body",
|
||||
published=True)
|
||||
self.assertEqual(Post.objects.filter(published=True).count(), 1)
|
||||
|
|
@ -136,7 +156,7 @@ class PostListTestCustomAction(BaseIntegrationTest):
|
|||
|
||||
class PostDetailViewTest(BaseIntegrationTest):
|
||||
def test_view_ok(self):
|
||||
post = Post.objects.create(title="a_post_title", body="body")
|
||||
post = Post.objects.create(title="A Post Title", body="body")
|
||||
response = self.client.get(reverse("admin2:blog_post_detail",
|
||||
args=(post.pk, )))
|
||||
self.assertContains(response, post.title)
|
||||
|
|
@ -153,15 +173,15 @@ class PostCreateViewTest(BaseIntegrationTest):
|
|||
"comment_set-INITIAL_FORMS": u'0',
|
||||
"comment_set-MAX_NUM_FORMS": u'',
|
||||
"comment_set-0-body": u'Comment Body',
|
||||
"title": "a_post_title",
|
||||
"title": "A Post Title",
|
||||
"body": "a_post_body",
|
||||
}
|
||||
|
||||
response = self.client.post(reverse("admin2:blog_post_create"),
|
||||
post_data,
|
||||
follow=True)
|
||||
self.assertTrue(Post.objects.filter(title="a_post_title").exists())
|
||||
Post.objects.get(title="a_post_title")
|
||||
self.assertTrue(Post.objects.filter(title="A Post Title").exists())
|
||||
Post.objects.get(title="A Post Title")
|
||||
Comment.objects.get(body="Comment Body")
|
||||
self.assertRedirects(response, reverse("admin2:blog_post_index"))
|
||||
|
||||
|
|
@ -174,14 +194,14 @@ class PostCreateViewTest(BaseIntegrationTest):
|
|||
"comment_set-TOTAL_FORMS": u'2',
|
||||
"comment_set-INITIAL_FORMS": u'0',
|
||||
"comment_set-MAX_NUM_FORMS": u'',
|
||||
"title": "a_post_title",
|
||||
"title": "A Post Title",
|
||||
"body": "a_post_body",
|
||||
"_addanother": ""
|
||||
}
|
||||
self.client.login(username='admin', password='password')
|
||||
response = self.client.post(reverse("admin2:blog_post_create"),
|
||||
post_data)
|
||||
Post.objects.get(title='a_post_title')
|
||||
Post.objects.get(title='A Post Title')
|
||||
self.assertRedirects(response, reverse("admin2:blog_post_create"))
|
||||
|
||||
def test_save_and_continue_editing_redirects_to_update(self):
|
||||
|
|
@ -206,13 +226,13 @@ class PostCreateViewTest(BaseIntegrationTest):
|
|||
|
||||
class PostDeleteViewTest(BaseIntegrationTest):
|
||||
def test_view_ok(self):
|
||||
post = Post.objects.create(title="a_post_title", body="body")
|
||||
post = Post.objects.create(title="A Post Title", body="body")
|
||||
response = self.client.get(reverse("admin2:blog_post_delete",
|
||||
args=(post.pk, )))
|
||||
self.assertContains(response, post.title)
|
||||
|
||||
def test_delete_post(self):
|
||||
post = Post.objects.create(title="a_post_title", body="body")
|
||||
post = Post.objects.create(title="A Post Title", body="body")
|
||||
response = self.client.post(reverse("admin2:blog_post_delete",
|
||||
args=(post.pk, )))
|
||||
self.assertRedirects(response, reverse("admin2:blog_post_index"))
|
||||
|
|
|
|||
Loading…
Reference in a new issue