Commits

Anonymous committed e1fa626

FreeBSD licensed. Safe HTML instead of markdown. New styles, new templates almost everything is cleaner and better.

  • Participants
  • Parent commits 1cb9586

Comments (0)

Files changed (33)

 syntax: glob
 *.pyc
+forum.egg*
-Property of the commons
+Copyright 1992-2010 Jökull Sólberg Auðunsson. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FREEBSD PROJECT OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+The views and conclusions contained in the software and documentation are those of the authors and should not be interpreted as representing official policies, either expressed or implied, of the FreeBSD Project.
+nag is a pluggable Django application to enable users to create 
+threads and post replies. The top level view is a dashboard that 
+aggregates all posts sorted by latest reply. Users post threads 
+into `boards` defined by the admin.
+
+Users edit posts in place; jQuery code is included. There is no markup 
+support. Whitelisted safe HTML is preferred. Have a look at 
+`sanitize.py` for the spec.
+
+Install
+-------
+
+Add forum app to your python path. Add to installed apps and run 
+`syncdb`. Uses `django.contrib.auth` for user management and 
+`django.contrib.admin` to manage forums.
+
 Dependencies
-============
+------------
 
-simplejson (only for testing)
++ html5lib
 
+Required settings:
 
-Default settings
-================
+`PAGINATE_POSTS_BY` - (integer value)
 
-PAGINATE_POSTS_BY = int
+Required contrib apps:
 
-Required
++ django.contrib.auth
++ django.contrib.sessions
++ django.contrib.admin
++ django.contrib.contenttypes
 
-Django contrib
+Context processor:
 
-django.contrib.markup
-django.contrib.auth
-django.contrib.sessions
-django.contrib.admin
-django.contrib.contenttypes
-django.contrib.markup
++ `forum.request.forum`
 
-Context processor
-
-forum.context_processors.forum

File __init__.py

Empty file removed.

File forum/admin.py

 from django.contrib import admin
 from django.utils.translation import ugettext as _
 
-from forum.models import Forum
+from forum.models import Forum, Thread
 
 class ForumForm(forms.ModelForm):
     
     form = ForumForm
 
 admin.site.register(Forum, ForumOptions)
+admin.site.register(Thread)

File forum/context_processors.py

-from django.conf import settings
-from forum.models import Forum
-
-def forum(request):
-    forum_list = Forum.objects.all()
-    return {'forum_list' : forum_list, }

File forum/forms.py

 from django import forms
+
 from forum.models import Thread, Post
 
+from sanitize import sanitize_html
+
 
 class PostForm(forms.ModelForm):
+    
     class Meta:
         model = Post
-        fields = ('body', 'markdown')
+        fields = ('body',)
         
+    def clean_body(self):
+        data = self.cleaned_data['body']
+        return sanitize_html(data)
         
-class ThreadForm(forms.Form):
-    subject = forms.CharField(max_length=250)
-    body = forms.CharField(widget=forms.Textarea)
-    markdown = forms.BooleanField(required=False, initial=True)
+        
+class ThreadForm(forms.ModelForm):
+    
+    class Meta:
+        model = Thread
+        fields = ('forum', 'subject')

File forum/management/__init__.py

Empty file added.

File forum/management/commands/__init__.py

Empty file added.

File forum/management/commands/countposts.py

+from django.core.management.base import NoArgsCommand
+from django.db.models import Count
+from django.contrib.auth.models import User
+from forum.models import Post
+
+class Command(NoArgsCommand):
+    help = "Display a list of users with a post count"
+
+    requires_model_validation = False
+
+    def handle_noargs(self, **options):
+        for user in User.objects.all().annotate(Count('post')).order_by('-post__count'):
+            print '%s: %d' % (user.first_name, user.post__count)

File forum/managers.py

-# coding=UTF8
+# encoding=utf-8
 
-from django.db.models import Manager
+from django.db.models import Manager, query
 from django.utils.datastructures import SortedDict
 
-class ThreadUserManager(Manager):
+
+class QuerySetManager(Manager):
     
-    def user_objects(self, user):
-        queryset = super(ThreadUserManager, self).get_query_set()
-        queryset = queryset.select_related('latest_post').extra(
-            select = SortedDict((
-                ('fresh', 'SELECT COUNT(*) FROM forum_userthreadrecord \
-                    WHERE forum_post.thread_id = forum_userthreadrecord.thread_id \
-                    AND forum_userthreadrecord.user_id = %s'),
-                ('post_count', 'SELECT COUNT(*) FROM forum_post \
-                    WHERE forum_post.thread_id = forum_thread.id \
-                    AND forum_post.visible = 1 \
-                    AND forum_post.user_id != %s'),
-            )),
-            select_params = [user.pk, user.pk]
-        ).extra(
-            order_by = ['-forum_post.created']
-        )
-        return queryset
+    def __getattr__(self, name):
+        return getattr(self.get_query_set(), name)
+
+    def get_query_set(self):
+        return self.QuerySet(self.model)
+
+
+class PublishedManager(QuerySetManager):
+    class QuerySet(query.QuerySet):
+        def published(self, **kwargs):
+            return self.filter(visible=True, **kwargs)
+
+
+class ThreadUserManager(QuerySetManager):
+    class QuerySet(PublishedManager.QuerySet):    
+        def user_objects(self, user):
+            queryset = self.select_related('latest_post').extra(
+                select = SortedDict((
+                    ('fresh', 'SELECT COUNT(*) FROM forum_userthreadrecord \
+                        WHERE forum_post.thread_id = forum_userthreadrecord.thread_id \
+                        AND forum_userthreadrecord.user_id = %s'),
+                    ('post_count', 'SELECT COUNT(*) FROM forum_post \
+                        WHERE forum_post.thread_id = forum_thread.id \
+                        AND forum_post.visible \
+                        AND forum_post.user_id != %s'),
+                )),
+                select_params = [user.pk, user.pk]
+            ).extra(
+                order_by = ['-forum_post.created']
+            )
+            return queryset

File forum/models.py

 from django.db import models
 from django.conf import settings
 from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
 from django.utils.translation import ugettext as _
 
-from forum.managers import ThreadUserManager
-
-PAGINATE_POSTS_BY = settings.PAGINATE_POSTS_BY
-
+from forum.managers import ThreadUserManager, PublishedManager
 
 class Forum(models.Model):
     
     created = models.DateTimeField(auto_now_add=True)
     title = models.CharField(max_length=250)
     slug = models.CharField(max_length=250)
-    color = models.CharField(max_length=6)
-    order = models.IntegerField(default=1)
+    color = models.CharField(max_length=250)
+    order = models.IntegerField(default=1, null=True)
     
     def __unicode__(self):
         return self.title
         verbose_name = _(u'Forum')
         verbose_name_plural = _(u'Forums')
         
