Commits

Josh VanderLinden committed 7c5d218 Merge

Merge with article_status

Comments (0)

Files changed (11)

 * Ability to post in the future
 * Article expiration facilities
 * Articles from email
+* Article statuses--"draft" and "finished" are there by default
 * Allows articles to be written in plain text/HTML or using Markdown,
   ReStructured Text, or Textile markup
 * Related articles
 * Follow-up articles
-* Disqus comments
+* Comments by Disqus
 * Article archive, with pagination
 * Internationalization-ready
 * Detects links in articles and creates a per-article index for you
         'acknowledge': True,
     }
 
+Article Statuses
+================
+
+As of ``1.9.6``, you may specify the state of an article when you save it.
+This allows you to begin composing an article, save it, and come back later to
+finish it.  In the past, this behavior was handled by not setting a publish
+date for the article.  However, saving an unfinished article with a non-live
+status allows superusers to view the article on the site as though it were
+live.  In the future, I plan to allow authors to view non-live versions of
+their articles.
+
+The default status for an article will always be the Article Status object with
+the lowest ``ordering`` value.  This includes negative integers.  If you want
+all articles to be ``Finished`` by default, go ahead and update the
+``ordering`` on that object to be less than the ``ordering`` value for the
+``Draft`` object (and/or any others you create).
+
 Good luck!  Please contact me with any questions or concerns you have with the
 project!
 

articles/__init__.py

-__version__ = '1.9.4-pre1'
+__version__ = '1.9.6-pre1'
 
 """
 The Pygments reStructuredText directive

articles/admin.py

 from django.contrib.sites.models import Site
 from django.utils.translation import ugettext_lazy as _
 from forms import ArticleAdminForm
-from models import Tag, Article
+from models import Tag, Article, ArticleStatus
+
+class ArticleStatusAdmin(admin.ModelAdmin):
+    list_display = ('name', 'is_live')
+    list_filter = ('is_live',)
+    search_fields = ('name',)
 
 class ArticleAdmin(admin.ModelAdmin):
-    list_display = ('title', 'author', 'publish_date', 'expiration_date', 'is_active')
-    list_filter = ('author', 'is_active', 'publish_date', 'expiration_date', 'sites')
+    list_display = ('title', 'status', 'author', 'publish_date', 'expiration_date', 'is_active')
+    list_filter = ('author', 'status', 'is_active', 'publish_date', 'expiration_date', 'sites')
     list_per_page = 25
     search_fields = ('title', 'keywords', 'description', 'content')
     date_hierarchy = 'publish_date'
     form = ArticleAdminForm
 
     fieldsets = (
-        (None, {'fields': ('title', 'content', 'tags', 'markup')}),
+        (None, {'fields': ('title', 'content', 'tags', 'markup', 'status')}),
         ('Metadata', {
             'fields': ('keywords', 'description',),
             'classes': ('collapse',)
 
 admin.site.register(Tag)
 admin.site.register(Article, ArticleAdmin)
+admin.site.register(ArticleStatus, ArticleStatusAdmin)
+

articles/feeds.py

         articles = cache.get(key)
 
         if articles is None:
-            articles = list(Article.objects.active().order_by('-publish_date')[:15])
+            articles = list(Article.objects.live().order_by('-publish_date')[:15])
             cache.set(key, articles, FEED_TIMEOUT)
 
         return articles
         articles = cache.get(key)
 
         if articles is None:
-            articles = list(obj.article_set.active().order_by('-publish_date'))
+            articles = list(obj.article_set.live().order_by('-publish_date'))
             cache.set(key, articles, FEED_TIMEOUT)
 
         return articles

articles/fixtures/initial_data.json

+[
+    {
+        "pk": 1, 
+        "model": "articles.articlestatus", 
+        "fields": {
+            "name": "Draft",
+            "ordering": 0,
+            "is_live": 0
+        }
+    },
+    {
+        "pk": 2, 
+        "model": "articles.articlestatus", 
+        "fields": {
+            "name": "Finished",
+            "ordering": 1,
+            "is_live": 1
+        }
+    }
+]
+

articles/fixtures/users.json

+[
+    {
+        "pk": 1, 
+        "model": "auth.user", 
+        "fields": {
+            "username": "superuser",
+            "is_superuser": 1
+        }
+    }
+]

articles/models.py

     class Meta:
         ordering = ('name',)
 
+class ArticleStatusManager(models.Manager):
+    def default(self):
+        return self.all()[0]
+
+class ArticleStatus(models.Model):
+    name = models.CharField(max_length=50)
+    ordering = models.IntegerField(default=0)
+    is_live = models.BooleanField(default=False, blank=True)
+
+    objects = ArticleStatusManager()
+
+    class Meta:
+        ordering = ('ordering', 'name')
+        verbose_name_plural = _('Article statuses')
+
+    def __unicode__(self):
+        return self.name
+
 class ArticleManager(models.Manager):
     def active(self):
         """
                 publish_date__lte=now,
                 is_active=True)
 
