msgbartop
by Brian Neal
msgbarbottom

30 Dec 08 Photo of the Day Progress

I’ve been working on the photo of the day application, and I have it all working except for the user comments. Things went pretty well, but I am reminded now and then that I am not an expert by any means with Python. Unlike the PHP version, I am not doing any crazy table locking at midnight to prevent race conditions. I created a custom management command that I intend for a cron job to run at midnight. This greatly simplifies things. The normal view logic simply reads the database to get the current photo. Only the management command needs to write to the current table. I am still maintaining a sequence of photos, this time in a Django CommaSeparatedIntegerField. The only tricky part is when I add a photo through the admin interface, I need to insert this new photo for the next day in the sequence. And likewise if I delete a photo, I need to remove it from the sequence. I accomplished this by overriding the Photo model’s save() and delete() methods. Inside those methods, I make calls to a custom model manager I wrote on the Sequence table. Django’s model managers are very cool, and allow you to easily add handy functionality.

There isn’t a lot of documentation on creating your own custom management commands in Django. However this isn’t really a problem, as the docs direct you to the source code. The source code is very well documented, and I was able to figure out what I needed to do by reading the comments in the code and examining the source for some standard management commands.

When I upload photos I now automatically generate a thumbnail. I know there are many Django plugins that already do this for you, but the code is so simple (< 10 lines?) with the PIL library there is no need to entangle my code with a 3rd party application for something this simple. I have one quirk. I used the strftime formatting in the upload_to parameter to my ImageFields to help minimize filename clashes. Whenever I save() a photo of the day, I update the thumbnail because I don’t know if the main image has changed or not. It might have changed, or maybe I am just updating the “photo of the day count” statistic on it. This causes the thumbnail to move to a different directory. Maybe there is a way to make it save in-place, I’m not sure yet. I’ll look into that a bit. In any event I don’t think that is a big deal, it will still minimize name clashes and it is all being managed correctly (stray files are not being left laying around).

Now I have to decide what to do about user comments. I’ve avoided this until now. I need to add comments to the polls and news items also. I will look at the contributed Django comments application a bit. At first glance, I like that it is generic and can be used on any model, and thus cuts down on duplicated code. On the other hand, I might want to use AJAX to update comments, and I’m not sure if that fits. The contributed comments application also seems to be geared for allowing anyone to comment, and thus contains a lot of spam and consistency checking that I won’t need (only registered users can comment on my site). There was a blog post about this I read somewhere. I need to dig that up.

I did some googling, and started researching my options for a Javascript editor for comments. My site users aren’t very technical, so having a GUI editor is probably a must. So far, on SG101 2.0, I have been using TinyMCE for submitting news stories and even in the private messages. But this may be overkill for comments (and even private messages). My users are used to BBCode since the current forum is using phpBB. However I’m not a huge fan of BBCode. Markdown looks interesting and seems to be widely used on the Django related blogs. There is a Javascript editor for Markdown called the WMD editor, but development on it seems to have stopped, and it still isn’t fully open sourced. However, the stackoverflow.com site is using it, so it must be decent. I also came across the markItUp! Universal Markup Editor which looks very interesting to me. I am thinking about using it in conjuction with Markdown. I’ll have to retrain my users a bit, but the GUI editor should make that easier. Having a nice editor with a preview function will be a step up from the existing site. And of course I will use my smiley application that I developed earlier with the shoutbox here.

So next I’m off to do some reading, thinking, and experimenting with comments.

Tags: , ,

26 Dec 08 What to do next…

So with the shoutbox out of the way (for now), I still have several major applications to write before tackling the 800 pound gorrilla in the room: forums. These applications are: some kind of Paypal donations tracker, a Photo of the Day app, the member map, and the event calendar.

In order to thoroughly test the donations application, I think I need to have live test code deployed in order to use Paypal’s developer sandbox. So I think I will wait on that until I get something deployed.

The event calendar will be interesting. I wrote a very complicated PHP-Nuke module called GCalendar for the current SG101. I’m a bit hesitant to port all that PHP code to Python and Django. First of all, I attempted to add repeating event logic to GCalendar, and it does seem to work for most cases, but it doesn’t do everything very well. There is no way, for example, to specify you want an event that repeats on the last Friday of every month. Granted, that doesn’t come up very often. I am currently thinking of leveraging Google Calendar. My idea is to let Google Calendar deal with the hairy calendar math, while all I need to do is manage the adding and deleting of events. And it would let me play with the Google gdata python client. Another benefit of doing this is that other people can embed the SG101 calender on their sites and blogs. So I think I will at least explore this route, falling back to a complete port of GCalendar if it doesn’t look like it will pan out. This is going to take some studying, so I think I’ll put this off for a bit also.

