Commits

Josh VanderLinden committed a55ff75

Initial import--still in the process of cleaning it up, so it's probably not useful to anyone out of the box just yet.

  • Participants
  • Parent commits b63664d

Comments (0)

Files changed (27)

articles/__init__.py

+from django.db.models.signals import post_save
+from django.core.mail import mail_admins
+from django.contrib.comments.models import Comment
+from django.conf import settings
+
+def notify_of_comment(sender, instance, created, **kwargs):
+    """
+    Only send new comment emails when we're not in development mode.
+    """
+    if not settings.DEBUG:
+        if created:
+            message = 'A new comment has been posted.\n'
+        else:
+            message = 'A comment has been updated.\n'
+        message += instance.get_as_text()
+
+        mail_admins('New Comment', message)
+
+post_save.connect(notify_of_comment, sender=Comment)
+
+"""
+    The Pygments reStructuredText directive
+    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+    This fragment is a Docutils_ 0.4 directive that renders source code
+    (to HTML only, currently) via Pygments.
+
+    To use it, adjust the options below and copy the code into a module
+    that you import on initialization.  The code then automatically
+    registers a ``sourcecode`` directive that you can use instead of
+    normal code blocks like this::
+
+        .. sourcecode:: python
+
+            My code goes here.
+
+    If you want to have different code styles, e.g. one with line numbers
+    and one without, add formatters with their names in the VARIANTS dict
+    below.  You can invoke them instead of the DEFAULT one by using a
+    directive option::
+
+        .. sourcecode:: python
+            :linenos:
+
+            My code goes here.
+
+    Look at the `directive documentation`_ to get all the gory details.
+
+    .. _Docutils: http://docutils.sf.net/
+    .. _directive documentation:
+       http://docutils.sourceforge.net/docs/howto/rst-directives.html
+
+    :copyright: 2007 by Georg Brandl.
+    :license: BSD, see LICENSE for more details.
+"""
+
+# Options
+# ~~~~~~~
+
+# Set to True if you want inline CSS styles instead of classes
+INLINESTYLES = False
+
+from pygments.formatters import HtmlFormatter
+
+# The default formatter
+DEFAULT = HtmlFormatter(noclasses=INLINESTYLES)
+
+# Add name -> formatter pairs for every variant you want to use
+VARIANTS = {
+    'linenos': HtmlFormatter(noclasses=INLINESTYLES, linenos=True),
+}
+
+from docutils import nodes
+from docutils.parsers.rst import directives
+
+from pygments import highlight
+from pygments.lexers import get_lexer_by_name, TextLexer
+
+def pygments_directive(name, arguments, options, content, lineno,
+                       content_offset, block_text, state, state_machine):
+    try:
+        lexer = get_lexer_by_name(arguments[0])
+    except ValueError:
+        # no lexer found - use the text one instead of an exception
+        lexer = TextLexer()
+    # take an arbitrary option if more than one is given
+    formatter = options and VARIANTS[options.keys()[0]] or DEFAULT
+    parsed = highlight(u'\n'.join(content), lexer, formatter)
+    parsed = '<div class="codeblock">%s</div>' % parsed
+    return [nodes.raw('', parsed, format='html')]
+
+pygments_directive.arguments = (1, 0, 1)
+pygments_directive.content = 1
+pygments_directive.options = dict([(key, directives.flag) for key in VARIANTS])
+
+directives.register_directive('sourcecode', pygments_directive)
+
+# create an alias, so we can use it with rst2pdf... leave the other for
+# backwards compatibility
+directives.register_directive('code-block', pygments_directive)

articles/admin.py

