Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure entry slugs fit in a slug field. Fixes #35 #38

Merged
merged 8 commits into from
Oct 18, 2015
33 changes: 33 additions & 0 deletions andablog/migrations/0004_shorten_entry_title.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

import time
from django.db import migrations, models


# The new, maximum length of titles.
TITLE_LENGTH = 255


def truncate_entry_titles(apps, schema_editor):
"""This function will truncate the values of Entry.title so they are
255 characters or less.
"""
Entry = apps.get_model("andablog", "Entry")

for entry in Entry.objects.all():
# Truncate to 255 characters (or less) but keep whole words intact.
while len(entry.title) > TITLE_LENGTH:
entry.title = ' '.join(entry.title.split()[:-1])
entry.save()


class Migration(migrations.Migration):

dependencies = [
('andablog', '0003_auto_20150826_2353'),
]

operations = [
migrations.RunPython(truncate_entry_titles)
]
24 changes: 24 additions & 0 deletions andablog/migrations/0005_auto_20151017_1747.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('andablog', '0004_shorten_entry_title'),
]

operations = [
migrations.AlterField(
model_name='entry',
name='slug',
field=models.SlugField(unique=True, editable=False, max_length=255),
),
migrations.AlterField(
model_name='entry',
name='title',
field=models.CharField(max_length=255),
),
]
32 changes: 27 additions & 5 deletions andablog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class Entry(TimeStampedModel):
Represents a blog Entry.
Uses TimeStampModel to provide created and modified fields
"""
title = models.CharField(max_length=500)
slug = models.SlugField(unique=True, editable=False)
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True, editable=False)
content = MarkupField()
is_published = models.BooleanField(default=False)
published_timestamp = models.DateTimeField(blank=True, null=True, editable=False)
Expand All @@ -35,12 +35,34 @@ class Meta:
def get_absolute_url(self):
return reverse('andablog:entrydetail', args=[self.slug])

def save(self, *args, **kwargs):
def _insert_timestamp(self, slug, max_length=255):
"""Appends a timestamp integer to the given slug, yet ensuring the
result is less than the specified max_length.
"""
timestamp = str(int(time.time()))
ts_len = len(timestamp) + 1
while len(slug) + ts_len > max_length:
slug = '-'.join(slug.split('-')[:-1])
slug = '-'.join([slug, timestamp])
return slug

def _slugify_title(self):
"""Slugify the Entry title, but ensure it's less than the maximum
number of characters. This method also ensures that a slug is unique by
appending a timestamp to any duplicate slugs.
"""
# Restrict slugs to their maximum number of chars, but don't split mid-word
self.slug = slugify(self.title)
while len(self.slug) > 255:
self.slug = '-'.join(self.slug.split('-')[:-1])

# Is the same slug as another entry?
if Entry.objects.filter(slug=self.slug).exclude(id=self.id).exists():
# Append time to differentiate
self.slug = "{}-{}".format(self.slug, int(time.time()))
# Append time to differentiate.
self.slug = self._insert_timestamp(self.slug)

def save(self, *args, **kwargs):
self._slugify_title()

# Time to publish?
if not self.published_timestamp and self.is_published:
Expand Down
33 changes: 31 additions & 2 deletions andablog/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
# -*- coding: utf-8 -*-

from django.test import TestCase
from django.utils.text import slugify
from django.utils import timezone
from django.utils.safestring import SafeText

from andablog import models

Expand All @@ -22,7 +22,36 @@ def setUp(self):

def test_slug_creation(self):
"""The slug field should automatically get set from the title during post creation"""
self.assertEqual(self.entry.slug, slugify(self.entry.title))
self.assertEqual(self.entry.slug, 'first-post')

def test__insert_timestamp(self):
"""Ensure the returned value contains a timestamp without going over the max length"""
# When given the `first-post` slug.
result = self.entry._insert_timestamp(self.entry.slug)
self.assertEqual(len(result.split('-')), 3)

# When given a string > 255 characters.
slug = '-'.join(['a'] * 250)
result = self.entry._insert_timestamp(slug)
self.assertLess(len(result), 255)

def test_long_slugs_should_not_get_split_midword(self):
"""The slug should not get split mid-word."""
self.entry.title = SafeText("Please tell me where everyone is getting their assumptions about me?" * 100)
self.entry.save()
# The ending should not be a split word.
self.assertEqual(self.entry.slug[-25:], 'everyone-is-getting-their')

def test_duplicate_long_slugs_should_get_a_timestamp(self):
"""If a long title has a shortened slug that is a duplicate, it should have a timestamp"""
self.entry.title = SafeText("Here's a really long title, for testing slug character restrictions")
self.entry.save()

duplicate_entry = models.Entry.objects.create(title=self.entry.title, content=self.entry.content)

self.assertNotEqual(self.entry.slug, duplicate_entry.slug)
# This is not ideal, but a portion of the original slug is in the duplicate
self.assertIn(self.entry.slug[:25], duplicate_entry.slug)

def test_new_duplicate(self):
"""The slug value should automatically be made unique if the slug is taken"""
Expand Down