I mentioned a couple fundamental issues off the bat with my website revamp using Django CMS.
I’m in the process of revamping my hub site, www.JTimothyKing.com. I had created a barebones site: only a single page of content, two columns (using djangocms-column
), and a row of social-media icons (using djangocms-picture
). I only made minimal changes to get it “good enough for now.”
There were still a couple unresolved issues, even before any serious theming, one of which was that the social-media icons are rendering with small, blue underlines in between. Before theming the site, I’d like to deal with this irksome quirk.
But oh! I thought, This also presents a perfect opportunity to apply my standard dev process to a Django project! Let’s go!
What’s the problem again?
I had inserted a row of social media icons as pictures, each linked to one of my social media accounts. The issue is that the picture template HTML has whitespace in inappropriate places, especially inside the <a>
tag content, which the browser faithfully renders as part of the link.
The generated HTML looks something like this:
<a href="http://facebook.jtimothyking.com/" >↵ <img src="facebook_icon.png" alt="Facebook" >↵ </a>↵ <a …
Note the whitespace between tags, which I’ve highlighted in red.
It really needs to be rendered without any whitespace between tags:
<a href="http://facebook.jtimothyking.com/" ><img src="facebook_icon.png" alt="Facebook" ></a><a …
(And it would be nice if the <img>
tag were also rendered with an end tag, like <img src="…" />
. That would satisfy the pedant in me.)
Set up the dev environment
Django CMS inserts pictures using the djangocms-picture
plugin. I’m going to need to tweak the code in that plugin’s template. (There’s only one, called picture.html
.)
To be fair, I don’t need to fix djangocms-picture
. I could create a picture template in my Django application. However, this will provide a deeper look into Django code development and the Django CMS plugin API.
I start by forking djangocms-picture
in GitHub, then cloning the project on my laptop. Following the procedure in README.rst
, I install the packages listed in tests/requirements.txt
into a virtual environment and run python setup.py test
. That generates lots of output, but near the end is:
Ran 1 test in 0.004s OK
And that makes me think that it worked.
More brand-new legacy code
Wait… What? One test?
We need an acronym for that. BNLC: when brand new code is written without proper automated tests.
Yes indeedily doodily. There’s a single test module, test_models.py
, which contains a single test class, PictureTestCase
, and that contains a single test method, test_picture_instance
. This method creates a Picture
in the database, pointing to an external URL, retrieves it by keying off of the same external URL, and then verifies that the retrieved object still points to the same URL.
EXAMPLE_IMAGE = 'https://www.google.com/images/logo.png' class PictureTestCase(TestCase): def setUp(self): Picture.objects.create( external_picture=EXAMPLE_IMAGE, ) def test_picture_instance(self): """Picture instance has been created""" picture = Picture.objects.get(external_picture=EXAMPLE_IMAGE) self.assertEqual(picture.external_picture, EXAMPLE_IMAGE)
I’m not exactly sure what that proves.
The Picture
class contains a lot of trivial configuration. That doesn’t need to be unit-tested, but it would probably be worth an integration test or two. However, the class also contains business logic, in the methods get_short_description()
, get_size()
, get_link()
, and clean()
. All of that is untested.
(The common practice of implementing business logic in ORM model classes is the subject of a whole other rant.)
But more to the point, for me right now: the template is completely untested. Again, one could make the case that the template is configuration, not code—although that’s a harder case to make, because the template does contain {% if %}
tags, lots of them. But even if it were pure configuration, it would justify an integration test.
The Django CMS code-contribution guidelines actually say:
Code must be tested. Your pull request should include unit-tests (that cover the piece of code you’re submitting, obviously)…
Running and writing tests is really important: a pull request that lowers our testing coverage will only be accepted with a very good reason; bug-fixing patches must demonstrate the bug with a test to avoid regressions and to check that the fix works.
That’s a really good policy! Yay for automated tests!
I don’t get it. Django has a rich testing framework. How much trouble could it be to create a Picture
, render it, and verify that the generated HTML says what we expect it to?
Let’s find out.
Write a test; watch it fail
Getting an initial test up and running was much more effort than one would hope. This is common for untested code. Writing tests often uncovers bugs you didn’t know were there. In this case, the more significant issue was incomplete or incorrect developer documentation (with both Django and Django CMS)—Thorough docstrings save lives—and how convoluted some of the underlying code is. That’s a completely different post, but for now, let me just say that the new relationship energy has worn off.
The only coding issue turned out to be in the test fixture’s setup.py
, in which I needed to add a key to the HELPER_SETTINGS
dictionary:
'THUMBNAIL_PROCESSORS': [ 'filer.thumbnail_processors.scale_and_crop_with_subject_location', ],
I’ll paste below what I ended up with for a test. You can see all the changes I’m making on GitHub.
I’ll just leave this code with you for now. In the next post, I’ll expound on it and pick through a few issues that I have with the underlying framework. Until then…
Still typing…
And may all your bars turn from red to green. (Oops. I guess we don’t do that in the Python world.)
from django.test import TestCase from django.test.client import RequestFactory from cms.api import add_plugin from cms.models import Placeholder from cms.plugin_rendering import ContentRenderer from djangocms_picture.cms_plugins import PicturePlugin class PicturePluginTestCase(TestCase): def setUp(self): self.renderer = ContentRenderer(request=RequestFactory()) def _create_instance(self, **data): placeholder = Placeholder.objects.create(slot='test') instance = add_plugin( placeholder, PicturePlugin, 'en', **data ) return instance def test_rendered_HTML(self): """ Creates plugin instances using a series of setups, and verifies that each renders correctly. """ link_url = 'https://www.google.com/' image_url = 'https://www.google.com/images/logo.png' cases = [ { 'name': 'external_picture', 'data': { 'external_picture': image_url, }, 'expected_html': '<img src="%s">' % image_url, }, { 'name': 'external_picture with link_url', 'data': { 'link_url': link_url, 'external_picture': image_url, }, 'expected_html': '<a href="%s"><img src="%s"></a>' % (link_url, image_url), }, ] for case in cases: instance = self._create_instance(**case['data']) html = self.renderer.render_plugin(instance, {}) self.assertHTMLEqual(html, case['expected_html'], case['name'])