+from django.contrib import admin
+from django.contrib.auth.models import User
+from codekoala.articles.models import Category, Article
+
+class CategoryAdmin(admin.ModelAdmin):
+    list_display = ('name', 'image')
+    prepopulated_fields = {'slug': ('name',)}
+
+class ArticleAdmin(admin.ModelAdmin):
+    list_display = ('title', 'author', 'publish_date', 'expiration_date', 'is_active')
+    list_filter = ('author', 'is_active', 'publish_date', 'expiration_date')
+    list_per_page = 25
+    search_fields = ('title', 'keywords', 'description', 'content')
+    date_hierarchy = 'publish_date'
+
+    fieldsets = (
+        (None, {'fields': ('title', 'content', 'categories')}),
+        ('Metadata', {
+            'fields': ('keywords', 'description',),
+            'classes': ('collapse',)
+        }),
+        ('Relationships', {
+            'fields': ('followup_for', 'related_articles'),
+            'classes': ('collapse',)
+        }),
+        ('Scheduling', {'fields': ('publish_date', 'expiration_date')}),
+        ('Advanced', {
+            'fields': ('slug', 'is_active', 'is_commentable', 'make_pdf'),
+            'classes': ('collapse',)
+        }),
+    )
+
+    filter_horizontal = ('categories', 'followup_for', 'related_articles')
+    prepopulated_fields = {'slug': ('title',)}
+
+    def save_model(self, request, obj, form, change):
+        try:
+            author = obj.author
+        except User.DoesNotExist:
+            obj.author = request.user
+        obj.save()
+
+    def queryset(self, request):
+        if request.user.is_superuser:
+            return self.model._default_manager.all()
+        else:
+            return self.model._default_manager.filter(author=request.user)
+
+admin.site.register(Category, CategoryAdmin)
+admin.site.register(Article, ArticleAdmin)

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
+
+SITE = Site.objects.get_current()
+
+class LatestEntries(Feed):
+    link = "/blog/"
+    description = "Updates to my blog"
+
+    def title(self):
+        return "%s Articles" % SITE.name
+
+    def items(self):
+        return Article.objects.active().order_by('-publish_date')[:5]
+
+    def item_author_name(self, item):
+        return item.author.username
+
+    def item_categories(self, item):
+        return [c.name for c in item.categories.all()] + [keyword.strip() for keyword in item.keywords.split(',')]
+
+    def item_pubdate(self, item):
+        return item.publish_date
+
+class CategoryFeed(Feed):
+    def get_object(self, bits):
+        if len(bits) != 1:
+            raise FeedDoesNotExist
+        return Category.objects.active().get(slug__exact=bits[0])
+
+    def title(self, obj):
+        return "%s: Newest Articles Tagged '%s'" % (SITE.name, obj.slug)
+
+    def link(self, obj):
+        if not obj:
+            raise FeedDoesNotExist
+        return obj.get_absolute_url()
+
+    def description(self, obj):
+        return "Articles Tagged '%s'" % obj.slug
+
+    def items(self, obj):
+        return self.item_set(obj)[:10]
+
+    def item_set(self, obj):
+        return obj.article_set.active().order_by('-publish_date')
+
+    def item_author_name(self, item):
+        return item.author.username
+
+    def item_author_link(self, item):
+        return reverse('articles_by_author', args=[item.author.username])
+
+    def item_pubdate(self, item):
+        return item.publish_date

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.db import models, connection
+from django.db.models import Q
+from django.contrib.auth.models import User
+from django.contrib.comments.models import Comment
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.sitemaps import ping_google
+from django.contrib.markup.templatetags.markup import restructuredtext
+from django.core.urlresolvers import reverse
+from django.conf import settings
+from datetime import datetime
+import commands
+import os
+import re
+import xmlrpclib
+import unicodedata
+
+WORD_LIMIT = getattr(settings, 'ARTICLES_TEASER_LIMIT', 75)
+
+class CategoryManager(models.Manager):
+    def active(self):
+        return self.get_query_set().filter(is_active=True)
+
+class Category(models.Model):
+    name = models.CharField(max_length=50)
+    slug = models.SlugField()
+    image = models.ImageField(upload_to='categories/', blank=True, null=True)
+    is_active = models.BooleanField(default=True, blank=True)
+
+    objects = CategoryManager()
+
+    def __unicode__(self):
+        return self.name
+
+    def get_absolute_url(self):
+        return reverse('articles_display_category', args=(self.slug,))
+
+    class Meta:
+        ordering = ('name',)
+        verbose_name_plural = 'categories'
+
+class ArticleManager(models.Manager):
+    def active(self):
+        """
+        Retrieves all active articles which have been published and have not yet
+        expired.
+        """
+        now = datetime.now()
+        return self.get_query_set().filter(
+                Q(expiration_date__isnull=True) |
+                Q(expiration_date__gte=now),
+                publish_date__lte=now,
+                is_active=True)
+
+    def uncategorized(self):
+        """
+        Find all articles that were not assigned a category.
+        """
+
+        return self.active().filter(categories__isnull=True)
+
+class Article(models.Model):
+    title = models.CharField(max_length=100)
+    slug = models.SlugField(unique_for_date='publish_date')
+    keywords = models.TextField(blank=True)
+    description = models.TextField(blank=True, help_text="If omitted, the description will be determined by the first bit of the article's content.")
+    author = models.ForeignKey(User)
+    content = models.TextField()
+    rendered_content = models.TextField()
+    categories = models.ManyToManyField(Category, help_text='Select any categories to classify this article.', blank=True)
+    followup_for = models.ManyToManyField('self', symmetrical=False, blank=True, help_text='Select any other articles that this article follows up on.', related_name='followups')
+    related_articles = models.ManyToManyField('self', blank=True)
+    is_active = models.BooleanField(default=True, blank=True)
+    is_commentable = models.BooleanField(default=True, blank=True)
+    make_pdf = models.BooleanField(default=True, blank=True)
+    publish_date = models.DateTimeField(default=datetime.now)
+    expiration_date = models.DateTimeField(blank=True, null=True)
+
+    objects = ArticleManager()
+
+    def __init__(self, *args, **kwargs):
+        super(Article, self).__init__(*args, **kwargs)
+
+        if self.id:
+            if not self.rendered_content or not len(self.rendered_content):
+                self.save()
+
+    def __unicode__(self):
+        return self.title
+
+    def save(self):
+        # make sure each article has a heading
+        if not self.content.startswith('==='):
+            line = '=' * len(self.title)
+            self.content = """%s
+%s
+%s
+
+:author: Josh VanderLinden <codekoala@gmail.com>
+:date: %s
+:Homepage: http://www.codekoala.com/
+
+%s
+""" % (line, self.title, line,
+       self.publish_date.strftime('%d %b %Y'), self.content)
+
+        # no more page breaks please
+        self.content = self.content.replace(PAGE_BREAK, '')
+
+        # let's use "code-block" instead of "sourcecode" for the PDFs
+        self.content = self.content.replace('.. sourcecode:: ',
+                                            '.. code-block:: ')
+
+        self.rendered_content = restructuredtext(self.content)
+        super(Article, self).save()
+
+        # don't save any .rst or .pdf files if we're in debug mode or it's
+        # specifically checked
+        #if settings.DEBUG or not self.make_pdf: return
+
+        # create a PDF version of the article
+        out_dir = os.path.join(settings.MEDIA_ROOT, 'articles')
+        rst_dir = os.path.join(out_dir, 'rst')
+        pdf_dir = os.path.join(out_dir, 'pdfs')
+
+        # ensure that the RST directory exists
+        try: os.makedirs(rst_dir)
+        except OSError: pass
+
+        # ensure that the PDF directory exists
+        try: os.makedirs(pdf_dir)
+        except OSError: pass
+
+        # save the RST
+        pdf_file = os.path.join(pdf_dir, self.slug + '.pdf')
+        rst_file = os.path.join(rst_dir, self.slug + '.rst')
+
+        real_content = clean_content = u_clean(self.content)
+
+        # now clean it up a bit more for the PDF
+        #clean_content = clean_content.replace('    :linenos:\r\n', '    :linenos: true\r\n')
+        clean_content = re.sub('(:(H|h)omepage:.*\n)',
+                                r'\1:URL: http://www.codekoala.com%s\n\n .. header::\n\n    Copyright (c) %i Josh VanderLinden\n\n .. footer::\n\n    page ###Page###\n' % (self.get_absolute_url(), self.publish_date.year), clean_content)
+        clean_content = re.sub('( .. image:: http://www.codekoala.com/static/)',
+                              r' .. image:: %s/' % settings.MEDIA_ROOT,
+                              clean_content)
+
+        # see if we have any comments on this article
+        content_type = ContentType.objects.get_for_model(Article)
+        comments = Comment.objects.filter(content_type=content_type, object_pk=str(self.id))
+        if comments.count():
+            # we do, so tack on the comments for the PDF
+            clean_content += '\n\nComments\n========\n\n'
+            for comment in comments:
+                clean_content += """%s said...\n%s
+
+%s
+
+Posted: %s
+
+""" % (u_clean(comment.name),
+        '-' * len(comment.name + ' said...'),
+        u_clean(comment.comment),
+        comment.submit_date)
+
+        #print clean_content
+        self.__save_pdf(rst_file, pdf_file, clean_content)
+
+        # try to access the PDF file.  If it's there, we can assume that all
+        # went well.  If it's not there, try removing the :linenos: option
+        if not os.access(pdf_file, os.R_OK):
+            clean_content = clean_content.replace('    :linenos:\r\n', '')
+            self.__save_pdf(rst_file, pdf_file, clean_content)
+
+        # now save the "real" RST
+        rst = open(rst_file, 'w')
+        rst.write(real_content)
+        rst.close()
+
+        if not settings.DEBUG:
+            # try to tell google that we have a new article
+            try:
+                ping_google()
+            except Exception:
+                pass
+
+    def __save_pdf(self, rst_file, pdf_file, content):
+        """
+        Attempts to save a PDF version of the article.
+        """
+
+        # save the PDF-appropriate RST
+        rst = open(rst_file, 'w')
+        rst.write(u_clean(content))
+        rst.close()
+
+        # generate the PDF
+        commands.getoutput('rst2pdf %s -o %s' % (rst_file, pdf_file))
+
+    def get_absolute_url(self):
+        info = self.publish_date.strftime('%Y/%b/%d').lower().split('/') + [self.slug]
+        return reverse('articles_display_article', args=info)
+
+    def _get_teaser(self):
+        if len(self.description.strip()):
+            text = self.description
+        else:
+            text = self.rendered_content
+
+        words = text.split(' ')
+        if len(words) > WORD_LIMIT:
+            text = '%s...' % ' '.join(words[:WORD_LIMIT])
+        return text
+    teaser = property(_get_teaser)
+
+    def next_article(self):
+        try:
+            article = Article.objects.active().exclude(id__exact=self.id).filter(publish_date__gte=self.publish_date).order_by('publish_date')[0]
+        except (Article.DoesNotExist, IndexError):
+            article = None
+        return article
+
+    def previous_article(self):
+        try:
+            article = Article.objects.active().exclude(id__exact=self.id).filter(publish_date__lte=self.publish_date).order_by('-publish_date')[0]
+        except (Article.DoesNotExist, IndexError):
+            article = None
+        return article
+
+    class Meta:
+        ordering = ('-publish_date', 'title')
+
+# wrap the save comment method so the PDF will be regenerated as soon as a new
+# comment is posted
+def update_pdf(func):
+    def new(obj, *args, **kwargs):
+        result = func(obj, *args, **kwargs)
+
+        # only save the object whose comment was just saved if it's an Article
+        if isinstance(obj.content_object, Article):
+            obj.content_object.save()
+
+        return result
+    return new
+
+Comment.save = update_pdf(Comment.save)
+
+def u_clean(s):
+    """
+    Cleans up dirty unicode text.
+    """
+    uni = ''
+    try:
+        # try this first
+        uni = str(s).decode('iso-8859-1')
+    except:
+        try:
+            # try utf-8 next
+            uni = str(s).decode('utf-8')
+        except:
+            # last resort method... one character at a time
+            if s and type(s) in (str, unicode):
+                for c in s:
+                    try:
+                        uni += unicodedata.normalize('NFKC', unicode(c))
+                    except:
+                        uni += '-'
+
+    return uni.encode('ascii', 'xmlcharrefreplace')

