Adam Knight avatar Adam Knight committed 0accf6c

New name for comments; fixed Drupal import; other updates.

Comments (0)

Files changed (22)

 
 SitePoet is the realization of something I started with PHP a long time ago.  I've always wanted a site I'd written but the mechanics of the database and so on really bog me down and made the several attempts fail.  So, this time, I'm going to start out simple and then build slowly, starting with something that can take a Drupal import and then adding features to that later on.
 
-h1. Plan
-
-Since other CMSes tend to feel restrictive after a while, I plan on using Django and public classes and add-ons to create a CMS for my own use.  Since I can specify the function of the site myself, I expect the following:
+h1. Today
 
 *CMS*
 
 * Dated posts (blog-ish or news-ish)
 * Static pages (/about or products)
-* Re-usable blocks, each with a target zone for the template (left sidebar, footer, etc.)
-* Versioned posts.  The mechanics of this could get interesting, but when a post is changed, the old copy should be around for the sake of archiving.  It should be available to the template as well, in case I choose to publish a version history.
-* Private posts.  The system should use Django's groups to allow a post to be read only by a specific group of logged-in users.
-* Multiple copyrights.  There should be License objects that hold either a logo or a text blurb along with a title for various methods of license (public domain, copyright, CC, etc.) and entries should have a menu to change it.
-* Image attachments via ImageField
-* General file attachments
 * Nested comments.  I love this feature in Drupal and plan to keep using it.  Hopefully I can make it more visible.
 * Tagging.  Moving away from categories, the site should allow for tags and include some navigation facility for them as well.
-* Spam protection.  Using either Akismet or Mollom, the site should check for and filter spammy content.  This may mean the introduction of a moderation queue.
-* Email subscriptions to entries.  Readers should be able to add themselves to a list per-entry to be notified when comments are made on the entry.  They must also have a way of removing themselves from this list.
-* Subscriptions to new content.  The site must have RSS and/or Atom feeds as well.  These should be: site, site comments, per-page comments, and per-tag.
-* Statistics.  The site should keep statistics internally for every page hit.  The information logged should be optimized so that there is minimal database locking.  This should happen in middleware and be outside of any transaction middleware that would block a result.  It should log at the end of the request so the result code can be included and should include the page processing time.
 
 *URLs*
 
-* URL management should be central.
-* When an object is saved, it should save a redirect with its current URL and its content type.
-* If the URL ever changes, that redirect will start to get hit and serve as a safety net for the object.  It will always redirect to the object's current URL as returned by get_absolute_url.
+* URL management is central. Objects know their URL and when it changes they register the new URL.  This has the cumulative effect of the system knowing every URL an object has ever had and then redirecting users to the object's new address properly.  When an object is unavailable the redirect machinery will properly return a 410 Gone response.
+
+h1. Near-Term
+
+* Template documentation
+* Decent default template
+* Subscriptions to new content.  The site must have RSS and/or Atom feeds as well.  These should be: site, site comments, per-page comments, and per-tag.
+* Versioned posts.  The mechanics of this could get interesting, but when a post is changed, the old copy should be around for the sake of archiving.  It should be available to the template as well, in case I choose to publish a version history.
+* Multiple copyrights.  There should be License objects that hold either a logo or a text blurb along with a title for various methods of license (public domain, copyright, CC, etc.) and entries should have a menu to change it.
+* General file attachments
+* Image attachments via ImageField
+* Add ability to set the default state of comments for sites that do not want to use it. Don't bother loading comment-related features (such as template tags) in such a case.
+
+h1. Someday
 
+* Allow SITE_ID and other variables to be set from a SitePoetSite object, or something like that.  That way one set of FCGI instances can serve all hosted sites instead of needing one class per site so that the settings file is done right.
+* Private posts.  The system should use Django's groups to allow a post to be read only by a specific group of logged-in users.
+* Spam protection.  Using either Akismet or Mollom, the site should check for and filter spammy content.  This may mean the introduction of a moderation queue.
 
-h1. ToDo Ideas
+h1. Maybe
 
-* Remove the archive browser from being rooted at /; add another urls.py entry for it so it can be repurposed.
-* Add apps for codepoetry and macgeekery, such as products (manages software releases, including appcasts), projects (connects to bitbucket, maybe?), questions (asked and answered, with email notifications), and user profiles (AdSense IDs, among other things).
 * Allow Drupal import to import a specific taxonomy as the section list instead of content types.