I also wrote a PHP-Nuke module called Member Map. This application allows members to add themselves to a Google map. It was a fun little app to do, and most of the code was written in Javascript (to interface with the Google Maps API). Since I just got the cool jQuery in Action book, I think I will take some time to learn about jQuery and rewrite my hand-crafted Javascript code to leverage jQuery. So I think I’ll start reading the book before tackling this application.

So that leaves the Photo of the Day feature. This is a popular attraction on the current site. One of my must have features is to make the site generate the thumbnail for me. Right now I have to generate this myself (usually I use the Gimp). Since I now have experience with the PIL when I wrote code to support avatars, this should not be too hard. In the current PHP implementation, I have some rather complicated code that runs on the first page request after midnight to choose the next photo of the day. I was having race conditions from different requests, and had to resort to table locking. For my Django site, I think I will handle this differently. I will use a cron job to run a custom manage.py shell command to choose the next photo of the day, and avoid all that table locking nonsense. The original code was developed back when I was using shared hosting without access to cron. Now I can stretch out and do it the “right” way.

I think I will finally stop avoiding user comments when I start the Photo of the Day. I will finally investigate Django’s comment system and see if it is right for my site. I sure hope so. I also need to decide if I am going to allow some kind of mark up in the comments. There is always TinyMCE, but it may be overkill. I know there is a Javascript Markdown editor out there, but it didn’t look like development on it was continuing, and it had a funny license as I recall. I’ll have to look into this again. I will also want to use my smiley system that I introduced for the shoutbox.

Tags:

24 Dec 08 Anatomy of a Shoutbox; Part 6 – Conclusion: Delete In Place

Welcome to part 6, the final post on the development of the new shoutbox. In case you missed the earlier parts, here they are: part 1, part 2, part 3, part 4, part 5. Here we are going to discuss the final feature: deleting a shout in place off the shout history page.

The idea here is to add a delete link on the shouts that the user created as she is viewing the shout history. We will use the jQuery library to dynamically add click handlers to these links. The click handler functions will make an AJAX post to the server and request that a shout be deleted. The server view function will verify that the user sending the request actually created the shout in question. If so, the shout will be deleted from the database and then a response sent back to the client. This response will be then be used by our Javascript to hide the deleted shout. Thus we will have deleted a shout without refreshing the entire page.

The first thing I did was modify the shout history template to output a link underneath a shout if the current user authored that shout. This template (shoutbox/view.html) was presented in part 5. I’ll just show the new code inside the for loop that adds these links:

    {% ifequal user shout.user %}
        | <a href="#" class="shout-del" id="shout-del-{{ shout.id }}">Delete</a>
    {% endifequal %}

The important thing to note is our links don’t go anywhere, they have the CSS class “shout-del”, and we give them an id of “shout-del-xx” where xx is the id of the shout from the database.

I then modified the shoutbox_app.js document ready function to look for all links that have the class “shout-del” and I attach a click function to them. The click function asks the user via the Javascript confirm() function if they really want to delete the shout. If they confirm, I then use javascript regular expression magic to pull out the shout’s  primary key from the <a> tag’s id, and send a POST request to the server to delete that shout. The relevant snippet is shown below.

     $('.shout-del').click(function () {
         if (confirm('Really delete this shout?')) {
             id = this.id;
             if (id.match(/shout-del-(\d+)/)) {
                $.post('/shout/delete/', { id : RegExp.$1 }, function(id) {
                    id = '#shout-del-' + id
                    $(id).parents('tr').hide();
                    $('div.shoutbox-history table tr:visible:even').removeClass('odd');
                    $('div.shoutbox-history table tr:visible:odd').addClass('odd');
                    }, 'text');
             }
         }
         return false;
     });

We are posting to the URL /shout/delete, and passing along the shout ID in the id parameter. We also supply a callback function that gets called when the delete happens successfully. More on that later. Finally the click function returns false so that the default action of clicking on a link is not executed.

We now turn to the server, and examine the view function that handles the URL /shout/delete. You’ll notice similar code as presented in part 5′s edit view function. This time I have discovered Django’s classes that derive from HttpResponse, and I use them in the error cases.