articles/templates/articles/_article_pages.html

+{% if pages %}
+{% ifnotequal page_count 1 %}
+{% with article.publish_date as date %}
+
+{% if next %}<div class="continued-on">Continued on <a href="{% url articles-display-article-page date.year,date|date:"b",date.day,article.slug,next %}">page {{ next }}</a>...</div>{% endif %}
+
+Pages: {% if first %}<a href="{% url articles-display-article-page date.year,date|date:"b",date.day,article.slug,first %}">&laquo;</a> {% endif %}
+{% if previous %}<a href="{% url articles-display-article-page date.year,date|date:"b",date.day,article.slug,previous %}">&lsaquo;</a> {% endif %}
+
+{% for p in pages %}
+{% ifequal p page %}
+<span class="articles-current-page">{{ p }}</span>
+{% else %}
+<a href="{% url articles-display-article-page date.year,date|date:"b",date.day,article.slug,p %}">{{ p }}</a>
+{% endifequal %}
+{% endfor %}
+
+{% if next %}<a href="{% url articles-display-article-page date.year,date|date:"b",date.day,article.slug,next %}">&rsaquo;</a> {% endif %}
+{% if last %}<a href="{% url articles-display-article-page date.year,date|date:"b",date.day,article.slug,last %}">&raquo;</a> {% endif %}
+
+{% endwith %}
+{% endifnotequal %}
+{% endif %}

articles/templates/articles/_article_teaser.html

+{% load humanize markup comments %}
+{% get_comment_count for articles.article article.id as comment_count %}
+{% with article.publish_date as date %}
+<div class="article-teaser">
+    <h3><a href="{{ article.get_absolute_url }}" title="Read the full article">{{ article.title }}</a></h3>
+    <h6 class="teaser-date">Posted {{ date|timesince }} ago by <a href="{% url articles-by-author article.author.username %}" title="View all articles by this author">{{ article.author }}</a></h6>
+    <div class="teaser">
+        {{ article.teaser|safe }}
+    </div>
+    <div class="infobar">
+        <div class="categories quiet">Filed in {% if article.categories.count %}{% for category in article.categories.all %}{% if not forloop.first %}{% ifnotequal article.categories.count 2 %},{% endifnotequal %} {% if forloop.last %}and{% endif %} {% endif %}<a href="{% url articles-display-category category.slug %}" title="View all articles in this category">{{ category }}</a>{% endfor %}{% else %}<a href="{% url articles-display-uncategorized %}" title="View all uncategorized articles">Uncategorized</a>{% endif %}</div>
+
+        <div class="commentsbar">
+            <a href="{{ article.get_absolute_url }}#comments">{{ comment_count }}</a> comment{{ comment_count|pluralize }} |
+            <a href="{{ article.get_absolute_url }}" title="Read the full article">read more...</a>
+        </div>
+        <div style="clear: both"></div>
+    </div>
+    
+    <hr class="content" />
+</div>
+{% endwith %}