-    def get_absolute_url(self):
-        return ('forum', (), { 
-                'forum_slug': self.slug, })
-    get_absolute_url = models.permalink(get_absolute_url)
+    def url(self):
+        return reverse('forum:forum', kwargs={'forum_slug':self.slug})
         
 
 class UserThreadRecord(models.Model):
         (5, _(u'Notice')),
     )
     created = models.DateTimeField(auto_now_add=True)
+    modified = models.DateTimeField(auto_now=True)
     status = models.IntegerField(choices=STATUS_CHOICES, default=1)
     subject = models.CharField(max_length=250)
     forum = models.ForeignKey(Forum)
     latest_post = models.ForeignKey('Post', related_name='latest_post', null=True)
+    first_post = models.ForeignKey('Post', related_name='first_post', null=True)
+    visible = models.BooleanField(default=True)
     
     objects = ThreadUserManager()
     
         ordering = ('-created',)
         verbose_name = _(u'Thread')
         verbose_name_plural = _(u'Threads')
-        
-    def get_absolute_url(self):
-        return ('thread', (), { 
-                'forum_slug': self.forum.slug,
-                'thread_id': str(self.pk), })
-    get_absolute_url = models.permalink(get_absolute_url)
     
     def last_page(self):
-        from math import floor
-        post_count = self.post_set.count()
-        return floor(float(post_count - 1) / float(settings.PAGINATE_POSTS_BY)) + 1
+        from math import ceil
+        post_count = self.post_set.published().count()
+        return ceil(post_count/float(settings.PAGINATE_POSTS_BY))
     
-    def get_latest_post_url(self):
-        return self.latest_post.get_absolute_url()
+    def url(self):
+        return reverse('forum:thread', args=[self.forum.slug, self.pk])
         
 
 class Post(models.Model):
     
     created = models.DateTimeField(auto_now_add=True)
-    modified = models.DateTimeField()
+    modified = models.DateTimeField(auto_now=True)
     body = models.TextField()
     thread = models.ForeignKey(Thread)
     user = models.ForeignKey(User)
-    markdown = models.BooleanField(default=True)
     visible = models.BooleanField(default=True)
     
+    objects = PublishedManager()
+    
     def __unicode__(self):
         return u'%s - %s' % (self.user.username, self.thread)
 
-    def save(self, *args, **kwargs):
-        self.modified = datetime.datetime.now()
+    def save(self, touch=True, *args, **kwargs):
         super(Post, self).save(*args, **kwargs)
-        if not Post.objects.filter(thread__pk__exact=self.thread.pk, visible=True).count():
-            # All posts have been hidden by their owners so we delete 
-            # the thread (with all the posts)
-            self.thread.delete()
-            return None
-        self.thread.latest_post = self
-        self.thread.save()
-        UserThreadRecord.objects.filter(thread__pk__exact=self.thread.pk)\
-            .exclude(user=self.user).delete()
+        if touch:
+            if not Post.objects.filter(thread__pk__exact=self.thread.pk, visible=True).count():
+                # All posts have been hidden by their owners so we delete 
+                # the thread (with all the posts)
+                self.thread.delete()
+                return None
+            self.thread.latest_post = self
+            self.thread.save()
+            UserThreadRecord.objects.filter(thread__pk__exact=self.thread.pk)\
+                .exclude(user=self.user).delete()
     