-* Allow SITE_ID and other variables to be set from a SitePoetSite object, or something like that.  That way one set of FCGI instances can serve all hosted sites instead of needing one class per site so that the settings file is done right.
-* Add ability to set the default state of comments for sites that do not want to use it. Don't bother loading comment-related features (such as template tags) in such a case.

comments/__init__.py

-# Based on an idea posted at http://root.abl.es/methods/1524/django-threaded-comments/
-
-from models import MPTTComment
-from forms import MPTTCommentForm
-
-def get_model():
-    return MPTTComment
-
-def get_form():
-    return MPTTCommentForm

comments/admin.py

-from django.contrib import admin
-import models
-
-class MPTTCommentAdmin(admin.ModelAdmin):
-    pass
-
-admin.site.register(models.MPTTComment, MPTTCommentAdmin);

comments/forms.py

-from django import forms
-from django.contrib.admin import widgets        
-from django.contrib.comments.forms import CommentForm                            
-from models import MPTTComment
-
-class MPTTCommentForm(CommentForm):
-    parent = forms.ModelChoiceField(queryset=MPTTComment.objects.all(), required=False, widget=forms.HiddenInput)
-
-    def get_comment_model(self):
-        # Use our custom comment model instead of the built-in one.
-        return MPTTComment
-
-    def get_comment_create_data(self):
-        # Use the data of the superclass, and add in the parent field field
-        data = super(MPTTCommentForm, self).get_comment_create_data()
-        data['parent'] = self.cleaned_data['parent']
-        return data

comments/models.py

-# from django.db import models
-from django.contrib.comments.models import Comment
-from mptt.models import MPTTModel, TreeForeignKey
-
-### COMMENTS ###
-
-class MPTTComment(MPTTModel, Comment):
-    """ Threaded comments - Add support for the parent comment store and MPTT traversal"""
-    # a link to comment that is being replied, if one exists
-    parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
-
-    class MPTTMeta:
-        # comments on one level will be ordered by date of creation
-        order_insertion_by=['submit_date']
-
-    class Meta:
-        ordering=['tree_id','lft']
-
-    @property
-    def depth(self):
-        return self.get_ancestors().count()

comments/templates/comments.html