articles/templates/articles/_calendar.html

+<table class="calendar">
+    <tr>
+        <th colspan="7">{{ date|date:"F Y" }}</th>
+    </tr>
+    <tr class="articles-calendar-weekdays">
+      <th>Sun</th>
+      <th>Mon</th>
+      <th>Tue</th>
+      <th>Wed</th>
+      <th>Thu</th>
+      <th>Fri</th>
+      <th>Sat</th>
+    </tr>
+    {% for week in days %}
+    <tr>
+      {% for day in week %}
+      <td{% ifequal day today.day %} class="today"{% endifequal %}>
+        {% with day|stringformat:"02i" as long_day %}
+        {% if day %}<a href="{% url articles-day-archive date.year,date|date:"b",long_day %}">{{ day }}</a>{% else %}&nbsp;{% endif %}
+        {% endwith %}
+      </td>
+      {% endfor %}
+    </tr>
+    {% endfor %}
+</table>

articles/templates/articles/_categories.html

+<ul class="articles-categories">
+{% for category in categories %}
+<li><a href="{% url articles-display-category category.slug %}" title="View all articles in this category">{{ category.name }}</a> ({{ category.article_set.count }})</li>
+{% endfor %}
+<li><a href="{% url articles-display-uncategorized %}" title="View all uncategorized articles">Uncategorized</a> ({{ uncategorized }})</li>
+</ul>

articles/templates/articles/_pagination.html

+{% if is_paginated %}
+<div class="pagination">
+<ul>
+    {% if page_obj.has_previous %}<li><a href="{% url URLname params %}">&lsaquo; newer</a></li>{% endif %}
+    {% for p in paginator.page_range %}
+    <li>
+        {% ifequal p page_obj.number %}<span class="active">{{ p }}</span>
+        {% else %}<a href="{{ page_urls.p }}">{{ p }}</a>{% endifequal %}
+    </li>
+    {% endfor %}
+    {% if page_obj.has_next %}<li><a href="{{ next_url }}">older &rsaquo;</a></li>{% endif %}
+</ul>
+</div>
+{% endif %}

articles/templates/articles/_recent_articles.html

+{% if articles %}
+{% for article in articles %}
+{% include 'articles/_article_teaser.html' %}
+{% endfor %}
+
+<div class="recent-articles-archive">
+    <h5><a href="{% url articles-archive %}">Article Archive</a></h5>
+</div>
+{% else %}
+<p>No articles to show.</p>
+{% endif %}

articles/templates/articles/article_archive.html

+{% extends 'base.html' %}
+
+{% block title %}Article Archive{% endblock %}
+
+{% block content %}
+<h2>Article Archives</h2>
+
+{% for date in date_list %}
+{% if forloop.first %}<ul class="articles-year-archive">{% endif %}
+<li><a href="{% url articles-year-archive date.year %}" title="View all articles published in {{ date|date:"Y" }}">{{ date|date:"Y" }}</a></li>
+{% if forloop.last %}</ul>
+<div style="clear: both;"></div>{% endif %}
+{% endfor %}
+
+{% if latest %}
+<h2>Latest Articles</h2>
+{% for article in latest %}
+{% include 'articles/_article_teaser.html' %}
+{% endfor %}
+{% endif %}
+{% endblock %}

articles/templates/articles/article_archive_day.html

+{% extends 'base.html' %}
+{% load humanize %}
+
+{% block title %}Article Archive: {{ day|naturalday }}{% endblock %}
+
+{% block content %}
+<h2>Article Archives: {{ day|naturalday }}</h2>
+
+{% if object_list %}
+{% for article in object_list %}
+{% include 'articles/_article_teaser.html' %}
+{% endfor %}
+{% else %}
+<p>No articles were posted on {{ day|date:"d F Y" }}.</p>
+{% endif %}
+
+<div style="text-align: center;">
+{% if previous_day %}<a href="{% url articles-day-archive previous_day.year,previous_day|date:"b",previous_day.day %}" title="View articles from {{ day|naturalday }}">&lsaquo; {{ previous_day|naturalday }}</a>{% endif %}
+{% if previous_day and next_day %} : {% endif %}
+{% if next_day %}<a href="{% url articles-day-archive next_day.year,next_day|date:"b",next_day.day %}" title="View articles from {{ day|naturalday }}">{{ next_day|naturalday }} &rsaquo;</a>{% endif %}
+</div>
+{% endblock %}

articles/templates/articles/article_archive_month.html

+{% extends 'base.html' %}
+{% load articles %}
+
+{% block title %}Article Archive: {{ month|date:"F Y" }}{% endblock %}
+
+{% block content %}
+{% comment %}
+<h2>Article Distribution</h2>
+
+<div style="text-align: center">
+<img src="http://chart.apis.google.com/chart?chs=200x875&amp;chd=t:{% articles_on_days month %}&amp;chm=N*f0*,000000,0,-1,11&amp;chds=0,{% most_articles_in_a_day month %},1,{% days_in_month month %}&amp;chxr=0,0,{% most_articles_in_a_day month %}&amp;cht=bhs&amp;chxt=x,y&amp;chxl=1:|{% days_in_month_range month %}|" alt="Article distribution for {{ month|date:"F Y" }}" title="Article distribution for {{ month|date:"F Y" }}" style="background-color: #fff; border: 1px solid #333; padding: 10px; margin: auto;" />
+</div>
+{% endcomment %}
+
+<h2>Article Archives: {{ month|date:"F Y" }} ({{ object_list|length }})</h2>
+
+{% for date in date_list %}
+{% if forloop.first %}<ul class="articles-month-archive">{% endif %}
+<li><a href="{% url articles-month-archive date.year,date|date:"b" %}" title="View all articles published in {{ date|date:"F Y" }}">{{ date|date:"F Y" }}</a></li>
+{% if forloop.last %}</ul>
+<div style="clear: both;"></div>{% endif %}
+{% endfor %}
+
+{% if object_list %}
+{% for article in object_list %}
+{% include 'articles/_article_teaser.html' %}
+{% endfor %}
+{% else %}
+<p>No articles have been posted for this month</p>
+{% endif %}
+
+<div style="text-align: center;">
+{% if previous_month %}<a href="{% url articles-month-archive previous_month.year,previous_month|date:"b" %}" title="View articles from the previous month">&lsaquo; {{ previous_month|date:"F Y" }}</a>{% endif %}
+{% if previous_month and next_month %} : {% endif %}
+{% if next_month %}<a href="{% url articles-month-archive next_month.year,next_month|date:"b" %}" title="View articles from the next month">{{ next_month|date:"F Y" }} &rsaquo;</a>{% endif %}
+</div>
+{% endblock %}