-    def get_absolute_url(self):
-        return u'%s?page=%d#post-%d' % (self.thread.get_absolute_url(), \
+    def url(self):
+        return u'%s?page=%d#post-%d' % (self.thread.url(), \
             self.thread.last_page(), self.pk)

File forum/request.py

+from forum.models import Forum
+
+def context(request):
+    return {'forum_list' : Forum.objects.all() }

File forum/sanitize.py

+"""Utilities for working with HTML."""
+import html5lib
+from html5lib import sanitizer, serializer, tokenizer, treebuilders, treewalkers
+
+class HTMLSanitizerMixin(sanitizer.HTMLSanitizerMixin):
+    acceptable_elements = ('a', 'abbr', 'acronym', 'address', 'b', 'big',
+        'blockquote', 'br', 'caption', 'center', 'cite', 'code', 'col',
+        'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'font',
+        'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', 'kbd',
+        'li', 'ol', 'p', 'pre', 'q', 's', 'samp', 'small', 'span', 'strike',
+        'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead',
+        'tr', 'tt', 'u', 'ul', 'var', 'object', 'embed')
+
+    acceptable_attributes = ('abbr', 'align', 'alt', 'axis', 'border',
+        'cellpadding', 'cellspacing', 'char', 'charoff', 'charset', 'cite',
+        'cols', 'colspan', 'datetime', 'dir', 'frame', 'headers', 'height',
+        'href', 'hreflang', 'hspace', 'lang', 'longdesc', 'name', 'nohref',
+        'noshade', 'nowrap', 'rel', 'rev', 'rows', 'rowspan', 'rules', 'scope',
+        'span', 'src', 'start', 'summary', 'title', 'type', 'valign', 'vspace',
+        'width')
+
+    allowed_elements = acceptable_elements
+    allowed_attributes = acceptable_attributes
+    allowed_css_properties = ()
+    allowed_css_keywords = ()
+    allowed_svg_properties = ()
+
+class HTMLSanitizer(tokenizer.HTMLTokenizer, HTMLSanitizerMixin):
+    def __init__(self, stream, encoding=None, parseMeta=True, useChardet=True,
+                 lowercaseElementName=True, lowercaseAttrName=True):
+        tokenizer.HTMLTokenizer.__init__(self, stream, encoding, parseMeta,
+                                         useChardet, lowercaseElementName,
+                                         lowercaseAttrName)
+
+    def __iter__(self):
+        for token in tokenizer.HTMLTokenizer.__iter__(self):
+            token = self.sanitize_token(token)
+            if token:
+                yield token
+
+def sanitize_html(html):
+    """Sanitizes an HTML fragment."""
+    p = html5lib.HTMLParser(tokenizer=HTMLSanitizer,
+                            tree=treebuilders.getTreeBuilder("dom"))
+    dom_tree = p.parseFragment(html)
+    walker = treewalkers.getTreeWalker("dom")
+    stream = walker(dom_tree)
+    s = serializer.HTMLSerializer(omit_optional_tags=False,
+                                  quote_attr_values=True)
+    output_generator = s.serialize(stream)
+    return u''.join(output_generator)

File forum/static/.DS_Store

Binary file added.

File forum/static/forum/forum.css

+body { color: #333; font-family: 'Lucida Grande', Verdana, Arial, sans-serif; }
+
+.container { width: 750px; }
+
+a { text-decoration: none; font-weight: bold; }
+h1 { font-size: 20px; }
+h1 span { font-weight: normal; color: #ccc; }
+h1 span a { color: #6d8dac; }
+h3 { font-size: 13px; }
+
+ol, ul { padding-left: 0; }
+
+td.date { white-space: nowrap; font-family: monospace; }
+td.post { height: 1em; overflow: hidden; white-space: nowrap; }
+
+strong { font-weight: bold; }
+em { font-style: italic; }
+textarea { font-size: 13px; line-height: 1.4em; padding: 2px; height: 170px; width: 440px; }
+hr { display: block; height: 1px; border: 1px solid gray; border-width: 1px 1px 0 0; }
+
+.clear { clear: both; }
+.left { float: left; }
+.right { float: right; }
+
+ol.pagination { margin: 10px 0; }
+ol.pagination li { display: inline; margin: 0 1px 0 0; border: 1px solid #E0E0E0; padding: 3px; background: #F0F0F0; line-height: 30px; }
+ol.pagination li.selected { border-color: transparent; background: transparent; }
+ol.pagination li a { color: #6d8dac; }
+
+#header { font-size: 20px; overflow: hidden; margin-top: 10px; }
+#header a { text-decoration: none; }
+.forum-nav { font-size: 18px; margin: 10px 0; text-transform: uppercase; font-weight: bold; }
+.forum-nav a { text-decoration: none; font-weight: normal; }
+#header .status { border-bottom: 1px solid #e0e0e0; font-size: 10px; padding: 10px 0 5px 0; margin-bottom: 10px; color: #888; }
+#header .status a { font-weight: normal; color: #666; text-decoration: underline; }
+
+#actions { border-top: 1px solid #f0f0f0; margin: 10px 0; }
+#actions li { padding: 4px 7px; font-weight: bold; font-size: 14px; }
+
+#cp { border-bottom: 1px solid #ccc; background: #e8e8e8; padding: 4px 8px; overflow: hidden; }
+#nav { border-bottom: 1px solid #ccc; background: #f0f0f0; padding: 4px 8px; overflow: hidden; margin-bottom: 14px; }
+#content {  }
+
+#thread-detail { width: 820px; padding-left: 0; }
+#thread-detail .post { margin-bottom: 20px; overflow: hidden; border-top: 1px dotted #ccc; padding-top: 9px; }
+#thread-detail .hidden { color: #e0e0e0; }
+#thread-detail .hidden .date { color: #e0e0e0; }
+#thread-detail .hidden a { color: #6d8dac; }
+#thread-detail .hidden img { opacity: .3; }
+#thread-detail .post .meta { width: 150px; float: left; }
+#thread-detail .post .meta h5 { font-size: 15px; white-space: nowrap; font-weight: bold; margin-bottom: 5px; }
+#thread-detail .post .meta h5.date { font-size: 12px; margin-bottom: 5px; font-weight: normal; }
+#thread-detail .post .meta ul.actions { font-size: 11px; font-weight: bold; padding-left: 0; }
+#thread-detail .post .meta ul.actions li { display: inline; padding-right: 4px; }
+#thread-detail .post .meta ul.actions li a { color: #6d8dac; }
+#thread-detail .post .meta img.avatar { margin: 4px 0; }
+#thread-detail .post .body { }
+#thread-detail .post .body form { margin-top: 5px !important; }
+#thread-detail .post .body blockquote { border-left: 2px solid #ccc; padding-left: 7px; margin: 10px 0 10px 3px; font-size: 11px; line-height: 1.1em; color: #666; }
+#thread-detail .post .body h1,
+#thread-detail .post .body h2,
+#thread-detail .post .body h3,
+#thread-detail .post .body h4,
+#thread-detail .post .body h5,
+#thread-detail .post .body h6,
+#thread-detail .post .body ol,
+#thread-detail .post .body ul,
+#thread-detail .post .body p { margin-bottom: 10px; }
+#thread-detail .post .body ul { padding-left: 0; }
+#thread-detail .post .body ol li { list-style: decimal outside; margin-left: 19px; }
+#thread-detail .post .body ul li { list-style: disc outside; margin-left: 19px; }
+
+#thread-list { text-decoration: none; }
+#thread-list tr td, 
+#thread-list th { padding: 3px 9px; }
+#thread-list td { vertical-align: top; }
+#thread-list td.first { font-weight: bold; letter-spacing: .1em; width: 300px; }
+#thread-list tr th { color: #777; font-weight: bold; background: #e0e0e0; }
+#thread-list tr.status_5 { background: pink; }
+/* #thread-list tr.even { background: #f0f0f0; } */
+#thread-list tr.even td { background: #FFFFFF; }
+#thread-list tr.unread td { background: #FAFAD2; }
+#thread-list a.cat { font-size: 9px; }
+#thread-list td.overview { font-size: 12px; line-height: 15px; }
+#thread-list tr.not-available { background: #ffff80; }
+
+a.cat { padding: 2px 3px; display: inline-block; color: white; background: #ccc; font-size: 11px; text-decoration: none; white-space: nowrap; }
+
+form { border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; background: #f0f0f0; margin: 20px 0; padding: 10px; width: 600px; }
+form p { overflow: hidden; margin-bottom: 10px; }
+form label { display: block; width: 100px; float: left; font-weight: bold; }
+form#new_thread input[type=text] { width: 400px; font-size: 20px; padding: 2px; }
+
+.forums { margin-bottom: 5px; }

File forum/static/forum/forum.js

+$(document).ready(function(){
+  
+  if($("#thread-detail").size()){
+    
+    var activate_edit_post_form = function(body, old){
+      
+      $("textarea", body).focus();
+    
+      $("input:submit", body).click(function(){
+        var form = $("form", body);
+        $.post(form.attr("action"), form.serialize(), function(response){
+          var response = eval('(' + response + ')');
+          var message = unescape(response['message']);
+          if(response['success']){
+            body.html(message);
+          }else{
+            body.html(message);
+            activate_edit_post_form(body, old);
+          }
+        });
+        return false;
+      
+      })
+    
+      $("a.cancel", body).click(function(){
+          body.html(old);
+          return false;
+      });
+    
+    }
+  
+    $(".post a.edit").click(function(){
+      var body = $(this).parents("li.post").find("div.body"),
+          old = body.html(),
+            level = 1,
+            down = true,
+            beat = function(){
+              if(body.find("form").size()) {
+                body.css("background","white");
+                return false;
+              }
+              var h = (level).toString(16);
+              body.css("background","#FFFF" + h + h);
+              if(level==0||level==15) down = !down;
+              down ? level=level-1 : level=level+1;
+              setTimeout(beat, 30);
+              return;
+            };
+      beat(); // Callback to animate heartbeat until AJAX form gets loaded in its place
+      body.load(this.href, null, function(){
+        activate_edit_post_form(body, old)
+      });
+      return false;
+    });
+  
+    $(".post a.toggle").click(function(){
+      var that = this;
+      $.getJSON(this.href, function(json){
+        if(json.success){ // success
+          $(that).html(json.message).parents("li.post").toggleClass("hidden");
+        }else{
+          alert("Server side error. Please try again.");
+        }
+      });
+      return false;
+    });
+  
+    $(".post a.toggle-thread").click(function(){
+      var that = this;
+      $.getJSON(this.href, function(json){
+        if(json.success){ // success
+          $(that).html(json.message).parents("li.post").toggleClass("hidden");
+        }else{
+          alert("Server side error. Please try again.");
+        }
+      });
+      return false;
+    });
+    
+  }
+  
+  $("table tbody tr:even").addClass("even");
+
+});
+

File forum/templates/forum/base.html

-{% extends "base.html" %}
-{% load i18n %}
-
-{% block section %}forum{% endblock %}
-
-{% block navigation %}
-    <div class="right" style="padding-left: 10px;">
-    {% block new_thread %}{% endblock %}
-    </div>
-    <div class="right">
-    {% for forum in forum_list %}
-        <a style="background: #{{ forum.color }};" class="cat" href="{{ forum.get_absolute_url }}">{{ forum }}</a>
-    {% endfor %}
-    </div>
-    <h1>{% block header %}<a href="{% url forum-front %}">{% trans "Forum" %}</a>{% endblock %}</h1>
-{% endblock %}
+{% extends "base.html" %}
+
+{% block title %}{{ block.super }} - Nag{% endblock title %}
+
+{% block extrahead %}
+<script src="{{ STATIC_URL }}forum/forum.js"></script>
+<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}forum/forum.css" />
+{% endblock extrahead %}
+
+{% block container %}
+  <div id="header">
+    <div class="right">
+      <div class="new-thread">
+        <a class="btn" href="{% url forum:thread-add %}">New thread</a>
+      </div>
+    </div>
+    <div class="left">
+      <a href="{% url forum:dashboard %}">Dashboard</a>
+    </div>
+  </div>
+  {% block forum %}
+  {% endblock forum %}
+{% endblock container %}

File forum/templates/forum/dashboard.html

 {% extends "forum/thread_list.html" %}
-{% load i18n %}
-
-{% block header %}
-<span>{{ block.super }}</span> {% trans "Dashboard" %}
-{% endblock %}
-
-{% block new_thread %}
-	<select class="forums">
-		<option>{% trans "New thread" %}</option>
-		{% for forum in forum_list %}
-		<option value="{% url forum.views.thread_add forum.slug %}">{{ forum.title }}</option>
-		{% endfor %}
-		{{ current }}
-	</select>
-{% endblock %}

File forum/templates/forum/edit_post.html

-{% load i18n %}
-<form method="post" action="{% url forum.views.post_edit post.pk %}">
+<form method="post" action="{% url forum:post-edit post.pk %}">
     {{ post_form.as_p }}
-    <input type="submit" value="{% trans "Submit" %}" /> <a href="#" class="cancel">{% trans "cancel" %}</a>
+    <input type="submit" value="Senda" /> <a href="#" class="cancel">cancel</a>
 </form>

File forum/templates/forum/pagination.html

+{% if page.has_previous or page.has_next %}
+
+    <ol class="pagination">
+    {% if page.has_previous %}
+       <li><a href="?page={{ page.previous_page_number }}">&larr; Newer</a></li>
+    {% endif %}
+    {% for page in paginator.page_range %}
+    {% ifequal request_page page %}
+       <li class="selected">{{ page }}</li>
+    {% else %}
+       <li><a href="?page={{ page }}">{{ page }}</a></li>
+    {% endifequal %}
+    {% endfor %}
+    {% if page.has_next %}
+       <li><a href="?page={{ page.next_page_number }}">Older &rarr;</a></li>
+    {% endif %}
+    </ol>
+    
+{% endif %}

File forum/templates/forum/thread_add.html

 {% extends "forum/base.html" %}
-{% load markup i18n %}
 
 {% block header %}
-<span>{{ block.super }} > <a href="{% url forum.views.thread_list forum.slug %}">{{ forum.title }}</a> > </span> {% trans "New thread" %} 
+<span>{{ block.super }} > <a href="{% url forum.views.thread_list forum.slug %}">{{ forum.title }}</a> > </span> Create thread
 {% endblock %}
 
-{% block content %}
+{% block forum %}
 
 <form method="post" action="./" id="thread_add">
-	{{ form.as_p }}
-	<input type="submit" value="{% trans "Submit" %}" />
+  {% if form.non_field_errors %}
+  {{ form.non_field_errors }}
+  {% endif %}
+  {% csrf_token %}
+  <div class="form-row">
+    {{ thread_form.forum.label_tag }}<br>
+    {{ thread_form.forum }}
+    {{ thread_form.forum.errors }}
+  </div>
+  <div class="form-row">
+    {{ thread_form.subject.label_tag }}<br>
+    <input id="id_subject" type="text" name="subject" maxlength="250" class="title">
+    {{ thread_form.subject.errors }}
+  </div>
+  <div class="form-row">
+    {{ post_form.body.label_tag }}<br>
+    {{ post_form.body }}
+    {{ post_form.body.errors }}
+  </div>
+  <p><label>&nbsp;</label><input type="submit" value="Senda"></p>	
 </form>
 
 {% endblock %}

File forum/templates/forum/thread_detail.html

-{% extends "forum/base.html" %}
-{% load markup i18n %}
+{% extends "forum/thread_list.html" %}
 
-{% block header %}
-<span>{{ block.super }} > <a href="../">{{ thread.forum }}</a> > </span>{{ thread.subject }}
-{% endblock %}
+{% load smart_linebreaks %}
 
-{% block content %}
+{% block title %}{{ block.super }} - {{ thread.subject }}{% endblock title %}
 
-    <ol id="thread-detail">
-    {% for post in page.object_list %}
-        <li class="post{% if not post.visible %} hidden{% endif %}" id="post-{{ post.id }}">
-            <div class="meta">
-                <h2>{{ post.user.username }}</h2>
-                <h2 class="date">{{ post.created|date:"d.m.Y H:i" }}</h2>
-                {% if user.profile %}
-                    <img class="avatar" src="{{ user.profile.get_avatar_url }}" />
-                {% endif %}
-                {% ifequal post.user request.user %}
-                <ul class="actions">
-                    <li>
-                        <a href="{% url forum.views.post_edit post.pk %}" class="edit">{% trans "Edit" %}</a>
-                    </li>
-                    <li>
-                        <a href="{% url forum.views.post_visibility post.pk %}" class="toggle">
-                            {% if post.visible %}{% trans "Hide" %}{% else %}{% trans "Show" %}{% endif %}
-                        </a>
-                    </li>
-                </ul>
-                {% endifequal %}
-            </div>
-            <div class="body">
-                {% if post.markdown %}
-                    {{ post.body|markdown }}
-                {% else %}
-                    {{ post.body|safe }}
-                {% endif %}
-            </div>
-        </li>
-    {% endfor %}
-    </ol>
+{% block forum %}
 
-    {% include "pagination.html" %}
+  <div class="forum-nav">
+    {{ thread.subject }}
+  </div>
 
-    <form method="post" action="./" id="fanswer">
-        {{ form.as_p }}
-        <input type="submit" value="{% trans "Submit" %}" />
-    </form>
+  <ol id="thread-detail">
+  {% for post in page.object_list %}
+    <li class="post{% if not post.visible %} hidden{% endif %}" id="post-{{ post.id }}">
+      <div class="meta span-4">
+        <h5>{{ post.user }}</h5>
+        <h5 class="date">{{ post.created|date:"j. b Y" }} {{ post.created|date:"H:i"}}</h5>
+        {% ifequal post.user request.user %}
+        <ul class="actions">
+          <li>
+            <a href="{% url forum:post-edit post.pk %}" class="edit">Edit</a>
+          </li>
+          {% ifequal post post.thread.first_post %}
+          <li>
+            <a href="{% url forum:thread-visibility thread.forum.slug thread.pk %}" class="toggle-thread">
+              {% if thread.visible %}Hide thread{% else %}Show thread{% endif %}</a>
+          </li>
+          {% else %}
+          <li>
+            <a href="{% url forum:post-visibility post.pk %}" class="toggle">
+              {% if post.visible %}Hide{% else %}Show{% endif %}</a>
+          </li>
+          {% endifequal %}
+        </ul>
+        {% endifequal %}
+      </div>
+      <div class="body span-12 last">
+        {{ post.body|safe|smart_linebreaks }}
+      </div>
+    </li>
+  {% endfor %}
+  </ol>
+
+  {% include "forum/pagination.html" %}
+
+  <form method="post" action="{% url forum:post-add thread.pk %}" id="fanswer">
+    {% if form.non_field_errors %}
+    {{ form.non_field_errors }}
+    {% endif %}
+    {% csrf_token %}
+    {{ form.body.errors }}
+    <label>Reply</label><br>
+    {{ form.body }}<br>
+    <input type="submit" value="Senda" />
+  </form>
 
 {% endblock %}

File forum/templates/forum/thread_list.html

 {% extends "forum/base.html" %}
-{% load markup i18n %}
 
-{% block header %}
-<span>{{ block.super }} ></span> {{ forum.title }}
-{% endblock %}
+{% block forum %}
 
-{% block new_thread %}
-<a href="{% url forum.views.thread_add forum.slug %}">{% trans "New thread" %}</a>
-{% endblock %}
+  <div class="forum-nav">
+    {% if forum %}
+    <a href="{% url forum:forum forum.slug %}">{{ forum.title }}</a>
+    {% endif %}
+  </div>
 
-{% block content %}
-
-    <table id="thread-list">
-        <col width="25" />
-        <th></th>
-        <th>{% trans "Title" %}</th>
-        <th>{% trans "Date" %}</th>
-        <th>{% trans "∑" %}</th>
-        <th>{% trans "Latest post" %}</th>
-        {% for thread in page.object_list %}
-        <tr class="thread status_{{ thread.status }}{% if not thread.fresh %} unread{% endif %}">
-            <td class="pointer"><span>&rarr;</span></td>
-            <td class="first">
-                <a style="background: #{{ thread.forum.color }};" class="cat" href="{{ thread.forum.get_absolute_url }}">{{ thread.forum }}</a>
-                <a href="{{ thread.get_latest_post_url }}">{{ thread.subject }}</a>
-            </td>
-            <td class="date">{{ thread.created|date:"j. M" }}</td>
-            <td>{{ thread.post_set.count }}</td>
-            <td class="post"><strong>{{ thread.latest_post.user.username }}</strong>: {{ thread.latest_post.body|markdown|striptags|truncatewords:"9"|slice:":70" }}</td>
-        </tr>
-        {% endfor %}
-    </table>
-    
-    {% include "pagination.html" %}
+  <table id="thread-list">
+    <col width="25" />
+    <th>Title</th>
+    <th>Recent reply</th>
+    <th>Date</th>
+    <th>∑</th>
+    <th></th>
+  {% for thread in page.object_list %}
+    {% if thread.visible or thread.first_post.user == request.user %}
+    <tr class="thread status_{{ thread.status }}{% if not thread.fresh %} unread{% endif %}">
+      <td class="first">
+        <a href="{{ thread.latest_post.url }}">{{ thread.subject }}</a>
+      </td>
+      <td class="post">
+        <strong>{{ thread.latest_post.user }}</strong> {{ thread.latest_post.created|timesince }}
+      </td>
+      <td class="date">{{ thread.created|date:"Y.m.d" }}</td>
+      <td>{{ thread.post_set.count }}</td>
+      <td>
+        <a style="background: #{{ thread.forum.color }};" class="cat" href="{{ thread.forum.url }}">
+          {{ thread.forum }}
+        </a>
+      </td>
+    </tr>
+    {% endif %}
+  {% endfor %}
+  </table>
+  
+  {% include "forum/pagination.html" %}
 
 {% endblock %}

File forum/templatetags/__init__.py

Empty file added.

File forum/templatetags/smart_linebreaks.py

+import re
+from django import template
+from django.utils.functional import allow_lazy
+from django.template.defaultfilters import stringfilter
+from django.utils.safestring import mark_safe, SafeData
+from django.utils.encoding import force_unicode
+from django.utils.html import escape
+register = template.Library()
+
+def smart_linebreaks_(value, autoescape=False):
+    """Converts newlines into <p> and <br />s."""
+    value = re.sub(r'\r\n|\r|\n', '\n', force_unicode(value)) # normalize newlines
+    value = re.sub(r'>\s+<', '><', force_unicode(value))
+    paras = re.split('\n{2,}', value)
+    if autoescape:
+        output = [u'<p>%s</p>' % escape(p).replace('\n', '<br />') for p in paras]
+    else:
+        output = []
+        for p in paras:
+            if p.startswith("<"):
+                output += [p]
+            else:
+                output += [u'<p>%s</p>' % p.replace('\n', '<br />')]
+    return u'\n\n'.join(output)
+smart_linebreaks_ = allow_lazy(smart_linebreaks_, unicode)
+
+@register.filter
+def smart_linebreaks(value, autoescape=None):
+    """
+    Replaces line breaks in plain text with appropriate HTML; a single
+    newline becomes an HTML line break (<br>) and a new line
+    followed by a blank line becomes a paragraph break (<p>).
+    Paragraphs beginning with an HTML block element (<div>) are not
+    turned into HTML paragraphs.
+    """
+    autoescape = autoescape and not isinstance(value, SafeData)
+    return mark_safe(smart_linebreaks_(value, autoescape))
+smart_linebreaks.is_safe = True
+smart_linebreaks.needs_autoescape = True
+smart_linebreaks = stringfilter(smart_linebreaks)

File forum/tests.py

 # encoding = UTF-8
 
-import simplejson
+try:
+    import json
+except ImportError:
+    import simplejson as json
 
 from django.test import TestCase
 from django.test.client import Client
         self.client = Client()
         self.client.login(username=self.user.username, password='larry')
         
-        response = self.client.post(reverse('thread-add', args=(self.forum.slug,)), {
+        response = self.client.post(reverse('forum:thread-add'), {
             'subject': u'My awesome thread',
             'body': u'Posting prowess.',
-            'markdown': True,
+            'forum': '1',
         })
+        self.assert_(response.status_code, 200)
         thread = Thread.objects.latest('created')
-        thread_url = reverse('thread', args=(thread.forum.slug, thread.pk))
+        thread_url = reverse('forum:thread', args=(thread.forum.slug, thread.pk))
         self.assertRedirects(response, thread_url, \
             target_status_code=200)
         
-        response = self.client.post(thread_url, {
-            'body': u'More awesome posts.',
-            'markdown': False,
+        response = self.client.post(reverse('forum:post-add', args=(thread.pk,)), {
+            'body': u'More awesome posts.'
         })
-        self.assertRedirects(response, reverse('forum-front'))
+        self.assertRedirects(response, reverse('forum:dashboard'))
         
         post = Post.objects.latest('created')
-        post_url = post.get_absolute_url()
-        self.failUnlessEqual(post_url, post.thread.latest_post.get_absolute_url())
+        post_url = post.url()
+        self.failUnlessEqual(post_url, post.thread.latest_post.url())
         
         _posted_body = u'Even more awesomeness.'
-        response = self.client.post(reverse('forum.views.post_edit', args=[post.pk]), {
-            'body': _posted_body,
-            'markdown': True,
+        response = self.client.post(reverse('forum:post-edit', args=[post.pk]), {
+            'body': _posted_body
         })
-        response_data = simplejson.loads(response.content)
+        response_data = json.loads(response.content)
         self.failUnlessEqual(response_data['success'], True)
         self.failUnlessEqual(Post.objects.get(pk=post.pk).body, _posted_body)
         
-        response = self.client.get(reverse('forum.views.post_visibility', args=[post.pk]))
-        response_data = simplejson.loads(response.content)
+        response = self.client.get(reverse('forum:post-visibility', args=[post.pk]))
+        response_data = json.loads(response.content)
         self.failUnlessEqual(response_data['success'], True)
         self.failUnlessEqual(Post.objects.get(pk=post.pk).visible, False)
         
         response = self.client.get(thread_url)
         self.failUnlessEqual(u'Even more awesomeness' in response.content, False)
         
-        response = self.client.get(reverse('forum.views.post_visibility', args=[post.pk]))
-        self.failUnlessEqual(response.status_code, 404)
+        response = self.client.get(reverse('forum:post-visibility', args=[post.pk]))
+        self.failUnlessEqual(response.status_code, 403)
         
         self.client.login(username=self.user.username, password='larry')
         for post in Post.objects.filter(user__username='larry', visible=True):

File forum/urls.py

 from django.conf.urls.defaults import *
 
 urlpatterns = patterns('forum.views',
+    url(r'^add/$', 'thread_add', name='thread-add'),
+    url(r'^thread/(?P<thread_id>\d+)/post/add/$', 'post_add', name="post-add"),
+    url(r'^post/(?P<post_id>\d+)/edit/$', 'post_edit', name="post-edit"),
+    url(r'^post/(?P<post_id>\d+)/toggle/$', 'post_visibility', name="post-visibility"),
     url(r'^(?P<forum_slug>[-\w]+)/$', 'thread_list', name='forum'),
     url(r'^(?P<forum_slug>[-\w]+)/(?P<thread_id>\d+)/$', 'thread_detail', name='thread'),
-    url(r'^(?P<forum_slug>[-\w]+)/add/$', 'thread_add', name='thread-add'),
-    url(r'^post/(?P<post_id>\d+)/edit/$', 'post_edit'),
-    url(r'^post/(?P<post_id>\d+)/toggle/$', 'post_visibility'),
-    url(r'^$', 'dashboard', name='forum-front'),
+    url(r'^(?P<forum_slug>[-\w]+)/(?P<thread_id>\d+)/toggle/$', 'thread_visible', name='thread-visibility'),
+    url(r'^$', 'dashboard', name='dashboard'),
 )

File forum/utils.py

-from django.template import loader, RequestContext
-from django.http import HttpResponse
-
-def response(request, template, context):
-    c = RequestContext(request, context)
-    t = loader.get_template(template)
-    return HttpResponse(t.render(c))

File forum/views.py

 
 import datetime, re
 
-import markdown
-
 from django.conf import settings
-from django.core.paginator import Paginator
+from django.core.paginator import Paginator, InvalidPage, EmptyPage
 from django.contrib.auth.decorators import login_required
 from django.db.models import Q
-from django.http import HttpResponseRedirect, HttpResponse, Http404
-from django.shortcuts import get_object_or_404, get_list_or_404
+from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponseForbidden
+from django.shortcuts import get_object_or_404, get_list_or_404, render_to_response
 from django.template.loader import render_to_string
+from django.template.defaultfilters import linebreaks
+from django.template import RequestContext
 from django.core.urlresolvers import reverse
 from django.utils.translation import ugettext as _
 
 from forum.models import Post, Thread, Forum, UserThreadRecord
 from forum.forms import PostForm, ThreadForm
 
-from utils import response
-
 @login_required
 def dashboard(request):
     request_page = request.GET.get('page', 1)
     paginator = Paginator(Thread.objects.user_objects(request.user), 50)
-    page = paginator.page(request_page)
-    return response(request, 'forum/dashboard.html', 
-        {'page': paginator.page(request_page), 'paginator': paginator, \
-            'request_page': int(request_page) })
+    try:
+        page = paginator.page(request_page)
+    except InvalidPage, EmptyPage:
+        raise Http404
+    return render_to_response('forum/dashboard.html', RequestContext(request, {
+        'page': paginator.page(request_page), 'paginator': paginator, \
+        'request_page': int(request_page) }))
 
 @login_required
 def thread_list(request, forum_slug):
     forum = get_object_or_404(Forum, slug=forum_slug)
     queryset = Thread.objects.user_objects(request.user).filter(forum=forum)
     paginator = Paginator(queryset, 50)
-    return response(request, 'forum/thread_list.html', 
-        {'page': paginator.page(request_page), 'paginator': paginator, \
-            'request_page': int(request_page), 'forum': forum })
+    try:
+        page = paginator.page(request_page)
+    except InvalidPage, EmptyPage:
+        raise Http404
+    return render_to_response('forum/thread_list.html', RequestContext(request, {
+        'page': page, 'paginator': paginator, \
+        'request_page': int(request_page), 'forum': forum }))
 
 @login_required
 def thread_detail(request, forum_slug, thread_id):
     request_page = request.GET.get('page', 1)
     thread = Thread.objects.get(pk=thread_id)
+    post_list = Post.objects.select_related('user').filter(
+            Q(user=request.user) | Q(visible=True), thread=thread
+            ).order_by('created')
+    paginator = Paginator(post_list, settings.PAGINATE_POSTS_BY)
+    try:
+        page = paginator.page(request_page)
+    except InvalidPage, EmptyPage:
+        raise Http404
+    post_form = PostForm()
+    record, created = UserThreadRecord.objects.get_or_create(user=request.user,thread=thread)
+    record.fresh = False
+    record.save()
+    return render_to_response('forum/thread_detail.html', RequestContext(request, {
+        'thread': thread,
+        'forum': thread.forum,
+        'request': request, 'form': post_form, 
+        'paginator': paginator, 
+        'page': page, 'request_page': int(request_page)}))
+
+
+@login_required
+def thread_visible(request, forum_slug, thread_id):
+    thread = Thread.objects.get(pk=thread_id)
+    if thread.first_post.user != request.user:
+        return HttpResponseForbidden()
+    thread.visible = not thread.visible
+    thread.save()
+    return render_to_response("response.json", RequestContext(request, {
+        'success': True,
+        'message': thread.visible and u'Fela þráð' or u'Birta þráð'
+    }))
+
+@login_required
+def thread_add(request):
+    if request.method=="POST":
+        thread_form = ThreadForm(request.POST)
+        post_form = PostForm(request.POST)
+        if all([f.is_valid() for f in (thread_form, post_form)]):
+            thread = thread_form.save()
+            post = post_form.save(commit=False)
+            post.thread = thread
+            post.user = request.user
+            post.save()
+            post.thread.first_post = post
+            post.thread.save()
+            return HttpResponseRedirect(reverse('forum:thread', args=(thread.forum.slug, thread.id)))
+    else:
+        thread_form = ThreadForm()
+        post_form = PostForm()
+    return render_to_response('forum/thread_add.html', RequestContext(request, {
+        'thread_form': thread_form, 
+        'post_form': post_form, }))
+
+@login_required
+def post_add(request, thread_id):
+    thread = Thread.objects.get(pk=thread_id)
     if request.method=="POST":
         post_form = PostForm(request.POST)
         if post_form.is_valid():
             post.user = request.user
             post.thread = thread
             post.save()
-            return HttpResponseRedirect(reverse('forum-front'))
-    post_list = Post.objects.select_related('user').filter(
-        Q(user=request.user) | Q(visible=True), thread=thread
-    ).order_by('created')
-    paginator = Paginator(post_list, settings.PAGINATE_POSTS_BY)
-    post_form = PostForm()
-    record, created = UserThreadRecord.objects.get_or_create(user=request.user,thread=thread)
-    record.fresh = False
-    record.save()
-    return response(request, 'forum/thread_detail.html', {'thread': thread, \
-        'request': request, 'form': post_form, 'paginator': paginator, \
-        'page': paginator.page(request_page), 'request_page': int(request_page)})
+    return HttpResponseRedirect(reverse('forum:dashboard'))
 
-@login_required
-def thread_add(request, forum_slug):
-    forum = Forum.objects.get(slug=forum_slug)  
-    if request.method=="POST":
-        form = ThreadForm(request.POST)
-        if form.is_valid():
-            thread = Thread.objects.create(subject=form.cleaned_data.pop('subject'), forum=forum)
-            thread.save()
-            post = Post.objects.create(user=request.user, thread=thread, **form.cleaned_data)
-            post.save()
-            return HttpResponseRedirect(reverse('thread', args=(forum.slug, thread.id)))
-    else:
-        thread_form = ThreadForm()
-    return response(request, 'forum/thread_add.html', {'forum': forum, 'form': thread_form, 'request': request })
+def html_to_textarea(markup):
+    "Reverses effects of `linebreaks` template filter."
+    markup = re.sub("</p>\s?<p>", "\n\n", markup)
+    markup = re.sub("<br\ ?/?>", "\n", markup)
+    markup = re.sub("</?p>", "", markup)
+    return markup
 
 @login_required
 def post_edit(request, post_id):
         post_form = PostForm(request.POST, instance=post)
         if post_form.is_valid():
             post = post_form.save()
-            success = True
-            message = post.body
-            if post.markdown:
-                message = markdown.Markdown(message)
+            message = linebreaks(post.body)
         else:
-            success = False
-            message = render_to_string('forum/edit_post.html', {'post_form': post_form, 'post': post, })
-        message = re.sub(r'(\r\n|\r|\n)', '', unicode(message))
-        return response(request, 'response.json', {
-            'success': success,
-            'message': message.replace('"','%22')
+            message = render_to_string('forum/edit_post.html', 
+                RequestContext(request, {
+                    'post_form': post_form, 'post': post, }))
+        message = '\\n'.join(message.splitlines())
+        return render_to_response('response.json', {
+            'success': post_form.is_valid(),
+            'message': message.replace('"','\\"')
         })
     else:
+        post.body = html_to_textarea(post.body)
         post_form = PostForm(instance=post)
-    return response(request, 'forum/edit_post.html', {'post_form': post_form, 'post': post, })
+    return render_to_response('forum/edit_post.html', RequestContext(request, {
+        'post_form': post_form, 'post': post, }))
     
 
 @login_required
 def post_visibility(request, post_id):
-    post = get_object_or_404(Post, pk=post_id, user=request.user)
+    post = get_object_or_404(Post, pk=post_id)
+    if post.user != request.user:
+        return HttpResponseForbidden()
     post.visible = not post.visible
     post.save()
-    return response(request, "response.json", {
+    post.thread.latest_post = post.thread.post_set.published().latest('created')
+    post.thread.save()
+    return render_to_response("response.json", RequestContext(request, {
         'success': True,
-        'message': post.visible and _(u'Hide') or _(u'Show')
-    })
+        'message': post.visible and u'Hide' or u'Show'
+    }))

File media/screen.css

-body { font-family: "Bitsream Vera Sans", helvetica, arial, sans-serif; font-size: 14px; line-height: 1.8em; padding: 20px; color: #333; }
-
-a { color: blue; text-decoration: underline; cursor: pointer; }
-h1 { font-size: 20px; }
-h1 span { font-weight: normal; color: #ccc; }
-h1 span a { color: #6d8dac; }
-h3 { font-size: 13px; }
-
-.date { color: #666; font-weight: normal !important; }
-
-strong { font-weight: bold; }
-em { font-style: italic; }
-textarea { font-size: 13px; line-height: 1.4em; padding: 2px; height: 170px; width: 440px; }
-hr { display: block; height: 1px; border: 1px solid gray; border-width: 1px 1px 0 0; }
-
-.clear { clear: both; }
-.left { float: left; }
-.right { float: right; }
-
-ol.pagination { margin: 10px 0; }
-ol.pagination li { display: inline; margin: 0 1px 0 0; border: 1px solid #E0E0E0; padding: 3px; background: #F0F0F0; line-height: 30px; }
-ol.pagination li.selected { border-color: transparent; background: transparent; }
-ol.pagination li a { color: #6d8dac; }
-
-#actions { border-top: 1px solid #f0f0f0; margin: 10px 0; }
-#actions li { padding: 4px 7px; font-weight: bold; font-size: 14px; }
-
-#cp { border-bottom: 1px solid #ccc; background: #e8e8e8; padding: 4px 8px; overflow: hidden; }
-#nav { border-bottom: 1px solid #ccc; background: #f0f0f0; padding: 4px 8px; overflow: hidden; margin-bottom: 14px; }
-
-#forum a.cat { padding: 2px 3px; margin-right: 4px; color: white; background: #ccc; font-size: 11px; text-decoration: none; }
-
-#forum #thread-detail { width: 820px; }
-#forum #thread-detail .post { margin-bottom: 20px; overflow: hidden; border-top: 1px dotted #ccc; padding-top: 9px; }
-#forum #thread-detail .hidden { color: #e0e0e0; }
-#forum #thread-detail .hidden .date { color: #e0e0e0; }
-#forum #thread-detail .hidden a { color: #6d8dac; }
-#forum #thread-detail .hidden img { opacity: .3; }
-#forum #thread-detail .post .meta { width: 120px; float: left; }
-#forum #thread-detail .post .meta h2 { font-size: 15px; white-space: nowrap; font-weight: bold; }
-#forum #thread-detail .post .meta h2.date { font-size: 12px;  }
-#forum #thread-detail .post .meta ul.actions { font-size: 11px; font-weight: bold; }
-#forum #thread-detail .post .meta ul.actions li { display: inline; padding-right: 4px; }
-#forum #thread-detail .post .meta ul.actions li a { color: #6d8dac; }
-#forum #thread-detail .post .meta img.avatar { margin: 4px 0; }
-#forum #thread-detail .post .body { float: left; padding-left: 10px; width: 690px; }
-#forum #thread-detail .post .body form { margin-top: 5px !important; }
-#forum #thread-detail .post .body blockquote { border-left: 2px solid #ccc; padding-left: 7px; margin: 10px 0 10px 3px; font-size: 11px; line-height: 1.1em; color: #666; }
-#forum #thread-detail .post .body h1,
-#forum #thread-detail .post .body h2,
-#forum #thread-detail .post .body h3,
-#forum #thread-detail .post .body h4,
-#forum #thread-detail .post .body h5,
-#forum #thread-detail .post .body h6,
-#forum #thread-detail .post .body ol,
-#forum #thread-detail .post .body ul,
-#forum #thread-detail .post .body p { margin-bottom: 10px; }
-#forum #thread-detail .post .body ol li { list-style: decimal outside; margin-left: 19px; }
-#forum #thread-detail .post .body ul li { list-style: disc outside; margin-left: 19px; }
-
-#forum #thread-list td.pointer span { color: transparent; font-weight: bold; }
-#forum #thread-list tr.selected td.pointer span { color: black; }
-#forum #thread-list { width: 100%; }
-#forum #thread-list tr td, table th { padding: 4px 7px; }
-#forum #thread-list td { vertical-align: top; }
-#forum #thread-list td.first { white-space: nowrap; }
-#forum #thread-list tr th { color: #777; font-size: 0.9em; font-weight: bold; background: #e0e0e0; }
-#forum #thread-list tr.status_5 { background: pink; }
-#forum #thread-list tr.even { background: #f0f0f0; }
-#forum #thread-list tr.unread .post { background: #ffff80; }
-
-#forum form { border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; background: #f0f0f0; margin: 20px 0; padding: 10px; width: 600px; }
-#forum form p { overflow: hidden; margin-bottom: 10px; }
-#forum form label { display: block; width: 100px; float: left; font-weight: bold; }
-#forum form#thread_add input[type=text] { width: 400px; font-size: 20px; padding: 2px; }
-
-#forum .markdown { color: #888; position: absolute; margin-left: 620px; font-size: 10px; line-height: 1.3em; }
-#forum .markdown ul { list-style: none; }
-#forum .markdown p { margin: 1em 0; }

File media/script.js

-$(document).ready(function(){
-  
-  if($("#thread-list").size()){
-  
-    pointer = $("#thread-list tr.thread:first").addClass("selected");
-  
-    $(this).keypress(function(e){
-      switch(e.which){
-        case 107: // k
-          if(pointer.next().size()){
-            pointer = pointer.removeClass("selected").next().addClass("selected");
-          }
-          break;
-        case 106: // j
-          if(pointer.prev(".thread").size()){
-            pointer = pointer.removeClass("selected").prev().addClass("selected");
-          }
-          break;
-        case 111: // o
-          window.location = pointer.find(".first a:last").attr("href");
-          break;
-        case 117: // u
-          history.go(0);
-          break;
-      }
-    });
-    
-  }
-  
-  if($("#thread-detail").size()){
-    
-    var replied = false;
-    
-    $("textarea").focus(function(e){
-      replied = true;
-    });
-  
-    $(this).keypress(function(e){
-      switch(e.which){
-        case 114: // r
-          if(!replied){
-            replied = true;
-            $("textarea:last").focus();
-          }
-          break;
-        case 117: // u
-          if(!replied){
-            history.back();
-          }
-          break;
-      }
-    });
-	
-  	var activate_edit_post_form = function(body, old){
-  		
-  		replied = true;
-  		
-  		$("textarea", body).focus();
-		
-  		$("input:submit", body).click(function(){
-			  var form = $("form", body);
-  			$.post(form.attr("action"), form.serialize(), function(response){
-  				var response = eval('(' + response + ')');
-  				var message = unescape(response['message']);
-  				if(response['success']){
-  					body.html(message);
-  				}else{
-  					body.html(message);
-  					activate_edit_post_form(body, old);
-  				}
-  			});
-  			return false;
-			
-  		})
-		
-  		$("a.cancel", body).click(function() {
-  		    body.html(old);
-  		});
-		
-  	}
-	
-  	$("#forum a.edit").click(function(){
-  		var body = $(this).parents("div.body"),
-  		    old = body.html(),
-  			  level = 1,
-  			  down = true,
-  			  beat = function(){
-  			  	if(body.find("form").size()) {
-  			  		body.css("background","white");
-  			  		return false;
-  			  	}
-  			  	var h = (level).toString(16);
-  			  	body.css("background","#FFFF" + h + h);
-  			  	if(level==0||level==15) down = !down;
-  			  	down ? level=level-1 : level=level+1;
-  			  	setTimeout(beat, 30);
-  			  	return;
-  			  };
-  		beat(); // Callback to animate heartbeat until AJAX form gets loaded in its place
-  		body.load(this.href, null, function(){
-  		  activate_edit_post_form(body, old)
-  		});
-  		return false;
-  	});
-	
-  	$("#forum a.toggle").click(function(){
-  		var that = this;
-  		$.getJSON(this.href, function(json){
-  			if(json.success){ // success
-  				$(that).html(json.message).parents("li.post").toggleClass("hidden");
-  			}else{
-  			  alert("Server side error. Please try again.");
-  			}
-  		});
-  		return false;
-  	});
-    
-  }
-	
-	$("table tr:even").addClass("even");
-    
-  $("#forum select.forums").change(function() {
-    window.location = $(this).val();
-  });
-	
-});
-
-
-
-
-
+from setuptools import setup, find_packages
+import sys, os
+
+version = '0.1'
+
+setup(name='forum', version=version,
+    packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+)
+