+    def live(self, user=None):
+        """Retrieves all live articles"""
+
+        qs = self.active()
+
+        if user is not None and user.is_superuser:
+            # superusers get to see all articles
+            return qs
+        else:
+            # only show live articles to regular users
+            return qs.filter(status__is_live=True)
+
 MARKUP_HELP = _("""Select the type of markup you are using in this article.
 <ul>
 <li><a href="http://daringfireball.net/projects/markdown/basics" target="_blank">Markdown Guide</a></li>
 class Article(models.Model):
     title = models.CharField(max_length=100)
     slug = models.SlugField(unique_for_year='publish_date')
+    status = models.ForeignKey(ArticleStatus, default=ArticleStatus.objects.default)
     author = models.ForeignKey(User)
     sites = models.ManyToManyField(Site, blank=True)
 
     teaser = property(_get_teaser)
 
     def get_next_article(self):
-        """Determines the next active article"""
+        """Determines the next live article"""
 
         if not self._next:
             try:
-                qs = Article.objects.active().exclude(id__exact=self.id)
+                qs = Article.objects.live().exclude(id__exact=self.id)
                 article = qs.filter(publish_date__gte=self.publish_date).order_by('publish_date')[0]
             except (Article.DoesNotExist, IndexError):
                 article = None
         return self._next
 
     def get_previous_article(self):
-        """Determines the previous active article"""
+        """Determines the previous live article"""
 
         if not self._previous:
             try:
-                qs = Article.objects.active().exclude(id__exact=self.id)
+                qs = Article.objects.live().exclude(id__exact=self.id)
                 article = qs.filter(publish_date__lte=self.publish_date).order_by('-publish_date')[0]
             except (Article.DoesNotExist, IndexError):
                 article = None

articles/templates/articles/_meta.html

     <h4>{% trans 'Tags' %}</h4>
     <p>{% if article.tags.count %}{% for tag in article.tags.all %}<a href="{{ tag.get_absolute_url }}">{{ tag.name }}</a> {% endfor %}{% else %}None{% endif %}</p>
 
-    {% for fu in article.followups.active %}
+    {% for fu in article.followups.live %}
     {% if forloop.first %}<h4 class="hasfollowup-header">{% trans 'Follow-Up Articles' %}</h4>
 
     <ul class="followups">{% endif %}
     {% if forloop.last %}</ul>{% endif %}
     {% endfor %}
 
-    {% for fu in article.followup_for.active %}
+    {% for fu in article.followup_for.live %}
     {% if forloop.first %}<h4 class="followup-header">{% trans 'Follows Up On' %}</h4>
 
     <ul class="followups">{% endif %}
     {% if forloop.last %}</ul>{% endif %}
     {% endfor %}
 
-    {% for ra in article.related_articles.active %}
+    {% for ra in article.related_articles.live %}
     {% if forloop.first %}<h4 class="related-header">{% trans 'Related Articles' %}</h4>
 
     <ul class="related-articles">{% endif %}

articles/templatetags/article_tags.py

 
 class GetCategoriesNode(template.Node):
     """
-    Retrieves a list of active article tags and places it into the context
+    Retrieves a list of live article tags and places it into the context
     """
     def __init__(self, varname):
         self.varname = varname
 
 def get_article_tags(parser, token):
     """
-    Retrieves a list of active article tags and places it into the context
+    Retrieves a list of live article tags and places it into the context
     """
     args = token.split_contents()
     argc = len(args)
         else:
             order = 'publish_date'
 
-        # get the active articles in the appropriate order
-        articles = Article.objects.active().order_by(order).select_related()
+        user = context.get('user', None)
+
+        # get the live articles in the appropriate order
+        articles = Article.objects.live(user=user).order_by(order).select_related()
 
         if self.count:
             # if we have a number of articles to retrieve, pull the first of them
         dt_archives = cache.get(cache_key)
         if dt_archives is None:
             archives = {}
+            user = context.get('user', None)
 
-            # iterate over all active articles
-            for article in Article.objects.active().select_related():
+            # iterate over all live articles
+            for article in Article.objects.live(user=user).select_related():
                 pub = article.publish_date
 
                 # see if we already have an article in this year

articles/tests.py

+from django.contrib.auth.models import User
+from django.test import TestCase
+
+from models import Article, ArticleStatus, Tag
+
+class ArticleTestCase(TestCase):
+    fixtures = ['users']
+
+    def setUp(self):
+        self.superuser = User.objects.filter(is_superuser=True)[0]
+
+    def new_article(self, title, content, tags=[], **kwargs):
+        a = Article(
+            title=title,
+            content=content,
+            author=self.superuser,
+            **kwargs
+        )
+        a.save()
+
+        if tags:
+            a.tags = tags
+            a.save()
+
+        return a
+
+    def test_unique_slug(self):
+        """Unique slugs"""
+
+        a1 = self.new_article('Same Slug', 'Some content')
+        a2 = self.new_article('Same Slug', 'Some more content')
+
+        self.assertNotEqual(a1.slug, a2.slug)
+
+    def test_active_articles(self):
+        """Active articles"""
+
+        a1 = self.new_article('New Article', 'This is a new article')
+        a2 = self.new_article('New Article', 'This is a new article', is_active=False)
+
+        self.assertEquals(len(Article.objects.active()), 1)
+
+    def test_default_status(self):
+        """Default status selection"""
+
+        default_status = ArticleStatus.objects.default()
+        other_status = ArticleStatus.objects.exclude(id=default_status.id)[0]
+
+        self.assertTrue(default_status.ordering < other_status.ordering)
+
+    def test_tagged_article_status(self):
+        """Tagged article status"""
+
+        t = Tag.objects.create(name='Django')
+
+        draft = ArticleStatus.objects.filter(is_live=False)[0]
+        finished = ArticleStatus.objects.filter(is_live=True)[0]
+
+        a1 = self.new_article('Tagged', 'draft', status=draft, tags=[t])
+        a2 = self.new_article('Tagged', 'finished', status=finished, tags=[t])
+
+        self.assertEqual(len(t.article_set.live()), 1)
+        self.assertEqual(len(t.article_set.active()), 2)
+
+    def test_new_article_status(self):
+        """New article status is default"""
+
+        default_status = ArticleStatus.objects.default()
+        article = self.new_article('New Article', 'This is a new article')
+        self.failUnless(article.status == default_status)
+
+    def test_live_articles(self):
+        """Only live articles"""
+
+        live_status = ArticleStatus.objects.filter(is_live=True)[0]
+        a1 = self.new_article('New Article', 'This is a new article')
+        a2 = self.new_article('New Article', 'This is a new article', is_active=False)
+        a3 = self.new_article('New Article', 'This is a new article', status=live_status)
+        a4 = self.new_article('New Article', 'This is a new article', status=live_status)
+
+        self.assertEquals(len(Article.objects.live()), 2)
+        self.assertEquals(len(Article.objects.live(self.superuser)), 3)
+

articles/views.py

     context = {}
     if tag:
         tag = get_object_or_404(Tag, name__iexact=tag)
-        articles = tag.article_set.active().select_related()
+        articles = tag.article_set.live(user=request.user).select_related()
         template = 'articles/display_tag.html'
         context['tag'] = tag
 
     elif username:
         # listing articles by a particular author
         user = get_object_or_404(User, username=username)
-        articles = user.article_set.active()
+        articles = user.article_set.live(user=request.user)
         template = 'articles/by_author.html'
         context['author'] = user
 
         # listing articles in a given month and year
         year = int(year)
         month = int(month)
-        articles = Article.objects.active().select_related().filter(publish_date__year=year, publish_date__month=month)
+        articles = Article.objects.live(user=request.user).select_related().filter(publish_date__year=year, publish_date__month=month)
         template = 'articles/in_month.html'
         context['month'] = datetime(year, month, 1)
 
     else:
         # listing articles with no particular filtering
-        articles = Article.objects.active()
+        articles = Article.objects.live(user=request.user)
         template = 'articles/article_list.html'
 
     # paginate the articles
     """Displays a single article."""
 
     try:
-        article = Article.objects.active().get(publish_date__year=year, slug=slug)
+        article = Article.objects.live(user=request.user).get(publish_date__year=year, slug=slug)
     except Article.DoesNotExist:
         raise Http404
 
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.