articles/templates/articles/article_archive_year.html

+{% extends 'base.html' %}
+{% load articles %}
+
+{% block title %}Article Archive: {{ year }}{% endblock %}
+
+{% block content %}
+<h2>Article Archives: {{ year }} ({{ object_list|length }})</h2>
+
+{% for date in date_list %}
+{% if forloop.first %}<ul class="articles-month-archive">{% endif %}
+<li><a href="{% url articles-month-archive date.year,date|date:"b" %}" title="View all articles published in {{ date|date:"F Y" }}">{{ date|date:"M Y" }}</a></li>
+{% if forloop.last %}</ul>
+<div style="clear: both;"></div>{% endif %}
+{% endfor %}
+
+<h2>Article Distribution</h2>
+
+<div style="text-align: center">
+<img src="http://chart.apis.google.com/chart?chs=350x200&amp;chd=t:{% articles_in_months year %}&amp;chm=N*f0*,000000,0,-1,11&amp;chds=0,{% most_articles_in_a_month year %}&amp;chxr=1,0,{% most_articles_in_a_month year %}&amp;cht=bvs&amp;chxt=x,y&amp;chxl=0:|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sept|Oct|Nov|Dec|" alt="Article distribution for {{ year }}" title="Article distribution for {{ year }}" style="background-color: #fff; border: 1px solid #333; padding: 10px; margin: auto;" />
+</div>
+
+{% if object_list %}
+<h2>Latest Articles</h2>
+{% for article in object_list %}
+{% include 'articles/_article_teaser.html' %}
+{% endfor %}
+{% endif %}
+{% endblock %}

articles/templates/articles/article_detail.html

