Commits

Josh VanderLinden committed 1d0013d

Added pagination utilities and finished cleaning things up to a point where the app should be usable

Comments (0)

Files changed (12)

articles/admin.py

             'classes': ('collapse',)
         }),
         ('Scheduling', {'fields': ('publish_date', 'expiration_date')}),
+        ('AddThis Button Options', {
+            'fields': ('use_addthis_button', 'addthis_use_author', 'addthis_username'),
+            'classes': ('collapse',)
+        }),
         ('Advanced', {
-            'fields': ('slug', 'is_active', 'is_commentable',),
+            'fields': ('slug', 'is_active', 'is_commentable', 'display_comments', 'login_required'),
             'classes': ('collapse',)
         }),
     )

articles/feeds.py

 from django.contrib.syndication.feeds import Feed
 from django.core.urlresolvers import reverse
 from django.contrib.sites.models import Site
-from .models import Article, Category
+from articles.models import Article, Category
 
 SITE = Site.objects.get_current()
 
 class LatestEntries(Feed):
-    link = "/blog/"
-    description = "Updates to my blog"
-
     def title(self):
         return "%s Articles" % SITE.name
 
+    def link(self):
+        return reverse('articles_archive')
+
     def items(self):
         return Article.objects.active().order_by('-publish_date')[:5]
 

articles/forms.py

-from django import forms
-from django.forms.fields import email_re
-from captcha import CaptchaField
-
-class SendArticleForm(forms.Form):
-    name = forms.CharField(label='Your Name')
-    email = forms.EmailField(label='Your E-mail Address')
-    recipients = forms.CharField(widget=forms.Textarea(attrs={'rows': 2}),
-                                 help_text='Please enter the e-mail address for each person you wish to receive this article.  Separate each address with a comma.')
-    message = forms.CharField(label='Personalized Message', widget=forms.Textarea,
-                              required=False, help_text="If you'd like to add a little personal touch to the message sent to the recipient(s), enter it here.")
-    security_code = CaptchaField()
-
-    def clean_recipients(self):
-        value = self.cleaned_data['recipients']
-        receivers = value.split(',')
-
-        for r in receivers:
-            r = r.strip()
-            if not email_re.search(r):
-                raise forms.ValidationError('Please verify that each e-mail address is valid.  It appears that "%s" is invalid.' % r)
-
-        return value

articles/models.py

 from django.core.urlresolvers import reverse
 from django.core.cache import cache
 from django.conf import settings
+from django.template.defaultfilters import striptags
 from django.utils.translation import ugettext_lazy as _
 from datetime import datetime
 from base64 import encodestring
         ('t', _('Textile'))
     ))
 MARKUP_DEFAULT = getattr(settings, 'ARTICLE_MARKUP_DEFAULT', 'h')
+USE_ADDTHIS_BUTTON = getattr(settings, 'USE_ADDTHIS_BUTTON', True)
+ADDTHIS_USE_AUTHOR = getattr(settings, 'ADDTHIS_USE_AUTHOR', True)
+DEFAULT_ADDTHIS_USER = getattr(settings, 'DEFAULT_ADDTHIS_USER', None)
 
 # regex used to find links in an article
 LINK_RE = re.compile('<a.*?href="(.*?)".*?>(.*?)</a>', re.I|re.M)
 
     is_active = models.BooleanField(default=True, blank=True)
     is_commentable = models.BooleanField(default=True, blank=True)
+    display_comments = models.BooleanField(default=True, blank=True)
     login_required = models.BooleanField(blank=True, help_text=_('Enable this if users must login before they can read this article.'))
 
+    use_addthis_button = models.BooleanField(_('Show AddThis button'), blank=True, default=USE_ADDTHIS_BUTTON, help_text=_('Check this to show an AddThis bookmark button when viewing an article.'))
+    addthis_use_author = models.BooleanField(_("Use article author's username"), blank=True, default=ADDTHIS_USE_AUTHOR, help_text=_("Check this if you want to use the article author's username for the AddThis button.  Respected only if the username field is left empty."))
+    addthis_username = models.CharField(_('AddThis Username'), max_length=50, blank=True, default=DEFAULT_ADDTHIS_USER, help_text=_('The AddThis username to use for the button.'))
+
     objects = ArticleManager()
 
     def __init__(self, *args, **kwargs):
     def __unicode__(self):
         return self.title
 