def delete(request):
    """This view deletes a shout. It is called by AJAX from the shoutbox history view."""
    if request.user.is_authenticated():
        id = request.POST.get('id', None)
        if id is None:
            return HttpResponseBadRequest()
        try:
            shout = Shout.objects.get(pk=id)
        except Shout.DoesNotExist:
            return HttpResponseBadRequest()
        if request.user != shout.user:
            return HttpResponseForbidden()
        shout.delete()
        return HttpResponse(id)

    return HttpResponseForbidden()

We check to see if the user is authenticated before proceeding. We then retrieve the shout ID from the POST QueryDict. From the ID we retrieve the actual shout from the database using the Django ORM. And finally, if and only if the user making the request is the same user that created the shout, we delete it. If all of that went well, we send back a positive HTTP response that includes the ID of the shout that was deleted.

Now refer back to our Javascript, posted above.  In the callback function we receive the ID as text from the server. We then convert the numeric ID to the CSS ID and use jQuery to select the parent <tr> of our <a> tag, and hide it. Wow! That was a very compact way to do all of that work.

The remaining logic takes care of the table presentation. I used CSS to style every other row in the shout history table with a class called “odd”. Since we just hid one row, that scheme got messed up. Luckily, the compact notation of jQuery again comes to the rescue and we can quickly restyle all odd visible rows with our class “odd”, taking care to remove the style from the new even rows first.

And there we have a shoutbox written in Django and Javascript for the new version of my site. Not bad for my first attempt, and it certainly was much easier than I imagined. All in all, there is a lot less Python code than the existing PHP code, and we even get neat-o AJAX’y features that the old one didn’t have. So I think we will go with this for the first pass of the site. Once I get more comfortable with jQuery I may decide to redo the scrolling itself with it. We could also look into posting shouts to the database without a complete page refresh. There is always something to go back and refactor as you learn more about your tools and best practices. I hope someone found this quick series of posts useful.

Tags: , , ,

20 Dec 08 Anatomy of a Shoutbox; Part 5 – Edit In Place

In this post I’ll show how I got an edit-in-place feature working with the shoutbox. In case you missed the earlier parts, here they are: part 1, part 2, part 3, part 4.

The first thing I did was create a “shout history” view. This allows you to view the shouts in a full page context. In my views.py file I created this:

def view(request, page=1):
    """This view allows one to view the shoutbox history."""
    paginator = DiggPaginator(Shout.objects.all(), SHOUTS_PER_PAGE, body=5, tail=3, margin=3, padding=2)
    try:
        the_page = paginator.page(int(page))
    except InvalidPage:
        raise Http404

    return render_to_response('shoutbox/view.html', {
        'page': the_page,
        },
        context_instance = RequestContext(request))

Here I am using the DiggPaginator, which is a very nice paginator that gives you “Digg-style” pagination. I then pass the page object to the template which does most of the interesting work.

In order to get the edit-in-place funcitonality to work, I am leveraging the Jeditable jQuery plugin. In order to use this plugin, in my shoutbox/view.html template I include <script> tags to bring in both the jQuery javascript library and the Jeditable plugin code. I then loop over the shouts for the page and display them along with the avatar and links to the user that made the shout. You’ll also see me using the smilify fiilter I discussed in part 4.

(more…)

Tags: , , , ,

16 Dec 08 Blog Upgraded

I just upgraded the blog to WordPress 2.7, and wow am I impressed. They sure did a great job of building the user interface in the admin section and making it customizable. The automatic upgrade feature sure looks cool as it was kind of tiring doing the whole ftp dance. We’ll see how well it works. I wonder if it will delete old files?

And all of a sudden today I am getting a torrent of comment spam. I’ve installed the akismet filter, I sure hope it works.

Let me know if you see anything out of place. So far it looks good.

14 Dec 08 Anatomy of a Shoutbox; Part 4 – Smilies!

I’ve been making some great progress on the shoutbox. I’ve added smilies and I used a jQuery plugin to get the edit-in-place working. It’s awesome! Meanwhile, while it is fresh in my mind, I’ll post about the smilies here.

In case you missed the earlier parts, here they are: part 1, part 2, part 3.