+{% extends 'base.html' %}
+{% load articles markup comments humanize smiley_tags gravatar cache %}
+
+{% block title %}Article: {{ object.title }}{% ifnotequal page 1 %}, page {{ page }}{% endifnotequal %}{% endblock %}
+
+{% block keywords %}{{ object.keywords }}{% endblock %}
+{% block description %}{{ object.description }}{% endblock %}
+{% block extra-head %}
+{{ block.super }}
+
+<style type="text/css">
+blockquote.quote {
+    margin: 20px;
+    padding: 5px;
+    background-color: #cfc880;
+    color: #333;
+    border: 5px solid #333;
+    -moz-border-radius: 5px;
+    -webkit-border-radius: 5px;
+}
+</style>
+<script type="text/javascript">
+$(document).ready(function () {
+    $('blockquote').addClass('quote');
+    $('blockquote:has(div.codeblock)').removeClass('quote');
+    $('blockquote:has(img)').removeClass('quote');
+});
+</script>
+{% endblock %}
+
+{% block content %}
+{% cache 300 article object %}
+<h1>{{ object.title }}{% ifnotequal page 1 %}, page {{ page }}{% endifnotequal %}</h1>
+
+<h6 class="article-date">Posted {{ object.publish_date|naturalday }} at {{ object.publish_date|date:"P" }} by <a href="{% url articles-by-author object.author.username %}" title="View all articles by this author">{{ object.author }}</a></h6>
+
+<div class="bookmark-links">
+    <a href="{% url send-article object.id %}" title="E-mail this article to someone"><img src="{{ MEDIA_URL }}images/mail.png" height="16" width="16" alt="E-mail this article" /></a>
+
+    <a href="http://reddit.com/submit" onclick="window.location='http://reddit.com/submit?url='+encodeURIComponent(location.href)+'&amp;title={{ object.title|urlencode }}';return false;" title="Bookmark this article on Reddit"><img src="{{ MEDIA_URL }}images/Reddit-16x16.png" height="16" width="16" alt="Reddit" /></a>
+
+    <a href="http://delicious.com/post" onclick="window.open('http://delicious.com/post?v=5&amp;noui&amp;jump=close&amp;url='+encodeURIComponent(location.href)+'&amp;title='+encodeURIComponent(document.title), 'delicious','toolbar=no,width=550,height=550'); return false;" title="Bookmark this on Delicious"><img src="{{ MEDIA_URL }}images/delicious-16x16.png" height="16" width="16" alt="del.icio.us" /></a>
+
+    <a href="http://digg.com/submit" onclick="window.location='http://digg.com/submit?url='+encodeURIComponent(location.href)+'&amp;title={{ object.title|urlencode }}';return false;" title="Digg this!"><img src="{{ MEDIA_URL }}images/digg-16x16.png" height="16" width="16" alt="digg" /></a>
+
+    <a href="http://www.mixx.com/submit" onclick="window.location='http://www.mixx.com/submit?page_url='+encodeURIComponent(location.href)+'&amp;title={{ object.title|urlencode }}&amp;description={{ object.description|urlencode }}';return false;" title="Add to Mixx!"><img src="http://www.mixx.com/images/buttons/mixx-button4.png" alt="Add to Mixx!" height="16" width="16" /></a>
+
+    <a href="http://www.stumbleupon.com/submit" onclick="window.location='http://www.stumbleupon.com/submit?url='+encodeURIComponent(location.href)+'&amp;title={{ object.title|urlencode }}';return false;"><img src="{{ MEDIA_URL }}images/Stumbleupon-16x16.png" alt="StumbleUpon" height="16" width="16"></a>
+
+    <a href="{{ MEDIA_URL }}articles/pdfs/{{ object.slug }}.pdf"><img src="{{ MEDIA_URL }}images/pdf-16x16.png" alt="Get the PDF version" height="16" width="16"></a>
+
+    {% if user.is_superuser %}
+    <a href="{% url admin_articles_article_change object.id %}"><img src="{{ MEDIA_URL }}images/edit.png" alt="Edit this article" height="16" width="16"></a>
+    {% endif %}
+</div>
+<div class="clear"></div>
+
+<div style="text-align: center">
+    <script type="text/javascript">
+    <!--
+    google_ad_client = "pub-7828344619183032";
+    /* 468x60, created 7/10/08 */
+    google_ad_slot = "3750736994";
+    google_ad_width = 468;
+    google_ad_height = 60;
+    //-->
+    </script>
+    <script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"></script>
+</div>
+
+{% comment %}{% if object.description %}<p class="articles-description">{{ object.description|restructuredtext }}</p>{% endif %}{% endcomment %}
+
+<div class="article">
+{{ object.rendered_content|smileys|safe }}
+</div>
+
+<div class="categories" style="width: 100%; font-size: 0.8em">Filed in {% if object.categories.count %}{% for category in object.categories.all %}{% if not forloop.first %}{% ifnotequal object.categories.count 2 %},{% endifnotequal %} {% if forloop.last %}and{% endif %} {% endif %}<a href="{% url articles-display-category category.slug %}" title="View all articles in this category">{{ category }}</a>{% endfor %}{% else %}<a href="{% url articles-display-uncategorized %}" title="View all uncategorized articles">Uncategorized</a>{% endif %}</div>
+<div style="clear: both"></div>
+
+{% if object.followups.active.count %}
+<hr class="content" />
+
+<h4 class="hasfollowup-header">Follow-Up Articles for "{{ object.title }}"</h4>
+{% for fu in object.followups.active %}
+{% if forloop.first %}<ul class="followups">{% endif %}
+    <li>
+        <a href="{{ fu.get_absolute_url }}" title="Read this follow-up article">{{ fu.title }}</a>, posted
+        {{ fu.publish_date|naturalday }} at {{ fu.publish_date|date:"P" }}
+    </li>
+{% if forloop.last %}</ul>{% endif %}
+{% endfor %}
+{% endif %}
+
+{% if object.followup_for.active.count %}
+<hr class="content" />
+
+<h4 class="followup-header">"{{ object.title }}" follows up on th{{ object.followup_for.count|pluralize:"is,ese" }} article{{ object.followup_for.count|pluralize }}</h4>
+{% for fu in object.followup_for.active %}
+{% if forloop.first %}<ul class="followups">{% endif %}
+    <li>
+        <a href="{{ fu.get_absolute_url }}" title="Read this article">{{ fu.title }}</a>, posted
+        {{ fu.publish_date|naturalday }} at {{ fu.publish_date|date:"P" }}
+    </li>
+{% if forloop.last %}</ul>{% endif %}
+{% endfor %}
+{% endif %}
+
+{% if object.related_articles.active.count %}
+<hr class="content" />
+
+<h4 class="related-header">Articles Related to "{{ object.title }}"</h4>
+{% for ra in object.related_articles.active %}
+{% if forloop.first %}<ul class="related-articles">{% endif %}
+    <li>
+        <a href="{{ ra.get_absolute_url }}" title="Read this related article">{{ ra.title }}</a>, posted
+        {{ ra.publish_date|naturalday }} at {{ ra.publish_date|date:"P" }}
+    </li>
+{% if forloop.last %}</ul>{% endif %}
+{% endfor %}
+{% endif %}
+
+<hr class="content" />
+
+{% if object.previous_article %}<div>
+    <strong>Previous Article:</strong>
+    <a href="{{ object.previous_article.get_absolute_url }}">{{ object.previous_article.title }}</a>
+</div>{% endif %}
+{% if object.next_article %}<div>
+    <strong>Next Article:</strong>
+    <a href="{{ object.next_article.get_absolute_url }}">{{ object.next_article.title }}</a>
+</div>{% endif %}
+
+{% ifequal page object.page_count %}
+<hr class="content" />
+
+<h4 class="comments-header" id="comments">Comments</h4>
+{% get_comment_list for object as comment_list %}
+{% if comment_list|length %}
+
+{% for comment in comment_list %}
+{% if forloop.first %}<table width="100%" class="comment-list">{% endif %}
+    <tr class="{% ifequal comment.user object.author %}author{% endifequal %} {% 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|smileys|safe|urlizetrunc:40 }}
+            <div class="posted-by quiet">
+                Posted on
+                {{ comment.submit_date|date:"j N Y \a\t P" }}
+            </div>
+        </td>
+    </tr>
+{% if forloop.last %}</table>{% endif %}
+{% endfor %}
+{% else %}
+<p>No comments have been posted for this article.</p>
+{% endif %}
+
+<hr class="content" />
+
+<h4 class="postcomment-header">Post a Comment</h4>
+{% render_comment_form for object %}
+{% endifequal %}
+{% endcache %}
+{% endblock %}

articles/templates/articles/article_list.html

+{% extends 'base.html' %}
+{% load articles humanize %}
+
+{% block title %}Article List, page {{ page_obj.number }}{% endblock %}
+
+{% block content %}
+<h2>Article Archives, page {{ page_obj.number }}</h2>
+<div class="quiet">
+    Viewing article{{ page_obj.object_list|length|pluralize }} {{ page_obj.start_index }} - {{ page_obj.end_index }} of {{ paginator.count|intcomma }} total article{{ paginator.count|pluralize }}.
+</div>
+
+{% for article in object_list %}
+{% include 'articles/_article_teaser.html' %}
+{% empty %}
+<p>No articles to display.</p>
+{% endfor %}
+
+{% if is_paginated %}
+<div class="pagination">
+<ul>
+    {% if page_obj.has_previous %}<li><a href="{% url articles-archive-page page_obj.previous_page_number %}">&lsaquo; newer</a></li>{% endif %}
+    {% for p in paginator.page_range %}
+    <li>
+        {% ifequal p page_obj.number %}<span class="active">{{ p }}</span>
+        {% else %}<a href="{% url articles-archive-page p %}">{{ p }}</a>{% endifequal %}
+    </li>
+    {% endfor %}
+    {% if page_obj.has_next %}<li><a href="{% url articles-archive-page page_obj.next_page_number %}">older &rsaquo;</a></li>{% endif %}
+</ul>
+</div>
+{% endif %}
+{% endblock %}