-{% load comments %}
-{% load mptt_tags %}
-
-{% get_comment_list for object as comments %}
-
-{% if comments %}
-{% recursetree comments %}
-    {% with node as comment %}
-    {% include "comment.html" %}
-    {% endwith %}
-    {# recursion! children of a given comment #}
-    {% if not node.is_leaf_node %}
-        {{ children }}
-    {% endif %}
-{% endrecursetree %}
-{% endif %}
-
-{% render_comment_form for object %}

comments/templates/comments/form.html

-{% load comments i18n %}
-<form action="{% comment_form_target %}" method="post">{% csrf_token %}
-  {% if next %}<div><input type="hidden" name="next" value="{{ next }}" /></div>{% endif %}
-  {% for field in form %}
-    {% if field.is_hidden %}
-      <div>{{ field }}</div>
-    {% else %}
-      {% if field.errors %}{{ field.errors }}{% endif %}
-      <p
-        {% if field.errors %} class="error"{% endif %}
-        {% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
-        {{ field.label_tag }} {{ field }}
-      </p>
-    {% endif %}
-  {% endfor %}
-  Something
-  {% if object.id %}
-  Something
-      <input type="hidden" name="parent" id="parent_id" value="{{ object.id }}" />
-  {% endif %}
-  <p class="submit">
-    <input type="submit" name="post" class="submit-post" value="{% trans "Post" %}" />
-    <input type="submit" name="preview" class="submit-preview" value="{% trans "Preview" %}" />
-  </p>
-</form>

comments/tests.py

-"""
-This file demonstrates writing tests using the unittest module. These will pass
-when you run "manage.py test".
-
-Replace this with more appropriate tests for your application.
-"""
-
-from django.test import TestCase
-
-
-class SimpleTest(TestCase):
-    def test_basic_addition(self):
-        """
-        Tests that 1 + 1 always equals 2.
-        """
-        self.assertEqual(1 + 1, 2)

comments/views.py

-# Create your views here.

drupal_support/management/commands/convertdrupal.py

 from sitepoet.models import *
 from sitepoet.templatetags.slugify import slugify
 
-from sitepoet.comments.models import MPTTComment
+from sitepoet.threadedcomments.models import MPTTComment
 
 def vancode2int(vancode):
     if len(vancode):
                 raise CommandError("No Drupal nodes found in the current database.")
             
             # Create some sections
-            (blog_section, c) = Section.objects.get_or_create(name="blog")
+            (blog_section, c) = Section.objects.get_or_create(name="Blog", slug="blog")
             if c:
                 if debug: print "* Created blog section."
                 blog_section.save()
             
-            (story_section, c) = Section.objects.get_or_create(name="story")
+            (story_section, c) = Section.objects.get_or_create(name="story", slug="story")
             if c:
                 if debug: print "* Created story section."
                 story_section.save()
-            
+    
+            blog_section = None # I don't want to change the URLs for these.
+    
             for node in node_list:
                 # Our document object
                 obj = None
         But, that's okay.  Drupal required MySQL anyway, so do the conversion on a copy of the DB there, then
         migrate to whatever else you want to use.
         '''
-        from django.db import connection
-        cursor = connection.cursor()
+        from django.db import connections
+        cursor = connections['drupal'].cursor()
         
         cursor.execute("SELECT cid FROM comments as c WHERE nid = %s ORDER BY SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))", [node.nid])
         rows = cursor.fetchall()

drupal_support/routers.py

-from models import *
+from django.db import connections
 
 class DrupalRouter(object):
     """
     A router to use the "drupal" database for Drupal objects, if it exists.
     """
+    app_name = 'drupal_support'
+    database_name = 'drupal'
+    
     def db_for_read(self, model, **hints):
-        if model._meta.app_label == 'drupal_support':
-            return 'drupal'
+        if model._meta.app_label == self.app_name:
+            return self.database_name
         return None
     
     def db_for_write(self, model, **hints):
-        if model._meta.app_label == 'drupal_support':
-            return 'drupal'
+        if model._meta.app_label == self.app_name:
+            return self.database_name
         return None
     
     def allow_relation(self, obj1, obj2, **hints):
         """
         If either model is in this app but both are not, reject.
         """
-        if obj1._meta.app_label == 'drupal_support' or \
-            obj2._meta.app_label == 'drupal_support':
+        if obj1._meta.app_label == self.app_name or \
+            obj2._meta.app_label == self.app_name:
             if obj1._meta.app_label == obj2._meta.app_label:
                 return True
             else:
         return None
     
     def allow_syncdb(self, db, model):
-        if db == 'drupal'or model._meta.app_label == 'drupal_support':
+        if db == self.database_name or model._meta.app_label == self.app_name:
             return False
         return None
     
     # Publishing status
     status = models.IntegerField("Status", choices=ENTRY_STATUSES, default=ENTRY_STATUS_PUBLISHED, help_text="Only published items will be visible on the site.")
-    date_published = models.DateTimeField("Date Published", null=True, blank=True, default=datetime.datetime.now, help_text="Item will become visible after this date.  Future posting is supported.")
+    date_published = models.DateTimeField("Date Published", default=datetime.datetime.now, help_text="Item will become visible after this date.  Future posting is supported.")
     date_hidden = models.DateTimeField("Date Hidden", null=True, blank=True, help_text="Item will be hidden past this date.  No value indicates a perpetual item (most common).")
         
     # Object managers
     
     class Meta:
         abstract = True
+        get_latest_by = 'date_published'
     
     @property
     def visible(self):
     def formatted_content(self):
         return format_text(self.content_format, self.content)
     
+    @property
+    def previous(self):
+        try:
+            return self.get_previous_by_date_published()
+        except self.DoesNotExist:
+            return None
+    
+    @property
+    def next(self):
+        try:
+            return self.get_next_by_date_published()
+        except self.DoesNotExist:
+            return None
+    
     def save(self, *args, **kwargs):
         update_teaser_history = (self.teaser and self.is_dirty_field("teaser"))
         update_content_history = (self.content and self.is_dirty_field("content"))
 
 class Section(SPObject):
     # TODO: Create admin class that auto-gens the slug
-    name = models.CharField("Section Name", max_length=255, null=False, blank=False)
-    slug = models.SlugField("Section Slug", max_length=255, null=False, blank=False)
+    name = models.CharField("Section Name", max_length=255)
+    slug = models.SlugField("Section Slug", max_length=255)
     # page = models.ForeignKey(Page, null=True, blank=True, help_text="A page to display instead of a section index.")
     
     class Meta:
     def allow_commenting(self):
         return self.allow_comments == COMMENTS_ENABLED
     
+    @models.permalink
     def get_absolute_url(self):
-        if self.section:
-            section = self.section.slug
-        else:
-            section = None
-        
         kwargs = {
-            'section': section,
             'year':    self.date_published.year,
             'month':   "%02d"%self.date_published.month,
             'day':     "%02d"%self.date_published.day,
             'slug':    self.slug,
         }
-        return reverse('story-detail', kwargs = kwargs)
+        
+        if self.section and len(self.section.slug):
+            kwargs['section'] = self.section.slug
+        
+        return ('story-detail', (), kwargs)
 
 ### REDIRECTION ###
 

templates/story.html

-{% load markup %}
-<div class="story {% if options.teasers %}teaser{% else %}full{% endif %}">
-    <h2>
-        <a href="{{object.get_absolute_url}}">{{object.title}}</a>
-    </h2>
-    <div class="byline">
-        By <span class="author">{{object.user.username}}</span> on <span class="date">{{object.date_published|date}}</span>
-    </div>
-    <div class="teaser">
-        {{object.formatted_teaser}}
-    </div>
-{% if not options.teasers %}
-    <div class="body">
-        {{object.formatted_content}}
-    </div>
-    <p class="post-info">
-        {% load tagging_tags %}
-        {% tags_for_object object as tags %}
-        {% if tags.count %}
-        <p /><span class="tags"><span class="post-item-key">Tags:</span> <span class="post-item-value">{% for tag in tags %}<a href="{% url tag tag.name %}">{{tag}}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</span></span></p>
-        {% endif %}
-    </p>
-{% endif %}
-</div>
+            <div class="story {% if options.teasers %}teaser{% else %}full{% endif %}">
+                <h2>
+                    <a href="{{object.get_absolute_url}}">{{object.title}}</a>
+                </h2>
+                <div class="byline">
+                    By <span class="author">{{object.user.username}}</span> on <span class="date">{{object.date_published|date}}</span>
+                </div>
+                <div class="teaser">
+                    {{object.formatted_teaser}}
+                </div>
+            {% if not options.teasers %}
+                <div class="body">
+                    {{object.formatted_content}}
+                </div>
+                <p class="post-info">
+                    {% load tagging_tags %}
+                    {% tags_for_object object as tags %}
+                    {% if tags.count %}
+                    <p /><span class="tags"><span class="post-item-key">Tags:</span> <span class="post-item-value">{% for tag in tags %}<a href="{% url tag tag.name %}">{{tag}}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</span></span></p>
+                    {% endif %}
+                </p>
+            {% endif %}
+            </div>

threadedcomments/__init__.py

+# Based on an idea posted at http://root.abl.es/methods/1524/django-threaded-comments/
+
+from models import MPTTComment
+from forms import MPTTCommentForm
+
+def get_model():
+    return MPTTComment
+
+def get_form():
+    return MPTTCommentForm

threadedcomments/admin.py

+from django.contrib import admin
+import models
+
+class MPTTCommentAdmin(admin.ModelAdmin):
+    pass
+
+admin.site.register(models.MPTTComment, MPTTCommentAdmin);

threadedcomments/forms.py

+from django import forms
+from django.contrib.admin import widgets        
+from django.contrib.comments.forms import CommentForm                            
+from models import MPTTComment
+
+class MPTTCommentForm(CommentForm):
+    parent = forms.ModelChoiceField(queryset=MPTTComment.objects.all(), required=False, widget=forms.HiddenInput)
+
+    def get_comment_model(self):
+        # Use our custom comment model instead of the built-in one.
+        return MPTTComment
+
+    def get_comment_create_data(self):
+        # Use the data of the superclass, and add in the parent field field
+        data = super(MPTTCommentForm, self).get_comment_create_data()
+        data['parent'] = self.cleaned_data['parent']
+        return data

threadedcomments/models.py

+from django.db import models
+from django.contrib.comments.models import Comment
+from mptt.models import MPTTModel, TreeForeignKey
+
+### COMMENTS ###
+
+class MPTTComment(MPTTModel, Comment):
+    """ Threaded comments - Add support for the parent comment store and MPTT traversal"""
+    # a link to comment that is being replied, if one exists
+    parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
+    title = models.CharField(null=True, blank=True, max_length=255)
+
+    class MPTTMeta:
+        # comments on one level will be ordered by date of creation
+        order_insertion_by=['submit_date']
+
+    class Meta:
+        ordering=['tree_id','lft']
+
+    @property
+    def depth(self):
+        return self.get_ancestors().count()

threadedcomments/templates/comments.html

+{% load comments %}
+{% load mptt_tags %}
+
+{% get_comment_list for object as comments %}
+
+{% if comments %}
+{% recursetree comments %}
+    {% with node as comment %}
+    {% include "comment.html" %}
+    {% endwith %}
+    {# recursion! children of a given comment #}
+    {% if not node.is_leaf_node %}
+        {{ children }}
+    {% endif %}
+{% endrecursetree %}
+{% endif %}
+
+{% render_comment_form for object %}

threadedcomments/templates/comments/form.html

+{% load comments i18n %}
+<form action="{% comment_form_target %}" method="post">{% csrf_token %}
+  {% if next %}<div><input type="hidden" name="next" value="{{ next }}" /></div>{% endif %}
+  {% for field in form %}
+    {% if field.is_hidden %}
+      <div>{{ field }}</div>
+    {% else %}
+      {% if field.errors %}{{ field.errors }}{% endif %}
+      <p
+        {% if field.errors %} class="error"{% endif %}
+        {% ifequal field.name "honeypot" %} style="display:none;"{% endifequal %}>
+        {{ field.label_tag }} {{ field }}
+      </p>
+    {% endif %}
+  {% endfor %}
+  {% if object.id %}
+      <input type="hidden" name="parent" id="parent_id" value="{{ object.id }}" />
+  {% endif %}
+  <p class="submit">
+    <input type="submit" name="post" class="submit-post" value="{% trans "Post" %}" />
+    <input type="submit" name="preview" class="submit-preview" value="{% trans "Preview" %}" />
+  </p>
+</form>

threadedcomments/tests.py

+"""
+This file demonstrates writing tests using the unittest module. These will pass
+when you run "manage.py test".
+
+Replace this with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+
+class SimpleTest(TestCase):
+    def test_basic_addition(self):
+        """
+        Tests that 1 + 1 always equals 2.
+        """
+        self.assertEqual(1 + 1, 2)

threadedcomments/views.py

+# Create your views here.
 from django.core.paginator import Paginator, InvalidPage, EmptyPage
 from django.core.xheaders import populate_xheaders
-from django.http import HttpResponse, HttpResponseRedirect, Http404
+from django.http import HttpResponse, HttpResponseRedirect, Http404, ImproperlyConfigured
 from django.template import loader, Context, RequestContext
 from django.shortcuts import render_to_response, get_object_or_404
 from django.conf import settings
 def story_detail(request, section=None, year=None, month=None, day=None, slug=None):
     # Fetch a story
     try:
-        story = Story.published.get(
-            section__slug = section,
-            date_published__year = int(year),
-            date_published__month = int(month),
-            date_published__day = int(day),
-            slug = slug
+        if section == None:
+            story = Story.published.get(
+                date_published__year = int(year),
+                date_published__month = int(month),
+                date_published__day = int(day),
+                slug = slug
             )
+        else:
+            story = Story.published.get(
+                section__slug = section,
+                date_published__year = int(year),
+                date_published__month = int(month),
+                date_published__day = int(day),
+                slug = slug
+                )
     except Story.DoesNotExist, e:
         # No result?  404.
         raise Http404
+    except Story.MultipleObjectsReturned, e:
+        # If you don't have a unique section-date-slug combo for your section (or lack thereof) then this triggers.
+        raise ImproperlyConfigured("Multiple objects exist for the specified section, date, and name.")
     
     # Prepare the template data
     context = {
         "title":  story.title,
         "object": story,
-        "next": story.get_absolute_url()
     }
-    
-    response = render_to_response("story_detail.html", context, context_instance=RequestContext(request))
+
+    template_name = "story_detail.html"
+    templates = []
+    if section:
+        templates.append("section/" + section + "/" + template_name)
+    templates.append(template_name)
+
+    response = render_to_response(templates, context, context_instance=RequestContext(request))
     populate_xheaders(request, response, Story, story.id)
     return response
 
     elif year:
         context['title'] = "%04d" % int(year)
     
+    template_name = "story_archive.html"
     templates = []
     if section:
-        templates.append("section/" + section + "/story_archive.html")
-    templates.append("story_archive.html")
+        templates.append("section/" + section + "/" + template_name)
+    templates.append(template_name)
     
     return render_to_response(templates, context, context_instance=RequestContext(request))
 
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.