One of the “fun” features of the shoutbox is the smilies, or emoticons. In a nutshell, certain character sequences are translated into small images in the “shout” text. Furthermore, all of the images are displayed below the shoutbox, and by clicking on them they get added to the shout the user is typing via some simple javascript.

I can see myself using this smiley function in lots of places: shoutbox, user comments, photo of the day, forums, etc. So I created a new application for it, separate from the shoutbox application.

I read up on django’s custom filter documentation, and spent a fair amount of time scratching my head over the auto-escaping feature. It suddenly occured to me that what I need is similar to the django urlize filter: go through some text and replace certain phrases with HTML. In the urlize case this is an <a> tag. In my case this is an <img> tag. I then studied the urlize filter source to steer me. Mine turned out a bit simpler as you will see below.

First let’s start with the model. I wanted to be able to easily edit and add smilies via the admin interface. The legacy shoutbox allowed many “phrases” to map to a single smiley, but it did this in a redundant way. I decided to keep things simple and only allow one phrase per smiley. Part of this was driven by the fact that if the user clicks on a smiley I need to add the phrase to the text they are typing. If there are multiple phrases, which do I use? I suppose I could have picked the first one, but this was going against my “let’s keep this simple for now” philosophy. Here is the model I came up with:

(more…)

Tags: , , ,

08 Dec 08 Anatomy of a Shoutbox; Part 3 – View & Template

And now we continue on with the shoutbox development. If you missed the earlier posts, here they are: part 1, part 2.

After getting the model and admin parts working (discussed in part 2), I turned to the view and template. I decided to create an inclusion tag for the shoutbox. My site’s base template will then use the tag to display the shoutbox on every page of the site.

Writing the inclusion tag itself was pretty easy. All I need to do is retrieve the last 10 shouts and render a template with the results. So, after studying the Django docs, I created my templatetags sub-directory, the obligatory empty __init__.py file, and my shoutbox_tags.py file, which contained this code:

from django import template
from shoutbox.models import Shout

register = template.Library()

@register.inclusion_tag('shoutbox/shoutbox.html', takes_context=True)
def shoutbox(context):
   shouts = Shout.objects.all()[:10]
   return {
      'shouts': shouts,
      'user': context['user'],
   }

I’m using the newer Python decorator syntax here. The first argument to the inclusion_tag decorator function is the name of the template I am going to render. The needs_context argument set to True says that I will want to access the context. The reason for this is I want to display a form for authenticated users, so I will need to query the user variable from the context for that.

All I do here is retrieve the last 10 shouts, then return a dictionary that will be fed to my template.

Now to write the actual template itself. I wrote this template in a series of “baby-steps”, which is my usual mode of operation. My steps were as follows:

  1. Write code that only displays the shouts as paragraphs inside a div tag.
  2. Played with the styles and wrote css to make it look decent.
  3. Got the form post working with the view function.
  4. Got the thing to scroll by leveraging some 3rd party javascript.

I should show you the template at each step, but I’m writing this after the fact, and it really didn’t change that much from beginning to end. I’ll present the final version here and discuss it.

{% extends 'side_block.html' %}
{% block block_title %}Shoutbox{% endblock %}
{% block block_content %}
{% if shouts %}
<div id="marqueecontainer" onmouseover="copyspeed=pausespeed" onmouseout="copyspeed=marqueespeed">
<div id="vmarquee" style="position: absolute; width: 98%;">
   {% for shout in shouts reversed %}
      <p>
      <span class="shoutbox-user">{{ shout.user.username }}:</span>
      <span class="shoutbox-shout">{{ shout.shout|urlizetrunc:15 }}</span>
      <span class="shoutbox-date">{{ shout.shout_date|date:"D M d Y H:i:s" }}</span>
      </p>
   {% endfor %}
</div>
</div>
{% endif %}
<center><a href="{% url shoutbox-view page=1 %}">Shout History</a></center>
{% if user.is_authenticated %}
<center>
<form action="{% url shoutbox-shout %}" method="post">
   <input type="text" maxlength="2048" size="20" name="msg" value="" />
   <br />
   <input type="submit" value="Shout" />
</form>
</center>
{% else %}
<p>Please login or register to shout.</p>
{% endif %}
{% endblock %}

First of all, this template extends another called side_block.html. I decided that all my “blocks” will have common styling and features, and I will collect that information there. This template will provide values for the block title and block content.