articles/templates/articles/article_list_uncategorized.html

+{% extends 'base.html' %}
+{% load articles %}
+
+{% block title %}Uncategorized Articles{% endblock %}
+
+{% block content %}
+<h2>Uncategorized Articles ({{ object_list|length }})</h2>
+
+{% for article in object_list %}
+{% include 'articles/_article_teaser.html' %}
+{% empty %}
+<p>No articles to display.</p>
+{% endfor %}
+{% endblock %}

articles/templates/articles/category_detail.html

+{% extends 'base.html' %}
+{% load articles humanize %}
+
+{% block title %}Article Category: {{ object.name }}, page {{ page_obj.number }}{% endblock %}
+
+{% block keywords %}article, category, {{ object.name }}{% endblock %}
+{% block description %}Articles posted in the {{ object.name }} category.{% endblock %}
+{% block extra-head %}
+<link rel="alternate" type="application/rss+xml" title="Code Koala Blog Articles Tagged: {{ object.name }}" href="/feeds/tags/{{ object.slug }}.rss" />
+{% endblock %}
+
+{% block content %}
+<h2>
+    Article Category: {{ object.name }}, page {{ page_obj.number }}
+    <a href="/feeds/tags/{{ object.slug }}.rss"><img src="/static/images/feed-icon-14x14.png" height="14" width="14" alt="RSS Feed" /></a>
+</h2>
+<div class="quiet">
+    Viewing article{{ page_obj.object_list|length|pluralize }} {{ page_obj.start_index }} - {{ page_obj.end_index }} of {{ paginator.count|intcomma }} total article{{ paginator.count|pluralize }}.
+</div>
+
+{% for article in page_obj.object_list %}
+{% include 'articles/_article_teaser.html' %}
+{% empty %}
+<p>No articles have been posted in this category.</p>
+{% endfor %}
+
+{% if is_paginated %}
+<div class="pagination">
+<ul>
+    {% if page_obj.has_previous %}<li><a href="{% url articles-display-category-page object.slug,page_obj.previous_page_number %}">&lsaquo; newer</a></li>{% endif %}
+    {% for p in paginator.page_range %}
+    <li>
+        {% ifequal p page_obj.number %}<span class="active">{{ p }}</span>
+        {% else %}<a href="{% url articles-display-category-page object.slug,p %}">{{ p }}</a>{% endifequal %}
+    </li>
+    {% endfor %}
+    {% if page_obj.has_next %}<li><a href="{% url articles-display-category-page object.slug,page_obj.next_page_number %}">older &rsaquo;</a></li>{% endif %}
+</ul>
+</div>
+{% endif %}
+{% endblock %}

articles/templates/articles/list_articles_by_author.html

+{% extends 'base.html' %}
+
+{% block title %}Articles by {{ author }}{% endblock %}
+
+{% block content %}
+<h2>Articles by {{ author }}</h2>
+
+{% if articles %}
+{% for article in articles %}
+{% include 'articles/_article_teaser.html' %}
+{% endfor %}
+{% else %}
+<p>No articles have been posted by this author</p>
+{% endif %}
+{% endblock %}

articles/templates/articles/list_uncategorized_articles.html

+{% extends 'base.html' %}
+
+{% block title %}Uncategorized Articles{% endblock %}
+
+{% block content %}
+<h2>Uncategorized Articles</h2>
+
+{% if articles %}
+{% for article in articles %}
+{% include 'articles/_article_teaser.html' %}
+{% endfor %}
+{% else %}
+<p>There are no uncategorized articles</p>
+{% endif %}
+{% endblock %}

articles/templates/articles/send_article.html

+{% extends 'base.html' %}
+
+{% block title %}Send Article: {{ article.title }}{% endblock %}
+
+{% block keywords %}send article link, e-mail link{% endblock %}
+{% block description %}This page allows you to send a link to {{ article.title }} to people you think might find it useful.{% endblock %}
+
+{% block content %}
+<h2>Send Article: {{ article.title }}</h2>
+
+{% if error %}<ul class="errorlist"><li>{{ error }}</li></ul>{% endif %}
+
+<p>Please fill out the form below in order to send a link to this article to whomever you think would benefit from reading it.</p>
+
+<form action="{% url send-article article.id %}" method="post">
+<table>
+{{ form }}
+    <tr>
+        <td colspan="2" class="buttons">
+            <input type="submit" value="Send Article" />
+        </td>
+    </tr>
+</table>
+</form>
+{% endblock %}

articles/templates/articles/send_article_email.txt

+{{ sender.name }} would like you to read an article entitled "{{ article.title }}" on {{ site.name }}.
+
+{% if message %}A personalized message from {{ sender.name }} follows:
+{{ message }}{% endif %}
+
+Click the following link to read the article!  http://{{ site.domain }}{{ article.get_absolute_url }}
+
+Thanks!

articles/templatetags/__init__.py

Empty file added.

articles/templatetags/articles.py