-    def save(self):
+    def save(self, *args):
         """
         Renders the article using the appropriate markup language.  Pings
         Google to let it know that this article has been updated.
         else:
             self.rendered_content = self.content
 
+        # if the author wishes to have an "AddThis" button on this article,
+        # make sure we have a username to go along with it.
+        if self.use_addthis_button and self.addthis_use_author and not self.addthis_username:
+            self.addthis_username = self.author.username
+
         if not settings.DEBUG:
             # try to tell google that we have a new article
             try:
             except Exception:
                 pass
 
+        super(Article, self).save(*args)
+
     def _get_article_links(self):
+        """
+        Find all links in this article.  When a link is encountered in the
+        article text, this will attempt to discover the title of the page it
+        links to.  If there is a problem with the target page, or there is no
+        title (ie it's an image or other binary file), the text of the link is
+        used as the title.  Once a title is determined, it is cached for a week
+        before it will be requested again.
+        """
         links = {}
+        keys = []
 
+        # find all links in the article
         for link in LINK_RE.finditer(self.rendered_content):
-            key = 'href_title_' + encodestring(link.group(1)).strip()
+            url = link.group(1)
+            key = 'href_title_' + encodestring(url).strip()
+
+            # look in the cache for the link target's title
             if not cache.get(key):
-                c = urllib.urlopen(link.group(1))
-                html = c.read()
-                c.close()
-                title = TITLE_RE.search(html)
-                if title: title = title.group(1)
-                else: title = link.group(2)
+                try:
+                    # open the URL
+                    c = urllib.urlopen(url)
+                    html = c.read()
+                    c.close()
 
-                cache.set(key, title, 86400)
+                    # try to determine the title of the target
+                    title = TITLE_RE.search(html)
+                    if title: title = title.group(1)
+                    else: title = link.group(2)
+                except:
+                    # if anything goes wrong (ie IOError), use the link's text
+                    title = link.group(2)
 
-            links[link.group(1)] = cache.get(key)
+                # cache the page title for a week
+                cache.set(key, title, 604800)
 
-        return links
+            # get the link target's title from cache
+            val = cache.get(key)
+            if val:
+                # add it to the list of links and titles
+                links[url] = val
+
+                # don't duplicate links to the same page
+                if url not in keys: keys.append(url)
+
+        # now go thru and sort the links according to where they appear in the
+        # article
+        sorted = []
+        for key in keys:
+            sorted.append((key, links[key]))
+
+        return tuple(sorted)
     links = property(_get_article_links)
 
+    def _get_word_count(self):
+        """
+        Stupid word counter for an article.
+        """
+        return len(striptags(self.rendered_content).split(' '))
+    word_count = property(_get_word_count)
+
     def get_absolute_url(self):
         return reverse('articles_display_article', args=[self.publish_date.year, self.slug])
 
     def _get_teaser(self):
+        """
+        Retrieve some part of the article or the article's description.
+        """
         if len(self.description.strip()):
             text = self.description
         else:

articles/templates/articles/_articles.html

 {% load i18n %}
 {% if forloop.first %}<ol class="article-list">{% endif %}
     <li>
-        <h3><a href="{{ article.get_absolute_url }}" title="{% trans 'Read this article' %}">{{ article.title }}</a><h3>
-        <div class="quiet">{% trans 'Posted on' %} {{ article.publish_date|date:"F jS, Y" }}</div>
+        <h3><a href="{{ article.get_absolute_url }}" title="{% trans 'Read this article' %}">{{ article.title }}</a></h3>
+        <div class="quiet">
+            {% trans 'Posted on' %} {{ article.publish_date|date:"F jS, Y" }}
+            {% trans 'by' %} <a href="{% url articles_by_author article.author.username %}" title="{% trans 'View articles posted by' %} {{ article.author.get_name }}">{{ article.author.get_name }}</a>
+        </div>
     </li>
 {% if forloop.last %}</ol>{% endif %}

articles/templates/articles/article_detail.html

 {% extends 'articles/base.html' %}
-{% load i18n %}
+{% load i18n humanize comments gravatar %}
 
 {% block title %}{% trans article.title %}{% endblock %}
 
 {% block content %}
 <div id="article-content">
-    <h2>{% trans article.title %}</h2>
+    <h2 class="title">{% trans article.title %}</h2>
 
     {{ article.rendered_content|safe }}
 </div>
 
 <div id="article-meta">
-    <p>Posted by <a href="{% url articles_by_author article.author.username %}" title="{% trans 'Read other articles by this author' %}">{{ article.author.get_name }}</a></p>
+    <h4>Meta</h4>
 
-    {{ article.links }}
+    <p><strong>{% trans 'Published' %}</strong>: {{ article.publish_date|naturalday }}</a>
+
+    <p><strong>{% trans 'Author' %}</strong>: <a href="{% url articles_by_author article.author.username %}" title="{% trans 'Read other articles by this author' %}">{{ article.author.get_name }}</a></p>
+
+    {% if article.is_commentable %}
+    {% get_comment_count for article as comment_count %}
+    <p><strong>{% trans 'Comments' %}</strong>: <a href="#comments">{{ comment_count }}</a></a>
+    {% endif %}
+
+    <p><strong>{% trans 'Word Count' %}</strong>: {{ article.word_count|intcomma }}</a>
+
+    {% if article.use_addthis_button and article.addthis_username %}
+    <!-- AddThis Button BEGIN -->
+    <div>
+        <script type="text/javascript">var addthis_pub="{{ article.addthis_username }}";</script>
+        <a href="http://www.addthis.com/bookmark.php?v=20" onmouseover="return addthis_open(this, '', '[URL]', '[TITLE]')" onmouseout="addthis_close()" onclick="return addthis_sendto()"><img src="http://s7.addthis.com/static/btn/lg-share-en.gif" width="125" height="16" alt="Bookmark and Share" style="border:0"/></a>
+        <script type="text/javascript" src="http://s7.addthis.com/js/200/addthis_widget.js"></script>
+    </div>
+    <!-- AddThis Button END -->
+    {% endif %}
+
+    <h4>{% trans 'Filed Under' %}</h4>
+    <p>{% if article.categories.count %}{% for category in article.categories.all %}{% if not forloop.first %}{% ifnotequal article.categories.count 2 %},{% endifnotequal %} {% if forloop.last %}{% trans 'and' %}{% endif %} {% endif %}<a href="{% url articles_display_category category.slug %}" title="{% trans 'View all articles in this category' %}">{{ category }}</a>{% endfor %}{% else %}<a href="{% url articles_display_category 'uncategorized' %}" title="{% trans 'View all uncategorized articles' %}">{% trans 'Uncategorized' %}</a>{% endif %}</p>
+
+    {% if article.followups.active.count %}
+    <h4 class="hasfollowup-header">{% trans 'Follow-Up Articles' %}</h4>
+    {% for fu in article.followups.active %}
+    {% if forloop.first %}<ul class="followups">{% endif %}
+        <li>
+            <a href="{{ fu.get_absolute_url }}" title="{% trans 'Read this follow-up article' %}">{{ fu.title }}</a>, {% trans 'posted' %} {{ fu.publish_date|naturalday }}
+        </li>
+    {% if forloop.last %}</ul>{% endif %}
+    {% endfor %}
+    {% endif %}
+
+    {% if article.followup_for.active.count %}
+    <h4 class="followup-header">Follows Up On</h4>
+    {% for fu in article.followup_for.active %}
+    {% if forloop.first %}<ul class="followups">{% endif %}
+        <li>
+            <a href="{{ fu.get_absolute_url }}" title="{% trans 'Read this article' %}">{{ fu.title }}</a>, {% trans 'posted' %} {{ fu.publish_date|naturalday }}
+        </li>
+    {% if forloop.last %}</ul>{% endif %}
+    {% endfor %}
+    {% endif %}
+
+    {% if articles.related_articles.active.count %}
+    <h4 class="related-header">Related Articles</h4>
+    {% for ra in articles.related_articles.active %}
+    {% if forloop.first %}<ul class="related-articles">{% endif %}
+        <li>
+            <a href="{{ ra.get_absolute_url }}" title="{% trans 'Read this related article' %}">{{ ra.title }}</a>, {% trans 'posted' %}  {{ ra.publish_date|naturalday }}
+        </li>
+    {% if forloop.last %}</ul>{% endif %}
+    {% endfor %}
+    {% endif %}
+
+    {% for url,title in article.links %}
+    {% if forloop.first %}<h4>{% trans 'Article Links' %}</h4>
+    <ol>{% endif %}
+        <li><a href="{{ url }}" title="{{ title }}">{{ title|safe|truncatewords:10 }}</a></li>
+    {% if forloop.last %}</ol>{% endif %}
+    {% endfor %}
 </div>
+<div class="clear"></div>
+
+{% if article.display_comments %}
+{% get_comment_list for article as comment_list %}
+{% if comment_list|length %}
+<div id="article-comments">
+    <h4 class="comments-header" id="comments">Comments</h4>
+
+    {% for comment in comment_list %}
+    {% if forloop.first %}<table class="comment-list">{% endif %}
+        <tr class="{% ifequal comment.user article.author %}author-comment{% endifequal %} {% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %}" id="c{{ comment.id }}">
+            <td class="avatar">
+                {% if comment.user %}<a href="{% url articles_by_author comment.user_name %}">{% endif %}
+                <img src="{% gravatar_for_email comment.email %}" class="gravatar" alt="Gravatar for {{ comment.user }}" /><br />
+                {% if comment.user %}
+                </a><a href="{% url articles_by_author comment.user_name %}">{{ comment.user_name }}</a>
+                {% else %}
+                {{ comment.name|escape }}
+                {% endif %}
+            </td>
+            <td>
+                {{ comment.comment|striptags|escape|linebreaksbr|safe|urlizetrunc:40 }}
+                <div class="posted-by quiet">
+                    {{ comment.submit_date|date:"j N Y \a\t P" }}
+                </div>
+            </td>
+        </tr>
+    {% if forloop.last %}</table>{% endif %}
+    {% endfor %}
+</div>
+{% endif %}{% endif %}
+
+{% if article.is_commentable %}
+<div id="article-comments-form">
+    <h4 class="postcomment-header">Post a Comment</h4>
+    {% render_comment_form for article %}
+</div>
+{% endif %}
 {% endblock %}

articles/templates/articles/base.html

 </div>
 
 {% block articles-content %}{% endblock %}
+
+{% if paginator and page_obj %}
+{% ifnotequal paginator.page_range|length 1 %}
+{% for p in paginator.page_range %}
+{% if forloop.first %}<ul class="pagination-pages">
+{% if page_obj.has_previous %}
+    <li><a href="{% get_page_url 1 %}">&laquo;</a></li>
+    <li><a href="{% get_page_url page_obj.previous_page_number %}">&lsaquo;</a></li>
+{% endif %}
+{% endif %}
+    <li><a href="{% get_page_url p %}"{% ifequal p page_obj.number %} class="current-page"{% endifequal %}>{{ p }}</a></li>
+{% if forloop.last %}
+{% if page_obj.has_next %}
+    <li><a href="{% get_page_url page_obj.next_page_number %}">&rsaquo;</a></li>
+    <li><a href="{% get_page_url paginator.num_pages %}">&raquo;</a></li>
+{% endif %}
+</ul>{% endif %}
+{% endfor %}
+{% endifnotequal %}
+{% endif %}
 {% endblock %}

articles/templates/articles/display_category.html

 {% extends 'articles/base.html' %}
 {% load i18n %}
 
-{% block title %}{% trans 'Articles By Category' %}: {{ category.name }}{% endblock %}
+{% block title %}{% trans 'Articles Categorized As' %}: {{ category.name }}{% endblock %}
 
 {% block articles-content %}
-<h2>{% trans 'Articles In Category' %} {{ category.name }}{% ifnotequal paginator.num_pages 1 %}, {% trans 'page' %} {{ page_obj.number }}{% endifnotequal %}</h2>
+<h2>{% trans 'Articles Categorized As' %} <em>{{ category.name }}</em>{% ifnotequal paginator.num_pages 1 %}, {% trans 'page' %} {{ page_obj.number }}{% endifnotequal %}</h2>
 
 {% for article in page_obj.object_list %}
 {% include 'articles/_articles.html' %}

articles/templates/articles/uncategorized_article_list.html

+{% extends 'articles/base.html' %}
+{% load i18n %}
+
+{% block title %}{% trans 'Uncategorized Articles' %}{% endblock %}
+
+{% block articles-content %}
+<h2>{% trans 'Uncategorized Articles' %}{% ifnotequal paginator.num_pages 1 %}, {% trans 'page' %} {{ page_obj.number }}{% endifnotequal %}</h2>
+
+{% for article in page_obj.object_list %}
+{% include 'articles/_articles.html' %}
+{% endfor %}
+{% endblock %}

articles/templatetags/article_tags.py

 from django import template
+from django.core.urlresolvers import resolve, reverse, Resolver404
 from articles.models import Article, Category
 from datetime import datetime
+import math
 
 register = template.Library()
 
 class GetCategoriesNode(template.Node):
+    """
+    Retrieves a list of active article categories and places it into the context
+    """
     def __init__(self, varname):
         self.varname = varname
 
         return ''
 
 def get_article_categories(parser, token):
+    """
+    Retrieves a list of active article categories and places it into the context
+    """
     args = token.split_contents()
     argc = len(args)
 
-    assert argc == 3 and args[1] == 'as'
+    try:
+        assert argc == 3 and args[1] == 'as'
+    except AssertionError:
+        raise template.TemplateSyntaxError('get_article_categories syntax: {% get_article_categories as varname %}')
 
     return GetCategoriesNode(args[2])
 
 class GetArticlesNode(template.Node):
+    """
+    Retrieves a set of article objects.
+
+    Usage::
+
+        {% get_articles 5 as varname %}
+
+        {% get_articles 5 as varname asc %}
+
+        {% get_articles 1 to 5 as varname %}
+
+        {% get_articles 1 to 5 as varname asc %}
+    """
     def __init__(self, varname, count=None, start=None, end=None, order='desc'):
         self.count = count
         self.start = start
         self.varname = varname.strip()
 
     def render(self, context):
+        # determine the order to sort the articles
         if self.order and self.order.lower() == 'desc':
             order = '-publish_date'
         else:
             order = 'publish_date'
 
+        # get the active articles in the appropriate order
         articles = Article.objects.active().order_by(order)
 
         if self.count:
+            # if we have a number of articles to retrieve, pull the first of them
             articles = articles[:self.count]
         else:
+            # get a range of articles
             articles = articles[(int(self.start) - 1):int(self.end)]
 
-        if len(articles) == 1: articles = articles[0]
+        # don't send back a list when we really don't need/want one
+        if len(articles) == 1 and not self.start: articles = articles[0]
 
+        # put the article(s) into the context
         context[self.varname] = articles
         return ''
 
     args = token.split_contents()
     argc = len(args)
 
-    assert argc in (4,6) or (argc in (5,7) and args[-1].lower() in ('desc', 'asc'))
+    try:
+        assert argc in (4,6) or (argc in (5,7) and args[-1].lower() in ('desc', 'asc'))
+    except AssertionError:
+        raise template.TemplateSyntaxError('Invalid get_articles syntax.')
 
+    # determine what parameters to use
     order = 'desc'
     count = start = end = varname = None
     if argc == 4: t, count, a, varname = args
                            varname=varname)
 
 class GetArticleArchivesNode(template.Node):
+    """
+    Retrieves a list of years and months in which articles have been posted.
+    """
     def __init__(self, varname):
         self.varname = varname
 
     def render(self, context):
         archives = {}
+
+        # iterate over all active articles
         for article in Article.objects.active():
             pub = article.publish_date
+
+            # see if we already have an article in this year
             if not archives.has_key(pub.year):
+                # if not, initialize a dict for the year
                 archives[pub.year] = {}
 
+            # make sure we know that we have an article posted in this month/year
             archives[pub.year][pub.month] = True
 
         dt_archives = []
+
+        # now sort the years, so they don't appear randomly on the page
         years = list(int(k) for k in archives.keys())
         years.sort()
+
+        # more recent years will appear first in the resulting collection
         years.reverse()
 
+        # iterate over all years
         for year in years:
-            months = []
+            # sort the months of this year in which articles were posted
             m = list(int(k) for k in archives[year].keys())
             m.sort()
-            for month in m:
-                months.append(datetime(year, month, 1))
-            dt_archives.append((
-                year, tuple(months)
-            ))
 
+            # now create a list of datetime objects for each month/year
+            months = [datetime(year, month, 1) for month in m]
+
+            # append this list to our final collection
+            dt_archives.append( ( year, tuple(months) ) )
+
+        # put our collection into the context
         context[self.varname] = dt_archives
         return ''
 
 def get_article_archives(parser, token):
+    """
+    Retrieves a list of years and months in which articles have been posted.
+    """
     args = token.split_contents()
     argc = len(args)
 
-    assert argc == 3
+    try:
+        assert argc == 3 and args[1] == 'as'
+    except AssertionError:
+        raise template.TemplateSyntaxError('get_article_archives syntax: {% get_article_archives as varname %}')
 
     return GetArticleArchivesNode(args[2])
 
+class DivideObjectListByNode(template.Node):
+    """
+    Divides an object list by some number to determine now many objects will
+    fit into, say, a column.
+    """
+    def __init__(self, object_list, divisor, varname):
+        self.object_list = template.Variable(object_list)
+        self.divisor = template.Variable(divisor)
+        self.varname = varname
+
+    def render(self, context):
+        # get the actual object list from the context
+        object_list = self.object_list.resolve(context)
+
+        # get the divisor from the context
+        divisor = int(self.divisor.resolve(context))
+
+        # make sure we don't divide by 0 or some negative number!!!!!!
+        assert divisor > 0
+
+        context[self.varname] = int(math.ceil(len(object_list) / float(divisor)))
+        return ''
+
+def divide_object_list(parser, token):
+    """
+    Divides an object list by some number to determine now many objects will
+    fit into, say, a column.
+    """
+    args = token.split_contents()
+    argc = len(args)
+
+    try:
+        assert argc == 6 and args[2] == 'by' and args[4] == 'as'
+    except AssertionError:
+        raise template.TemplateSyntaxError('divide_object_list syntax: {% divide_object_list object_list by divisor as varname %}')
+
+    return DivideObjectListByNode(args[1], args[3], args[5])
+
+class GetPageURLNode(template.Node):
+    """
+    Determines the URL of a pagination page link based on the page from which
+    this tag is called.
+    """
+    def __init__(self, page_num, varname=None):
+        self.page_num = template.Variable(page_num)
+        self.varname = varname
+
+    def render(self, context):
+        url = None
+
+        # get the page number we're linking to from the context
+        page_num = self.page_num.resolve(context)
+
+        try:
+            # determine what view we are using based upon the path of this page
+            view, args, kwargs = resolve(context['request'].path)
+        except (Resolver404, KeyError):
+            raise ValueError('Invalid pagination page.')
+        else:
+            # set the page parameter for this view
+            kwargs['page'] = page_num
+
+            # get the new URL from Django
+            url = reverse(view, args=args, kwargs=kwargs)
+
+        if self.varname:
+            # if we have a varname, put the URL into the context and return nothing
+            context[self.varname] = url
+            return ''
+
+        # otherwise, return the URL directly
+        return url
+
+def get_page_url(parser, token):
+    """
+    Determines the URL of a pagination page link based on the page from which
+    this tag is called.
+    """
+    args = token.split_contents()
+    argc = len(args)
+    varname = None
+
+    try:
+        assert argc in (2, 4)
+    except AssertionError:
+        raise template.TemplateSyntaxError('get_page_url syntax: {% get_page_url page_num as varname %}')
+
+    if argc == 4: varname = args[3]
+
+    return GetPageURLNode(args[1], varname)
+
+# register dem tags!
 register.tag(get_articles)
 register.tag(get_article_categories)
-register.tag(get_article_archives)
+register.tag(get_article_archives)
+register.tag(divide_object_list)
+register.tag(get_page_url)
     url(r'^$', views.display_blog_page, name='articles_archive'),
     url(r'^page/(?P<page>\d+)/$', views.display_blog_page, name='articles_archive_page'),
 
-    url(r'^uncategorized/$', views.display_blog_page, name='articles_uncategorized'),
-    url(r'^uncategorized/page/(?P<page>\d+)/$', views.display_blog_page, name='articles_uncategorized_page'),
-
     url(r'^category/(?P<category>.*)/page/(?P<page>\d+)/$', views.display_blog_page, name='articles_display_category_page'),
     url(r'^category/(?P<category>.*)/$', views.display_blog_page, name='articles_display_category'),
 
+    (r'^category/(?P<category>uncategorized)/$', views.display_blog_page),
+    (r'^category/(?P<category>uncategorized)/page/(?P<page>\d+)/$', views.display_blog_page),
+
     url(r'^author/(?P<username>.*)/page/(?P<page>\d+)/$', views.display_blog_page, name='articles_by_author_page'),
     url(r'^author/(?P<username>.*)/$', views.display_blog_page, name='articles_by_author'),
 

articles/views.py

 from django.conf import settings
 from django.contrib.auth.models import User
 from django.core.paginator import Paginator
-from django.http import HttpResponsePermanentRedirect
+from django.core.urlresolvers import reverse
+from django.http import HttpResponsePermanentRedirect, Http404
 from django.shortcuts import render_to_response, get_object_or_404
 from django.template import RequestContext
 from articles.models import Article, Category
 ARTICLE_PAGINATION = getattr(settings, 'ARTICLE_PAGINATION', 20)
 
 def display_blog_page(request, category=None, username=None, year=None, month=None, page=1):
+    """
+    Handles all of the magic behind the pages that list articles in any way.
+    Yes, it's dirty to have so many URLs go to one view, but I'd rather do that
+    than duplicate a bunch of code.  I'll probably revisit this in the future.
+    """
     context = {}
 
     if category:
-        category = get_object_or_404(Category, slug=category)
-        articles = category.article_set.all()
-        template = 'articles/display_category.html'
-        context['category'] = category
+        # listing articles in a category
+        if category == 'uncategorized':
+            articles = Article.objects.uncategorized()
+            template = 'articles/uncategorized_article_list.html'
+        else:
+            category = get_object_or_404(Category, slug=category)
+            articles = category.article_set.all()
+            template = 'articles/display_category.html'
+            context['category'] = category
     elif username:
+        # listing articles by a particular author
         user = get_object_or_404(User, username=username)
         articles = user.article_set.all()
         template = 'articles/by_author.html'
         context['author'] = user
     elif year and month:
+        # listing articles in a given month and year
         year = int(year)
         month = int(month)
         articles = Article.objects.active().filter(publish_date__year=year, publish_date__month=month)
         template = 'articles/in_month.html'
         context['month'] = datetime(year, month, 1)
     else:
-        if request.path.startswith('/uncategorized/'):
-            articles = Article.objects.uncategorized()
-        else:
-            articles = Article.objects.active()
+        # listing articles with no particular filtering
+        articles = Article.objects.active()
         template = 'articles/article_list.html'
 
+    # paginate the articles
     paginator = Paginator(articles, ARTICLE_PAGINATION,
                           orphans=int(ARTICLE_PAGINATION / 4))
     page = paginator.page(page)
                               context_instance=RequestContext(request))
 
 def display_article(request, year, slug, template='articles/article_detail.html'):
-    article = get_object_or_404(Article, publish_date__year=year, slug=slug)
+    """
+    Displays a single article.
+    """
+
+    try:
+        article = Article.objects.active().get(publish_date__year=year, slug=slug)
+    except Article.DoesNotExist:
+        raise Http404
+
+    # make sure the user is logged in if the article requires it
+    if article.login_required and not request.user.is_authenticated():
+        return HttpResponseRedirect('/accounts/login/?next=' + request.path)
+
     return render_to_response(template,
                               {'article': article},
                               context_instance=RequestContext(request))
 
 def redirect_to_article(request, year, month, day, slug):
+    # this is a little snippet to handle URLs that are formatted the old way.
     article = get_object_or_404(Article, publish_date__year=year, slug=slug)
     return HttpResponsePermanentRedirect(article.get_absolute_url())