Lines 4-16 display the 10 latest shouts. The two divs are needed for the javascript scrolling, which I’ll explain in a minute, so you can ignore them for now. The for loop is the real action here. I simply loop over the shouts, producing a paragraph for each one. The ordering we setup for the shouts in the model is reverse chronological order. On the current SG101 site, the legacy shoutbox scrolls up, displaying the oldest of the 10 shouts first. To achieve this, we use the nifty reversed keyword on the for loop, which gives me exactly what I want.

The other interesting thing in the loop is the use of the urlizetrunc filter. As you may recall from part 1, one of my requirements is to minimize or truncate URL’s. I headed down the path of writing my own function to do this, and spent about half hour on it, trying to come to terms with Django’s escaping protocol, when it suddenly occurred to me that I can’t be the first person who had a use-case for this. So I quickly scanned the Django filter documentation, and sure enough, a function close enough to what I wanted was already in the can! The legacy shoutbox shortens URLs by simply displaying them as the text [URL]. The urlize filter doesn’t do this, but urlizetrunc is close enough to what I need that I used it as-is. It simply truncates URLs after a certain length.

The last half of the template decides whether to display a shout form to the user, depending on whether she is authenticated or not. If not authenticated, I simply display a reminder to login or register (hmm, I need to make those links to the appropriate pages). If authenticated, I display the shout form. I could have used a Django form here; perhaps the inclusion tag could have passed it to the template. However, the form is so simple I didn’t even bother. All I need is one text input and a submit button, which I would have had to write anyway.

I will now show you the view function that this form posts to. It is quite simple.

@login_required
def shout(request):
    if request.method == 'POST':
        msg = request.POST.get('msg', '')
        if msg != '':
            shout = Shout(user=request.user, shout=msg)
            shout.save()

    return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))

The first thing to notice is I have decorated my view function with the login_required decorator. This ensures only registered users can shout. Sure, we only display the form to registered users, but that doesn’t stop someone from crafting their own form and posting to the same URL. I then pull the msg out of the POST Querydict, and let it default to the empty string if it isn’t there. If the msg isn’t empty, I create and save a new Shout object, recording the user that made it and the msg value. As you may recall, the shout_date field will be automatically initialized to the current date and time by Django when I save the new shout. I then redirect to the same page the user was looking at (recall that the shoutbox will appear on every page of the site). If this can’t be determined (which normally should not happen) I simply go to the site root.

With this machinery in place, I was able to submit shouts just like the current (legacy) shoutbox on SG101. Sweet! This was fun, but now it gets even better. How to make it scroll?

I had no interest in writing the scrolling code, so I spent a little while googling. I did not consider reusing the legacy shoutbox javascript, as it required the javascript to be sent with the page (it was dynamic), and the code wasn’t properly escaped and broke XHTML compliance. I really want to learn more about the jQuery library, and I did look for a jQuery plugin for this task. However I decided to save jQuery for another day, and found some nice code on the venerable Dynamic Drive website. This code was similar to the legacy shoutbox code, so I was comfortable with it. I liked it because it simply scrolled a div upwards, and required no inline javascript. I think the legacy shoutbox script must have used an earlier version of this same script because it was so similar. To integrate this with my template code, I simply had to wrap my shouts inside 2 divs, and include the javascript in my site’s base template. And wow it worked on the first try, much to my surprise. I was smiling at how quickly this came together.

Wow, we have nearly all the functionality that I wanted completed already. All that is left is the ability to review the shout history, and let users edit and delete their shouts. I completed the shout history portion, so I will show that in the next post. For editing and deleting, I think I might want to do something AJAX-y and let the user edit the shout in-place. Hmmm, maybe I will learn some jQuery to make that easier. And maybe then I can rewrite the scrolling with jQuery. More to come!

Tags: , ,

07 Dec 08 Anatomy of a Shoutbox; Part 2 – Model and Admin

One of the things that really attracted me to Django was that it has an ORM. Of course I had never seen an ORM before and didn’t even know that term until later. An ORM (Object-Relational Mapping) allows you to seamlessly create Python objects from database tables, and vice versa. So you never have to code your own SQL unless you really want to do something fancy.

For the shoutbox app, we need a very simple model. Here is my models.py file:

from django.db import models
from django.contrib.auth.models import User

class Shout(models.Model):
   user = models.ForeignKey(User)
   shout_date = models.DateTimeField(auto_now_add=True)
   shout = models.TextField()

   def __unicode__(self):
      shout = self.shout[:60]
      return u'Shout from %s: %s' % (self.user.username, shout)

   class Meta:
      ordering = ('-shout_date', )