+from django import template
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from pygments import highlight
+from pygments.lexers import get_lexer_by_name
+from pygments.formatters import HtmlFormatter
+from ..models import Article, Category
+import datetime, calendar
+
+register = template.Library()
+
+def show_recent_articles(context, count=5):
+    articles = Article.objects.active()[:count]
+    return {'articles': articles}
+register.inclusion_tag('articles/_recent_articles.html', takes_context=True)(show_recent_articles)
+
+def show_categories():
+    return {'categories': Category.objects.active(),
+            'uncategorized': len(Article.objects.uncategorized())}
+register.inclusion_tag('articles/_categories.html')(show_categories)
+
+def get_article_page(article, page=1):
+    return article.get_page(page)
+register.simple_tag(get_article_page)
+
+def month_counts(year):
+    months = [0 for i in range(12)]
+    for a in Article.objects.filter(publish_date__year=int(year)):
+        months[a.publish_date.month - 1] += 1
+    return months
+
+def most_articles_in_a_day(year):
+    return max(day_counts(year))
+register.simple_tag(most_articles_in_a_day)
+
+def most_articles_in_a_month(year):
+    return max(month_counts(year))
+register.simple_tag(most_articles_in_a_month)
+
+def articles_in_months(year):
+    return ','.join([str(c) for c in month_counts(year)])
+register.simple_tag(articles_in_months)
+from django.conf.urls.defaults import *
+from django.views.generic import date_based, list_detail
+from .models import Category, Article
+import views
+from datetime import datetime
+
+categories = {
+    'queryset': Category.objects.active().iterator(),
+}
+
+articles = {
+    'queryset': Article.objects.active().iterator(),
+    'date_field': 'publish_date',
+    'extra_context': {'page': 1},
+}
+
+uncategorized = {
+    'queryset': Article.objects.uncategorized(),
+    'paginate_by': 10,
+    'allow_empty': True,
+    'template_name': 'articles/article_list_uncategorized.html'
+}
+
+articles_month = articles.copy()
+articles_month['allow_empty'] = True
+
+articles_year = articles_month.copy()
+articles_year['make_object_list'] = True
+
+articles_archive = articles_month.copy()
+articles_archive['paginate_by'] = 10
+del articles_archive['date_field']
+
+urlpatterns = patterns('',
+    url(r'^author/(?P<user_id>[-\w]+)/$',
+        views.list_articles_by_author, name='articles_by_author'),
+
+    # categories
+    url(r'^category/(?P<slug>[-\w]+)/$',
+        views.category_detail, name='articles_display_category'),
+    url(r'^category/(?P<slug>[-\w]+)/page/(?P<page>\d+)/$',
+        views.category_detail, name='articles_display_category_page'),
+    url(r'^uncategorized/$',
+        list_detail.object_list, uncategorized, name='articles_display_uncategorized'),
+    url(r'^uncategorized/page/(?P<page>\d+)/$',
+        list_detail.object_list, uncategorized, name='articles_display_uncategorized_page'),
+
+    # articles
+    url(r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/(?P<slug>[-\w]+)/',
+        date_based.object_detail, articles, name='articles_display_article'),
+    url(r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/(?P<day>\w{1,2})/$',
+        date_based.archive_day, articles_month, name='articles_day_archive'),
+    url(r'^(?P<year>\d{4})/(?P<month>[a-z]{3})/$',
+        date_based.archive_month, articles_month, name='articles_month_archive'),
+    url(r'^(?P<year>\d{4})/$',
+        date_based.archive_year, articles_year, name='articles_year_archive'),
+
+    url(r'^$',
+        list_detail.object_list, articles_archive, name='articles_archive'),
+    url(r'^page/(?P<page>\d+)/$',
+        list_detail.object_list, articles_archive, name='articles_archive_page'),
+
+    url(r'^send/article/(?P<article_id>\d+)/$', views.send_article, name='send_article'),
+)

articles/views.py

+from django.shortcuts import render_to_response, get_object_or_404
+from django.template import RequestContext, loader, Context
+from django.contrib.sites.models import Site
+from django.core.mail import send_mail
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from django.core.paginator import Paginator
+from django.http import HttpResponseRedirect
+from django.conf import settings
+from .models import Article, Category
+from .forms import SendArticleForm
+
+def list_articles_by_author(request, user_id,
+                            template='articles/list_articles_by_author.html'):
+    user = get_object_or_404(User, username=user_id)
+
+    return render_to_response(template,
+                              {'articles': user.article_set.active(),
+                               'author': user},
+                              context_instance=RequestContext(request))
+
+def category_detail(request, slug, page=1, \
+                    template='articles/category_detail.html', paginate_by=10):
+    """
+    Displays a blog category and articles that have been assigned to that
+    category.  Articles within a given category are paginated by 10 entries.
+    """
+    category = get_object_or_404(Category,
+                                 slug=slug,
+                                 is_active=True)
+
+    paginator = Paginator(category.article_set.active(),
+                          paginate_by,
+                          orphans=5)
+    page_obj = paginator.page(int(page))
+
+    return render_to_response(template,
+                              {'paginator': paginator,
+                               'page_obj': page_obj,
+                               'object': category,
+                               'is_paginated': (paginator.num_pages > 1)},
+                              context_instance=RequestContext(request))
+
+def display_article_page(request, year, month, day, slug, page):
+    article = get_object_or_404(Article, slug=slug, is_active=True)
+
+    return render_to_response('articles/article_detail.html',
+                              {'object': article, 'page': int(page)},
+                              context_instance=RequestContext(request))
+
+def send_article(request, article_id):
+    article = get_object_or_404(Article,
+                                pk=int(article_id),
+                                is_active=True)
+    error = None
+
+    if request.method == 'POST':
+        form = SendArticleForm(request.POST)
+        if form.is_valid():
+            site = Site.objects.get_current()
+            name = form.cleaned_data['name']
+            email = form.cleaned_data['email']
+
+            c = Context({
+                'article': article,
+                'sender': {
+                    'name': name,
+                    'email': email
+                },
+                'message': form.cleaned_data['message'],
+                'site': site,
+            })
+            t = loader.get_template('articles/send_article_email.txt')
+
+            subject = '%s %s has sent you an article on %s!' % (settings.EMAIL_SUBJECT_PREFIX,
+                                                                name, site.name)
+            message = t.render(c)
+
+            try:
+                for r in form.cleaned_data['receivers'].split(','):
+                    send_mail(subject, message, email, [r.strip()])
+            except:
+                error = 'Failed to send article to %s' % r
+            else:
+                error = '%s was sent to all recipients' % article.title
+
+                return HttpResponseRedirect(article.get_absolute_url())
+    else:
+        form = SendArticleForm()
+
+    return render_to_response('articles/send_article.html',
+                              {'form': form,
+                               'article': article,
+                               'error': error},
+                              context_instance=RequestContext(request))