There is nothing too out of the ordinary here. Each shout was made by a registered user, so we need a ForeignKey to the User table. We record the date and time the shout was made (using auto_now_add initializes this field for us when the object is saved), and finally the shout itself is just a TextField.

The __unicode__ function will be used in the admin interface. Here we just show who the shout is from and then a truncated version of the shout. Finally, we can tell Django our default ordering of Shouts is in reverse chronological order via the ordering attribute in the embedded Meta class.

Now we turn to the admin.py file to enable and configure an automatic admin interface for Shouts. Again, this is an amazing feature of Django that really sold me on the idea of trying this framework out.

from django.contrib import admin
from shoutbox.models import Shout

class ShoutAdmin(admin.ModelAdmin):
   list_display = ('shout_date', '__unicode__')
   raw_id_fields = ('user', )

admin.site.register(Shout, ShoutAdmin)

Here we derive a class from Django’s admin.ModelAdmin class to configure our admin interface. Our main display listing should consist of two columns: the shout_date and then the __unicode__ representation of each Shout. This will invoke the __unicode__ member function on each Shout object displayed.

Here I am using the raw_id_fields feature for the first time. Since I have over 2000 users currently on SG101, I do not want to load all that information into a select box everytime I drill into a Shout. The raw_id_field simply displays the raw user id along with the username. If I really want to change the user, the field has a pop-up control to allow me to load all the usernames. I should have done this on a few other applications I previously wrote. This is the constant refactoring I wrote about earlier.

And that’s it! After running syncdb I can now add Shouts to the database via the admin interface, all in less than 25 lines of code from me! In part 3 I will show how I display the shoutbox as a “block” (to borrow the PHP-Nuke term) on the website. To do this, I created one of my first Django template tags and incorporate some 3rd party javascript to accomplish the scrolling of the shouts. Stay tuned.

Tags: , ,

03 Dec 08 Posting Code

I guess if I am going to start posting code, I need to make sure I can do that with WordPress. It turns out there are a couple of plugins available to make this task easier. I am currently trying out SyntaxHighlighter Plus for syntax highlighting, and Visual Code Editor to allow one to continue using the visual editor when posting code.

So let’s see how they work!

Here is some C++:

#include <iostream>
#include <ostream>
int main()
{
std::cout << "Hello World!" << std::endl;
return 0;
}

And here is some Python:

def index(request, page=1):
stories = Story.objects.all()
paginator = create_paginator(stories)
try:
the_page = paginator.page(int(page))
except InvalidPage:
raise Http404

Pretty cool! A big thank-you to the plugin authors!

Tags: ,

02 Dec 08 Anatomy of a Shoutbox; Part 1- Requirements

One of the applications from the current SG101 website I want to bring over is the shoutbox. Shoutboxes are very popular in the Nuke community, and I was very surprised at how much my users took to it. It may very well be the 2nd most popular application after the forums. It is kind of a community Twitter for the site (hmmm did the Twitter guys get the idea for Twitter from the lowly shout box?).

The current site is using a shoutbox script developed by the now defunct ourscripts.net site. The script was obviously a labor of love by the original developer, but to my taste it is over-engineered and somewhat heavy handed in terms of resources it uses. It is highly configurable; it has admin options for changing the style (theme), user banning, IP banning, a bad word filter, configurable emoticons, etc. After running this script for over two years now I think I really only need the following features.

  1. Only registered users can shout.
  2. Shouts should scroll (users love this for some reason).
  3. Any URLs in the shout should be made clickable (and preferably shortened or truncated).
  4. Emoticons (smilies) are absolutely required.
  5. Shout history should be displayable.
  6. Users should be able to edit or delete their shouts in the history.
  7. XHTML compliant (the current script is not).
  8. The admin should be able to edit and delete shouts.

And that’s basically it. I may decide to have the thing auto-prune shouts, as Django doesn’t currently have a bulk delete feature in the auto-generated admin interface (but that is coming in Django 1.1).

I’m going to be using emoticons in other comment and forum applications all over the site, so thinking ahead here, it is probably best to create that functionality in a separate Django application that the shoutbox will use.

Okay, in keeping with my limited time and my new attempt to blog more but in more manageable pieces, I’ll stop here. The next few posts will detail the stages of development of this application.

Tags: , ,