changeset 1:dbd703f7d63a

Initial import of sg101 stuff from private repository.
author gremmie
date Mon, 06 Apr 2009 02:43:12 +0000
parents 900ba3c7b765
children f3ad863505bf
files gpp/__init__.py gpp/accounts/__init__.py gpp/accounts/admin.py gpp/accounts/forms.py gpp/accounts/models.py gpp/accounts/urls.py gpp/accounts/views.py gpp/bio/__init__.py gpp/bio/admin.py gpp/bio/forms.py gpp/bio/models.py gpp/bio/templatetags/__init__.py gpp/bio/templatetags/avatar_tags.py gpp/bio/urls.py gpp/bio/views.py gpp/bulletins/__init__.py gpp/bulletins/admin.py gpp/bulletins/models.py gpp/comments/__init__.py gpp/comments/admin.py gpp/comments/forms.py gpp/comments/models.py gpp/comments/templatetags/__init__.py gpp/comments/templatetags/comment_tags.py gpp/comments/urls.py gpp/comments/views.py gpp/contact/__init__.py gpp/contact/forms.py gpp/contact/urls.py gpp/contact/views.py gpp/core/__init__.py gpp/core/admin.py gpp/core/functions.py gpp/core/logging.py gpp/core/models.py gpp/core/paginator.py gpp/core/views.py gpp/core/widgets.py gpp/downloads/__init__.py gpp/downloads/admin.py gpp/downloads/forms.py gpp/downloads/models.py gpp/downloads/templatetags/__init__.py gpp/downloads/templatetags/downloads_tags.py gpp/downloads/urls.py gpp/downloads/views.py gpp/gcalendar/__init__.py gpp/gcalendar/admin.py gpp/gcalendar/admin_views.py gpp/gcalendar/calendar.py gpp/gcalendar/forms.py gpp/gcalendar/models.py gpp/gcalendar/settings.py gpp/gcalendar/urls.py gpp/gcalendar/views.py gpp/irc/__init__.py gpp/irc/models.py gpp/irc/templatetags/__init__.py gpp/irc/templatetags/irc_tags.py gpp/irc/urls.py gpp/irc/views.py gpp/legal/__init__.py gpp/legal/admin.py gpp/legal/models.py gpp/legal/urls.py gpp/legal/views.py gpp/manage.py gpp/membermap/__init__.py gpp/membermap/admin.py gpp/membermap/forms.py gpp/membermap/models.py gpp/membermap/signals.py gpp/membermap/urls.py gpp/membermap/views.py gpp/messages/__init__.py gpp/messages/admin.py gpp/messages/forms.py gpp/messages/management/__init__.py gpp/messages/management/commands/__init__.py gpp/messages/management/commands/purge_messages.py gpp/messages/models.py gpp/messages/templatetags/__init__.py gpp/messages/templatetags/messages_tags.py gpp/messages/urls.py gpp/messages/utils.py gpp/messages/views.py gpp/news/__init__.py gpp/news/admin.py gpp/news/feeds.py gpp/news/forms.py gpp/news/models.py gpp/news/urls.py gpp/news/views.py gpp/podcast/__init__.py gpp/podcast/admin.py gpp/podcast/models.py gpp/podcast/urls.py gpp/podcast/views.py gpp/polls/__init__.py gpp/polls/admin.py gpp/polls/forms.py gpp/polls/models.py gpp/polls/urls.py gpp/polls/views.py gpp/potd/__init__.py gpp/potd/admin.py gpp/potd/management/__init__.py gpp/potd/management/commands/__init__.py gpp/potd/management/commands/pick_potd.py gpp/potd/models.py gpp/potd/templatetags/__init__.py gpp/potd/templatetags/potd_tags.py gpp/potd/urls.py gpp/potd/views.py gpp/settings.py gpp/shoutbox/__init__.py gpp/shoutbox/admin.py gpp/shoutbox/forms.py gpp/shoutbox/models.py gpp/shoutbox/templatetags/__init__.py gpp/shoutbox/templatetags/shoutbox_tags.py gpp/shoutbox/urls.py gpp/shoutbox/views.py gpp/smiley/__init__.py gpp/smiley/admin.py gpp/smiley/models.py gpp/smiley/templatetags/__init__.py gpp/smiley/templatetags/smiley_tags.py gpp/smiley/views.py gpp/templates/404.html gpp/templates/500.html gpp/templates/accounts/login.html gpp/templates/accounts/logout.html gpp/templates/accounts/password_change.html gpp/templates/accounts/register.html gpp/templates/accounts/register_failure.html gpp/templates/accounts/register_success.html gpp/templates/accounts/register_thanks.html gpp/templates/accounts/registration_email.txt gpp/templates/admin/base_site.html gpp/templates/admin/gcalendar/change_list.html gpp/templates/base.html gpp/templates/bio/avatar.html gpp/templates/bio/base.html gpp/templates/bio/edit_profile.html gpp/templates/bio/markdown.html gpp/templates/bio/members.html gpp/templates/bio/view_profile.html gpp/templates/comments/comment.html gpp/templates/comments/comment_form.html gpp/templates/comments/comment_list.html gpp/templates/comments/markdown.html gpp/templates/comments/markdown_preview.html gpp/templates/contact/contact_email.txt gpp/templates/contact/contact_form.html gpp/templates/contact/contact_thanks.html gpp/templates/core/pagination.html gpp/templates/core/pagination_query.html gpp/templates/downloads/add.html gpp/templates/downloads/download.html gpp/templates/downloads/download_comments.html gpp/templates/downloads/download_list.html gpp/templates/downloads/download_summary.html gpp/templates/downloads/index.html gpp/templates/downloads/markdown.html gpp/templates/downloads/navigation.html gpp/templates/downloads/search_results.html gpp/templates/downloads/thanks.html gpp/templates/gcalendar/edit.html gpp/templates/gcalendar/event.html gpp/templates/gcalendar/google_sync.html gpp/templates/gcalendar/index.html gpp/templates/gcalendar/markdown.html gpp/templates/gcalendar/thanks_add.html gpp/templates/gcalendar/thanks_edit.html gpp/templates/irc/irc_block.html gpp/templates/irc/view.html gpp/templates/legal/view.html gpp/templates/membermap/index.html gpp/templates/membermap/markdown.html gpp/templates/messages/base.html gpp/templates/messages/compose.html gpp/templates/messages/inbox.html gpp/templates/messages/markdown.html gpp/templates/messages/notification_email.txt gpp/templates/messages/options.html gpp/templates/messages/outbox.html gpp/templates/messages/trash.html gpp/templates/messages/view.html gpp/templates/news/archive_index.html gpp/templates/news/base.html gpp/templates/news/category_index.html gpp/templates/news/feed_description.html gpp/templates/news/feed_title.html gpp/templates/news/index.html gpp/templates/news/send_story.html gpp/templates/news/send_story_email.txt gpp/templates/news/story.html gpp/templates/news/story_summary.html gpp/templates/news/submit_news.html gpp/templates/news/tag_index.html gpp/templates/podcast/base.html gpp/templates/podcast/detail.html gpp/templates/podcast/feed.xml gpp/templates/podcast/index.html gpp/templates/polls/index.html gpp/templates/polls/poll.html gpp/templates/polls/poll_results.html gpp/templates/polls/poll_vote.html gpp/templates/potd/potd_block.html gpp/templates/potd/view.html gpp/templates/shoutbox/render_shout.html gpp/templates/shoutbox/shoutbox.html gpp/templates/shoutbox/view.html gpp/templates/side_block.html gpp/templates/smiley/smiley_farm.html gpp/templates/weblinks/add_link.html gpp/templates/weblinks/base.html gpp/templates/weblinks/index.html gpp/templates/weblinks/link.html gpp/templates/weblinks/link_summary.html gpp/templates/weblinks/report_link.html gpp/templates/weblinks/search_results.html gpp/templates/weblinks/view_links.html gpp/urls.py gpp/weblinks/__init__.py gpp/weblinks/admin.py gpp/weblinks/forms.py gpp/weblinks/models.py gpp/weblinks/urls.py gpp/weblinks/views.py media/css/base.css media/css/bio.css media/css/blueprint/ie.css media/css/blueprint/print.css media/css/blueprint/screen.css media/css/comments.css media/css/downloads.css media/css/gcalendar.css media/css/membermap.css media/css/messages.css media/css/news.css media/css/pagination.css media/css/polls.css media/css/potd.css media/css/shoutbox.css media/css/shoutbox_app.css media/css/tab-nav.css media/css/weblinks.css media/downloads/stars/rating_half.gif media/downloads/stars/rating_off.gif media/downloads/stars/rating_on.gif media/downloads/stars/rating_over.gif media/icons/application_edit.png media/icons/calendar_add.png media/icons/calendar_delete.png media/icons/calendar_edit.png media/icons/cross.png media/icons/email.png media/icons/email_go.png media/icons/feed.png media/icons/flag_red.png media/icons/image_edit.png media/icons/key.png media/icons/link.png media/icons/note.png media/js/bio.js media/js/comments.js media/js/downloads/add.js media/js/downloads/rating.js media/js/downloads_admin.js media/js/gcalendar.js media/js/gcalendar_edit.js media/js/markitup/jquery.markitup.js media/js/markitup/jquery.markitup.pack.js media/js/markitup/readme.txt media/js/markitup/sets/default/images/bold.png media/js/markitup/sets/default/images/clean.png media/js/markitup/sets/default/images/image.png media/js/markitup/sets/default/images/italic.png media/js/markitup/sets/default/images/link.png media/js/markitup/sets/default/images/picture.png media/js/markitup/sets/default/images/preview.png media/js/markitup/sets/default/images/stroke.png media/js/markitup/sets/default/set.js media/js/markitup/sets/default/style.css media/js/markitup/sets/markdown/images/bold.png media/js/markitup/sets/markdown/images/code.png media/js/markitup/sets/markdown/images/h1.png media/js/markitup/sets/markdown/images/h2.png media/js/markitup/sets/markdown/images/h3.png media/js/markitup/sets/markdown/images/h4.png media/js/markitup/sets/markdown/images/h5.png media/js/markitup/sets/markdown/images/h6.png media/js/markitup/sets/markdown/images/italic.png media/js/markitup/sets/markdown/images/link.png media/js/markitup/sets/markdown/images/list-bullet.png media/js/markitup/sets/markdown/images/list-numeric.png media/js/markitup/sets/markdown/images/picture.png media/js/markitup/sets/markdown/images/preview.png media/js/markitup/sets/markdown/images/quotes.png media/js/markitup/sets/markdown/readme.txt media/js/markitup/sets/markdown/set.js media/js/markitup/sets/markdown/style.css media/js/markitup/skins/markitup/images/bg-container.png media/js/markitup/skins/markitup/images/bg-editor-bbcode.png media/js/markitup/skins/markitup/images/bg-editor-dotclear.png media/js/markitup/skins/markitup/images/bg-editor-html.png media/js/markitup/skins/markitup/images/bg-editor-json.png media/js/markitup/skins/markitup/images/bg-editor-markdown.png media/js/markitup/skins/markitup/images/bg-editor-textile.png media/js/markitup/skins/markitup/images/bg-editor-wiki.png media/js/markitup/skins/markitup/images/bg-editor-xml.png media/js/markitup/skins/markitup/images/bg-editor.png media/js/markitup/skins/markitup/images/handle.png media/js/markitup/skins/markitup/images/menu.png media/js/markitup/skins/markitup/images/submenu.png media/js/markitup/skins/markitup/style.css media/js/markitup/skins/simple/images/handle.png media/js/markitup/skins/simple/images/menu.png media/js/markitup/skins/simple/images/submenu.png media/js/markitup/skins/simple/style.css media/js/markitup/templates/preview.css media/js/markitup/templates/preview.html media/js/membermap.js media/js/messages/box.js media/js/messages/compose.js media/js/shoutbox.js media/js/shoutbox_app.js media/js/tiny_mce_init_admin.js media/js/tiny_mce_init_messages.js media/js/tiny_mce_init_std.js
diffstat 301 files changed, 10752 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/accounts/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,13 @@
+"""This file contains the automatic admin site definitions for the accounts Models"""
+
+from django.contrib import admin
+from accounts.models import IllegalUsername
+from accounts.models import IllegalEmail
+from accounts.models import PendingUser
+
+class PendingUserAdmin(admin.ModelAdmin):
+   list_display = ('username', 'email', 'date_joined')
+
+admin.site.register(IllegalUsername)
+admin.site.register(IllegalEmail)
+admin.site.register(PendingUser, PendingUserAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/accounts/forms.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,95 @@
+"""forms for the accounts application"""
+
+from django import forms
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from django.template.loader import render_to_string
+from django.contrib.sites.models import Site
+
+from core.models import SiteConfig
+from core.functions import send_mail
+from accounts.models import PendingUser
+from accounts.models import IllegalUsername
+from accounts.models import IllegalEmail
+
+
+class RegisterForm(forms.Form):
+   """Form used to register with the website"""
+   username = forms.RegexField(max_length = 30, regex = r'^\w+$',
+      error_messages = {'invalid' : 'Your username must be 30 characters or less and contain only letters, numbers and underscores.'})
+   email = forms.EmailField()
+   password1 = forms.CharField(label = "Password", widget = forms.PasswordInput)
+   password2 = forms.CharField(label = "Password confirmation", widget = forms.PasswordInput)
+   agree_tos = forms.BooleanField(required = True, label = 'I agree to the Terms of Service',
+      error_messages = {'required' : 'You have not agreed to our Terms of Service'})
+   agree_privacy = forms.BooleanField(required = True, label = 'I agree to the Privacy Policy',
+      error_messages = {'required' : 'You have not agreed to our Privacy Policy'})
+
+   def clean_username(self):
+      username = self.cleaned_data['username']
+      try:
+         User.objects.get(username = username)
+      except User.DoesNotExist:
+         try:
+            PendingUser.objects.get(username = username)
+         except PendingUser.DoesNotExist:
+            try:
+               IllegalUsername.objects.get(username = username)
+            except IllegalUsername.DoesNotExist:
+               return username
+            raise forms.ValidationError("That username is not allowed.")
+         raise forms.ValidationError("A pending user with that username already exists.")
+      raise forms.ValidationError("A user with that username already exists.")
+
+   def clean_email(self):
+      email = self.cleaned_data['email']
+      try:
+         User.objects.get(email = email)
+      except User.DoesNotExist:
+         try:
+            PendingUser.objects.get(email = email)
+         except PendingUser.DoesNotExist:
+            try:
+               IllegalEmail.objects.get(email = email)
+            except IllegalEmail.DoesNotExist:
+               return email
+            raise forms.ValidationError("That email address is not allowed.")
+         raise forms.ValidationError("A pending user with that email already exists.")
+      raise forms.ValidationError("A user with that email already exists.")
+
+   def clean_password2(self):
+      password1 = self.cleaned_data.get("password1", "")
+      password2 = self.cleaned_data["password2"]
+      if password1 != password2:
+         raise forms.ValidationError("The two password fields didn't match.")
+      return password2
+
+   def save(self):
+      pending_user = PendingUser.objects.create_pending_user(self.cleaned_data['username'],
+            self.cleaned_data['email'],
+            self.cleaned_data['password1'])
+
+      # Send the confirmation email
+
+      site = Site.objects.get_current()
+      site_config = SiteConfig.objects.get_current()
+
+      activation_link = 'http://%s%s' % (site.domain, reverse('accounts.views.register_confirm', 
+            kwargs = {'username' : pending_user.username, 'key' : pending_user.key}))
+
+      msg = render_to_string('accounts/registration_email.txt',
+            {
+               'site_name' : site.name,
+               'site_domain' : site.domain,
+               'user_email' : pending_user.email,
+               'activation_link' : activation_link,
+               'username' : pending_user.username,
+               'raw_password' : self.cleaned_data['password1'],
+               'admin_email' : site_config.admin_email,
+            })
+
+      subject = 'Registration Confirmation for ' + site.name
+      send_mail(subject, msg, site_config.admin_email, [self.cleaned_data['email']])
+
+      return pending_user
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/accounts/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,90 @@
+"""Contains models for the accounts application"""
+
+import datetime
+import random
+import string
+import hashlib
+import base64
+
+from django.db import models
+from django.contrib import auth
+from django.conf import settings
+
+
+class IllegalUsername(models.Model):
+   """model to represent the list of illegal usernames"""
+   username = models.CharField(max_length = 30, db_index = True)
+
+   def __unicode__(self):
+      return self.username
+
+   class Meta:
+      ordering = ('username', )
+
+
+class IllegalEmail(models.Model):
+   """model to represent the list of illegal/restricted email addresses"""
+   email = models.EmailField(db_index = True)
+
+   def __unicode__(self):
+      return self.email
+
+   class Meta:
+      ordering = ('email', )
+
+
+class PendingUserManager(models.Manager):
+   """user manager for PendingUser model"""
+
+   create_count = 0
+
+   def create_pending_user(self, username, email, password):
+      '''creates a new pending user and saves it to the database'''
+
+      temp_user = auth.models.User()
+      temp_user.set_password(password)
+
+      now = datetime.datetime.now() 
+      pending_user = self.model(None, 
+            username, 
+            email, 
+            temp_user.password, 
+            now, 
+            self._make_key())
+
+      pending_user.save()
+      self.create_count += 1
+      return pending_user
+
+
+   def purge_expired(self):
+      expire_time = datetime.datetime.now() - datetime.timedelta(days = 1)
+      expired_pending_users = self.filter(date_joined__lt = expire_time)
+      expired_pending_users.delete()
+
+
+   def _make_key(self):
+      s = ''.join(random.sample(string.printable, 8))
+      delta = datetime.date.today() - datetime.date(1846, 12, 28)
+      days = base64.urlsafe_b64encode(str(delta * 10))
+      key = hashlib.sha1(settings.SECRET_KEY +
+         unicode(self.create_count) +
+         unicode(s) +
+         unicode(days)).hexdigest()[::2]
+      return key
+
+
+class PendingUser(models.Model):
+   """model for holding users while they go through the email registration cycle"""
+
+   username = models.CharField(max_length = 30, db_index = True)
+   email = models.EmailField()
+   password = models.CharField(max_length = 128)
+   date_joined = models.DateTimeField(default = datetime.datetime.now, db_index = True)
+   key = models.CharField(max_length = 20, editable = True)
+
+   objects = PendingUserManager()
+
+   def __unicode__(self):
+      return self.username
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/accounts/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,26 @@
+"""urls for the accounts application"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('accounts.views',
+    url(r'^register/$', 'register', name='accounts-register'),
+    (r'^register/thanks/$', 'register_thanks'),
+    (r'^register/confirm/(?P<username>\w{1,30})/(?P<key>[a-zA-Z0-9]{20})/$', 'register_confirm'),
+)
+
+urlpatterns += patterns('',
+    url(r'^login/',
+        'django.contrib.auth.views.login',
+        kwargs={'template_name': 'accounts/login.html'},
+        name='accounts-login'),
+    url(r'^logout/',
+        'django.contrib.auth.views.logout',
+        kwargs={'template_name': 'accounts/logout.html'},
+        name='accounts-logout'), 
+    (r'^password/', 
+        'django.contrib.auth.views.password_change', 
+        {'template_name': 'accounts/password_change.html',
+         'post_change_redirect': '/accounts/profile/'}),
+)
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/accounts/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,86 @@
+"""views for the accounts application"""
+
+import datetime
+import settings
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.contrib import auth
+from django.http import HttpResponseRedirect
+from django.core.urlresolvers import reverse
+
+from accounts.models import PendingUser
+from accounts.forms import RegisterForm
+
+
+#######################################################################
+
+def register(request):
+   if request.user.is_authenticated():
+      return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL)
+
+   if request.method == 'POST':
+      form = RegisterForm(request.POST)
+      if form.is_valid():
+         form.save()
+         return HttpResponseRedirect(reverse('accounts.views.register_thanks'))
+   else:
+      form = RegisterForm()
+
+   return render_to_response('accounts/register.html', {
+            'form': form,
+         },
+         context_instance = RequestContext(request))
+
+#######################################################################
+
+def register_thanks(request):
+   if request.user.is_authenticated():
+      return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL)
+
+   return render_to_response('accounts/register_thanks.html',
+         context_instance = RequestContext(request))
+
+#######################################################################
+
+def register_confirm(request, username, key):
+   if request.user.is_authenticated():
+      return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL)
+   
+   # purge expired users
+
+   PendingUser.objects.purge_expired()
+
+   try:
+      pending_user = PendingUser.objects.get(username = username)
+   except PendingUser.DoesNotExist:
+      return render_to_response('accounts/register_failure.html', {
+         'username': username,
+         },
+         context_instance = RequestContext(request))
+
+   if pending_user.key != key:
+      return render_to_response('accounts/register_failure.html', {
+         'username': username,
+         },
+         context_instance = RequestContext(request))
+
+   new_user = auth.models.User()
+
+   new_user.username = pending_user.username
+   new_user.first_name = ''
+   new_user.last_name = ''
+   new_user.email = pending_user.email
+   new_user.password = pending_user.password    # already been hashed
+   new_user.is_staff = False
+   new_user.is_active = True
+   new_user.is_superuser = False
+   new_user.last_login = datetime.datetime.now()
+   new_user.date_joined = new_user.last_login
+
+   new_user.save()
+   pending_user.delete()
+
+   return render_to_response('accounts/register_success.html', {
+      'username': username,
+      },
+      context_instance = RequestContext(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/bio/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,15 @@
+"""
+This file contains the admin definitions for the bio application.
+"""
+
+from django.contrib import admin
+from bio.models import UserProfile
+
+class UserProfileAdmin(admin.ModelAdmin):
+    search_fields = ('user__username', 'user__first_name', 'user__last_name', 'user__email')
+    exclude = ('profile_html', 'signature_html')
+
+admin.site.register(UserProfile, UserProfileAdmin)
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/bio/forms.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,118 @@
+"""
+This file contains the forms used by the bio application.
+"""
+from PIL import ImageFile
+from PIL import Image
+
+try:
+    from cStringIO import StringIO
+except:
+    from StringIO import StringIO
+
+from django import forms
+from django.conf import settings
+from django.core.files.base import ContentFile
+from django.contrib.auth.models import User
+
+from bio.models import UserProfile
+
+
+class EditUserForm(forms.ModelForm):
+    """Form for editing the fields of the User model."""
+    email = forms.EmailField(label='Email', required=True)
+    class Meta:
+        model = User
+        fields = ('first_name', 'last_name', 'email')
+
+
+class EditUserProfileForm(forms.ModelForm):
+    """Form for editing the fields of the UserProfile model."""
+    location = forms.CharField(required=False, widget=forms.TextInput(attrs={'size' : 64 }))
+    occupation = forms.CharField(required=False, widget=forms.TextInput(attrs={'size' : 64 }))
+    interests = forms.CharField(required=False, widget=forms.TextInput(attrs={'size' : 64 }))
+    website_1 = forms.URLField(required=False, widget=forms.TextInput(attrs={'size' : 64 }))
+    website_2 = forms.URLField(required=False, widget=forms.TextInput(attrs={'size' : 64 }))
+    website_3 = forms.URLField(required=False, widget=forms.TextInput(attrs={'size' : 64 }))
+
+    class Meta:
+        model = UserProfile
+        exclude = ('user', 'avatar', 'profile_html', 'signature_html')
+
+    class Media:
+        css = {
+            'all': ('js/markitup/skins/markitup/style.css',
+                    'js/markitup/sets/markdown/style.css',
+                    'http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.0/themes/redmond/jquery-ui.css',
+                    ),
+        }
+        js = (
+            'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js',
+            'js/markitup/jquery.markitup.pack.js',
+            'js/markitup/sets/markdown/set.js',
+            'http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.0/jquery-ui.js',
+            'js/bio.js',
+        )
+
+
+def get_image(file):
+    """
+    Returns a PIL Image from the supplied file.
+    Throws ValidationError if the file does not parse as an image file.
+    """
+    parser = ImageFile.Parser()
+    for chunk in file.chunks():
+        parser.feed(chunk)
+    try:
+        image = parser.close()
+        return image
+    except IOError:
+        pass
+    raise forms.ValidationError("Upload a valid image. " +
+            "The file you uploaded was either not an image or a corrupted image.")
+
+
+def scale_image(image, size):
+    """Scales an image file if necessary."""
+
+    # don't upscale
+    if (size, size) >= image.size:
+        return image
+
+    (w, h) = image.size
+    if w > h:
+        diff = (w - h) / 2
+        image = image.crop((diff, 0, w - diff, h))
+    elif h > w:
+        diff = (h - w) / 2
+        image = image.crop((0, diff, w, h - diff))
+    image = image.resize((size, size), Image.ANTIALIAS)
+    return image
+
+
+class UploadAvatarForm(forms.Form):
+    """Form used to change a user's avatar"""
+    avatar_file = forms.ImageField(required=False)
+    image = None
+
+    def clean_avatar_file(self):
+        file = self.cleaned_data['avatar_file']
+        if file is not None:
+            if file.size > settings.MAX_AVATAR_SIZE_BYTES:
+                raise forms.ValidationError("Please upload a file smaller than %s bytes." % \
+                        settings.MAX_AVATAR_SIZE)
+            self.image = get_image(file)
+            self.format = self.image.format
+        return file
+
+    def get_file(self):
+        if self.image is not None:
+            self.image = scale_image(self.image, settings.MAX_AVATAR_SIZE_PIXELS)
+            s = StringIO()
+            self.image.save(s, self.format)
+            return ContentFile(s.getvalue())
+        return None
+
+    def get_filename(self):
+        return self.cleaned_data['avatar_file'].name
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/bio/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,58 @@
+"""
+Contains models for the bio application.
+I would have picked profile for this application, but that is already taken, apparently.
+"""
+
+import os.path
+
+from django.db import models
+from django.contrib import auth
+from django.conf import settings
+from django.template.loader import render_to_string
+
+
+def avatar_file_path_for_user(username, filename):
+    return os.path.join(settings.AVATAR_DIR, 'users', username, filename)
+
+def avatar_file_path(instance, filename):
+    return avatar_file_path_for_user(instance.user.username, filename)
+
+
+class UserProfile(models.Model):
+    """model to represent additional information about users"""
+
+    user = models.ForeignKey(auth.models.User, unique=True)
+    location = models.CharField(max_length=128, blank=True)
+    birthday = models.DateField(blank=True, null=True,
+            help_text='Optional; the year is not shown to others')
+    occupation = models.CharField(max_length=128, blank=True)
+    interests = models.CharField(max_length=255, blank=True)
+    website_1 = models.URLField(verify_exists=False, blank=True)
+    website_2 = models.URLField(verify_exists=False, blank=True)
+    website_3 = models.URLField(verify_exists=False, blank=True)
+    profile_text = models.TextField(blank=True)
+    profile_html = models.TextField(blank=True)
+    hide_email = models.BooleanField(default=True)
+    icq = models.CharField('ICQ', max_length=15, blank=True)
+    aim = models.CharField('AIM', max_length=18, blank=True)
+    yim = models.CharField('YIM', max_length=25, blank=True)
+    msnm = models.CharField('MSN', max_length=25, blank=True)
+    twitter = models.CharField(max_length=64, blank=True)
+    signature = models.TextField(blank=True)
+    signature_html = models.TextField(blank=True)
+    avatar = models.ImageField(upload_to=avatar_file_path, blank=True)
+
+    def __unicode__(self):
+        return self.user.username
+
+    class Meta:
+        ordering = ('user__username', )
+
+    def save(self, *args, **kwargs):
+        html = render_to_string('bio/markdown.html', {'data': self.profile_text})
+        self.profile_html = html.strip()
+        html = render_to_string('bio/markdown.html', {'data': self.signature})
+        self.signature_html = html.strip()
+        super(UserProfile, self).save(*args, **kwargs)
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/bio/templatetags/avatar_tags.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,32 @@
+"""
+Template tags for the bio application. 
+"""
+from django import template
+from django.conf import settings
+
+register = template.Library()
+
+
+@register.simple_tag
+def avatar(user, align='bottom'):
+   alt = user.username
+   title = alt
+   try:
+      profile = user.get_profile()
+   except:
+      profile = None
+   if profile is None or profile.avatar.name == '':
+      url = settings.AVATAR_DEFAULT_URL
+      width = settings.MAX_AVATAR_SIZE_PIXELS
+      height = settings.MAX_AVATAR_SIZE_PIXELS
+   else:
+      url = profile.avatar.url
+      width = profile.avatar.width
+      height = profile.avatar.height
+
+   style = ''
+   if align == 'left':
+      style = 'style="float:left;margin-right:3px;"'
+
+   return u"""<img src="%s" alt="%s" title="%s" width="%s" height="%s" border="0" class="avatar" %s />""" % (
+         url, alt, title, width, height, style)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/bio/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,22 @@
+"""urls for the bio application"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('bio.views',
+    url(r'^members/(?P<type>user|date)/page/(?P<page>\d+)/$', 
+        'member_list', 
+        name='bio-members_full'),
+    url(r'^me/$', 'my_profile', name='bio-me'),
+    url(r'^view/(?P<username>\w{1,30})/$', 'view_profile', name='bio-view_profile'),
+    url(r'^edit/$', 'edit_profile', name='bio-edit_profile'),
+    url(r'^avatar/$', 'change_avatar', name='bio-change_avatar'),
+)
+
+urlpatterns += patterns('django.views.generic.simple',
+    url(r'^members/$', 
+        'redirect_to', 
+        {'url': '/profile/members/user/page/1/'},
+        name='bio-members'),
+)
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/bio/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,138 @@
+"""
+Views for the bio application.
+"""
+
+from django.shortcuts import render_to_response
+from django.shortcuts import get_object_or_404
+from django.template import RequestContext
+from django.contrib import auth
+from django.http import HttpResponseRedirect
+from django.core.urlresolvers import reverse
+from django.contrib.auth.decorators import login_required
+
+from bio.models import UserProfile
+from bio.forms import UploadAvatarForm
+from bio.forms import EditUserForm
+from bio.forms import EditUserProfileForm
+from core.paginator import DiggPaginator
+
+#######################################################################
+
+def get_profile(user):
+    try:
+        profile = user.get_profile()
+    except:
+        profile = UserProfile()
+        profile.user = user
+    return profile
+
+#######################################################################
+
+def member_list(request, type='user', page=1):
+    if type == 'user':
+        users = auth.models.User.objects.all().order_by('username')
+    else:
+        users = auth.models.User.objects.all().order_by('date_joined')
+
+    paginator = DiggPaginator(users, 10, body=5, tail=3, margin=3, padding=2)
+    try:
+        the_page = paginator.page(int(page))
+    except InvalidPage:
+        raise Http404
+
+    return render_to_response('bio/members.html', {
+        'page': the_page,
+        'type': type,
+        }, 
+        context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def my_profile(request):
+    profile = get_profile(request.user)
+
+    return render_to_response('bio/view_profile.html', {
+        'subject': request.user, 
+        'profile': profile, 
+        'hide_email': False,
+        'this_is_me': True,
+        }, 
+        context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def view_profile(request, username):
+
+    user = get_object_or_404(auth.models.User, username = username)
+    if user == request.user:
+        return HttpResponseRedirect(reverse('bio.views.my_profile'))
+
+    profile = get_profile(user)
+
+    # work around MySQL's handling of Boolean
+    hide_email = bool(profile.hide_email)
+    
+    return render_to_response('bio/view_profile.html', {
+        'subject': user, 
+        'profile': profile, 
+        'hide_email': hide_email,
+        'this_is_me': False,
+        }, 
+        context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def edit_profile(request):
+    if request.method == 'POST':
+        if request.POST.get('submit_button', 'Cancel') == 'Cancel':
+            return HttpResponseRedirect(reverse('bio.views.my_profile'))
+        profile = get_profile(request.user)
+        user_form = EditUserForm(request.POST, instance=request.user)
+        profile_form = EditUserProfileForm(request.POST, instance=profile)
+        if user_form.is_valid() and profile_form.is_valid():
+            user_form.save()
+            profile = profile_form.save(commit=False)
+            profile.user = request.user
+            profile.save()
+            return HttpResponseRedirect(reverse('bio.views.my_profile'))
+    else:
+        profile = get_profile(request.user)
+        user_form = EditUserForm(instance=request.user)
+        profile_form = EditUserProfileForm(instance=profile)
+
+    return render_to_response('bio/edit_profile.html', {
+        'user_form': user_form,
+        'profile_form': profile_form,
+         }, 
+        context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def change_avatar(request):
+    if request.method == 'POST':
+        form = UploadAvatarForm(request.POST, request.FILES)
+        if form.is_valid():
+            profile = get_profile(request.user)
+            file = form.get_file()
+            if profile.avatar.name != '':
+                profile.avatar.delete(save=False)
+            if file is not None:
+                profile.avatar.save(form.get_filename(), file, save=False)
+            profile.save()
+
+            request.user.message_set.create(message='Avatar updated.')
+            return HttpResponseRedirect(reverse('bio-me'))
+    else:
+        form = UploadAvatarForm()
+
+    return render_to_response('bio/avatar.html', {
+        'form': form,
+         }, 
+        context_instance = RequestContext(request))
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/bulletins/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,18 @@
+'''
+This file contains the automatic admin site definitions for the Bulletins models.
+'''
+
+from django.contrib import admin
+from bulletins.models import Bulletin
+
+class BulletinAdmin(admin.ModelAdmin):
+   list_display = ('title', 'start_date', 'end_date', 'is_enabled')
+   list_filter = ('start_date', 'end_date', 'is_enabled')
+   search_fields = ('title', 'text')
+   date_hierarchy = 'start_date'
+
+   class Media:
+      js = ('js/tiny_mce/tiny_mce.js', 'js/tiny_mce_init_admin.js')
+
+
+admin.site.register(Bulletin, BulletinAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/bulletins/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,38 @@
+"""Models for the bulletins app.
+Bulletins allow the sited admins to display and manage important notices for the website.
+"""
+
+import datetime
+from django.db import models
+from django.db.models import Q
+
+
+class BulletinManager(models.Manager):
+   """Manager for the Bulletin model."""
+
+   def get_current(self):
+      now = datetime.datetime.now()
+      return self.filter(
+            Q(is_enabled=True),
+            Q(start_date__lte=now),
+            Q(end_date__isnull=True) | Q(end_date__gte=now))
+
+
+class Bulletin(models.Model):
+   """Model to represent site bulletins."""
+   title = models.CharField(max_length=200)
+   text = models.TextField()
+   start_date = models.DateTimeField(db_index=True,
+         help_text='Start date for when the bulletin will be active.',)
+   end_date = models.DateTimeField(blank=True, null=True, db_index=True,
+         help_text='End date for the bulletin. Leave blank to keep it open-ended.')
+   is_enabled = models.BooleanField(default=True, db_index=True,
+         help_text='Check to allow the bulletin to be viewed on the site.')
+
+   objects = BulletinManager()
+
+   def __unicode__(self):
+      return self.title
+
+   class Meta:
+      ordering = ('-start_date', )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/comments/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,31 @@
+"""
+This file contains the automatic admin site definitions for the comment models.
+"""
+from django.contrib import admin
+from comments.models import Comment
+from comments.models import CommentFlag
+
+class CommentAdmin(admin.ModelAdmin):
+    fieldsets = (
+        (None,
+           {'fields': ('content_type', 'object_id', )}
+        ),
+        ('Content',
+           {'fields': ('user', 'comment')}
+        ),
+        ('Metadata',
+           {'fields': ('ip_address', 'is_public', 'is_removed')}
+        ),
+     )
+    list_display = ('__unicode__', 'content_type', 'object_id', 'ip_address', 'creation_date', 'is_public', 'is_removed')
+    list_filter = ('creation_date', 'is_public', 'is_removed')
+    date_hierarchy = 'creation_date'
+    ordering = ('-creation_date', )
+    search_fields = ('comment', 'user__username', 'ip_address')
+    raw_id_fields = ('user', 'content_type')
+
+class CommentFlagAdmin(admin.ModelAdmin):
+    list_display = ('__unicode__', 'get_comment_url')
+
+admin.site.register(Comment, CommentAdmin)
+admin.site.register(CommentFlag, CommentFlagAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/comments/forms.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,77 @@
+"""
+Forms for the comments application.
+"""
+import datetime
+from django import forms
+from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
+
+from comments.models import Comment
+
+COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000)
+
+class CommentForm(forms.Form):
+    comment = forms.CharField(label='',
+            min_length=1, 
+            max_length=COMMENT_MAX_LENGTH,
+            widget=forms.Textarea)
+    content_type = forms.CharField(widget=forms.HiddenInput)
+    object_pk = forms.CharField(widget=forms.HiddenInput)
+
+    def __init__(self, target_object, data=None, initial=None):
+        self.target_object = target_object
+        if initial is None:
+            initial = {}
+        initial.update({
+            'content_type': str(self.target_object._meta),
+            'object_pk': str(self.target_object.pk),
+            })
+        super(CommentForm, self).__init__(data=data, initial=initial)
+
+    def get_comment_object(self, user, ip_address):
+        """
+        Return a new (unsaved) comment object based on the information in this
+        form. Assumes that the form is already validated and will throw a
+        ValueError if not.
+        """
+        if not self.is_valid():
+            raise ValueError("get_comment_object may only be called on valid forms")
+
+        new = Comment(
+            content_type = ContentType.objects.get_for_model(self.target_object),
+            object_id = self.target_object.pk,
+            user = user,
+            comment = self.cleaned_data["comment"],
+            ip_address = ip_address,
+            is_public = True,
+            is_removed = False,
+        )
+
+        # Check that this comment isn't duplicate. (Sometimes people post comments
+        # twice by mistake.) If it is, fail silently by returning the old comment.
+        today = datetime.date.today()
+        possible_duplicates = Comment.objects.filter(
+            content_type = new.content_type,
+            object_id = new.object_id,
+            user = new.user,
+            creation_date__year = today.year,
+            creation_date__month = today.month,
+            creation_date__day = today.day,
+        )
+        for old in possible_duplicates:
+            if old.comment == new.comment:
+                return old
+
+        return new
+
+    class Media:
+        css = {
+            'all': ('js/markitup/skins/markitup/style.css',
+                    'js/markitup/sets/markdown/style.css')
+        }
+        js = (
+            'js/jquery-1.2.6.min.js',
+            'js/comments.js',
+            'js/markitup/jquery.markitup.pack.js',
+            'js/markitup/sets/markdown/set.js',
+        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/comments/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,73 @@
+"""
+Models for the comments application.
+"""
+from django.db import models
+from django.conf import settings
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes import generic
+from django.contrib.auth.models import User
+from django.template.loader import render_to_string
+
+
+COMMENT_MAX_LENGTH = getattr(settings, 'COMMENT_MAX_LENGTH', 3000)
+
+class CommentManager(models.Manager):
+    """Manager for the Comment model class."""
+
+    def for_object(self, obj, filter_public=True):
+        """QuerySet for all comments for a particular model instance."""
+        ct = ContentType.objects.get_for_model(obj)
+        qs = self.get_query_set().filter(content_type__pk=ct.id,
+                object_id=obj.id)
+        if filter_public:
+            qs = qs.filter(is_public=True)
+        return qs
+
+
+class Comment(models.Model):
+    """My own version of a Comment class that can attach comments to any other model."""
+    content_type = models.ForeignKey(ContentType)
+    object_id = models.PositiveIntegerField()
+    content_object = generic.GenericForeignKey('content_type', 'object_id')
+    user = models.ForeignKey(User)
+    comment = models.TextField(max_length=COMMENT_MAX_LENGTH)
+    html = models.TextField(blank=True)
+    creation_date = models.DateTimeField(auto_now_add=True)
+    ip_address = models.IPAddressField('IP Address')
+    is_public = models.BooleanField(default=True, 
+            help_text='Uncheck this field to make the comment invisible.')
+    is_removed = models.BooleanField(default=False,
+            help_text='Check this field to replace the comment with a ' \
+                    '"This comment has been removed" message')
+
+    # Attach manager
+    objects = CommentManager()
+
+    def __unicode__(self):
+        return u'%s: %s...' % (self.user.username, self.comment[:50])
+
+    class Meta:
+        ordering = ('creation_date', )
+
+    def save(self, force_insert=False, force_update=False):
+        html = render_to_string('comments/markdown.html', {'data': self.comment})
+        self.html = html.strip()
+        super(Comment, self).save(force_insert, force_update)
+
+
+class CommentFlag(models.Model):
+    """This model represents a user flagging a comment as inappropriate."""
+    user = models.ForeignKey(User)
+    comment = models.ForeignKey(Comment)
+    flag_date = models.DateTimeField(auto_now_add=True)
+
+    def __unicode__(self):
+        return u'Comment ID %s flagged by %s' % (self.comment_id, self.user.username)
+
+    class Meta:
+        ordering = ('flag_date', )
+
+    def get_comment_url(self):
+        return '<a href="/admin/comments/comment/%s">Comment</a>' % self.comment.id
+    get_comment_url.allow_tags = True
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/comments/templatetags/comment_tags.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,167 @@
+"""
+Template tags for our Comments application.
+We support the following template tags:
+    {% get_comment_count for [object] as [var] %}
+    {% get_comment_list for [object] as [var] %}`
+    {% get_comment_form for [object] as [var] %}`
+    {% render_comment_form for [object] %}
+    {% render_comment_list [object] %}
+"""
+from django import template
+from django.template.loader import render_to_string
+from django.contrib.contenttypes.models import ContentType
+
+from comments.models import Comment
+from comments.forms import CommentForm
+
+
+register = template.Library()
+
+
+class GetCommentCountNode(template.Node):
+    def __init__(self, obj, var):
+        self.object = template.Variable(obj)
+        self.as_var = var
+
+    def render(self, context):
+        object = self.object.resolve(context)
+        qs = Comment.objects.for_object(object)
+        context[self.as_var] = qs.count()
+        return ''
+
+@register.tag
+def get_comment_count(parser, token):
+    """
+    Gets the comment count for the specified object and makes it available in the 
+    template context under the variable name specified.
+    Syntax:
+        {% get_comment_count for [object] as [varname] %}
+    """
+    try:
+        (tag, for_word, obj, as_word, var) = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError, "%r tag requires exactly 4 arguments" % token.contents.split()[0]
+
+    if for_word != 'for':
+        raise template.TemplateSyntaxError("First argument in %r tag must be 'for'" % tag)
+
+    if as_word != 'as':
+        raise template.TemplateSyntaxError("Third argument in %r tag must be 'as'" % tag)
+
+    return GetCommentCountNode(obj, var)
+
+
+class GetCommentListNode(template.Node):
+    def __init__(self, obj, var):
+        self.object = template.Variable(obj)
+        self.as_var = var
+
+    def render(self, context):
+        object = self.object.resolve(context)
+        qs = Comment.objects.for_object(object)
+        context[self.as_var] = list(qs)
+        return ''
+
+
+@register.tag
+def get_comment_list(parser, token):
+    """
+    Gets a list of comments for the specified object and makes it available in the 
+    template context under the variable name specified.
+    Syntax:
+        {% get_comment_list for [object] as [varname] %}
+    """
+    try:
+        (tag, for_word, obj, as_word, var) = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError, "%r tag requires exactly 4 arguments" % token.contents.split()[0]
+
+    if for_word != 'for':
+        raise template.TemplateSyntaxError("First argument in %r tag must be 'for'" % tag)
+
+    if as_word != 'as':
+        raise template.TemplateSyntaxError("Third argument in %r tag must be 'as'" % tag)
+
+    return GetCommentListNode(obj, var)
+
+
+class GetCommentFormNode(template.Node):
+    def __init__(self, obj, var):
+        self.object = template.Variable(obj)
+        self.as_var = var
+
+    def render(self, context):
+        object = self.object.resolve(context)
+        context[self.as_var] = CommentForm(object)
+        return ''
+
+
+@register.tag
+def get_comment_form(parser, token):
+    """
+    Gets the comment form for an object and makes it available in the 
+    template context under the variable name specified.
+    Syntax:
+        {% get_comment_form for [object] as [varname] %}
+    """
+    try:
+        (tag, for_word, obj, as_word, var) = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError, "%r tag requires exactly 4 arguments" % token.contents.split()[0]
+
+    if for_word != 'for':
+        raise template.TemplateSyntaxError("First argument in %r tag must be 'for'" % tag)
+
+    if as_word != 'as':
+        raise template.TemplateSyntaxError("Third argument in %r tag must be 'as'" % tag)
+
+    return GetCommentFormNode(obj, var)
+
+
+class RenderCommentFormNode(template.Node):
+    def __init__(self, obj):
+        self.object = template.Variable(obj)
+
+    def render(self, context):
+        object = self.object.resolve(context)
+        context.push()
+        form_str = render_to_string('comments/comment_form.html', {
+            'form': CommentForm(object),
+            },
+            context)
+        context.pop()
+        return form_str
+
+
+@register.tag
+def render_comment_form(parser, token):
+    """
+    Renders a comment form for the specified object using the template 
+    comments/comment_form.html.
+    Syntax:
+        {% render_comment_form for [object] %}
+    """
+    try:
+        (tag, for_word, obj) = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError, "%r tag requires exactly 2 arguments" % token.contents.split()[0]
+
+    if for_word != 'for':
+        raise template.TemplateSyntaxError("First argument in %r tag must be 'for'" % tag)
+
+    return RenderCommentFormNode(obj)
+
+
+@register.inclusion_tag('comments/comment_list.html', takes_context=True)
+def render_comment_list(context, object):
+    """
+    Renders the comments for the specified object using the template
+    comments/comment_list.html.
+    Syntax:
+        {% render_comment_list [object] %}
+    """
+    qs = Comment.objects.for_object(object).select_related('user')
+    return {'comments': qs,
+          'MEDIA_URL': context['MEDIA_URL'],
+          }
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/comments/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,10 @@
+"""
+URLs for the comments application.
+"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('comments.views',
+    url(r'^flag/$', 'flag_comment', name='comments-flag'),
+    url(r'^markdown/$', 'markdown_preview', name='comments-markdown_preview'),
+    url(r'^post/$', 'post_comment', name='comments-post'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/comments/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,126 @@
+"""
+Views for the comments application.
+"""
+from django.contrib.auth.decorators import login_required
+from django.core.exceptions import ObjectDoesNotExist
+from django.http import HttpResponse
+from django.http import HttpResponseRedirect
+from django.http import HttpResponseBadRequest
+from django.http import HttpResponseForbidden
+from django.db.models import get_model
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.utils.html import escape
+from django.views.decorators.http import require_POST
+
+from core.functions import email_admins
+from comments.forms import CommentForm
+from comments.models import Comment
+from comments.models import CommentFlag
+
+@login_required
+@require_POST
+def post_comment(request):
+    """
+    This function handles the posting of comments. If successful, returns
+    the comment text as the response. This function is mean't to be the target
+    of an AJAX post.
+    """
+    # Look up the object we're trying to comment about
+    ctype = request.POST.get('content_type', None)
+    object_pk = request.POST.get('object_pk', None)
+    if ctype is None or object_pk is None:
+        return HttpResponseBadRequest('Missing content_type or object_pk field.')
+
+    try:
+        model = get_model(*ctype.split('.', 1))
+        target = model.objects.get(pk=object_pk)
+    except TypeError:
+        return HttpResponseBadRequest(
+            "Invalid content_type value: %r" % escape(ctype))
+    except AttributeError:
+        return HttpResponseBadRequest(
+            "The given content-type %r does not resolve to a valid model." % \
+                escape(ctype))
+    except ObjectDoesNotExist:
+        return HttpResponseBadRequest(
+            "No object matching content-type %r and object PK %r exists." % \
+                (escape(ctype), escape(object_pk)))
+
+    # Can we comment on the target object?
+    if hasattr(target, 'can_comment_on'):
+        if callable(target.can_comment_on):
+            can_comment_on = target.can_comment_on()
+        else:
+            can_comment_on = target.can_comment_on
+    else:
+        can_comment_on = True
+
+    if not can_comment_on:
+        return HttpResponseForbidden('Cannot comment on this item.')
+
+    # Check form validity
+
+    form = CommentForm(target, request.POST)
+    if not form.is_valid():
+        return HttpResponseBadRequest('Invalid comment; missing parameters?')
+
+    # else, create and save the comment
+
+    comment = form.get_comment_object(request.user, request.META.get("REMOTE_ADDR", None))
+    comment.save()
+
+    # return the rendered comment
+    return render_to_response('comments/comment.html', {
+        'comment': comment,
+        },
+        context_instance = RequestContext(request))
+    
+
+@require_POST
+def flag_comment(request):
+    """
+    This function handles the flagging of comments by users. This function should
+    be the target of an AJAX post.
+    """
+    if not request.user.is_authenticated():
+        return HttpResponse('Please login or register to flag a comment.')
+
+    id = request.POST.get('id', None)
+    if id is None:
+        return HttpResponseBadRequest('No id')
+
+    try:
+        comment = Comment.objects.get(pk=id)
+    except Comment.DoesNotExist:
+        return HttpResponseBadRequest('No comment with id %s' % id)
+
+    flag = CommentFlag(user=request.user, comment=comment)
+    flag.save()
+    email_admins('A Comment Has Been Flagged', """Hello,
+
+A user has flagged a comment for review.
+""")
+    return HttpResponse('The comment was flagged. A moderator will review the comment shortly. ' \
+            'Thanks for helping to improve the discussions on this site.')
+
+
+@require_POST
+def markdown_preview(request):
+    """
+    This function should be the target of an AJAX POST. It takes the 'data' parameter
+    from the POST parameters and returns a rendered HTML page from the data, which
+    is assumed to be in markdown format. The HTML page is suitable for the preview 
+    function for a javascript editor such as markItUp.
+    """
+    if not request.user.is_authenticated():
+        return HttpResponseForbidden('This service is only available to logged in users.')
+
+    data = request.POST.get('data', None)
+    if data is None:
+        return HttpResponseBadRequest('No data')
+
+    return render_to_response('comments/markdown_preview.html', {
+        'data': data,
+        },
+        context_instance = RequestContext(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/contact/forms.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,47 @@
+"""forms for the contact application"""
+
+from django import forms
+from django.conf import settings
+from django.template.loader import render_to_string
+from django.contrib.sites.models import Site
+from core.functions import send_mail
+
+
+class ContactForm(forms.Form):
+   """Form used to contact the website admins"""
+   name = forms.CharField(label = "Your Name", max_length = 61,
+         widget = forms.TextInput(attrs = {'size' : 50 }))
+   email = forms.EmailField(label = "Your Email",
+         widget = forms.TextInput(attrs = {'size' : 50 }))
+   subject = forms.CharField(max_length = 64,
+         widget = forms.TextInput(attrs = {'size' : 50 }))
+   honeypot = forms.CharField(max_length = 64, required = False,
+         label = 'If you enter anything in this field your message will be treated as spam')
+   message = forms.CharField(label = "Your Message", 
+         widget = forms.Textarea(attrs = {'rows' : 16, 'cols' : 50}), 
+         max_length = 3000)
+
+   recipient_list = [mail_tuple[1] for mail_tuple in settings.MANAGERS]
+
+   def clean_honeypot(self):
+      value = self.cleaned_data['honeypot']
+      if value:
+         raise forms.ValidationError(self.fields['honeypot'].label)
+      return value
+
+   def save(self):
+      # Send the feedback message email
+
+      site = Site.objects.get_current()
+
+      msg = render_to_string('contact/contact_email.txt',
+            {
+               'site_name' : site.name,
+               'user_name' : self.cleaned_data['name'],
+               'user_email' : self.cleaned_data['email'],
+               'message' : self.cleaned_data['message'],
+            })
+
+      subject = site.name + ' Feedback: ' + self.cleaned_data['subject']
+      send_mail(subject, msg, self.cleaned_data['email'], self.recipient_list)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/contact/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,7 @@
+"""urls for the contact application"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('contact.views',
+   url(r'^$', 'contact_form', name='contact-form'),
+   (r'^thanks/$', 'contact_thanks'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/contact/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,33 @@
+# Create your views here.
+
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.http import HttpResponseRedirect
+from django.core.urlresolvers import reverse
+
+from contact.forms import ContactForm
+from core.functions import get_full_name
+
+
+def contact_form(request):
+   if request.method == 'POST':
+      form = ContactForm(request.POST)
+      if form.is_valid():
+         form.save()
+         return HttpResponseRedirect(reverse('contact.views.contact_thanks'))
+   else:
+      initial_data = {}
+      if request.user.is_authenticated():
+         name = get_full_name(request.user)
+         initial_data = {'name' : name, 'email' : request.user.email}
+
+      form = ContactForm(initial = initial_data)
+
+   return render_to_response('contact/contact_form.html', 
+         {'form' : form}, 
+         context_instance = RequestContext(request))
+
+
+def contact_thanks(request):
+   return render_to_response('contact/contact_thanks.html', 
+         context_instance = RequestContext(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/core/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,16 @@
+"""This file contains the automatic admin site definitions for the core Models"""
+
+from django.contrib import admin
+from core.models import SiteConfig
+from core.models import DebugLog
+
+class SiteConfigAdmin(admin.ModelAdmin):
+   pass
+
+class DebugLogAdmin(admin.ModelAdmin):
+   list_display = ('__unicode__', 'level')
+   ordering = ('-timestamp', )
+   date_hierarchy = 'timestamp'
+
+admin.site.register(SiteConfig, SiteConfigAdmin)
+admin.site.register(DebugLog, DebugLogAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/core/functions.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,77 @@
+"""This file houses various core utility functions for GPP"""
+
+import django.core.mail
+from django.contrib.sites.models import Site
+from django.conf import settings
+
+from core import logging
+from lxml.html.clean import Cleaner
+
+html_cleaner = Cleaner(scripts=True,
+        javascript=True,
+        comments=True,
+        style=True,
+        links=True,
+        meta=True,
+        page_structure=True,
+        processing_instructions=True,
+        embedded=True,
+        frames=True,
+        forms=True,
+        annoying_tags=True,
+        remove_unknown_tags=True,
+        safe_attrs_only=True,
+        host_whitelist=['www.youtube.com'],
+        whitelist_tags=['object', 'param', 'embed'],
+        )
+
+
+def send_mail(subject, message, from_email, recipient_list, 
+        fail_silently = False, auth_user = None, auth_password = None):
+    """The main gpp send email function.
+    Use this function to send email from the site. It will obey debug settings and
+    log all emails.
+    """
+
+    if settings.GPP_SEND_EMAIL:
+        django.core.mail.send_mail(subject, message, from_email, recipient_list,
+                fail_silently, auth_user, auth_password)
+
+    logging.info('EMAIL:\nFrom: %s\nTo: %s\nSubject: %s\nMessage:\n%s' % 
+            (from_email, str(recipient_list), subject, message))
+
+
+def email_admins(subject, message):
+    """Emails the site admins. Goes through the site send_mail function."""
+    site = Site.objects.get_current()
+    subject = '[%s] %s' % (site.name, subject)
+    send_mail(subject, 
+            message, 
+            '%s@%s' % (settings.GPP_NO_REPLY_EMAIL, site.domain),
+            [mail_tuple[1] for mail_tuple in settings.ADMINS])
+
+
+def email_managers(subject, message):
+    """Emails the site managers. Goes through the site send_mail function."""
+    site = Site.objects.get_current()
+    subject = '[%s] %s' % (site.name, subject)
+    send_mail(subject, 
+            msg, 
+            '%s@%s' % (settings.GPP_NO_REPLY_EMAIL, site.domain),
+            [mail_tuple[1] for mail_tuple in settings.MANAGERS])
+
+
+def clean_html(s):
+    """Cleans HTML of dangerous tags and content."""
+    if s:
+        return html_cleaner.clean_html(s)
+    return s
+
+
+def get_full_name(user):
+    """Returns the user's full name if available, otherwise falls back
+    to the username."""
+    full_name = user.get_full_name()
+    if full_name:
+        return full_name
+    return user.username
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/core/logging.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,36 @@
+'''This module adds a simple logging facility to the portal.
+Applications can log information to a database table for debugging.
+The logger is similar to the python logging module.
+The verbosity of the logging is controlled via settings.GPP_LOG_LEVEL. 
+'''
+
+from settings import GPP_LOG_LEVEL
+from core.models import DebugLog
+
+DEBUG = 10
+INFO = 20
+WARNING = 30
+ERROR = 40
+CRITICAL = 50
+
+def log(level, msg):
+   if GPP_LOG_LEVEL <= level:
+      log_item = DebugLog()
+      log_item.level = level
+      log_item.msg = msg
+      log_item.save()
+
+def debug(msg):
+   log(DEBUG, msg)
+
+def info(msg):
+   log(INFO, msg)
+
+def warning(msg):
+   log(WARNING, msg)
+
+def error(msg):
+   log(WARNING, msg)
+
+def critical(msg):
+   log(WARNING, msg)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/core/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,82 @@
+"""
+This file contains the core Models used in gpp.
+The SiteConfig information is cached in a similar manner to django.contrib.sites.
+"""
+
+from django.db import models
+
+SITE_CACHE = {}
+
+class SiteConfigManager(models.Manager):
+   def get_current(self):
+      """
+      Returns the current SiteConfig based on the SITE_ID in the
+      project's settings. The SiteConfig object is cached the first
+      time it's retrieved from the database.
+      """
+      from django.conf import settings
+      try:
+         sid = settings.SITE_ID
+      except AttributeError:
+         from django.core.exceptions import ImproperlyConfigured
+         raise ImproperlyConfigured("You're using the Django \"sites framework\" without having set the SITE_ID setting. " +
+               "Create a site in your database and set the SITE_ID setting to fix this error.")
+      try:
+         current_site_config = SITE_CACHE[sid]
+      except KeyError:
+         current_site_config = self.get(pk = sid)
+         SITE_CACHE[sid] = current_site_config
+      return current_site_config
+
+
+      def clear_cache(self):
+         """Clears the SiteConfig object cache."""
+         global SITE_CACHE
+         SITE_CACHE = {}
+
+
+class SiteConfig(models.Model):
+   """model to represent the site's basic configuration and settings""" 
+   site_slogan = models.CharField(max_length = 128, blank = True)
+   admin_name = models.CharField(max_length = 64)
+   admin_email = models.EmailField()
+   date_created = models.DateField()
+
+   objects = SiteConfigManager()
+
+
+   def __unicode__(self):
+      return u'SiteConfig ' + unicode(self.id)
+
+
+   def delete(self):
+      pk = self.pk
+      super(SiteConfig, self).delete()
+      try:
+         del(SITE_CACHE[pk])
+      except KeyError:
+         pass
+
+
+class DebugLog(models.Model):
+   '''Model to represent debug logs used during development; arbitary text can be stored'''
+
+   LOG_LEVELS = (
+      (0, 'Not Set'),
+      (10, 'Debug'),
+      (20, 'Info'),
+      (30, 'Warning'),
+      (40, 'Error'),
+      (50, 'Critical'),
+   )
+
+   timestamp = models.DateTimeField(auto_now_add = True)
+   level = models.IntegerField(choices = LOG_LEVELS)
+   msg = models.TextField()
+
+   def __unicode__(self):
+      return '%s - %s' % (self.timestamp.strftime('%m/%d/%Y %H:%M:%S'),
+            self.msg[:64])
+
+   class Meta:
+      ordering = ('-timestamp', )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/core/paginator.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,286 @@
+"""
+Digg.com style paginator.
+References:
+http://www.djangosnippets.org/snippets/773/
+http://blog.elsdoerfer.name/2008/05/26/diggpaginator-update/
+http://blog.elsdoerfer.name/2008/03/06/yet-another-paginator-digg-style/
+"""
+import math
+from django.core.paginator import \
+    Paginator, QuerySetPaginator, Page, InvalidPage
+
+__all__ = (
+    'InvalidPage',
+    'ExPaginator',
+    'DiggPaginator',
+    'QuerySetDiggPaginator',
+)
+
+class ExPaginator(Paginator):
+    """Adds a ``softlimit`` option to ``page()``. If True, querying a
+    page number larger than max. will not fail, but instead return the
+    last available page.
+
+    This is useful when the data source can not provide an exact count
+    at all times (like some search engines), meaning the user could
+    possibly see links to invalid pages at some point which we wouldn't
+    want to fail as 404s.
+
+    >>> items = range(1, 1000)
+    >>> paginator = ExPaginator(items, 10)
+    >>> paginator.page(1000)
+    Traceback (most recent call last):
+    InvalidPage: That page contains no results
+    >>> paginator.page(1000, softlimit=True)
+    <Page 100 of 100>
+
+    # [bug] graceful handling of non-int args
+    >>> paginator.page("str")
+    Traceback (most recent call last):
+    InvalidPage: That page number is not an integer
+    """
+    def _ensure_int(self, num, e):
+        # see Django #7307
+        try:
+            return int(num)
+        except ValueError:
+            raise e
+
+    def page(self, number, softlimit=False):
+        try:
+            return super(ExPaginator, self).page(number)
+        except InvalidPage, e:
+            number = self._ensure_int(number, e)
+            if number > self.num_pages and softlimit:
+                return self.page(self.num_pages, softlimit=False)
+            else:
+                raise e
+
+class DiggPaginator(ExPaginator):
+    """
+    Based on Django's default paginator, it adds "Digg-style" page ranges
+    with a leading block of pages, an optional middle block, and another
+    block at the end of the page range. They are available as attributes
+    on the page:
+
+    {# with: page = digg_paginator.page(1) #}
+    {% for num in page.leading_range %} ...
+    {% for num in page.main_range %} ...
+    {% for num in page.trailing_range %} ...
+
+    Additionally, ``page_range`` contains a nun-numeric ``False`` element
+    for every transition between two ranges.
+
+    {% for num in page.page_range %}
+        {% if not num %} ...  {# literally output dots #}
+        {% else %}{{ num }}
+        {% endif %}
+    {% endfor %}
+
+    Additional arguments passed to the constructor allow customization of
+    how those bocks are constructed:
+
+    body=5, tail=2
+
+    [1] 2 3 4 5 ... 91 92
+    |_________|     |___|
+    body            tail
+              |_____|
+              margin
+
+    body=5, tail=2, padding=2
+
+    1 2 ... 6 7 [8] 9 10 ... 91 92
+            |_|     |__|
+             ^padding^
+    |_|     |__________|     |___|
+    tail    body             tail
+
+    ``margin`` is the minimum number of pages required between two ranges; if
+    there are less, they are combined into one.
+
+    When ``align_left`` is set to ``True``, the paginator operates in a
+    special mode that always skips the right tail, e.g. does not display the
+    end block unless necessary. This is useful for situations in which the
+    exact number of items/pages is not actually known.
+
+    # odd body length
+    >>> print DiggPaginator(range(1,1000), 10, body=5).page(1)
+    1 2 3 4 5 ... 99 100
+    >>> print DiggPaginator(range(1,1000), 10, body=5).page(100)
+    1 2 ... 96 97 98 99 100
+
+    # even body length
+    >>> print DiggPaginator(range(1,1000), 10, body=6).page(1)
+    1 2 3 4 5 6 ... 99 100
+    >>> print DiggPaginator(range(1,1000), 10, body=6).page(100)
+    1 2 ... 95 96 97 98 99 100
+
+    # leading range and main range are combined when close; note how
+    # we have varying body and padding values, and their effect.
+    >>> print DiggPaginator(range(1,1000), 10, body=5, padding=2, margin=2).page(3)
+    1 2 3 4 5 ... 99 100
+    >>> print DiggPaginator(range(1,1000), 10, body=6, padding=2, margin=2).page(4)
+    1 2 3 4 5 6 ... 99 100
+    >>> print DiggPaginator(range(1,1000), 10, body=5, padding=1, margin=2).page(6)
+    1 2 3 4 5 6 7 ... 99 100
+    >>> print DiggPaginator(range(1,1000), 10, body=5, padding=2, margin=2).page(7)
+    1 2 ... 5 6 7 8 9 ... 99 100
+    >>> print DiggPaginator(range(1,1000), 10, body=5, padding=1, margin=2).page(7)
+    1 2 ... 5 6 7 8 9 ... 99 100
+
+    # the trailing range works the same
+    >>> print DiggPaginator(range(1,1000), 10, body=5, padding=2, margin=2, ).page(98)
+    1 2 ... 96 97 98 99 100
+    >>> print DiggPaginator(range(1,1000), 10, body=6, padding=2, margin=2, ).page(97)
+    1 2 ... 95 96 97 98 99 100
+    >>> print DiggPaginator(range(1,1000), 10, body=5, padding=1, margin=2, ).page(95)
+    1 2 ... 94 95 96 97 98 99 100
+    >>> print DiggPaginator(range(1,1000), 10, body=5, padding=2, margin=2, ).page(94)
+    1 2 ... 92 93 94 95 96 ... 99 100
+    >>> print DiggPaginator(range(1,1000), 10, body=5, padding=1, margin=2, ).page(94)
+    1 2 ... 92 93 94 95 96 ... 99 100
+
+    # all three ranges may be combined as well
+    >>> print DiggPaginator(range(1,151), 10, body=6, padding=2).page(7)
+    1 2 3 4 5 6 7 8 9 ... 14 15
+    >>> print DiggPaginator(range(1,151), 10, body=6, padding=2).page(8)
+    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+    >>> print DiggPaginator(range(1,151), 10, body=6, padding=1).page(8)
+    1 2 3 4 5 6 7 8 9 ... 14 15
+
+    # no leading or trailing ranges might be required if there are only
+    # a very small number of pages
+    >>> print DiggPaginator(range(1,80), 10, body=10).page(1)
+    1 2 3 4 5 6 7 8
+    >>> print DiggPaginator(range(1,80), 10, body=10).page(8)
+    1 2 3 4 5 6 7 8
+    >>> print DiggPaginator(range(1,12), 10, body=5).page(1)
+    1 2
+
+    # test left align mode
+    >>> print DiggPaginator(range(1,1000), 10, body=5, align_left=True).page(1)
+    1 2 3 4 5
+    >>> print DiggPaginator(range(1,1000), 10, body=5, align_left=True).page(50)
+    1 2 ... 48 49 50 51 52
+    >>> print DiggPaginator(range(1,1000), 10, body=5, align_left=True).page(97)
+    1 2 ... 95 96 97 98 99
+    >>> print DiggPaginator(range(1,1000), 10, body=5, align_left=True).page(100)
+    1 2 ... 96 97 98 99 100
+
+    # padding: default value
+    >>> DiggPaginator(range(1,1000), 10, body=10).padding
+    4
+
+    # padding: automatic reduction
+    >>> DiggPaginator(range(1,1000), 10, body=5).padding
+    2
+    >>> DiggPaginator(range(1,1000), 10, body=6).padding
+    2
+
+    # padding: sanity check
+    >>> DiggPaginator(range(1,1000), 10, body=5, padding=3)
+    Traceback (most recent call last):
+    ValueError: padding too large for body (max 2)
+    """
+    def __init__(self, *args, **kwargs):
+        self.body = kwargs.pop('body', 10)
+        self.tail = kwargs.pop('tail', 2)
+        self.align_left = kwargs.pop('align_left', False)
+        self.margin = kwargs.pop('margin', 4)  # TODO: make the default relative to body?
+        # validate padding value
+        max_padding = int(math.ceil(self.body/2.0)-1)
+        self.padding = kwargs.pop('padding', min(4, max_padding))
+        if self.padding > max_padding:
+            raise ValueError('padding too large for body (max %d)'%max_padding)
+        super(DiggPaginator, self).__init__(*args, **kwargs)
+
+    def page(self, number, *args, **kwargs):
+        """Return a standard ``Page`` instance with custom, digg-specific
+        page ranges attached.
+        """
+
+        page = super(DiggPaginator, self).page(number, *args, **kwargs)
+        number = int(number) # we know this will work
+
+        # easier access
+        num_pages, body, tail, padding, margin = \
+            self.num_pages, self.body, self.tail, self.padding, self.margin
+
+        # put active page in middle of main range
+        main_range = map(int, [
+            math.floor(number-body/2.0)+1,  # +1 = shift odd body to right
+            math.floor(number+body/2.0)])
+        # adjust bounds
+        if main_range[0] < 1:
+            main_range = map(abs(main_range[0]-1).__add__, main_range)
+        if main_range[1] > num_pages:
+            main_range = map((num_pages-main_range[1]).__add__, main_range)
+
+        # Determine leading and trailing ranges; if possible and appropriate,
+        # combine them with the main range, in which case the resulting main
+        # block might end up considerable larger than requested. While we
+        # can't guarantee the exact size in those cases, we can at least try
+        # to come as close as possible: we can reduce the other boundary to
+        # max padding, instead of using half the body size, which would
+        # otherwise be the case. If the padding is large enough, this will
+        # of course have no effect.
+        # Example:
+        #     total pages=100, page=4, body=5, (default padding=2)
+        #     1 2 3 [4] 5 6 ... 99 100
+        #     total pages=100, page=4, body=5, padding=1
+        #     1 2 3 [4] 5 ... 99 100
+        # If it were not for this adjustment, both cases would result in the
+        # first output, regardless of the padding value.
+        if main_range[0] <= tail+margin:
+            leading = []
+            main_range = [1, max(body, min(number+padding, main_range[1]))]
+            main_range[0] = 1
+        else:
+            leading = range(1, tail+1)
+        # basically same for trailing range, but not in ``left_align`` mode
+        if self.align_left:
+            trailing = []
+        else:
+            if main_range[1] >= num_pages-(tail+margin)+1:
+                trailing = []
+                if not leading:
+                    # ... but handle the special case of neither leading nor
+                    # trailing ranges; otherwise, we would now modify the
+                    # main range low bound, which we just set in the previous
+                    # section, again.
+                    main_range = [1, num_pages]
+                else:
+                    main_range = [min(num_pages-body+1, max(number-padding, main_range[0])), num_pages]
+            else:
+                trailing = range(num_pages-tail+1, num_pages+1)
+
+        # finally, normalize values that are out of bound; this basically
+        # fixes all the things the above code screwed up in the simple case
+        # of few enough pages where one range would suffice.
+        main_range = [max(main_range[0], 1), min(main_range[1], num_pages)]
+
+        # make the result of our calculations available as custom ranges
+        # on the ``Page`` instance.
+        page.main_range = range(main_range[0], main_range[1]+1)
+        page.leading_range = leading
+        page.trailing_range = trailing
+        page.page_range = reduce(lambda x, y: x+((x and y) and [False])+y,
+            [page.leading_range, page.main_range, page.trailing_range])
+
+        page.__class__ = DiggPage
+        return page
+
+class DiggPage(Page):
+    def __str__(self):
+        return " ... ".join(filter(None, [
+                            " ".join(map(str, self.leading_range)),
+                            " ".join(map(str, self.main_range)),
+                            " ".join(map(str, self.trailing_range))]))
+
+class QuerySetDiggPaginator(DiggPaginator, QuerySetPaginator):
+    pass
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/core/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,1 @@
+# Create your views here.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/core/widgets.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,40 @@
+"""
+Various useful widgets for the GPP application.
+"""
+
+from django import forms
+from django.utils.safestring import mark_safe
+from django.core.urlresolvers import reverse
+
+
+class AutoCompleteUserInput(forms.TextInput):
+    class Media:
+        css = {
+            'all': ('js/jquery-autocomplete/jquery.autocomplete.css',)
+        }
+        js = (
+            'js/jquery-autocomplete/lib/jquery.js',
+            'js/jquery-autocomplete/lib/jquery.bgiframe.min.js',
+            'js/jquery-autocomplete/lib/jquery.ajaxQueue.js',
+            'js/jquery-autocomplete/jquery.autocomplete.js'
+        )
+
+    def render(self, name, value, attrs=None):
+        url = reverse('messages-ajax_users')
+        output = super(AutoCompleteUserInput, self).render(name, value, attrs)
+        return output + mark_safe(u'''\
+<script type="text/javascript">
+jQuery("#id_%s").autocomplete("%s", {
+    width: 150,
+    max: 10,
+    highlight: false,
+    multiple: false,
+    scroll: true,
+    scrollHeight: 300,
+    matchContains: true,
+    autoFill: true,
+});
+</script>''' % (name, url))
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/downloads/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,43 @@
+"""
+This file contains the automatic admin site definitions for the downloads models.
+"""
+from django.contrib import admin
+from downloads.models import Download
+from downloads.models import Category
+from downloads.models import AllowedExtension
+from downloads.models import VoteRecord
+
+class DownloadAdmin(admin.ModelAdmin):
+    exclude = ('html', )
+    list_display = ('title', 'user', 'category', 'date_added', 'ip_address',
+            'hits', 'average_score', 'size', 'is_public')
+    list_filter = ('date_added', 'is_public', 'category', 'user', 'ip_address')
+    date_hierarchy = 'date_added'
+    ordering = ('-date_added', )
+    search_fields = ('title', 'description', 'user__username')
+    raw_id_fields = ('user', )
+    save_on_top = True
+
+    class Media:
+        css = {
+            'all': ('js/markitup/skins/markitup/style.css',
+                    'js/markitup/sets/markdown/style.css')
+        }
+        js = (
+            'js/jquery-1.2.6.min.js',
+            'js/markitup/jquery.markitup.pack.js',
+            'js/markitup/sets/markdown/set.js',
+            'js/downloads_admin.js',
+        )
+
+
+class VoteRecordAdmin(admin.ModelAdmin):
+    list_display = ('user', 'download', 'vote_date')
+    list_filter = ('user', 'download')
+    date_hierarchy = 'vote_date'
+
+
+admin.site.register(Download, DownloadAdmin)
+admin.site.register(Category)
+admin.site.register(AllowedExtension)
+admin.site.register(VoteRecord, VoteRecordAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/downloads/forms.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,45 @@
+"""
+Forms for the downloads application.
+"""
+import os
+
+from django import forms
+
+from downloads.models import Download
+from downloads.models import AllowedExtension
+
+
+class SearchForm(forms.Form):
+    """Downloads search form."""
+    text = forms.CharField(max_length=30)
+
+    def query(self):
+        return self.cleaned_data['text']
+
+
+class AddDownloadForm(forms.ModelForm):
+    """Form to allow adding downloads."""
+
+    def clean_file(self):
+        file = self.cleaned_data['file']
+        ext = os.path.splitext(file.name)[1]
+        allowed_exts = AllowedExtension.objects.get_extension_list()
+        if ext in allowed_exts:
+            return file
+        raise forms.ValidationError('The file extension "%s" is not allowed.' % ext)
+
+    class Meta:
+        model = Download
+        fields = ('title', 'category', 'description', 'file')
+        
+    class Media:
+        css = {
+            'all': ('js/markitup/skins/markitup/style.css',
+                    'js/markitup/sets/markdown/style.css')
+        }
+        js = (
+            'js/jquery-1.2.6.min.js',
+            'js/downloads/add.js',
+            'js/markitup/jquery.markitup.pack.js',
+            'js/markitup/sets/markdown/set.js',
+        )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/downloads/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,118 @@
+"""
+Models for the downloads application.
+"""
+import os
+
+import datetime
+from django.db import models
+from django.contrib.auth.models import User
+from django.template.loader import render_to_string
+from django.template.defaultfilters import filesizeformat
+
+
+class Category(models.Model):
+    """Downloads belong to categories."""
+    title = models.CharField(max_length=64)
+    description = models.TextField(blank=True)
+
+    class Meta:
+        verbose_name_plural = 'Categories'
+        ordering = ('title', )
+
+    def __unicode__(self):
+        return self.title
+
+    def num_downloads(self):
+        return Download.public_objects.filter(category=self.pk).count()
+
+
+def download_path(instance, filename):
+    """
+    Creates a path for a download. Uses the current date to avoid filename
+    clashes. Uses the current microsecond also to make the directory name
+    harder to guess.
+    """
+    now = datetime.datetime.now()
+    parts = ['downloads']
+    parts.extend([str(p) for p in (now.year, now.month, now.day, now.microsecond)])
+    parts.append(filename)
+    return os.path.join(*parts)
+
+
+class PublicDownloadManager(models.Manager):
+    """The manager for all public downloads."""
+    def get_query_set(self):
+        return super(PublicDownloadManager, self).get_query_set().filter(is_public=True)
+
+
+class Download(models.Model):
+    """Model to represent a download."""
+    title = models.CharField(max_length=128)
+    category = models.ForeignKey(Category)
+    description = models.TextField()
+    html = models.TextField(blank=True)
+    file = models.FileField(upload_to=download_path)
+    user = models.ForeignKey(User)
+    date_added = models.DateTimeField(auto_now_add=True)
+    ip_address = models.IPAddressField('IP Address')
+    hits = models.IntegerField(default=0)
+    average_score = models.FloatField(default=0.0)
+    total_votes = models.IntegerField(default=0)
+    is_public = models.BooleanField(default=False, db_index=True)
+
+    # Managers:
+    objects = models.Manager()
+    public_objects = PublicDownloadManager()
+
+    def __unicode__(self):
+        return self.title
+
+    def save(self, force_insert=False, force_update=False):
+        html = render_to_string('downloads/markdown.html', {'data': self.description})
+        self.html = html.strip()
+        super(Download, self).save(force_insert, force_update)
+
+    def vote(self, vote_value):
+        """receives a vote_value and updates internal score accordingly"""
+        total_score = self.average_score * self.total_votes
+        total_score += vote_value
+        self.total_votes += 1
+        self.average_score = total_score / self.total_votes
+        return self.average_score
+
+    def size(self):
+        return filesizeformat(self.file.size)
+
+
+class AllowedExtensionManager(models.Manager):
+    def get_extension_list(self):
+        return self.values_list('extension', flat=True)
+
+
+class AllowedExtension(models.Model):
+    """Model to represent the list of allowed file extensions."""
+    extension = models.CharField(max_length=8)
+
+    objects = AllowedExtensionManager()
+
+    def __unicode__(self):
+        return self.extension
+
+    class Meta:
+        ordering = ('extension', )
+
+
+class VoteRecord(models.Model):
+    """Model to record the date that a user voted on a download."""
+    download = models.ForeignKey(Download)
+    user = models.ForeignKey(User)
+    vote_date = models.DateTimeField(auto_now_add=True)
+
+    def __unicode__(self):
+        return "%s voted on '%s' on %s" % (
+                self.user.username, 
+                self.download.title, 
+                self.vote_date.strftime('%b %d, %Y %H:%M:%S'))
+
+    class Meta:
+        ordering = ('-vote_date', )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/downloads/templatetags/downloads_tags.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,13 @@
+"""
+Template tags for the downloads application.
+"""
+from django import template
+
+from downloads.forms import SearchForm
+
+
+register = template.Library()
+
+@register.inclusion_tag('downloads/navigation.html')
+def downloads_navigation():
+    return {'search_form': SearchForm()}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/downloads/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,21 @@
+"""
+URLs for the downloads application.
+"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('downloads.views',
+    url(r'^$', 'index', name='downloads-index'),
+    url(r'^add/$', 'add', name='downloads-add'),
+    url(r'^category/(?P<category>\d+)/(?P<sort>title|date|rating|hits)/page/(?P<page>\d+)/$', 
+       'category',
+       name='downloads-category'),
+    url(r'^comments/(\d+)/$', 'comments', name='downloads-comments'),
+    url(r'^(\d+)/$', 'download', name='downloads-download'),
+    url(r'^new/$', 'new', name='downloads-new'),
+    url(r'^popular/$', 'popular', name='downloads-popular'),
+    url(r'^random/$', 'random_download', name='downloads-random'),
+    url(r'^rate/$', 'rate_download', name='downloads-rate'),
+    url(r'^rating/$', 'rating', name='downloads-rating'),
+    url(r'^search/page/(?P<page>\d+)/$', 'search', name='downloads-search'),
+    url(r'^thanks/$', 'thanks', name='downloads-add_thanks'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/downloads/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,252 @@
+"""
+Views for the downloads application.
+"""
+import random
+
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.contrib.auth.decorators import login_required
+from django.http import Http404
+from django.http import HttpResponse
+from django.http import HttpResponseRedirect
+from django.http import HttpResponseForbidden
+from django.http import HttpResponseBadRequest
+from django.core.urlresolvers import reverse
+from django.db.models import Q
+from django.views.decorators.http import require_POST
+
+from core.paginator import DiggPaginator
+from core.functions import email_admins
+from downloads.models import Category
+from downloads.models import Download
+from downloads.models import VoteRecord
+from downloads.forms import AddDownloadForm
+from downloads.forms import SearchForm
+
+#######################################################################
+
+DLS_PER_PAGE = 10
+
+def create_paginator(dls):
+   return DiggPaginator(dls, DLS_PER_PAGE, body=5, tail=3, margin=3, padding=2)
+
+#######################################################################
+
+@login_required
+def index(request):
+    categories = Category.objects.all()
+    total_dls = Download.public_objects.all().count()
+    return render_to_response('downloads/index.html', {
+        'categories': categories,
+        'total_dls': total_dls,
+        },
+        context_instance = RequestContext(request))
+
+#######################################################################
+# Maps URL component to database field name for the Download table:
+
+DOWNLOAD_FIELD_MAP = {
+   'title': 'title', 
+   'date': '-date_added',
+   'rating': '-average_score',
+   'hits': '-hits'
+}
+
+@login_required
+def category(request, category, sort='title', page='1'):
+    try:
+        cat = Category.objects.get(pk=category)
+    except Category.DoesNotExist:
+        raise Http404
+
+    if sort not in DOWNLOAD_FIELD_MAP:
+        sort = 'title'
+    order_by = DOWNLOAD_FIELD_MAP[sort]
+
+    downloads = Download.public_objects.filter(category=cat.pk).order_by(order_by)
+    paginator = create_paginator(downloads)
+    try:
+        the_page = paginator.page(int(page))
+    except InvalidPage:
+        raise Http404
+
+    return render_to_response('downloads/download_list.html', {
+        's' : sort,
+        'category' : cat,
+        'page' : the_page, 
+        }, 
+        context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def new(request):
+    downloads = Download.public_objects.order_by('-date_added')[:DLS_PER_PAGE]
+    return render_to_response('downloads/download_summary.html', {
+        'downloads' : downloads,
+        'title' : 'Newest Downloads',
+        }, 
+        context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def popular(request):
+    downloads = Download.public_objects.order_by('-hits')[:DLS_PER_PAGE]
+    return render_to_response('downloads/download_summary.html', {
+        'downloads' : downloads,
+        'title' : 'Popular Downloads',
+        }, 
+        context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def rating(request):
+    downloads = Download.public_objects.order_by('-average_score')[:DLS_PER_PAGE]
+    return render_to_response('downloads/download_summary.html', {
+        'downloads' : downloads,
+        'title' : 'Highest Rated Downloads',
+        }, 
+        context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def download(request, id):
+    download = Download.public_objects.get(pk=id)
+    if download is None:
+        raise Http404
+    return _redirect_download(download)
+
+#######################################################################
+
+def _redirect_download(download):
+    download.hits += 1
+    download.save()
+    return HttpResponseRedirect(download.file.url)
+
+#######################################################################
+
+@login_required
+def comments(request, id):
+    download = Download.public_objects.get(pk=id)
+    if download is None:
+        raise Http404
+    return render_to_response('downloads/download_comments.html', {
+        'download' : download,
+        }, 
+        context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def random_download(request):
+    ids = Download.public_objects.values_list('id', flat=True)
+    if not ids:
+        raise Http404
+    id = random.choice(ids)
+    download = Download.objects.get(pk=id)
+    return _redirect_download(download)
+
+#######################################################################
+
+@login_required
+def add(request):
+    if request.method == 'POST':
+        form = AddDownloadForm(request.POST, request.FILES)
+        if form.is_valid():
+            dl = form.save(commit=False)
+            dl.user = request.user
+            dl.ip_address = request.META.get('REMOTE_ADDR', None)
+            dl.is_public = False
+            dl.save()
+            email_admins('New download for approval', """Hello,
+
+A user has uploaded a new download for your approval.
+""")
+            return HttpResponseRedirect(reverse('downloads-add_thanks'))
+    else:
+        form = AddDownloadForm()
+
+    return render_to_response('downloads/add.html', {
+        'add_form': form,
+        },
+        context_instance=RequestContext(request))
+
+#######################################################################
+
+@login_required
+def thanks(request):
+    return render_to_response('downloads/thanks.html', {
+        },
+        context_instance=RequestContext(request))
+
+#######################################################################
+
+@login_required
+def search(request, page=1):
+    if request.method == 'POST':
+        form = SearchForm(request.POST)
+        if form.is_valid():
+            query_text = form.query()
+            page = 1
+        else:
+            return HttpResponseRedirect(reverse('downloads-index'))
+    else:
+        if 'query' in request.GET:
+            query_text = request.GET['query']
+        else:
+            return HttpResponseRedirect(reverse('downloads-index'))
+
+    dls = Download.objects.filter(
+            Q(title__icontains = query_text) |
+            Q(description__icontains = query_text)).order_by('title')
+    paginator = create_paginator(dls)
+    try:
+        the_page = paginator.page(int(page))
+    except EmptyPage:
+        dls = Download.objects.none()
+    except InvalidPage:
+        raise Http404
+
+    return render_to_response('downloads/search_results.html', {
+        'query': query_text,
+        'page': the_page, 
+        }, 
+        context_instance = RequestContext(request))
+
+#######################################################################
+
+@require_POST
+def rate_download(request):
+    """This function is called by AJAX to rate a download."""
+    if request.user.is_authenticated():
+        id = request.POST.get('id', None)
+        rating = request.POST.get('rating', None)
+        if id is None or rating is None:
+            return HttpResponseBadRequest('Missing id or rating.')
+        
+        try:
+            rating = int(rating)
+        except ValueError:
+            return HttpResponseBadRequest('Invalid rating.')
+
+        # rating will be from 0-4
+        rating = min(5, max(1, rating))
+
+        try:
+            download = Download.public_objects.get(pk=id)
+        except Download.DoesNotExist:
+            return HttpResponseBadRequest('Invalid download id.')
+
+        # prevent multiple votes from the same user
+        vote_record, created = VoteRecord.objects.get_or_create(download=download, user=request.user)
+        if created:
+            new_score = download.vote(rating)
+            download.save()
+            return HttpResponse(str(new_score))
+        else:
+            return HttpResponse('-1')
+
+    return HttpResponseForbidden('You must be logged in to rate a download.')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/gcalendar/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,33 @@
+"""
+This file contains the automatic admin site definitions for the gcalendar application.
+"""
+from django.contrib import admin
+from django.http import HttpResponse
+from django.conf.urls.defaults import *
+
+from gcalendar.models import Event
+from gcalendar.admin_views import google_sync
+
+
+class EventAdmin(admin.ModelAdmin):
+    list_display = ('what', 'user', 'start_date', 'where', 'date_submitted',
+            'status', 'needs_approval')
+    list_filter = ('start_date', 'status')
+    search_fields = ('what', 'where', 'description')
+    raw_id_fields = ('user', )
+    exclude = ('html', 'google_id')
+    save_on_top = True
+
+    def get_urls(self):
+        urls = super(EventAdmin, self).get_urls()
+        my_urls = patterns('',
+            url(r'^google_sync/$', 
+                self.admin_site.admin_view(google_sync), 
+                name="gcalendar-google_sync")
+        )
+        return my_urls + urls
+
+
+admin.site.register(Event, EventAdmin)
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/gcalendar/admin_views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,47 @@
+"""
+Admin views for the gcalendar application.
+"""
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
+from gcalendar.models import Event
+from gcalendar.forms import PasswordForm
+from gcalendar.calendar import Calendar
+from gcalendar.calendar import CalendarError
+import gcalendar.settings
+
+
+def google_sync(request):
+    """View to synchronize approved event changes with Google calendar."""
+    events = Event.pending_events.all()
+    messages = []
+    errors = []
+    if request.method == 'POST':
+        form = PasswordForm(request.POST)
+        if form.is_valid():
+            try:
+                cal = Calendar(gcalendar.settings.EMAIL,
+                        form.cleaned_data['password'],
+                        gcalendar.settings.CALENDAR_ID)
+                cal.sync_events(events)
+            except CalendarError, e:
+                errors = e.errs
+                events = Event.pending_events.all()
+                form = PasswordForm()
+            else:
+                messages.append('All events processed successfully.')
+                events = Event.objects.none()
+                form = PasswordForm()
+
+    else:
+        form = PasswordForm()
+
+    return render_to_response('gcalendar/google_sync.html', {
+        'messages': messages,
+        'errors': errors,
+        'events': events,
+        'form': form,
+        },
+        context_instance=RequestContext(request))
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/gcalendar/calendar.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,145 @@
+"""
+This file contains the calendar class wich abstracts the Google gdata API for working with
+Google Calendars.
+"""
+import datetime
+import pytz
+
+from django.utils.tzinfo import FixedOffset
+from gdata.calendar.service import CalendarService
+from gdata.calendar import CalendarEventFeed
+from gdata.calendar import CalendarEventEntry
+from gdata.calendar import Who
+from gdata.calendar import Where
+from gdata.calendar import When
+from gdata.service import BadAuthentication
+import atom
+
+from gcalendar.models import Event
+
+
+class CalendarError(Exception):
+    def __init__(self, errs):
+        self.errs = errs
+
+    def __str__(self):
+        return repr(self.errs)
+
+
+class Calendar(object):
+    DATE_FMT = '%Y-%m-%d'
+    DATE_TIME_FMT = DATE_FMT + 'T%H:%M:%S'
+    DATE_TIME_TZ_FMT = DATE_TIME_FMT + '.000Z'
+
+    def __init__(self, email, password, calendar_id='default'): 
+        self.client = CalendarService()
+        self.client.email = email
+        self.client.password = password
+        self.client.source = 'Google-Calendar_Python_GCalendar'
+        self.insert_feed = '/calendar/feeds/%s/private/full' % calendar_id
+        self.batch_feed = '%s/batch' % self.insert_feed
+        try:
+            self.client.ProgrammaticLogin()
+        except BadAuthentication:
+            raise CalendarError(['Incorrect password'])
+        except Exception, e:
+            raise CalendarError([e])
+
+    def sync_events(self, qs):
+        request_feed = CalendarEventFeed()
+        for model in qs:
+            if model.status == Event.NEW_APRV:
+                event = CalendarEventEntry()
+                request_feed.AddInsert(entry=self._populate_event(model, event))
+            elif model.status == Event.EDIT_APRV:
+                event = self._retrieve_event(model)
+                request_feed.AddUpdate(entry=self._populate_event(model, event))
+            elif model.status == Event.DEL_APRV:
+                event = self._retrieve_event(model)
+                request_feed.AddDelete(entry=event)
+            else:
+                assert False, 'unexpected status in sync_events'
+
+        response_feed = self.client.ExecuteBatch(request_feed, self.batch_feed)
+        err_msgs = []
+        for entry in response_feed.entry:
+            i = int(entry.batch_id.text)
+            code = int(entry.batch_status.code)
+
+            error = False
+            if qs[i].status == Event.NEW_APRV or qs[i].status == Event.EDIT_APRV:
+                if (code == 201 and qs[i].status == Event.NEW_APRV) or \
+                        (code == 200 and qs[i].status == Event.EDIT_APRV):
+                    qs[i].google_id = entry.id.text
+                    qs[i].status = Event.ON_CAL
+                    qs[i].save()
+                else:
+                    error = True
+            elif qs[i].status == Event.DEL_APRV:
+                if code == 200:
+                    qs[i].delete()
+                else:
+                    error = True
+
+            if error:
+                err_msgs.append('%s - (%d) %s' % \
+                        (qs[i].title, code, entry.batch_status.reason))
+
+        if len(err_msgs) > 0:
+            raise CalendarError(err_msgs)
+
+    def _retrieve_event(self, model):
+        try:
+            event = self.client.GetCalendarEventEntry(model.google_id)
+        except Exception:
+            raise CalendarError(['Could not retrieve event from Google: %s' % model.what])
+        return event
+
+    def _populate_event(self, model, event):
+        """Populates a gdata event from an Event model object."""
+        event.title = atom.Title(text=model.what)
+        event.content = atom.Content(text=model.html)
+        event.where = [Where(value_string=model.where)]
+        event.who = [Who(name=model.user.username, email=model.user.email)]
+
+        if model.all_day:
+            start_time = self._make_time(model.start_date)
+            if model.start_date == model.end_date:
+                end_time = None
+            else:
+                end_time = self._make_time(model.end_date)
+        else:
+            start_time = self._make_time(model.start_date, model.start_time, model.time_zone)
+            end_time = self._make_time(model.end_date, model.end_time, model.time_zone)
+
+        event.when = [When(start_time=start_time, end_time=end_time)]
+        return event
+    
+    def _make_time(self, date, time=None, tz_name=None):
+        """
+        Returns the gdata formatted date/time string given a date, optional time,
+        and optional time zone name (e.g. 'US/Pacific'). If the time zone name is None,
+        no time zone info will be added to the string.
+        """
+
+        if time is not None:
+            d = datetime.datetime.combine(date, time)
+        else:
+            d = datetime.datetime(date.year, date.month, date.day)
+
+        if time is None:
+            s = d.strftime(self.DATE_FMT)
+        elif tz_name is None:
+            s = d.strftime(self.DATE_TIME_FMT)
+        else:
+            try:
+                tz = pytz.timezone(tz_name)
+            except pytz.UnknownTimeZoneError:
+                raise CalendarError(['Invalid time zone: %s' (tz_name,)])
+            local = tz.localize(d)
+            zulu = local.astimezone(FixedOffset(0))
+            s = zulu.strftime(self.DATE_TIME_TZ_FMT)
+
+        return s
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/gcalendar/forms.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,159 @@
+"""
+Forms for the gcalendar application.
+"""
+import datetime
+import pytz
+from django import forms
+
+from gcalendar.models import Event
+
+
+TIME_CHOICES = (
+    ('00:00', '12:00 am (00:00)'),
+    ('00:30', '12:30 am (00:30)'),
+    ('01:00', '1:00 am (01:00)'),
+    ('01:30', '1:30 am (01:30)'),
+    ('02:00', '2:00 am (02:00)'),
+    ('02:30', '2:30 am (02:30)'),
+    ('03:00', '3:00 am (03:00)'),
+    ('03:30', '3:30 am (03:30)'),
+    ('04:00', '4:00 am (04:00)'),
+    ('04:30', '4:30 am (04:30)'),
+    ('05:00', '5:00 am (05:00)'),
+    ('05:30', '5:30 am (05:30)'),
+    ('06:00', '6:00 am (06:00)'),
+    ('06:30', '6:30 am (06:30)'),
+    ('07:00', '7:00 am (07:00)'),
+    ('07:30', '7:30 am (07:30)'),
+    ('08:00', '8:00 am (08:00)'),
+    ('08:30', '8:30 am (08:30)'),
+    ('09:00', '9:00 am (09:00)'),
+    ('09:30', '9:30 am (09:30)'),
+    ('10:00', '10:00 am (10:00)'),
+    ('10:30', '10:30 am (10:30)'),
+    ('11:00', '11:00 am (11:00)'),
+    ('11:30', '11:30 am (11:30)'),
+    ('12:00', '12:00 am (12:00)'),
+    ('12:30', '12:30 am (12:30)'),
+    ('13:00', '1:00 pm (13:00)'),
+    ('13:30', '1:30 pm (13:30)'),
+    ('14:00', '2:00 pm (14:00)'),
+    ('14:30', '2:30 pm (14:30)'),
+    ('15:00', '3:00 pm (15:00)'),
+    ('15:30', '3:30 pm (15:30)'),
+    ('16:00', '4:00 pm (16:00)'),
+    ('16:30', '4:30 pm (16:30)'),
+    ('17:00', '5:00 pm (17:00)'),
+    ('17:30', '5:30 pm (17:30)'),
+    ('18:00', '6:00 pm (18:00)'),
+    ('18:30', '6:30 pm (18:30)'),
+    ('19:00', '7:00 pm (19:00)'),
+    ('19:30', '7:30 pm (19:30)'),
+    ('20:00', '8:00 pm (20:00)'),
+    ('20:30', '8:30 pm (20:30)'),
+    ('21:00', '9:00 pm (21:00)'),
+    ('21:30', '9:30 pm (21:30)'),
+    ('22:00', '10:00 pm (22:00)'),
+    ('22:30', '10:30 pm (22:30)'),
+    ('23:00', '11:00 pm (23:00)'),
+    ('23:30', '11:30 pm (23:30)'),
+)
+
+
+class EventEntryForm(forms.ModelForm):
+    what = forms.CharField(widget=forms.TextInput(attrs={'size': 60}))
+    start_date = forms.DateField(widget=forms.TextInput(attrs={'size': 10}))
+    start_time = forms.TimeField(required=False, widget=forms.Select(choices=TIME_CHOICES))
+    end_date = forms.DateField(widget=forms.TextInput(attrs={'size': 10}))
+    end_time = forms.TimeField(required=False, widget=forms.Select(choices=TIME_CHOICES))
+    time_zone = forms.CharField(required=False, widget=forms.HiddenInput())
+    where = forms.CharField(required=False, widget=forms.TextInput(attrs={'size': 60}))
+
+    TIME_FORMAT = '%H:%M'
+    DEFAULT_START_TIME = '19:00'
+    DEFAULT_END_TIME = '20:00'
+
+    class Meta:
+        model = Event
+        fields = ('what', 'start_date', 'start_time', 'end_date', 'end_time',
+            'all_day', 'time_zone', 'where', 'description')
+
+    class Media:
+        css = {
+            'all': ('js/markitup/skins/markitup/style.css',
+                    'js/markitup/sets/markdown/style.css',
+                    'http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.0/themes/redmond/jquery-ui.css',
+                    'css/gcalendar.css',
+                    )
+        }
+        js = (
+            'http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js',
+            'js/markitup/jquery.markitup.pack.js',
+            'js/markitup/sets/markdown/set.js',
+            'http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.0/jquery-ui.js',
+            'js/gcalendar.js',
+        )
+
+    def __init__(self, *args, **kwargs):
+        initial = kwargs.get('initial', {})
+        instance = kwargs.get('instance', None)
+
+        if len(args) == 0:      # no POST arguments
+            if instance is None:
+                init_day = datetime.date.today().strftime('%m/%d/%Y')
+                if 'start_date' not in initial:
+                    initial['start_date'] = init_day
+                if 'end_date' not in initial:
+                    initial['end_date'] = init_day
+                if 'start_time' not in initial:
+                    initial['start_time'] = self.DEFAULT_START_TIME
+                if 'end_time' not in initial:
+                    initial['end_time'] = self.DEFAULT_END_TIME
+            else:
+                if instance.all_day:
+                    initial['start_time'] = self.DEFAULT_START_TIME
+                    initial['end_time'] = self.DEFAULT_END_TIME
+                else:
+                    if 'start_time' not in initial:
+                        initial['start_time'] = instance.start_time.strftime(self.TIME_FORMAT)
+                    if 'end_time' not in initial:
+                        initial['end_time'] = instance.end_time.strftime(self.TIME_FORMAT)
+
+            kwargs['initial'] = initial
+
+        super(EventEntryForm, self).__init__(*args, **kwargs)
+
+    def clean(self):
+        start_date = self.cleaned_data.get('start_date')
+        start_time = self.cleaned_data.get('start_time')
+        all_day = self.cleaned_data.get('all_day')
+        end_date = self.cleaned_data.get('end_date')
+        end_time = self.cleaned_data.get('end_time')
+
+        if start_date and start_time and (all_day or (end_date and end_time)):
+            if all_day:
+                start = start_date
+                end = end_date
+            else:
+                start = datetime.datetime.combine(start_date, start_time)
+                end = datetime.datetime.combine(end_date, end_time)
+            if start > end:
+                raise forms.ValidationError("The start date of the event "
+                        "is after the ending time!")
+
+        return self.cleaned_data
+
+    def clean_time_zone(self):
+        tz = self.cleaned_data['time_zone']
+        try:
+            pytz.timezone(tz)
+        except pytz.UnknownTimeZoneError:
+            raise forms.ValidationError("Invalid timezone.")
+        return tz
+
+
+class PasswordForm(forms.Form):
+    password = forms.CharField(widget=forms.PasswordInput())
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/gcalendar/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,101 @@
+"""
+Models for the gcalendar application.
+"""
+from django.db import models
+from django.db.models import Q
+from django.contrib.auth.models import User
+from django.template.loader import render_to_string
+
+
+class PendingEventManager(models.Manager):
+    """A manager for pending events."""
+
+    def get_query_set(self):
+        """Returns a queryset of events that have been approved to update
+        the Google calendar."""
+        return super(PendingEventManager, self).get_query_set().filter(
+                Q(status=Event.NEW_APRV) | \
+                Q(status=Event.EDIT_APRV) | \
+                Q(status=Event.DEL_APRV)
+            )
+
+
+class Event(models.Model):
+    """Model to represent calendar events."""
+
+    # Event status codes:
+    (NEW, NEW_APRV, EDIT_REQ, EDIT_APRV, DEL_REQ, DEL_APRV, ON_CAL) = range(7)
+
+    STATUS_CHOICES = (
+        (NEW, 'New'),
+        (NEW_APRV, 'New Approved'),
+        (EDIT_REQ, 'Edit Request'),
+        (EDIT_APRV, 'Edit Approved'),
+        (DEL_REQ, 'Delete Request'),
+        (DEL_APRV, 'Delete Approved'),
+        (ON_CAL, 'On Calendar'),
+    )
+
+    REPEAT_CHOICES = (
+        ('none', 'Does not repeat'),
+        ('daily', 'Daily'),
+        ('weekly', 'Weekly'),
+        ('monthly', 'Monthly'),
+        ('yearly', 'Yearly')
+    )
+
+    user = models.ForeignKey(User)
+    what = models.CharField(max_length=255)
+    start_date = models.DateField()
+    start_time = models.TimeField(null=True, blank=True)
+    end_date = models.DateField()
+    end_time = models.TimeField(null=True, blank=True)
+    time_zone = models.CharField(max_length=64, blank=True)
+    all_day = models.BooleanField(default=False)
+    repeat = models.CharField(max_length=7, choices=REPEAT_CHOICES)
+    repeat_interval = models.IntegerField(null=True, blank=True,
+        help_text='Only valid for repeating events.')
+    until_date = models.DateField(null=True, blank=True,
+        help_text='Only valid for repeating events; leave blank for no end date.')
+    weekly_sun = models.BooleanField(default=False, verbose_name='Weekly on Sun',
+        help_text='Only valid for weekly repeats.')
+    weekly_mon = models.BooleanField(default=False, verbose_name='Weekly on Mon',
+        help_text='Only valid for weekly repeats.')
+    weekly_tue = models.BooleanField(default=False, verbose_name='Weekly on Tue',
+        help_text='Only valid for weekly repeats.')
+    weekly_wed = models.BooleanField(default=False, verbose_name='Weekly on Wed',
+        help_text='Only valid for weekly repeats.')
+    weekly_thu = models.BooleanField(default=False, verbose_name='Weekly on Thu',
+        help_text='Only valid for weekly repeats.')
+    weekly_fri = models.BooleanField(default=False, verbose_name='Weekly on Fri',
+        help_text='Only valid for weekly repeats.')
+    weekly_sat = models.BooleanField(default=False, verbose_name='Weekly on Sat',
+        help_text='Only valid for weekly repeats.')
+    monthly_by_day = models.BooleanField(default=False,
+        help_text='Only valid for monthly repeats; Checked: By day of the month, Unchecked: By day of the week.')
+    where = models.CharField(max_length=255, blank=True)
+    description = models.TextField(blank=True)
+    html = models.TextField(blank=True)
+    date_submitted = models.DateTimeField(auto_now_add=True)
+    google_id = models.CharField(max_length=255, blank=True)
+    status = models.SmallIntegerField(choices=STATUS_CHOICES, default=NEW, db_index=True)
+
+    objects = models.Manager()
+    pending_events = PendingEventManager()
+
+    def __unicode__(self):
+        return self.what
+
+    class Meta:
+        ordering = ('-date_submitted', )
+
+    def save(self, *args, **kwargs):
+        html = render_to_string('gcalendar/markdown.html', {'data': self.description})
+        self.html = html.strip()
+        super(Event, self).save(*args, **kwargs)
+
+    def needs_approval(self):
+        return self.status in (self.NEW, self.EDIT_REQ, self.DEL_REQ)
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/gcalendar/settings.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,8 @@
+"""
+This file contains the user tweakable settings for the gcalendar application.
+"""
+
+EMAIL = 'bgneal@gmail.com'
+CALENDAR_ID = 'i81lu3fkh57sgqqenogefd9v78@group.calendar.google.com'
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/gcalendar/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,15 @@
+"""
+URLs for the gcalendar application.
+"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('gcalendar.views',
+    url(r'^$', 'index', name='gcalendar-index'),
+    url(r'^add/$', 'add_event', name='gcalendar-add'),
+    url(r'^change/$', 'edit_events', name='gcalendar-edit_events'),
+    url(r'^change/(\d+)/$', 'edit_event', name='gcalendar-edit_event'),
+    url(r'^delete/$', 'delete_event', name='gcalendar-delete'),
+    url(r'^thanks/add/$', 'add_thanks', name='gcalendar-add_thanks'),
+    url(r'^thanks/change/$', 'edit_thanks', name='gcalendar-edit_thanks'),
+)
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/gcalendar/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,114 @@
+"""
+Views for the gcalendar application.
+"""
+
+from django.contrib.auth.decorators import login_required
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse
+from django.http import HttpResponseBadRequest
+from django.http import HttpResponseForbidden
+from django.http import HttpResponseRedirect
+from django.http import Http404
+from django.shortcuts import render_to_response
+from django.shortcuts import get_object_or_404
+from django.template import RequestContext
+
+from gcalendar.forms import EventEntryForm
+from gcalendar.models import Event
+
+
+def index(request):
+    return render_to_response('gcalendar/index.html', {
+        },
+        context_instance = RequestContext(request))
+
+
+@login_required
+def add_event(request):
+    if request.method == 'POST':
+        form = EventEntryForm(request.POST)
+        if form.is_valid():
+            event = form.save(commit=False)
+            event.user = request.user
+            event.repeat = 'none'
+            event.save()
+            return HttpResponseRedirect(reverse('gcalendar-add_thanks'))
+    else:
+        form = EventEntryForm()
+
+    return render_to_response('gcalendar/event.html', {
+        'title': 'Add Calendar Event',
+        'form': form,
+        },
+        context_instance = RequestContext(request))
+
+
+@login_required
+def add_thanks(request):
+    return render_to_response('gcalendar/thanks_add.html', {
+        },
+        context_instance = RequestContext(request))
+
+
+@login_required
+def edit_events(request):
+    events = Event.objects.filter(user=request.user, status=Event.ON_CAL).order_by('start_date')
+    return render_to_response('gcalendar/edit.html', {
+        'events': events,
+        },
+        context_instance = RequestContext(request))
+
+
+@login_required
+def edit_event(request, event_id):
+    event = get_object_or_404(Event, pk=event_id)
+    if event.user != request.user:
+        raise Http404
+
+    if request.method == 'POST':
+        form = EventEntryForm(request.POST, instance=event)
+        if form.is_valid():
+            event = form.save(commit=False)
+            event.user = request.user
+            event.repeat = 'none'
+            event.status = Event.EDIT_REQ
+            event.save()
+            return HttpResponseRedirect(reverse('gcalendar-edit_thanks'))
+    else:
+        form = EventEntryForm(instance=event)
+
+    return render_to_response('gcalendar/event.html', {
+        'title': 'Change Calendar Event',
+        'form': form,
+        },
+        context_instance = RequestContext(request))
+
+
+@login_required
+def edit_thanks(request):
+    return render_to_response('gcalendar/thanks_edit.html', {
+        },
+        context_instance = RequestContext(request))
+
+
+def delete_event(request):
+    """This view marks an event for deletion. It is called via AJAX."""
+    if request.user.is_authenticated():
+        id = request.POST.get('id', None)
+        if id is None or not id.isdigit():
+            return HttpResponseBadRequest()
+        try:
+            event = Event.objects.get(pk=id)
+        except Event.DoesNotExist:
+            return HttpResponseBadRequest()
+        if request.user != event.user:
+            return HttpResponseForbidden()
+
+        event.status = Event.DEL_REQ
+        event.save()
+        return HttpResponse(id)
+
+    return HttpResponseForbidden()
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/irc/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,15 @@
+"""Models for the IRC application. 
+The IRC application simply reports who is in the site's IRC chatroom. A bot in the channel updates
+the table and we read it.
+"""
+from django.db import models
+
+class IrcChannel(models.Model):
+   name = models.CharField(max_length=30)
+   last_update = models.DateTimeField()
+
+   def __unicode__(self):
+      return self.name
+
+   class Meta:
+      ordering = ('name', )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/irc/templatetags/irc_tags.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,14 @@
+"""
+Template tags for the IRC application.
+"""
+from django import template
+from irc.models import IrcChannel
+
+register = template.Library()
+
+@register.inclusion_tag('irc/irc_block.html')
+def irc_status():
+    nicks = IrcChannel.objects.all()
+    return {
+        'nicks': nicks,
+    }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/irc/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,6 @@
+"""urls for the IRC application"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('irc.views',
+   url(r'^$', 'view', name='irc-main'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/irc/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,12 @@
+"""views for the IRC application"""
+
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
+from irc.models import IrcChannel
+
+def view(request):
+   nicks = IrcChannel.objects.all()
+   return render_to_response('irc/view.html',
+         {'nicks': nicks},
+         context_instance = RequestContext(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/legal/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,9 @@
+"""This file contains the automatic admin site definitions for the legal models"""
+
+from django.contrib import admin
+from legal.models import Policy
+
+class PolicyAdmin(admin.ModelAdmin):
+   list_display = ('title', 'policy_type')
+
+admin.site.register(Policy, PolicyAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/legal/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,15 @@
+"""models for the legal module"""
+
+from django.db import models
+
+class Policy(models.Model):
+   policy_type = models.SlugField(help_text = 'A slug for database lookups')
+   title = models.CharField(max_length = 64)
+   text = models.TextField()
+   revised_date = models.DateField()
+
+   def __unicode__(self):
+      return self.title
+
+   class Meta:
+      verbose_name_plural = 'Policies'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/legal/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,6 @@
+"""urls for the legal application"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('legal.views',
+   (r'^policy/(\w+)/$', 'view'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/legal/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,13 @@
+"""views for the legal application"""
+
+from django.shortcuts import render_to_response
+from django.shortcuts import get_object_or_404
+from django.template import RequestContext
+
+from legal.models import Policy
+
+def view(request, policy_name):
+   policy = get_object_or_404(Policy, policy_type = policy_name)
+   return render_to_response('legal/view.html',
+         {'policy' : policy},
+         context_instance = RequestContext(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/manage.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+from django.core.management import execute_manager
+try:
+    import settings # Assumed to be in the same directory.
+except ImportError:
+    import sys
+    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+    sys.exit(1)
+
+if __name__ == "__main__":
+    execute_manager(settings)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/membermap/__init__.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,1 @@
+import membermap.signals
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/membermap/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,18 @@
+"""
+Admin definitions for the member map application models.
+"""
+
+from django.contrib import admin
+
+from membermap.models import MapEntry
+
+class MapEntryAdmin(admin.ModelAdmin):
+   exclude = ('json', )
+   list_display = ('user', 'location', 'lat', 'lon', 'date_updated')
+   list_filter = ('date_updated', )
+   date_hierarchy = 'date_updated'
+   ordering = ('-date_updated', )
+   search_fields = ('user', 'location', 'message')
+   raw_id_fields = ('user', )
+   
+admin.site.register(MapEntry, MapEntryAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/membermap/forms.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,24 @@
+"""
+Forms for the member map application.
+"""
+from django import forms
+from membermap.models import MapEntry
+
+
+class MapEntryForm(forms.ModelForm):
+
+    class Meta:
+        model = MapEntry
+        fields = ('location', 'message')
+
+    class Media:
+        css = {
+            'all': ('js/markitup/skins/markitup/style.css',
+                    'js/markitup/sets/markdown/style.css')
+        }
+        js = (
+            'js/markitup/jquery.markitup.pack.js',
+            'js/markitup/sets/markdown/set.js',
+        )
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/membermap/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,38 @@
+"""
+Models for the member map application.
+"""
+from django.db import models
+from django.contrib.auth.models import User
+from django.template.loader import render_to_string
+from django.template.defaultfilters import escapejs
+import django.utils.simplejson as json
+
+
+# Create your models here.
+class MapEntry(models.Model):
+    """Represents a user's entry on the map."""
+    user = models.ForeignKey(User)
+    location = models.CharField(max_length=255)
+    lat = models.FloatField()
+    lon = models.FloatField()
+    message = models.TextField(blank=True)
+    json = models.TextField(blank=True)
+    date_updated = models.DateTimeField(auto_now_add=True)
+
+    def __unicode__(self):
+        return u'Entry for %s' % self.user.username
+
+    class Meta:
+        ordering = ('-date_updated', )
+        verbose_name_plural = 'map entries'
+
+    def save(self, force_insert=False, force_update=False):
+        msg = render_to_string('membermap/markdown.html', {'entry': self}).strip()
+        self.json = json.dumps({'name': self.user.username,
+            'lat': '%10.6f' % self.lat,
+            'lon': '%10.6f' % self.lon,
+            'message': msg,
+            })
+        super(MapEntry, self).save(force_insert, force_update)
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/membermap/signals.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,21 @@
+"""
+Signal handlers for the membermap application.
+We want to detect changes to the UserProfile model. If that person is on
+the map, re-save her MapEntry so that any avatar changes get picked up.
+"""
+from django.db.models.signals import post_save
+from bio.models import UserProfile
+from membermap.models import MapEntry
+
+
+def on_profile_save(sender, **kwargs):
+    if 'instance' in kwargs:
+        profile = kwargs['instance']
+        map_entry = MapEntry.objects.get(user=profile.user)
+        if map_entry is not None:
+            map_entry.save()
+
+
+post_save.connect(on_profile_save, sender=UserProfile)
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/membermap/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,11 @@
+"""
+URLs for the member map application.
+"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('membermap.views',
+    url(r'^$', 'index', name='membermap-index'),
+    url(r'^add/$', 'add', name='membermap-add'),
+    url(r'^delete/$', 'delete', name='membermap-delete'),
+    url(r'^query/$', 'query', name='membermap-query'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/membermap/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,108 @@
+"""
+Views for the membermap application.
+"""
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.http import HttpResponse
+from django.http import HttpResponseBadRequest
+from django.http import HttpResponseForbidden
+from django.views.decorators.http import require_POST
+
+from membermap.models import MapEntry
+from membermap.forms import MapEntryForm
+
+
+def index(request):
+    entry = None
+    if request.user.is_authenticated():
+        try:
+            entry = MapEntry.objects.get(user=request.user)
+        except MapEntry.DoesNotExist:
+            pass
+    if entry is not None:
+        form = MapEntryForm(initial={
+            'location': entry.location,
+            'message': entry.message})
+    else:
+        form = MapEntryForm()
+
+    return render_to_response('membermap/index.html', {
+        'form': form,
+        },
+        context_instance = RequestContext(request))
+
+
+def query(request):
+    """
+    This view is called by AJAX. If the user is logged in, return
+    a JSON object that consists of:
+        "users" : array of user objects
+        "recent" : array of usernames recently modified
+    """
+    if request.user.is_authenticated():
+        qs = MapEntry.objects.values_list('json', flat=True).order_by('user__username')
+        s = '{"users":[' + ','.join(qs) + '], "recent":['
+
+        names = MapEntry.objects.values_list('user__username', flat=True)[:10]
+        s += ','.join(['"%s"' % name for name in names])
+        s += ']}'
+        return HttpResponse(s, content_type='application/json')
+
+    return HttpResponseForbidden('You must be logged in.')
+
+
+@require_POST
+def add(request):
+    """
+    This view is called by AJAX to add/update the user to the map.
+    It returns the new JSON representation of the user.
+    """
+    if not request.user.is_authenticated():
+        return HttpResponseForbidden('You must be logged in.')
+
+    loc = request.POST.get('loc', None)
+    lat = request.POST.get('lat', None)
+    lon = request.POST.get('lon', None)
+    msg = request.POST.get('msg', '')
+
+    if loc is None or lat is None or lon is None:
+        return HttpResponseBadRequest('Missing parameters')
+
+    try:
+        lat = float(lat)
+        lon = float(lon)
+    except ValueError:
+        return HttpResponseBadRequest('Invalid lat/lon')
+
+    try:
+        entry = MapEntry.objects.get(user=request.user)
+    except MapEntry.DoesNotExist:
+        entry = MapEntry(user=request.user)
+
+    entry.location = loc
+    entry.lat = lat
+    entry.lon = lon
+    entry.message = msg
+    entry.save()
+
+    return HttpResponse(entry.json, content_type='application/json')
+
+
+@require_POST
+def delete(request):
+    """
+    This view is called by AJAX to delete the user from the map.
+    """
+    if not request.user.is_authenticated():
+        return HttpResponseForbidden('You must be logged in.')
+
+    try:
+        entry = MapEntry.objects.get(user=request.user)
+    except MapEntry.DoesNotExist:
+        pass
+    else:
+        entry.delete()
+
+    return HttpResponse('')
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/messages/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,10 @@
+"""
+This file contains the automatic admin site definitions for the Message models.
+"""
+
+from django.contrib import admin
+from messages.models import Message
+from messages.models import Options
+
+admin.site.register(Message)
+admin.site.register(Options)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/messages/forms.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,100 @@
+"""
+Forms for the messages application.
+"""
+
+from django import forms
+from django.contrib.auth.models import User
+from django.conf import settings
+from django.contrib.sites.models import Site
+from django.core.urlresolvers import reverse
+from django.template.loader import render_to_string
+
+from core.functions import send_mail
+from core.functions import get_full_name
+from core.widgets import AutoCompleteUserInput
+from messages.models import Message
+from messages.models import Options
+
+
+class ComposeForm(forms.Form):
+   """
+   This form is used to compose private messages.
+   """
+   receiver = forms.CharField(label='To', 
+         max_length=30,
+         widget=AutoCompleteUserInput())
+   subject = forms.CharField(max_length=120, widget=forms.TextInput(attrs={'size': 52}))
+   message = forms.CharField(widget=forms.Textarea(attrs={'rows': 20, 'cols': 80}))
+   attach_signature = forms.BooleanField(label='Attach Signature?', required=False)
+
+   def __init__(self, user, *args, **kwargs):
+      forms.Form.__init__(self, *args, **kwargs)
+      self.user = user
+      options = Options.objects.for_user(user)
+      self.fields['attach_signature'].initial = options.attach_signature
+
+   def clean_receiver(self):
+      receiver = self.cleaned_data['receiver']
+      try:
+         self.rcvr_user = User.objects.get(username=receiver)
+      except User.DoesNotExist:
+         raise forms.ValidationError("That username does not exist.")
+      if self.user.username.lower() == receiver.lower():
+         raise forms.ValidationError("You can't send a message to yourself.")
+      return receiver
+
+   def save(self, sender, parent_msg=None):
+      receiver = self.rcvr_user
+      subject = self.cleaned_data['subject']
+      message = self.cleaned_data['message']
+      attach_signature = self.cleaned_data['attach_signature']
+
+      new_msg = Message(
+         sender=sender,
+         receiver=receiver,
+         subject=subject,
+         message=message,
+         signature_attached=attach_signature,
+      )
+      new_msg.save()
+      if parent_msg is not None:
+         parent_msg.reply_date = new_msg.send_date
+         parent_msg.save()
+
+      receiver_opts = Options.objects.for_user(receiver)
+      if receiver_opts.notify_email:
+         notify_receiver(new_msg)
+
+   class Media:
+      css = {
+         'all': ('js/markitup/skins/markitup/style.css',
+                 'js/markitup/sets/markdown/style.css')
+      }
+      js = (
+         'js/messages/compose.js',
+         'js/markitup/jquery.markitup.pack.js',
+         'js/markitup/sets/markdown/set.js',
+      )
+
+
+class OptionsForm(forms.ModelForm):
+   class Meta:
+      model = Options
+
+
+def notify_receiver(new_msg):
+   """
+   This function creates the notification email to notify a user of
+   a new private message.
+   """
+   site = Site.objects.get_current()
+
+   email_body = render_to_string('messages/notification_email.txt', {
+            'site': site,
+            'msg': new_msg,
+            'options_url': reverse('messages-options'),
+         })
+
+   subject = 'New private message for %s at %s' % (new_msg.receiver.username, site.name)
+   from_email = settings.GPP_NO_REPLY_EMAIL + '@' + site.domain
+   send_mail(subject, email_body, from_email, [new_msg.receiver.email])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/messages/management/commands/purge_messages.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,20 @@
+"""
+purge_messages is a custom manage.py command for the messages application. 
+It is intended to be called from a cron job to purge messages that have been
+deleted by both sender and receiver.
+"""
+
+from django.core.management.base import NoArgsCommand
+
+from messages.models import Message
+
+
+class Command(NoArgsCommand):
+    help = "Delete messages that have been sent to the trash by both sender and receiver."
+
+    def handle_noargs(self, **options):
+        Message.objects.filter(sender_delete_date__isnull=False,
+                receiver_delete_date__isnull=False).delete()
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/messages/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,104 @@
+"""Models for the messages application."""
+
+import datetime
+from django.db import models
+from django.db.models import Q
+from django.contrib.auth.models import User
+from django.template.loader import render_to_string
+
+
+class MessageManager(models.Manager):
+    """The manager for the Message model. Provides convenience functions."""
+
+    def inbox(self, user):
+        return self.filter(receiver=user,
+            receiver_delete_date__isnull=True)
+
+    def outbox(self, user):
+        return self.filter(sender=user,
+            sender_delete_date__isnull=True)
+
+    def trash(self, user):
+        return self.filter(
+            Q(sender=user, sender_delete_date__isnull=False) |
+            Q(receiver=user, receiver_delete_date__isnull=False)
+        )
+
+    def unread_count(self, user):
+        return self.filter(receiver=user, read_date__isnull=True).count()
+
+
+class Message(models.Model):
+    """The Message is a model for a private message between users."""
+    sender = models.ForeignKey(User, related_name='sender')
+    receiver = models.ForeignKey(User, related_name='receiver')
+    send_date = models.DateTimeField('Date Sent', null=True, blank=True)
+    read_date = models.DateTimeField('Date Received', null=True, blank=True)
+    reply_date = models.DateTimeField('Date Replied', null=True, blank=True)
+    subject = models.CharField(max_length=120)
+    message = models.TextField()
+    html = models.TextField()
+    sender_delete_date = models.DateTimeField(null=True, blank=True)
+    receiver_delete_date = models.DateTimeField(null=True, blank=True)
+    signature_attached = models.BooleanField(default=False)
+
+    objects = MessageManager()
+
+    def unread(self):
+        """returns True if the message has not been read yet"""
+        return self.read_date is None
+
+    def replied_to(self):
+        """returns True if the message has been replied to"""
+        return self.reply_date is not None
+
+    def is_deleted(self, user):
+        """returns True if the message has been deleted by the user"""
+        if (user == self.sender and self.sender_delete_date is not None) or (
+            user == self.receiver and self.receiver_delete_date is not None):
+            return True
+        return False
+
+    def save(self, force_insert = False, force_update = False):
+        if not self.id:
+            self.send_date = datetime.datetime.now()
+        html = render_to_string('messages/markdown.html', {'data': self.message})
+        self.html = html.strip()
+        super(Message, self).save(force_insert, force_update)
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('messages.views.view', [str(self.id)])
+
+    def __unicode__(self):
+        return self.subject
+
+    class Meta:
+        ordering = ('-send_date', )
+
+
+class OptionsManager(models.Manager):
+    """Manager class for Options model."""
+
+    def for_user(self, user):
+        try:
+            opts = self.get(user=user)
+        except Options.DoesNotExist:
+            opts = Options(user=user)
+            opts.save()
+        return opts
+
+
+class Options(models.Model):
+    """Options is a model for user private message options."""
+    user = models.ForeignKey(User, editable=False)
+    attach_signature = models.BooleanField("Always attach signature?", default=True)
+    notify_email = models.BooleanField("Notify me of new messages via email?", default=False)
+
+    objects = OptionsManager()
+
+    def __unicode__(self):
+        return self.user.username
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/messages/templatetags/messages_tags.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,25 @@
+"""
+Template tags for the messages application.
+"""
+from django import template
+from django.core.urlresolvers import reverse
+
+from messages.models import Message
+
+
+register = template.Library()
+
+@register.simple_tag
+def unread_messages(user):
+    inbox_url = reverse('messages-inbox')
+    unread_count = Message.objects.unread_count(user)
+    if unread_count == 0:
+        link_text = u"Private Messages"
+    elif unread_count == 1:
+        link_text = u"1 New Message"
+    else:
+        link_text = u"%s New Messages" % unread_count
+    return u'<a href="%s">%s</a>' % (inbox_url, link_text)
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/messages/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,25 @@
+"""urls for the Messages application"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('messages.views',
+    url(r'^inbox/$', 'inbox', name='messages-inbox'),
+    url(r'^outbox/$', 'outbox', name='messages-outbox'),
+    url(r'^trash/$', 'trash', name='messages-trash'),
+    url(r'^view/(\d+)/$', 'view', name='messages-view'),
+    url(r'^reply/(\d+)/$', 'reply', name='messages-reply'),
+    url(r'^compose/$', 'compose', name='messages-compose'),
+    url(r'^compose/(\w+)/$', 'compose', name='messages-compose_to'),
+    url(r'^delete/$', 'delete_bulk', name='messages-delete_bulk'),
+    url(r'^delete/(\d+)/$', 'delete', name='messages-delete'),
+    url(r'^undelete/$', 'undelete_bulk', name='messages-undelete_bulk'),
+    url(r'^undelete/(\d+)/$', 'undelete', name='messages-undelete'),
+    url(r'^options/$', 'options', name='messages-options'),
+    url(r'^ajax/users/$', 'ajax_users', name='messages-ajax_users'),
+)
+
+urlpatterns += patterns('django.views.generic.simple',
+    (r'^$', 'redirect_to', {'url': 'inbox/'}),
+)
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/messages/utils.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,29 @@
+"""
+This file contains various helper utility functions for the messages
+application.
+"""
+
+from django.utils.text import wrap
+
+
+def reply_subject(subject):
+   """
+   Builds a subject line for a reply.
+   If the subject already starts with Re: then return the subject.
+   Otherwise, prepend Re: to the subject and return it.
+   """
+   if subject.startswith('Re: '):
+      return subject
+   return 'Re: ' + subject
+
+
+def quote_message(who, date, message):
+   """
+   Builds a message reply by quoting the existing message in a
+   typical email-like fashion. The quoting is compatible with Markdown.
+   """
+   header = '> On %s, %s wrote:\n>\n' % (date.strftime('%a, %b %d %Y, %I:%M %p'), who)
+   lines = wrap(message, 55).split('\n')
+   for i, line in enumerate(lines):
+      lines[i] = '> ' + line
+   return header + '\n'.join(lines)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/messages/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,310 @@
+"""Views for the messages application"""
+
+import datetime
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.contrib.auth.models import User
+from django.http import HttpResponseRedirect
+from django.http import HttpResponse
+from django.http import HttpResponseBadRequest
+from django.http import HttpResponseForbidden
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import get_object_or_404
+from django.core.urlresolvers import reverse
+from django.http import Http404
+
+from messages.models import Message
+from messages.models import Options
+from messages.forms import ComposeForm
+from messages.forms import OptionsForm
+from messages.utils import reply_subject
+from messages.utils import quote_message
+
+
+BOX_MAP = {
+    'inbox': 'messages-inbox',
+    'outbox': 'messages-outbox',
+    'trash': 'messages-trash',
+}
+
+
+def box_redirect(request):
+    """
+    Determines which box to redirect to by looking for a GET or
+    POST parameter.
+    """
+    if request.method == 'GET':
+        box = request.GET.get('box', 'inbox')
+    else:
+        box = request.POST.get('box', 'inbox')
+    if BOX_MAP.has_key(box):
+        url = reverse(BOX_MAP[box])
+    else:
+        url = reverse(BOX_MAP['inbox'])
+    return HttpResponseRedirect(url)
+
+
+@login_required
+def inbox(request):
+    """Displays the inbox for the user making the request."""
+    msgs = Message.objects.inbox(request.user)
+    return render_to_response('messages/inbox.html', {
+        'msgs': msgs,
+        }, 
+        context_instance = RequestContext(request))
+
+
+@login_required
+def outbox(request):
+    """Displays the outbox for the user making the request."""
+    msgs = Message.objects.outbox(request.user)
+    return render_to_response('messages/outbox.html', {
+        'msgs': msgs,
+        }, 
+        context_instance = RequestContext(request))
+
+
+@login_required
+def trash(request):
+    """Displays the trash for the user making the request."""
+    msgs = Message.objects.trash(request.user)
+    return render_to_response('messages/trash.html', {
+        'msgs': msgs,
+        }, 
+        context_instance = RequestContext(request))
+
+
+@login_required
+def view(request, msg_id):
+    """
+    View a given message. Only the sender or receiver can see
+    the message.
+    """
+    msg = get_object_or_404(Message, pk=msg_id)
+    if msg.sender != request.user and msg.receiver != request.user:
+        raise Http404
+
+    if msg.receiver == request.user and msg.read_date is None:
+        msg.read_date = datetime.datetime.now()
+        msg.save()
+
+    box = request.GET.get('box', None)
+
+    return render_to_response('messages/view.html', {
+        'box': box,
+        'msg': msg,
+        'is_deleted': msg.is_deleted(request.user),
+        }, 
+        context_instance = RequestContext(request))
+
+
+@login_required
+def reply(request, msg_id):
+    """
+    Process or prepare the compose form in order to reply
+    to a given message.
+    """
+    msg = get_object_or_404(Message, pk=msg_id)
+
+    if request.method == "POST":
+        if request.POST.get('submit_button', 'Cancel') == 'Cancel':
+            return box_redirect(request)
+        compose_form = ComposeForm(request.user, request.POST)
+        if compose_form.is_valid():
+            compose_form.save(sender=request.user, parent_msg=msg)
+            request.user.message_set.create(message='Reply sent.')
+            return box_redirect(request)
+    else:
+        if msg.receiver == request.user:
+            receiver_name = msg.sender.username
+        else:
+            # replying to message in outbox
+            receiver_name = msg.receiver.username
+
+        form_data = {
+            'receiver': receiver_name,
+            'subject': reply_subject(msg.subject),
+            'message': quote_message(msg.sender, msg.send_date, msg.message),
+            'box': request.GET.get('box', 'inbox'),
+        }
+
+        compose_form = ComposeForm(request.user, initial=form_data)
+
+    return render_to_response('messages/compose.html', {
+        'compose_form': compose_form,
+        }, 
+        context_instance = RequestContext(request))
+
+
+@login_required
+def compose(request, receiver=None):
+    """
+    Process or prepare the compose form in order to create
+    a new message.
+    """
+    if request.method == "POST":
+        if request.POST.get('submit_button', 'Cancel') == 'Cancel':
+            return HttpResponseRedirect(reverse('messages-inbox'))
+        compose_form = ComposeForm(request.user, request.POST)
+        if compose_form.is_valid():
+            compose_form.save(sender=request.user)
+            request.user.message_set.create(message='Message sent.')
+            return HttpResponseRedirect(reverse('messages-inbox'))
+    else:
+        if receiver is not None:
+            form_data = {
+                'receiver': receiver,
+            }
+            compose_form = ComposeForm(request.user, initial=form_data)
+        else:
+            compose_form = ComposeForm(request.user)
+
+    return render_to_response('messages/compose.html', {
+        'compose_form': compose_form,
+        }, 
+        context_instance = RequestContext(request))
+
+
+@login_required
+def delete(request, msg_id):
+    """
+    Deletes a given message. The user must be either the sender or
+    receiver for this to succeed.
+    """
+    msg = get_object_or_404(Message, pk=msg_id)
+    if msg.sender == request.user:
+        msg.sender_delete_date = datetime.datetime.now()
+    elif msg.receiver == request.user:
+        msg.receiver_delete_date = datetime.datetime.now()
+    else:
+        raise Http404
+    msg.save()
+    request.user.message_set.create(message='Message sent to trash.')
+
+    return box_redirect(request)
+
+
+@login_required
+def delete_bulk(request):
+    """
+    Deletes messages in bulk. The message ID's to be deleted are expected
+    to be in the delete POST array. The user must be either the sender
+    or receiver for this to succeed.
+    """
+    if request.method == "POST":
+        delete_ids = request.POST.getlist('delete_ids')
+        try:
+            delete_ids = [int(id) for id in delete_ids]
+        except ValueError:
+            raise Http404
+        msgs = Message.objects.filter(id__in = delete_ids)
+        now = datetime.datetime.now()
+        for msg in msgs:
+            if msg.sender == request.user:
+                msg.sender_delete_date = now
+                msg.save()
+            elif msg.receiver == request.user:
+                msg.receiver_delete_date = now
+                msg.save()
+        request.user.message_set.create(message='Messages sent to the trash.')
+
+    return box_redirect(request)
+
+
+@login_required
+def undelete(request, msg_id):
+    """
+    Undeletes a given message. The user must be either the sender or
+    receiver for this to succeed.
+    """
+    msg = get_object_or_404(Message, pk=msg_id)
+    if msg.sender == request.user:
+        msg.sender_delete_date = None
+    elif msg.receiver == request.user:
+        msg.receiver_delete_date = None
+    else:
+        raise Http404
+    msg.save()
+    request.user.message_set.create(message='Message retrieved from the trash.')
+
+    return box_redirect(request)
+
+
+@login_required
+def undelete_bulk(request):
+    """
+    Undeletes messages in bulk. The message ID's to be deleted are expected
+    to be in the delete POST array. The user must be either the sender
+    or receiver for this to succeed.
+    """
+    if request.method == "POST":
+        undelete_ids = request.POST.getlist('undelete_ids')
+        try:
+            undelete_ids = [int(id) for id in undelete_ids]
+        except ValueError:
+            raise Http404
+        msgs = Message.objects.filter(id__in = undelete_ids)
+        for msg in msgs:
+            if msg.sender == request.user:
+                msg.sender_delete_date = None
+                msg.save()
+            elif msg.receiver == request.user:
+                msg.receiver_delete_date = None
+                msg.save()
+        request.user.message_set.create(message='Messages retrieved from the trash.')
+
+    return box_redirect(request)
+
+
+@login_required
+def options(request):
+    """
+    View to display/change user options.
+    """
+    if request.method == "POST":
+        if request.POST.get('submit_button', 'Cancel') == 'Cancel':
+            return HttpResponseRedirect(reverse('messages-inbox'))
+        options = Options.objects.for_user(request.user)
+        form = OptionsForm(request.POST, instance=options)
+        if form.is_valid():
+            form.save()
+            request.user.message_set.create(message='Options saved.')
+            return HttpResponseRedirect(reverse('messages-inbox'))
+    else:
+        try:
+            options = Options.objects.for_user(request.user)
+        except:
+            options = Options()
+            options.user = request.user
+            options.save()
+
+        form = OptionsForm(instance=options)
+
+    return render_to_response('messages/options.html', {
+        'form': form,
+        }, 
+        context_instance = RequestContext(request))
+
+
+def ajax_users(request):
+    """
+    If the user is authenticated, return a string of usernames whose names start with
+    the 'q' GET parameter, limited by the 'limit' GET parameters. The names are separated
+    by newlines.
+    If the user is not authenticated, return an empty string.
+    This is used by the auto-complete function in the compose form.
+    """
+    q = request.GET.get('q', None)
+    if q is None:
+        return HttpResponseBadRequest()
+
+    if request.user.is_authenticated():
+        q = request.GET.get('q', ' ')
+        limit = int(request.GET.get('limit', 10))
+        users = User.objects.filter(username__istartswith=q).values_list('username', flat=True)[:limit]
+        user_list = u"\n".join(users)
+        return HttpResponse(user_list)
+    return HttpResponseForbidden()
+
+
+# vim: ts=4 sw=4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/news/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,32 @@
+"""
+This file contains the automatic admin site definitions for the News models.
+"""
+
+from django.contrib import admin
+from news.models import PendingStory
+from news.models import Story
+from news.models import Category
+
+class PendingStoryAdmin(admin.ModelAdmin):
+   list_display = ('title', 'date_submitted', 'submitter')
+   list_filter = ('date_submitted', )
+   search_fields = ('title', 'short_text', 'long_text')
+   date_hierarchy = 'date_submitted'
+
+   class Media:
+      js = ('js/tiny_mce/tiny_mce.js', 'js/tiny_mce_init_admin.js')
+
+
+class StoryAdmin(admin.ModelAdmin):
+   list_display = ('title', 'date_published', 'submitter', 'category')
+   list_filter = ('date_published', 'category')
+   search_fields = ('title', 'short_text', 'long_text')
+   date_hierarchy = 'date_published'
+
+   class Media:
+      js = ('js/tiny_mce/tiny_mce.js', 'js/tiny_mce_init_admin.js')
+
+
+admin.site.register(Category)
+admin.site.register(Story, StoryAdmin)
+admin.site.register(PendingStory, PendingStoryAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/news/feeds.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,28 @@
+"""
+This file contains the feed classes for the news application.
+"""
+
+from django.contrib.syndication.feeds import Feed
+from news.models import Story
+
+
+class LatestNewsFeed(Feed):
+   """The Feed class for the news application"""
+
+   title = 'SurfGuitar101.com News Feed'
+   link = '/feeds/news/'
+   description = 'News articles and stories from SurfGuitar101.com'
+   copyright = 'Copyright (C) 2008, Brian Neal'
+   ttl = '720'
+
+   title_template = 'news/feed_title.html'
+   description_template = 'news/feed_description.html'
+   
+   def items(self):
+      return Story.objects.order_by('-date_published')[:5]
+
+   def item_pubdate(self, item):
+      return item.date_published
+
+   def item_categories(self, item):
+      return (item.category.title, )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/news/forms.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,53 @@
+"""
+Forms for the news application.
+"""
+
+from django import forms
+from news.models import PendingStory
+from news.models import Category
+
+
+class AddNewsForm(forms.ModelForm):
+   """Form for a user to submit a news story to the admins for review."""
+   title = forms.CharField(widget=forms.TextInput(attrs={'size': 52}))
+   short_text = forms.CharField(widget=forms.Textarea(attrs={'rows': 20, 'cols': 80}))
+   long_text = forms.CharField(required=False, widget=forms.Textarea(attrs={'rows': 20, 'cols': 80}))
+
+   class Meta:
+      model = PendingStory
+      exclude = ('submitter', 'date_submitted', 'allow_comments', 'approved', 'tags')
+
+   class Media:
+      js = ('js/tiny_mce/tiny_mce.js', 'js/tiny_mce_init_std.js')
+
+
+class SearchNewsForm(forms.Form):
+   """Form for a user to search news stories."""
+   text = forms.CharField(max_length=30)
+   category = forms.ModelChoiceField(label='', 
+         required=False,
+         empty_label='(All Categories)',
+         queryset=Category.objects.all())
+
+   def get_query(self):
+      return self.cleaned_data['text']
+
+   def get_category(self):
+      cat = self.cleaned_data['category']
+      if cat:
+         return cat
+      return None
+
+
+class SendStoryForm(forms.Form):
+   """Form for sending a news story via email to a friend."""
+   friend_name = forms.CharField(label="Friend's Name", max_length=64)
+   friend_email = forms.EmailField(label="Friend's Email")
+
+   def email(self):
+      return self.cleaned_data['friend_email']
+      
+   def name(self):
+      return self.cleaned_data['friend_name']
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/news/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,87 @@
+"""
+Models for the news application.
+"""
+
+import datetime
+from django.db import models
+from django.contrib import auth
+from tagging.fields import TagField
+
+
+class Category(models.Model):
+   """News stories belong to categories"""
+   title = models.CharField(max_length = 64)
+   icon = models.ImageField(upload_to='news/categories/', blank=True)
+
+   def __unicode__(self):
+      return self.title
+
+   def num_stories(self):
+      return News.objects.filter(category = self.pk).count()
+
+   class Meta:
+      verbose_name_plural = 'Categories'
+      ordering = ('title', )
+
+
+class PendingStory(models.Model):
+   """Stories submitted by users are held pending admin approval"""
+   title = models.CharField(max_length=255)
+   submitter = models.ForeignKey(auth.models.User)
+   category = models.ForeignKey(Category)
+   short_text = models.TextField()
+   long_text = models.TextField(blank=True)
+   date_submitted = models.DateTimeField(auto_now_add=True, db_index=True)
+   allow_comments = models.BooleanField(default=True)
+   approved = models.BooleanField(default=False)
+   tags = TagField()
+
+   def save(self, force_insert = False, force_update = False):
+      if self.approved:
+         Story.objects.create(title=self.title,
+               submitter=self.submitter,
+               category=self.category,
+               short_text=self.short_text,
+               long_text=self.long_text,
+               allow_comments=self.allow_comments,
+               date_published=datetime.datetime.now(),
+               tags=self.tags)
+         self.delete()
+      else:
+         super(PendingStory, self).save(force_insert, force_update)
+
+   def __unicode__(self):
+      return self.title
+
+   class Meta:
+      ordering = ('-date_submitted', )
+      verbose_name_plural = 'Pending Stories'
+
+
+class Story(models.Model):
+   """Model for news stories"""
+   title = models.CharField(max_length=255)
+   submitter = models.ForeignKey(auth.models.User)
+   category = models.ForeignKey(Category)
+   short_text = models.TextField()
+   long_text = models.TextField(blank=True)
+   allow_comments = models.BooleanField(default=True)
+   date_published = models.DateTimeField(db_index=True)
+   tags = TagField()
+
+   @models.permalink
+   def get_absolute_url(self):
+      return ('news.views.story', [str(self.id)])
+
+   def __unicode__(self):
+      return self.title
+
+   class Meta:
+      ordering = ('-date_published', )
+      verbose_name_plural = 'Stories'
+
+   def can_comment_on(self):
+      now = datetime.datetime.now()
+      delta = now - self.date_published
+      return delta.days < 30
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/news/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,26 @@
+"""urls for the News application"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('news.views',
+   url(r'^page/(?P<page>\d+)/$', 'index', name='news-index_page'),
+   url(r'^archive/$', 'archive_index', name='news-archive_index'),
+   url(r'^archive/(?P<year>\d{4})/(?P<month>\d\d?)/page/(?P<page>\d+)/$', 
+      'archive', 
+      name='news-archive_page'),
+   (r'^categories/$', 'category_index'),
+   (r'^category/(?P<category>\d+)/page/(?P<page>\d+)/$', 'category'),
+   (r'^email/(\d+)/$', 'email_story'),
+   (r'^email/thanks/$', 'email_thanks'),
+   url(r'^search/page/(?P<page>\d+)/$', 'search', name='news-search_page'),
+   (r'^story/(\d+)/$', 'story'),
+   (r'^submit/$', 'submit'),
+   (r'^submit/thanks/$', 'submit_thanks'),
+   url(r'^tags/$', 'tags', name='news-tag_index'),
+   url(r'^tag/(?P<tag_name>[^/]+)/page/(?P<page>\d+)/$', 
+      'tag', 
+      name='news-tag_page'),
+)
+
+urlpatterns += patterns('django.views.generic.simple',
+   (r'^$', 'redirect_to', {'url': 'page/1/'}),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/news/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,274 @@
+"""
+Views for the News application.
+"""
+
+import datetime
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.template.loader import render_to_string
+from django.contrib import auth
+from django.http import HttpResponseRedirect
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import get_object_or_404
+from django.core.urlresolvers import reverse
+from django.db.models import Q
+from django.contrib.sites.models import Site
+from django.http import Http404
+
+from tagging.models import Tag
+from tagging.models import TaggedItem
+
+from core.functions import clean_html
+from core.functions import send_mail
+from core.functions import get_full_name
+from core.paginator import DiggPaginator
+from news.models import Category
+from news.models import PendingStory
+from news.models import Story
+from news.forms import AddNewsForm
+from news.forms import SearchNewsForm
+from news.forms import SendStoryForm
+
+NEWS_PER_PAGE = 2
+
+#######################################################################
+
+def create_paginator(stories):
+   return DiggPaginator(stories, NEWS_PER_PAGE, body=5, tail=3, margin=3, padding=2)
+
+#######################################################################
+
+def index(request, page=1):
+   stories = Story.objects.all()
+   paginator = create_paginator(stories)
+   try:
+      the_page = paginator.page(int(page))
+   except InvalidPage:
+      raise Http404
+
+   return render_to_response('news/index.html', {
+      'title': 'Main Index',
+      'page': the_page,
+      'search_form': SearchNewsForm(),
+      }, 
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def archive_index(request):
+   dates = Story.objects.dates('date_published', 'month', order='DESC')
+   return render_to_response('news/archive_index.html', {
+      'title': 'News Archive',
+      'dates': dates, 
+      'search_form': SearchNewsForm(),
+      }, 
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def archive(request, year, month, page=1):
+   stories = Story.objects.filter(date_published__year=year, date_published__month=month)
+   paginator = create_paginator(stories)
+   try:
+      the_page = paginator.page(int(page))
+   except InvalidPage:
+      raise Http404
+
+   month_name = datetime.date(int(year), int(month), 1).strftime('%B')
+
+   return render_to_response('news/index.html', {
+      'title': 'Archive for %s, %s' % (month_name, year),
+      'page': the_page,
+      'search_form': SearchNewsForm(),
+      }, 
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def category_index(request):
+   categories = Category.objects.all()
+   cat_list = []
+   for cat in categories:
+      cat_list.append((cat, cat.story_set.all()[:10]))
+
+   return render_to_response('news/category_index.html', {
+      'cat_list': cat_list, 
+      'search_form': SearchNewsForm(),
+      }, 
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def category(request, category, page=1):
+   category = get_object_or_404(Category, pk=category)
+   stories = Story.objects.filter(category=category)
+   paginator = create_paginator(stories)
+   try:
+      the_page = paginator.page(int(page))
+   except InvalidPage:
+      raise Http404
+
+   return render_to_response('news/index.html', {
+      'title': 'Category: ' + category.title,
+      'page': the_page,
+      'search_form': SearchNewsForm(),
+      }, 
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def search(request, page=1):
+   if request.method == 'POST':
+      form = SearchNewsForm(request.POST)
+      if form.is_valid():
+         query_text = form.get_query()
+         category = form.get_category()
+         page = 1
+      else:
+         return HttpResponseRedirect(reverse('news.views.index'))
+   else:
+      if 'query' in request.GET:
+         query_text = request.GET['query']
+         category = request.GET.get('category', None)
+      else:
+         return HttpResponseRedirect(reverse('news.views.index'))
+
+   if category is not None:
+      stories = Story.objects.filter(category=category)
+      cat_qual = ' in category "%s"' % category.title
+   else:
+      stories = Story.objects.all()
+      cat_qual = ''
+
+   stories = stories.filter(
+         Q(title__icontains=query_text) |
+         Q(short_text__icontains=query_text) |
+         Q(long_text__icontains=query_text)).order_by('-date_published')
+
+   paginator = create_paginator(stories)
+   try:
+      the_page = paginator.page(int(page))
+   except InvalidPage:
+      raise Http404
+
+   return render_to_response('news/index.html', {
+      'title': 'Search Results for "%s"%s' % (query_text, cat_qual),
+      'query': query_text,
+      'page': the_page,
+      'search_form': SearchNewsForm(),
+      }, 
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def story(request, story_id):
+   story = get_object_or_404(Story, pk=story_id)
+   return render_to_response('news/story.html', {
+      'story': story,
+      'search_form': SearchNewsForm(),
+      },
+      context_instance=RequestContext(request))
+
+#######################################################################
+
+@login_required
+def submit(request):
+   if request.method == "POST":
+      add_form = AddNewsForm(request.POST)
+      if add_form.is_valid():
+         pending_story = add_form.save(commit=False)
+         pending_story.submitter = request.user
+         pending_story.short_text = clean_html(pending_story.short_text)
+         pending_story.long_text = clean_html(pending_story.long_text)
+         pending_story.save()
+         return HttpResponseRedirect(reverse('news.views.submit_thanks'))
+   else:
+      add_form = AddNewsForm()
+
+   return render_to_response('news/submit_news.html', {
+      'add_form': add_form,
+      'search_form': SearchNewsForm(),
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def submit_thanks(request):
+   return render_to_response('news/submit_news.html', {
+      'search_form': SearchNewsForm(),
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def tags(request):
+   tags = Tag.objects.cloud_for_model(Story)
+   return render_to_response('news/tag_index.html', {
+      'tags': tags,
+      'search_form': SearchNewsForm(),
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def tag(request, tag_name, page=1):
+   tag = get_object_or_404(Tag, name=tag_name)
+   stories = TaggedItem.objects.get_by_model(Story, tag)
+   paginator = create_paginator(stories)
+   try:
+      the_page = paginator.page(int(page))
+   except InvalidPage:
+      raise Http404
+
+   return render_to_response('news/index.html', {
+      'title': 'Stories with tag: "%s"' % tag_name,
+      'page': the_page,
+      'search_form': SearchNewsForm(),
+      }, 
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def email_story(request, story_id):
+   story = get_object_or_404(Story, pk=story_id)
+   if request.method == 'POST':
+      send_form = SendStoryForm(request.POST)
+      if send_form.is_valid():
+         to_name = send_form.name()
+         to_email = send_form.email()
+         from_name = get_full_name(request.user)
+         from_email = request.user.email
+         site = Site.objects.get_current()
+
+         msg = render_to_string('news/send_story_email.txt',
+               {
+                  'to_name': to_name,
+                  'sender_name': from_name,
+                  'site_name' : site.name,
+                  'site_url' : site.domain,
+                  'story_title': story.title,
+                  'story_link': story.get_absolute_url(),
+               })
+
+         subject = 'Interesting Story at ' + site.name
+         send_mail(subject, msg, from_email, [to_email])
+         return HttpResponseRedirect(reverse('news.views.email_thanks'))
+   else:
+      send_form = SendStoryForm()
+
+   return render_to_response('news/send_story.html', {
+      'send_form': send_form,
+      'story': story,
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def email_thanks(request):
+   return render_to_response('news/send_story.html', {
+      },
+      context_instance = RequestContext(request))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/podcast/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,19 @@
+'''
+This file contains the automatic admin site definitions for the podcast models.
+'''
+
+from django.contrib import admin
+from podcast.models import Channel
+from podcast.models import Item
+
+
+class ItemInline(admin.StackedInline):
+   model = Item
+   extra = 1
+
+
+class ChannelAdmin(admin.ModelAdmin):
+   inlines = (ItemInline, )
+
+
+admin.site.register(Channel, ChannelAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/podcast/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,57 @@
+"""Models for the podcast application."""
+
+from django.db import models
+
+EXPLICIT_CHOICES = (
+      ('yes', 'Yes'),
+      ('no', 'No'),
+      ('clean', 'Clean'),
+   )
+
+
+class Channel(models.Model):
+   """Model to represent the Channel properties"""
+
+   title = models.CharField(max_length=255)
+   link = models.URLField(verify_exists=False)
+   language = models.CharField(max_length=16)
+   copyright = models.CharField(max_length=255)
+   subtitle = models.CharField(max_length=255)
+   author = models.CharField(max_length=64)
+   description = models.CharField(max_length=255)
+   owner_name = models.CharField(max_length=64)
+   owner_email = models.EmailField()
+   image = models.ImageField(upload_to='podcast')
+   category = models.CharField(max_length=64)
+   explicit = models.CharField(max_length=8, choices=EXPLICIT_CHOICES)
+
+   def __unicode__(self):
+      return self.title
+
+
+class Item(models.Model):
+   """Model to represent a channel item"""
+   channel = models.ForeignKey(Channel)
+   title = models.CharField(max_length=255)
+   author = models.CharField(max_length=255)
+   subtitle = models.CharField(max_length=255)
+   summary = models.TextField()
+   enclosure_url = models.URLField(verify_exists=False)
+   enclosure_length = models.IntegerField()
+   enclosure_type = models.CharField(max_length=32)
+   guid = models.CharField(max_length=255)
+   pubdate = models.DateTimeField()
+   duration = models.CharField(max_length=16)
+   keywords = models.CharField(max_length=255)
+   explicit = models.CharField(max_length=8, choices=EXPLICIT_CHOICES)
+
+   @models.permalink
+   def get_absolute_url(self):
+      return ('podcast.views.detail', [str(self.id)])
+
+   def __unicode__(self):
+      return self.title
+
+   class Meta:
+      ordering = ('-pubdate', )
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/podcast/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,8 @@
+"""urls for the podcast application"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('podcast.views',
+   url(r'^$', 'index', name='podcast-main'),
+   (r'^(\d+)/$', 'detail'),
+   (r'^feed.xml/$', 'feed'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/podcast/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,40 @@
+"""Views for the podcast application"""
+
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.shortcuts import get_object_or_404
+
+from podcast.models import Channel
+from podcast.models import Item
+
+
+def index(request):
+   try:
+      channel = Channel.objects.get(pk=1)
+   except Channel.DoesNotExist:
+      channel = None
+
+   return render_to_response('podcast/index.html', {
+      'channel': channel, 
+      },
+      context_instance = RequestContext(request))
+
+
+def detail(request, id):
+   podcast = get_object_or_404(Item, pk = id)
+   return render_to_response('podcast/detail.html', {
+      'channel': podcast.channel,
+      'podcast': podcast, 
+      },
+      context_instance = RequestContext(request))
+   
+
+def feed(request):
+   try:
+      channel = Channel.objects.get(pk=1)
+   except Channel.DoesNotExist:
+      channel = None
+   return render_to_response('podcast/feed.xml', {
+      'channel': channel, 
+      },
+      context_instance = RequestContext(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/polls/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,23 @@
+'''
+This file contains the automatic admin site definitions for the polls models.
+'''
+
+from django.contrib import admin
+from polls.models import Poll
+from polls.models import Choice
+
+
+class ChoiceInline(admin.TabularInline):
+   model = Choice
+   extra = 3
+
+
+class PollAdmin(admin.ModelAdmin):
+   list_display = ('question', 'start_date', 'end_date', 'is_enabled')
+   inlines = (ChoiceInline, )
+   list_filter = ('start_date', 'end_date')
+   search_fields = ('question', )
+   date_hierarchy = 'start_date'
+
+
+admin.site.register(Poll, PollAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/polls/forms.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,16 @@
+"""Forms for the Polls application."""
+
+from django import forms
+
+from polls.models import Choice
+
+
+class VoteForm(forms.Form):
+   """Form for voting in a poll."""
+   choices = forms.ModelChoiceField(label='', empty_label=None,
+         queryset=Choice.objects.none(), widget=forms.RadioSelect)
+
+   def __init__(self, poll, *args, **kwargs):
+      super(VoteForm, self).__init__(*args, **kwargs)
+      self.fields['choices'].queryset = poll.choice_set.all()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/polls/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,84 @@
+'''
+Models for the Polls application.
+'''
+
+import datetime
+from django.db import models
+from django.db.models import Q
+
+
+class PollManager(models.Manager):
+   """Manager for the Poll model"""
+
+   def get_current_polls(self):
+      now = datetime.datetime.now()
+      return self.filter(
+            Q(is_enabled=True),
+            Q(start_date__lte=now),
+            Q(end_date__isnull=True) | Q(end_date__gte=now))
+
+   def get_old_polls(self):
+      now = datetime.datetime.now()
+      return self.filter(
+            Q(is_enabled=True),
+            Q(end_date__isnull=False),
+            Q(end_date__lt=now))
+
+
+class Poll(models.Model):
+   """Model to represent polls"""
+   start_date = models.DateTimeField(db_index=True,
+         help_text='Date/time the poll will be eligible for voting.',)
+   end_date = models.DateTimeField(blank=True, null=True, db_index=True,
+         help_text='Date/time the poll will be ineligible for voting. '\
+            'Leave blank for an open ended poll.')
+   is_enabled = models.BooleanField(default=True, db_index=True,
+         help_text='Check to allow the poll to be viewed on the site.')
+   question = models.CharField(max_length=200)
+
+   objects = PollManager()
+
+   def __unicode__(self):
+      return self.question
+
+   class Meta:
+      ordering = ('-start_date', )
+      get_latest_by = 'start_date'
+
+   @models.permalink
+   def get_absolute_url(self):
+      return ('polls.views.poll_detail', [str(self.id)])
+
+   def results(self):
+      """
+      Returns a tuple; element 0 is the total votes, element 1 is a list of
+      {choice, votes, pct}
+      """
+      choices = []
+      total_votes = 0
+      for choice in self.choice_set.all():
+         total_votes += choice.votes
+         choices.append({'choice': choice.choice, 'votes': choice.votes, 'pct': 0.0})
+
+      if total_votes > 0:
+         for choice in choices:
+            choice['pct'] = float(choice['votes']) / total_votes * 100.0
+
+      return (total_votes, choices)
+
+   def is_open(self):
+      now = datetime.datetime.now()
+      return self.start_date <= now and (not self.end_date or now <= self.end_date)
+
+   def can_comment_on(self):
+      return self.is_open()
+
+
+class Choice(models.Model):
+   """Model for poll choices"""
+   poll = models.ForeignKey(Poll)
+   choice = models.CharField(max_length = 200)
+   votes = models.IntegerField(default = 0)
+
+   def __unicode__(self):
+      return self.choice
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/polls/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,9 @@
+"""urls for the polls application"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('polls.views',
+   url(r'^$', 'poll_index', name='polls-main'),
+   (r'^(?P<poll_id>\d+)/$', 'poll_detail'),
+   (r'^(?P<poll_id>\d+)/results/$', 'poll_results'),
+   (r'^(?P<poll_id>\d+)/vote/$', 'poll_vote'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/polls/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,77 @@
+"""Views for the polls application"""
+
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import get_object_or_404
+from django.http import Http404
+from django.http import HttpResponseRedirect
+from django.core.urlresolvers import reverse
+
+from polls.models import Poll
+from polls.models import Choice
+from polls.forms import VoteForm
+
+#######################################################################
+
+def poll_index(request):
+   current_polls = Poll.objects.get_current_polls()
+   old_polls = Poll.objects.get_old_polls()
+   return render_to_response('polls/index.html', {
+      'current_polls': current_polls, 
+      'old_polls': old_polls, 
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def poll_detail(request, poll_id):
+   poll = get_object_or_404(Poll, pk = poll_id)
+   if not poll.is_enabled:
+      raise Http404
+
+   return render_to_response('polls/poll.html', {
+      'poll': poll, 
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def poll_vote(request, poll_id):
+   poll = get_object_or_404(Poll, pk = poll_id)
+   if not poll.is_enabled:
+      raise Http404
+   if not poll.is_open():
+      return HttpResponseRedirect(reverse('polls.views.poll_results', args=[poll_id]))
+
+   if request.method == "POST":
+      vote_form = VoteForm(poll, request.POST)
+      if vote_form.is_valid():
+         choice_id = request.POST.get('choices', None)
+         choice = get_object_or_404(Choice, pk = choice_id)
+         choice.votes += 1
+         choice.save()
+         return HttpResponseRedirect(reverse('polls.views.poll_results', args=[poll_id]))
+   
+   vote_form = VoteForm(poll)
+
+   return render_to_response('polls/poll_vote.html', {
+      'poll': poll, 
+      'vote_form': vote_form,
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def poll_results(request, poll_id):
+   poll = get_object_or_404(Poll, pk = poll_id)
+   total_votes, choices = poll.results()
+   return render_to_response('polls/poll_results.html', {
+      'poll': poll, 
+      'total_votes': total_votes,
+      'choices': choices,
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/potd/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,19 @@
+"""
+This file contains the admin definitions for the POTD application.
+"""
+
+from django.contrib import admin
+from potd.models import Photo
+from potd.models import Current
+from potd.models import Sequence
+
+class PhotoAdmin(admin.ModelAdmin):
+    exclude = ('thumb', )
+    raw_id_fields = ('user', )
+
+class CurrentAdmin(admin.ModelAdmin):
+    raw_id_fields = ('potd', )
+
+admin.site.register(Photo, PhotoAdmin)
+admin.site.register(Current, CurrentAdmin)
+admin.site.register(Sequence)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/potd/management/commands/pick_potd.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,74 @@
+"""
+pick_potd is a custom manage.py command for the POTD application. 
+It is intended to be called from a cron job at midnight to pick the
+new POTD.
+"""
+
+import random
+from django.core.management.base import NoArgsCommand
+
+from potd.models import Current
+from potd.models import Sequence
+from potd.models import Photo
+
+def get_sequence():
+    try:
+        s = Sequence.objects.get(pk=1)
+        if s.seq:
+            return [int(x) for x in s.seq.split(',')]
+    except:
+        pass
+    return []
+
+def new_sequence():
+    the_ids = Photo.objects.values_list('id', flat=True).order_by('id')
+    ids = []
+    for id in the_ids.iterator():
+        ids.append(int(id))
+
+    random.shuffle(ids)
+    try:
+        s = Sequence.objects.get(pk=1)
+    except Sequence.DoesNotExist:
+        s = Sequence()
+
+    s.seq = ','.join([str(id) for id in ids])
+    s.save()
+    return ids
+
+class Command(NoArgsCommand):
+    help = "Chooses the next POTD. Run this command at midnight to update the POTD."
+    #requires_model_validation = False
+
+    def handle_noargs(self, **options):
+        try:
+            c = Current.objects.get(pk=1)
+            current = c.potd.pk
+        except Current.DoesNotExist:
+            c = Current()
+            current = None
+
+        seq = get_sequence()
+        if current is None or len(seq) == 0 or current == seq[-1]:
+            # time to generate a new random sequence
+            seq = new_sequence()
+            # set current to the first one in the sequence
+            if len(seq) > 0:
+                try:
+                    c.potd = Photo.objects.get(pk=seq[0])
+                    c.potd.potd_count += 1
+                    c.potd.save()
+                    c.save()
+                except:
+                    pass
+        else:
+            # find current in the sequence, pick the next one
+            try:
+                i = seq.index(current)
+                c.potd = Photo.objects.get(pk=seq[i + 1])
+                c.potd.potd_count += 1
+                c.potd.save()
+                c.save()
+            except:
+                pass
+                    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/potd/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,136 @@
+"""
+Models for the Photo Of The Day (POTD) application.
+"""
+import os
+from PIL import ImageFile
+from PIL import Image
+try:
+   from cStringIO import StringIO
+except:
+   from StringIO import StringIO
+
+from django.db import models
+from django.contrib.auth.models import User
+from django.core.files.base import ContentFile
+
+POTD_THUMB_WIDTH = 120
+
+def scale_image(image):
+    (w, h) = image.size
+    if w <= POTD_THUMB_WIDTH:
+        return image
+    scale_factor = float(POTD_THUMB_WIDTH) / w
+    new_height = int(scale_factor * h)
+    return image.resize((POTD_THUMB_WIDTH, new_height), Image.ANTIALIAS)
+
+
+class Photo(models.Model):
+    """Model to represent a POTD"""
+    photo = models.ImageField(upload_to='potd/%Y/%m/%d')
+    thumb = models.ImageField(upload_to='potd/%Y/%m/%d/thumbs', blank=True, null=True)
+    caption = models.CharField(max_length=128)
+    description = models.TextField()
+    user = models.ForeignKey(User)
+    date_added = models.DateField(auto_now_add=True)
+    potd_count = models.IntegerField(default=0)
+
+    def __unicode__(self):
+        return u'%s (%s)' % (self.caption, self.pk)
+
+    class Meta:
+        ordering = ('-date_added', '-caption')
+
+    def save(self, force_insert=False, force_update=False):
+
+        if self.thumb:
+            self.thumb.delete(save=False)
+
+        parser = ImageFile.Parser()
+        for chunk in self.photo.chunks():
+            parser.feed(chunk)
+        image = parser.close()
+        format = image.format
+        image = scale_image(image)
+        s = StringIO()
+        image.save(s, format)
+        thumb_name = os.path.basename(self.photo.path)
+        self.thumb.save(thumb_name, ContentFile(s.getvalue()), save=False)
+        
+        super(Photo, self).save(force_insert, force_update)
+        Sequence.objects.insert_photo(self.pk)
+
+    def delete(self):
+        Sequence.objects.remove_photo(self.pk)
+        super(Photo, self).delete()
+
+    def can_comment_on(self):
+        return Current.objects.get_current_id() == self.id
+
+
+class CurrentManager(models.Manager):
+    def get_current_photo(self):
+        try:
+            c = self.get(pk=1)
+            return c.potd
+        except Current.DoesNotExist:
+            return None
+
+    def get_current_id(self):
+        potd = self.get_current_photo()
+        if potd is not None:
+            return potd.pk
+        return None
+
+
+class Current(models.Model):
+    """This model simply stores the current POTD."""
+    potd = models.ForeignKey(Photo)
+
+    objects = CurrentManager()
+
+    def __unicode__(self):
+        return self.potd.__unicode__()
+
+    class Meta:
+        verbose_name_plural = 'Current'
+
+
+class SequenceManager(models.Manager):
+    def insert_photo(self, photo_id):
+        current = Current.objects.get_current_id()
+        if current is not None:
+            try:
+                s = self.get(pk=1)
+                seq = [int(x) for x in s.seq.split(',')]
+                if photo_id not in seq:
+                    i = seq.index(current)
+                    seq.insert(i + 1, photo_id)
+                    s.seq = ','.join([str(x) for x in seq])
+                    s.save()
+            except:
+                pass
+
+    def remove_photo(self, photo_id):
+        try:
+            s = self.get(pk=1)
+            seq = [int(x) for x in s.seq.split(',')]
+            if photo_id in seq:
+                seq.remove(photo_id)
+                s.seq = ','.join([str(x) for x in seq])
+                s.save()
+        except:
+            pass
+
+
+class Sequence(models.Model):
+    """This model stores the sequence of photos for the POTD."""
+    seq = models.CommaSeparatedIntegerField(max_length=4096)
+
+    objects = SequenceManager()
+
+    def __unicode__(self):
+        return self.seq
+
+    class Meta:
+        verbose_name_plural = 'Sequence'
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/potd/templatetags/potd_tags.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,14 @@
+"""
+Template tags for the POTD application. 
+"""
+from django import template
+from potd.models import Current
+
+register = template.Library()
+
+@register.inclusion_tag('potd/potd_block.html')
+def photo_of_the_day():
+   potd = Current.objects.get_current_photo()
+   return {
+      'potd': potd,
+   }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/potd/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,8 @@
+"""
+URLs for the POTD application.
+"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('potd.views',
+    url(r'^$', 'view', name='potd-view'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/potd/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,16 @@
+"""
+Views for the POTD application.
+"""
+
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
+from potd.models import Current
+
+
+def view(request):
+    potd = Current.objects.get_current_photo()
+    return render_to_response('potd/view.html', {
+        'potd': potd,
+        },
+        context_instance = RequestContext(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/settings.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,142 @@
+# Django settings for gpp project.
+
+import os
+import platform
+import local_settings
+project_path = os.path.abspath(os.path.split(__file__)[0])
+
+DEBUG = local_settings.DEBUG
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+    ('Brian Neal', 'admin@surfguitar101.com'),
+)
+
+AUTH_PROFILE_MODULE = 'bio.userprofile'
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = local_settings.DATABASE_ENGINE
+DATABASE_NAME = local_settings.DATABASE_NAME
+DATABASE_USER = local_settings.DATABASE_USER
+DATABASE_PASSWORD = local_settings.DATABASE_PASSWORD
+DATABASE_HOST = local_settings.DATABASE_HOST
+DATABASE_PORT = local_settings.DATABASE_PORT
+
+INTERNAL_IPS = local_settings.INTERNAL_IPS
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = local_settings.TIME_ZONE
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = local_settings.SITE_ID
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = False
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = local_settings.MEDIA_ROOT
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+# Examples: "http://media.lawrence.com", "http://example.com/media/"
+MEDIA_URL = local_settings.MEDIA_URL
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = local_settings.ADMIN_MEDIA_PREFIX
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = local_settings.SECRET_KEY
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+    'django.template.loaders.filesystem.load_template_source',
+    'django.template.loaders.app_directories.load_template_source',
+#     'django.template.loaders.eggs.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+    'django.middleware.common.CommonMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+)
+
+ROOT_URLCONF = 'gpp.urls'
+
+TEMPLATE_DIRS = (
+    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+    # Always use forward slashes, even on Windows.
+    # Don't forget to use absolute paths, not relative paths.
+    os.path.join(project_path, 'templates'),
+)
+
+TEMPLATE_CONTEXT_PROCESSORS = (
+   "django.core.context_processors.auth",
+   "django.core.context_processors.debug",
+   "django.core.context_processors.request",
+   "django.core.context_processors.media"
+)
+
+INSTALLED_APPS = (
+    'django.contrib.admin',
+    'django.contrib.admindocs',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.sites',
+    'django.contrib.markup',
+    'tagging',
+    'accounts',
+    'bio',
+    'bulletins',
+    'comments',
+    'contact',
+    'core',
+    'downloads',
+    'gcalendar',
+    'irc',
+    'legal',
+    'membermap',
+    'messages',
+    'news',
+    'podcast',
+    'polls',
+    'potd',
+    'shoutbox',
+    'smiley',
+    'weblinks',
+)
+
+LOGIN_URL = '/accounts/login/'
+LOGIN_REDIRECT_URL = '/profile/me/'
+LOGOUT_URL = '/accounts/logout/'
+
+FILE_UPLOAD_PERMISSIONS = 0640
+
+#######################################################################
+# Tagging Specific Settings
+#######################################################################
+FORCE_LOWERCASE_TAGS = True
+MAX_TAG_LENGTH = 50
+
+#######################################################################
+# GPP Specific Settings
+#######################################################################
+GPP_LOG_LEVEL = 0
+GPP_SEND_EMAIL = False
+GPP_NO_REPLY_EMAIL = 'no_reply'
+AVATAR_DIR = 'avatars'
+MAX_AVATAR_SIZE_BYTES = 2 * 1024 * 1024
+MAX_AVATAR_SIZE_PIXELS = 100
+AVATAR_DEFAULT_URL = MEDIA_URL + AVATAR_DIR + '/default.png'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/shoutbox/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,11 @@
+"""
+This file contains the automatic admin site definitions for the shoutbox models.
+"""
+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)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/shoutbox/forms.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,24 @@
+"""
+Forms for the Shoutbox application.
+"""
+
+import re
+from django import forms
+
+url_re = re.compile('('
+   r'^https?://' # http:// or https://
+   r'(?:(?:[A-Z0-9-]+\.)+[A-Z]{2,6}|' #domain...
+   r'localhost|' #localhost...
+   r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip
+   r'(?::\d+)?' # optional port
+   r'(?:/?|/\S+))', re.IGNORECASE)
+
+
+class ShoutBoxForm(forms.Form):
+   msg = forms.CharField(label='', max_length=2048, required=True)
+
+   def get_shout(self):
+      msg = self.cleaned_data['msg']
+      msg = re.sub(url_re, r'<a href="\1">URL</a>', msg)
+      return msg
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/shoutbox/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,17 @@
+"""
+Models for the shoutbox application.
+"""
+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', )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/shoutbox/templatetags/shoutbox_tags.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,15 @@
+"""
+Template tags for the shoutbox application. 
+"""
+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.select_related('user')[:10]
+   return {
+      'shouts': shouts,
+      'user': context['user'],
+   }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/shoutbox/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,13 @@
+"""
+Urls for the Shoutbox application.
+"""
+
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('shoutbox.views',
+    url(r'^delete/$', 'delete', name='shoutbox-delete'),
+    url(r'^edit/$', 'edit', name='shoutbox-edit'),
+    url(r'^shout/$', 'shout', name='shoutbox-shout'),
+    url(r'^text/$', 'text', name='shoutbox-text'),
+    url(r'^view/(?P<page>\d+)/$', 'view', name='shoutbox-view'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/shoutbox/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,103 @@
+"""
+Views for the Shoutbox application.
+"""
+
+import re
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.http import HttpResponse
+from django.http import HttpResponseBadRequest
+from django.http import HttpResponseForbidden
+from django.http import HttpResponseRedirect
+from django.contrib.auth.decorators import login_required
+
+from core.paginator import DiggPaginator
+from shoutbox.forms import ShoutBoxForm
+from shoutbox.models import Shout
+
+SHOUTS_PER_PAGE = 10
+
+@login_required
+def shout(request):
+    if request.method == 'POST':
+        msg = request.POST.get('msg', '').strip()
+        if msg != '':
+            shout = Shout(user=request.user, shout=msg)
+            shout.save()
+            
+    return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
+
+
+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))
+   
+
+shout_id_re = re.compile(r'shout-(\d+)')
+
+def text(request):
+    """This view function retrieves the text of a shout; it is used in the in-place
+    editing of shouts on the shoutbox history view."""
+    if request.user.is_authenticated():
+        m = shout_id_re.match(request.GET.get('id', ''))
+        if m is None:
+            return HttpResponseBadRequest()
+        try:
+            shout = Shout.objects.get(pk=m.group(1))
+        except Shout.DoesNotExist:
+            return HttpResponseBadRequest()
+        return HttpResponse(shout.shout)
+
+    return HttpResponseForbidden()
+
+
+def edit(request):
+    """This view accepts a shoutbox edit from the shoutbox history view."""
+    if request.user.is_authenticated():
+        m = shout_id_re.match(request.POST.get('id', ''))
+        if m is None:
+            return HttpResponseBadRequest()
+        try:
+            shout = Shout.objects.get(pk=m.group(1))
+        except Shout.DoesNotExist:
+            return HttpResponseBadRequest()
+        if request.user != shout.user:
+            return HttpResponseForbidden()
+        new_shout = request.POST.get('value', '').strip()
+        if new_shout == '':
+            return HttpResponseBadRequest()
+        shout.shout = new_shout
+        shout.save()
+        return render_to_response('shoutbox/render_shout.html', {
+            'shout': shout,
+            },
+            context_instance = RequestContext(request))
+
+    return HttpResponseForbidden()
+
+
+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 or not id.isdigit():
+            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()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/smiley/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,11 @@
+"""
+This file contains the automatic admin site definitions for the Smiley models.
+"""
+
+from django.contrib import admin
+from smiley.models import Smiley
+
+class SmileyAdmin(admin.ModelAdmin):
+    list_display = ('title', 'code', 'html')
+
+admin.site.register(Smiley, SmileyAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/smiley/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,52 @@
+"""
+Models for the smiley application.
+"""
+from django.db import models
+
+
+class SmileyManager(models.Manager):
+    smiley_map = None
+    smilies = None
+
+    def get_smiley_map(self):
+        if self.smiley_map is None:
+            smilies = self.all()
+            self.smiley_map = {}
+            for s in smilies:
+                self.smiley_map[s.code] = s.html()
+        return self.smiley_map
+
+    def get_smilies(self):
+        if self.smilies is None:
+            self.smilies = self.all()
+        return self.smilies
+
+    def clear_cache(self):
+        self.smiley_map = None
+        self.smileis = None
+
+
+class Smiley(models.Model):
+    image = models.ImageField(upload_to='smiley/images/')
+    title = models.CharField(max_length=32)
+    code = models.CharField(max_length=32)
+
+    objects = SmileyManager()
+
+    class Meta:
+        verbose_name_plural = 'Smilies'
+        ordering = ('title', )
+
+    def __unicode__(self):
+        return self.title
+
+    def get_absolute_url(self):
+        return self.image.url
+
+    def html(self):
+        if self.image:
+            return u'<img src="%s" alt="%s" title="%s" />' % \
+                    (self.get_absolute_url(), self.title, self.title)
+        return u''
+    html.allow_tags = True
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/smiley/templatetags/smiley_tags.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,42 @@
+"""
+Template tags for the smiley application. 
+"""
+import re
+from django import template
+from django.template.defaultfilters import stringfilter
+from django.utils.html import conditional_escape
+from django.utils.safestring import mark_safe
+from django.utils.safestring import SafeData
+
+from smiley.models import Smiley
+
+register = template.Library()
+
+word_split_re = re.compile(r'(\s+)')
+
+@register.filter
+@stringfilter
+def smilify(value, autoescape=False):
+    """A filter to "smilify" text by replacing text with HTML img tags of smilies."""
+    if not autoescape or isinstance(value, SafeData):
+        esc = lambda x: x
+    else:
+        esc = conditional_escape
+
+    smiley_map = Smiley.objects.get_smiley_map()
+
+    words = word_split_re.split(value)
+    for i, word in enumerate(words):
+        if word in smiley_map:
+            words[i] = smiley_map[word]
+        else:
+            words[i] = esc(words[i])
+    return mark_safe(u''.join(words))
+smilify.needs_autoescape = True
+
+
+@register.inclusion_tag('smiley/smiley_farm.html')
+def smiley_farm():
+    """An inclusion tag that displays all of the smilies in clickable form."""
+    return {'smilies': Smiley.objects.get_smilies(), }
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/smiley/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,1 @@
+# Create your views here.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/404.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head><title>Page Not Found</title>
+</head>
+<body>
+
+   <h1>Not Found</h1>
+
+   <p>The requested URL {{ request.path|escape }} was not found on this server.</p>
+
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/500.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head><title>Internal Server Error</title>
+</head>
+<body>
+
+   <h1>Internal Server Error</h1>
+
+   <p>We're sorry, that page is currently unavailable due to a server misconfiguration.</p>
+   <p>The server administrator has been notified, and we apologise for any inconvenience.</p>
+
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/accounts/login.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,20 @@
+{% extends 'base.html' %}
+{% block title %}Login{% endblock %}
+{% block content %}
+<h2>Login</h2>
+{% if form.errors %}
+<p>Your username and password didn't match. Please try again.</p>
+{% endif %}
+
+<form method="post" action=".">
+<table>
+<tr><td>{{ form.username.label_tag }}:</td><td>{{ form.username }}</td></tr>
+<tr><td>{{ form.password.label_tag }}:</td><td>{{ form.password }}</td></tr>
+<tr><td>&nbsp;</td><td><input type="submit" value="Login" />
+      <input type="hidden" name="next" value="{{ next }}" /></td></tr>
+</table>
+</form>
+<p>Forgot your password? You can reset it <a href="{% url accounts.views.register %}">here</a>.</p>
+<p>Don't have an account? Why don't you <a href="{% url accounts.views.register %}">register</a>?</p>
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/accounts/logout.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+{% block title %}Logged Out{% endblock %}
+{% block content %}
+<h2>Logged Out</h2>
+<p>You are now logged out of SurfGuitar101.com. Thanks for spending some quality time with us today. Tell all your
+friends about us and we hope we see you soon!</p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/accounts/password_change.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,12 @@
+{% extends 'base.html' %}
+{% block title %}Change Password{% endblock %}
+{% block content %}
+<h2>Change Password</h2>
+<form method="post" action=".">
+<table>
+{{ form.as_table }}
+<tr><td>&nbsp;</td><td><input type="submit" value="Change Password" />
+      <input type="button" value="Cancel" onclick="history.back(); return true;" /></td></tr>
+</table>
+</form>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/accounts/register.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,22 @@
+{% extends 'base.html' %}
+{% block title %}New User Registration{% endblock %}
+{% block content %}
+<h2>New User Registration</h2>
+<p>Thank you for your interest in become a member of our community. Please keep the following in mind when
+registering for your account here:</p>
+<ul>
+   <li>Your username must be 30 characters or less, please use letters, digits, and underscores
+   only.</li>
+   <li>An email address is required to use this site. A confirmation email will be sent to the
+   address you supply, and it is necessary to complete the registration process.</li>
+   <li>You must agree to our <a href="{% url legal.views.view "tos" %}" target="_blank">Terms of Service</a>.</li>
+   <li>You must agree to our <a href="{% url legal.views.view "privacy" %}" target="_blank">Privacy Policy</a>.</li>
+</ul>
+<form action="." method="post">
+<table>
+   {{ form.as_table }}
+   <tr><td>&nbsp;</td><td><input type="submit" value="Submit" />
+         <input type="button" value="Cancel" onclick="history.back(); return true;" /></td></tr>
+</table>
+</form>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/accounts/register_failure.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,9 @@
+{% extends 'base.html' %}
+{% block title %}Registration Error{% endblock %}
+{% block content %}
+<h2>Registration Error</h2>
+<p>We're sorry, but we don't have any registration information available for the user {{ username }}. Registration
+information is only good for 24 hours, and it may have expired. If you think this may have happened, please
+<a href="{% url accounts.views.register %}">register again</a>. Sorry for the inconvenience.</p>
+<p>If you keep seeing this message, please contact the site staff for assistance.</p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/accounts/register_success.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,8 @@
+{% extends 'base.html' %}
+{% block title %}Registration Confirmed{% endblock %}
+{% block content %}
+<h2>Congratulations, Your Account Has Been Created</h2>
+<p>Your registration of the user <strong>{{ username }}</strong> has been successful. Welcome to the site!<p>
+<p>Please proceed to the <a href="{% url django.contrib.auth.views.login %}">login page</a> to log into the site
+with your username <strong>{{ username }}</strong> and the password you registered with.</p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/accounts/register_thanks.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,8 @@
+{% extends 'base.html' %}
+{% block title %}Registration Complete{% endblock %}
+{% block content %}
+<h2>Thanks for Registering!</h2>
+<p>A confirmation email has just been sent to the email address you provided. To complete
+the registration process, please follow the instructions in the confirmation email.</p>
+<p>If you do not receive the email, please check any spam folders.</p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/accounts/registration_email.txt	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,20 @@
+Hello,
+
+Welcome to {{ site_name }}!
+
+We have received a request from the email address {{ user_email }} to register an account at {{ site_domain }}.
+
+To finish the registration process, please visit the link below to activate your account. The link will expire in 24 hours, after which you will have to register again.
+
+{{ activation_link }}
+
+Here is the account information:
+
+Username: {{ username }}
+Password: {{ raw_password }}
+
+If you did not register with {{ site_domain }}, simply ignore this email. If you have questions or problems, please send an email to {{ admin_email }}.
+
+Regards,
+
+The staff at {{ site_name }}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/admin/base_site.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,10 @@
+{% extends "admin/base.html" %}
+{% load i18n %}
+
+{% block title %}{{ title }} | {% trans 'GPP Site Admin' %}{% endblock %}
+
+{% block branding %}
+<h1 id="site-name">{% trans 'Gremmies Portal Project Site Administration' %}</h1>
+{% endblock %}
+
+{% block nav-global %}{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/admin/gcalendar/change_list.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,8 @@
+{% extends "admin/change_list.html" %}
+{% block object-tools %}
+{% if has_add_permission %}
+<ul class="object-tools"><li><a href="add/{% if is_popup %}?_popup=1{% endif %}" class="addlink">Add {{ name }}</a></li>
+<li><a href="google_sync/">Google Sync</a></li>
+</ul>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/base.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+{% load shoutbox_tags %}
+{% load irc_tags %}
+{% load potd_tags %}
+{% load messages_tags %}
+<head><title>SurfGuitar101.com | {% block title %}{% endblock %}</title>
+<meta http-equiv="Content-Type" content="text/html" />
+<meta http-equiv="Content-Language" content="en-US" />
+<meta name="robots" content="all" />
+<meta name="Author" content="Brian Neal" />
+<meta name="copyright" content="&copy; 2009 Brian Neal" />
+<link rel="stylesheet" href="{{ MEDIA_URL }}css/blueprint/screen.css" type="text/css" media="screen, projection" />
+<link rel="stylesheet" href="{{ MEDIA_URL }}css/blueprint/print.css" type="text/css" media="print" /> 
+<!--[if IE]>
+<link rel="stylesheet" href="{{ MEDIA_URL }}css/blueprint/ie.css" type="text/css" media="screen, projection" />
+<![endif]-->
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/base.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/shoutbox.css" />
+<!--<script type="text/javascript" src="{{ MEDIA_URL }}js/shoutbox.js"></script>-->
+{% block custom_head %}{% endblock %}
+{% block custom_css %}{% endblock %}
+{% block custom_js %}{% endblock %}
+<!-- <link rel="shortcut icon" type="image/vnd.microsoft.com" href="{{ MEDIA_URL }}images/favicon.ico" /> -->
+</head>
+<body>
+<div id="page" class="container">
+<div id="header" class="span-24">
+   <h1>SurfGuitar101</h1>
+   <p>Header content</p>
+      <ul>
+         {% if user.is_authenticated %}
+         <li>{% unread_messages user %}</li>
+         <li><a href="{% url bio-me %}">My Profile</a></li>
+         <li><a href="{% url accounts-logout %}">Logout</a></li>
+         {% else %}
+         <li><a href="{% url accounts-login %}">Login</a></li>
+         <li><a href="{% url accounts-register %}">Register</a></li>
+         {% endif %}
+      </ul> 
+</div>
+
+<div id="content-secondary" class="span-4 append-1">
+   <ul class="nav-left">
+      <li><a href="{% url news-index_page page=1 %}">Home</a></li>
+      <li><a href="{% url news-index_page page=1 %}">News</a></li>
+      <li><a href="{% url gcalendar-index %}">Calendar</a></li>
+      <li><a href="{% url contact-form %}">Contact</a></li>
+      <li><a href="{% url irc-main %}">IRC</a></li>
+      <li><a href="{% url bio-members %}">Member List</a></li>
+      <li><a href="{% url membermap-index %}">Member Map</a></li>
+      <li><a href="{% url messages-inbox %}">Private Messages</a></li>
+      <li><a href="{% url podcast-main %}">Podcast</a></li>
+      <li><a href="{% url polls-main %}">Polls</a></li>
+      <li><a href="{% url potd-view %}">Photo of the Day</a></li>
+      <li><a href="{% url weblinks-main %}">Links</a></li>
+      <li><a href="{% url downloads-index %}">Downloads</a></li>
+   </ul>
+   {% photo_of_the_day %}
+   {% shoutbox %}
+   {% irc_status %}
+</div>
+
+<div id="content-primary" class="span-19 last">
+   {% block content %}
+   {% endblock %}
+</div>
+
+
+<div id="footer" class="span-24">
+   <p>Website &copy; 2008 by Brian Neal</p>
+</div>
+
+{% if debug %}
+<div id="debug" class="span-24">
+<ol>
+{% for s in sql_queries %}
+<li>{{ s.sql }} : <b>({{ s.time }})</b></li>
+{% endfor %}
+</ol>
+</div>
+{% endif %}
+
+</div>
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/bio/avatar.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,23 @@
+{% extends 'bio/base.html' %}
+{% load avatar_tags %}
+{% block title %}Change My Avatar{% endblock %}
+{% block content %}
+<h2>Change My Avatar</h2>
+   <p>This is your current avatar:</p>
+   <p>{% avatar user %}</p>
+   <p>
+   To change your avatar, upload a file using the form, below. You may leave the
+   form blank to reset your avatar to the default.
+   </p>
+   <p>
+   Your file must be a recognizable graphic file, such as jpeg, gif, png, etc. 
+   The maximum size of an avatar is 100 x 100 pixels. If your uploaded file is larger than 
+   this it will be scaled down to 100 x 100 pixels. For best results, please ensure your
+   image is square.
+   </p>
+   <form enctype="multipart/form-data" method="POST" action=".">
+      {{ form.as_p }}
+      <input type="submit" value="Update Avatar" />
+      <input type="button" value="Cancel" onclick="history.back(); return true;" />
+   </form>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/bio/base.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,7 @@
+{% extends 'base.html' %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/bio.css" />
+{% block bio_css %}{% endblock %}
+{% endblock %}
+{% block content %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/bio/edit_profile.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,26 @@
+{% extends 'bio/base.html' %}
+{% load avatar_tags %}
+{% block title %}Edit Profile{% endblock %}
+{% block custom_js %}
+   {{ profile_form.media }}
+{% endblock %}
+{% block content %}
+<div class="user_profile">
+<h2>Edit Profile for {{ user.username }}</h2>
+<form action="{% url bio-edit_profile %}" method="post">
+<table>
+   <tr>
+      <td>
+         <a href="{% url bio-change_avatar %}"><img src="{{ MEDIA_URL }}icons/image_edit.png" alt="Change Avatar" /></a>
+         <a href="{% url bio-change_avatar %}">Change Avatar</a></td>
+      <td>{% avatar user %}</td>
+   </tr>
+   {{ user_form.as_table }}
+   {{ profile_form.as_table }}
+   <tr><td>&nbsp;</td><td><input type="submit" name="submit_button" value="Save" />
+         <input type="submit" name="submit_button" value="Cancel" /></td></tr>
+</table>
+</form>
+</div>
+<br />
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/bio/markdown.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,3 @@
+{% load markup %}
+{% load smiley_tags %}
+{{ data|markdown:"safe"|smilify }}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/bio/members.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,50 @@
+{% extends 'bio/base.html' %}
+{% load avatar_tags %}
+{% block title %}Member List{% endblock %}
+{% block bio_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/tab-nav.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/pagination.css" />
+{% endblock %}
+{% block content %}
+<h2>Member List</h2>
+
+{% if page.object_list %}
+<ul class="tab-nav">
+   <li><a href="{% url bio-members_full type="user",page="1" %}"
+      {% ifequal type "user" %}class="active" {% endifequal %}>User</a></li>
+   <li><a href="{% url bio-members_full type="date",page="1" %}"
+      {% ifequal type "date" %}class="active" {% endifequal %}>Date</a></li>
+</ul>
+
+{% include 'core/pagination.html' %}
+<div class="members-list">
+<table>
+<tr>
+<th>Avatar</th>
+<th>Username</th>
+<th>Full Name</th>
+<th>Location</th>
+<th>Date Joined</th>
+<th>Contact</th>
+</tr>
+{% for u in page.object_list %}
+<tr class="{% cycle 'even' 'odd' %}">
+   <td><a href="{% url bio-view_profile username=u.username %}">{% avatar u %}</a></td>
+   <td><a href="{% url bio-view_profile username=u.username %}" title="View profile for {{ u.username }}">{{ u.username }}</a></td>
+   <td>{{ u.get_full_name }}</td>
+   <td>{{ u.get_profile.location }}</td>
+   <td>{{ u.date_joined|date:"M. d, Y" }}</td>
+   <td>
+      {% ifnotequal user u %}<a href="{% url messages-compose_to u.username %}">
+         <img src="{{ MEDIA_URL }}icons/note.png" alt="PM" title="Send private message" /></a>{% endifnotequal %}
+      {% if not u.get_profile.hide_email %}<a href="mailto:{{ u.email }}">
+         <img src="{{ MEDIA_URL }}icons/email.png" alt="Email" title="Send Email" /></a>{% endif %}
+   </td>
+</tr>
+{% endfor %}
+</table>
+</div>
+
+{% include 'core/pagination.html' %}
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/bio/view_profile.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,79 @@
+{% extends 'bio/base.html' %}
+{% load avatar_tags %}
+{% block title %}User Profile for {{ subject.username }}{% endblock %}
+{% block content %}
+<div class="user_profile">
+   <h2>User Profile for {{ subject.username }}</h2>
+   <p>{% avatar subject %}</p>
+<table>
+   <tr><th>Full Name</th><td>{{ subject.get_full_name }}</td></tr>
+   <tr><th>Date Joined</th><td>{{ subject.date_joined|date:"F d, Y" }}</td></tr>
+   <tr><th>Last Login</th><td>{{ subject.last_login|date:"F d, Y @ H:i" }}</td></tr>
+   <tr><th>Active Member</th><td>{{ subject.is_active|yesno:"Yes,No" }}</td></tr>
+   <tr><th>Staff Member</th><td>{{ subject.is_staff|yesno:"Yes,No" }}</td></tr>
+   {% if profile.location %}
+   <tr><th>Location</th><td>{{ profile.location }}</td></tr>
+   {% endif %}
+   {% if profile.occupation %}
+   <tr><th>Occupation</th><td>{{ profile.occupation }}</td></tr>
+   {% endif %}
+   {% if profile.birthday %}
+   <tr><th>Birthday</th><td>{{ profile.birthday|date:"F d" }}</td></tr>
+   {% endif %}
+   {% if profile.interests %}
+   <tr><th>Interests</th><td>{{ profile.interests }}</td></tr>
+   {% endif %}
+   {% if profile.website_1 %}
+   <tr><th>Website 1</th><td><a href="{{ profile.website_1 }}">{{ profile.website_1 }}</a></td></tr>
+   {% endif %}
+   {% if profile.website_2 %}
+   <tr><th>Website 2</th><td><a href="{{ profile.website_2 }}">{{ profile.website_2 }}</a></td></tr>
+   {% endif %}
+   {% if profile.website_3 %}
+   <tr><th>Website 3</th><td><a href="{{ profile.website_3 }}">{{ profile.website_3 }}</a></td></tr>
+   {% endif %}
+   {% if not profile.hide_email %}
+   <tr><th>Email</th><td>{{ subject.email }}</td></tr>
+   {% endif %}
+   {% if profile.icq %}
+   <tr><th>ICQ</th><td>{{ profile.icq }}</td></tr>
+   {% endif %}
+   {% if profile.aim %}
+   <tr><th>AIM</th><td>{{ profile.aim }}</td></tr>
+   {% endif %}
+   {% if profile.yim %}
+   <tr><th>YIM</th><td>{{ profile.yim }}</td></tr>
+   {% endif %}
+   {% if profile.msnm %}
+   <tr><th>MSN</th><td>{{ profile.msnm }}</td></tr>
+   {% endif %}
+   {% if profile.twitter %}
+   <tr><th>Twitter</th><td><a href="{{ profile.twitter }}">{{ profile.twitter }}</a></td></tr>
+   {% endif %}
+   {% if profile.profile_html %}
+   <tr><th>Profile</th><td>{{ profile.profile_html|safe }}</td></tr>
+   {% endif %}
+   {% if profile.signature_html %}
+   <tr><th>Signature</th><td>{{ profile.signature_html|safe }}</td></tr>
+   {% endif %}
+</table>
+</div>
+{% if this_is_me %}
+<ul>
+   <li><a href="{% url bio-edit_profile %}"><img src="{{ MEDIA_URL }}icons/application_edit.png" alt="Edit Profile" /></a>
+   <a href="{% url bio-edit_profile %}">Edit Profile</a></li>
+   <li><a href="{% url bio-change_avatar %}"><img src="{{ MEDIA_URL }}icons/image_edit.png" alt="Change Avatar" /></a>
+      <a href="{% url bio-change_avatar %}">Change Avatar</a></li>
+   <li><a href="{% url django.contrib.auth.views.password_change %}"><img src="{{ MEDIA_URL }}icons/key.png" alt="Change Password" /></a>
+      <a href="{% url django.contrib.auth.views.password_change %}">Change Password</a></li>
+</ul>
+{% else %}
+{% if user.is_authenticated %}
+<p>
+<a href="{% url messages-compose_to subject.username %}">
+   <img src="{{ MEDIA_URL }}icons/note.png" alt="PM" title="Send Private Message" /></a>
+<a href="{% url messages-compose_to subject.username %}">Send a private message to {{ subject.username }}</a>
+</p>
+{% endif %}
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/comments/comment.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,25 @@
+{% load avatar_tags %}
+{% load markup %}
+{% load smiley_tags %}
+<li>
+<div class="comment">
+<div class="comment-avatar">
+<a href="{% url bio-view_profile username=comment.user.username%}">{% avatar comment.user %}</a>
+</div>
+{% if comment.is_removed %}
+<div class="comment-text-removed"><p>This comment has been removed.</p></div>
+{% else %}
+<div class="comment-text">{{ comment.html|safe }}</div>
+{% endif %}
+<div class="comment-details">
+<a href="{% url bio-view_profile username=comment.user.username%}" 
+    title="View profile for {{ comment.user.username }}">{{ comment.user.username }}</a> |
+{{ comment.creation_date|date:"d-M-Y H:i:s" }}
+{% if not comment.is_removed %}
+| <a href="#" class="comment-flag" id="fc-{{ comment.id }}" 
+   title="Flag this comment as spam, abuse, or a violation of site rules.">
+   <img src="{{ MEDIA_URL }}icons/flag_red.png" alt="Flag" /></a>
+{% endif %}
+</div>
+</div>
+</li>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/comments/comment_form.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,11 @@
+{% if user.is_authenticated %}
+<form action="{% url comments-post %}" method="post" id="comment-form">
+{{ form.as_p }}
+<input type="submit" name="post" value="Post Comment" id="comment-form-post"/>
+</form>
+{% else %}
+<p>
+Please <a href="{% url accounts-login %}">login</a> or
+<a href="{% url accounts-register %}">register</a> to leave a comment.
+</p>
+{% endif %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/comments/comment_list.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,5 @@
+<ol id="comment-list">
+{% for comment in comments %}
+{% include 'comments/comment.html' %}
+{% endfor %}
+</ol>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/comments/markdown.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,3 @@
+{% load markup %}
+{% load smiley_tags %}
+{{ data|markdown:"safe"|smilify }}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/comments/markdown_preview.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,14 @@
+{% load markup %}
+{% load smiley_tags %}
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>Markdown Preview</title>
+<link rel="stylesheet" type="text/css" href="~/templates/preview.css" />
+</head>
+{{ data|markdown:"safe"|smilify }}
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/contact/contact_email.txt	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,8 @@
+Feedback Message from {{ site_name }}:
+
+Sender's Name: {{ user_name }}
+Sender's Email: {{ user_email }}
+Message:
+
+{{ message }}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/contact/contact_form.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,17 @@
+{% extends 'base.html' %}
+{% block title %}Contact{% endblock %}
+{% block content %}
+<h2>Contact Us</h2>
+<p>Please use the following form to contact the site management. Your feedback and comments are very
+important to us.</p>
+<form action="{% url contact.views.contact_form %}" method="post">
+<table>
+   <tr><th>{{ form.name.label }}:</th><td>{{ form.name.errors }}{{ form.name }}</td></tr>
+   <tr><th>{{ form.email.label }}:</th><td>{{ form.email.errors }}{{ form.email }}</td></tr>
+   <tr><th>{{ form.subject.label }}:</th><td>{{ form.subject.errors }}{{ form.subject }}</td></tr>
+   <tr style="display:none"><th>{{ form.honeypot.label }}:</th><td>{{ form.honeypot }}</td></tr>
+   <tr><th>{{ form.message.label }}:</th><td>{{ form.message.errors }}{{ form.message }}</td></tr>
+   <tr><td>&nbsp;</td><td><input type="submit" value="Send" /></td></tr>
+</table>
+</form>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/contact/contact_thanks.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,6 @@
+{% extends 'base.html' %}
+{% block title %}Thanks For The Feedback{% endblock %}
+{% block content %}
+<h2>Your Message Has Been Sent</h2>
+<p>Thank you for the message, it has been emailed to the site management.</p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/core/pagination.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,22 @@
+<div class="pagination">
+<ul>
+{% if page.has_previous %}
+<li class="prev"><a href="../{{ page.previous_page_number }}/" title="Go to page {{ page.previous_page_number }}">&laquo; Previous</a></li>
+{% endif %}
+{% for num in page.page_range %}
+{% if num %}
+{% ifequal num page.number %}
+<li class="current">{{ num }}</li>
+{% else %}
+<li class="page"><a href="../{{ num }}/" title="Go to page {{ num }}">{{ num }}</a></li>
+{% endifequal %}
+{% else %}
+<li>&hellip;</li>
+{% endif %}
+{% endfor %}
+{% if page.has_next %}
+<li class="next"><a href="../{{ page.next_page_number }}/" title="Go to page {{ page.next_page_number }}">Next &raquo;</a></li>
+{% endif %}
+</ul>
+</div>
+<br clear="left" />
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/core/pagination_query.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,22 @@
+<div class="pagination">
+<ul>
+{% if page.has_previous %}
+<li class="prev"><a href="../{{ page.previous_page_number }}/?query={{ query }}" title="Go to page {{ page.previous_page_number }}">&laquo; Previous</a></li>
+{% endif %}
+{% for num in page.page_range %}
+{% if num %}
+{% ifequal num page.number %}
+<li class="current">{{ num }}</li>
+{% else %}
+<li class="page"><a href="../{{ num }}/?query={{ query }}" title="Go to page {{ num }}">{{ num }}</a></li>
+{% endifequal %}
+{% else %}
+<li>&hellip;</li>
+{% endif %}
+{% endfor %}
+{% if page.has_next %}
+<li class="next"><a href="../{{ page.next_page_number }}/?query={{ query }}" title="Go to page {{ page.next_page_number }}">Next &raquo;</a></li>
+{% endif %}
+</ul>
+</div>
+<br clear="left" />
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/downloads/add.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,46 @@
+{% extends 'base.html' %}
+{% load downloads_tags %}
+{% block title %}Add Download{% endblock %}
+{% block custom_css %}
+{{ add_form.media }}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/downloads.css" />
+{% endblock %}
+{% block content %}
+<h2>Downloads</h2>
+{% downloads_navigation %}
+<h3>Add Download</h3>
+
+<p>Use the following form to upload a file. Please note the following:</p>
+<ul>
+    <li>Please submit an upload only once.</li>
+    <li>All files are reviewed before being made available in the Downloads area.</li>
+    <li>Only certain file types are allowed.</li>
+    <li>Your username and IP address will be recorded, so please do not abuse the system.</li>
+</ul>
+
+<h4>Terms of Use</h4>
+
+<p>By uploading a file, you agree to the following terms:</p>
+<ol>
+    <li>You are allowing SurfGuitar101 to host the file for an undetermined amount of time.</li>
+    <li>This agreement will serve as your "Written" consent for SurfGuitar101 to host the file.</li>
+    <li>The owners of SurfGuitar101 are absolved of any liability claims resulting from the use of or hosting of your file.</li>
+    <li>You acknowledge you have permission to upload and distribute the file.</li>
+    <li>The file may be removed at any time at the discretion of the staff of SurfGuitar101.</li>
+</ol>
+
+<form action="." method="post" enctype="multipart/form-data" id="downloads-add">
+<fieldset>
+<legend>Upload Form</legend>
+<table>
+{{ add_form.as_table }}
+<tr>
+   <th>&nbsp;</th>
+   <td>
+      <input type="submit" name="submit_button" value="Add Download" />
+   </td>
+</table>
+</fieldset>
+</form>
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/downloads/download.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,24 @@
+{% load comment_tags %}
+{% get_comment_count for download as comment_count %}
+<dt>
+<a href="{% url downloads-download download.id %}">{{ download.title }}</a>
+</dt>
+<dd>
+{{ download.html|safe }}
+<table>
+<tr>
+    <th>Added By:</th>
+        <td><a href="{% url bio-view_profile download.user.username %}">{{ download.user.username }}</a></td>
+    <th>Date:</th><td>{{ download.date_added|date:"M d, Y" }}</td>
+    <th>Size:</th><td>{{ download.size }}</td>
+</tr>
+<tr>
+    <th>Category:</th><td>{{ download.category.title }}</td>
+    <th>Hits:</th><td>{{ download.hits }}</td>
+</tr>
+<tr>
+    <th>Rating:</th><td><div class="rating" id="rating-{{ download.id }}">{{ download.average_score|floatformat }}</div></td>
+    <th><a href="{% url downloads-comments download.id %}">Comments</a>:</th><td>{{ comment_count }}</td>
+</tr>
+</table>
+</dd>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/downloads/download_comments.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,32 @@
+{% extends 'base.html' %}
+{% load downloads_tags %}
+{% load comment_tags %}
+{% block title %}Downloads Comments{% endblock %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/comments.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/downloads.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}js/markitup/skins/markitup/style.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}js/markitup/sets/markdown/style.css" />
+{% endblock %}
+{% block custom_js %}
+<script type="text/javascript" src="{{ MEDIA_URL }}js/jquery-1.2.6.min.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/comments.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/markitup/jquery.markitup.pack.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/markitup/sets/markdown/set.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/downloads/rating.js"></script>
+{% endblock %}
+{% block content %}
+<h2>Downloads</h2>
+{% downloads_navigation %}
+<h3>Download Comments For {{ download.title }}</h3>
+
+<dl>
+{% include 'downloads/download.html' %}
+</dl>
+
+{% get_comment_count for download as comment_count %}
+<p>This download has <span id="comment-count">{{ comment_count }}</span> comment{{ comment_count|pluralize }}.</p>
+<hr />
+{% render_comment_list download %}
+{% render_comment_form for download %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/downloads/download_list.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,42 @@
+{% extends 'base.html' %}
+{% load downloads_tags %}
+{% block title %}Downloads: {{ category.title }}{% endblock %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/downloads.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/tab-nav.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/pagination.css" />
+{% endblock %}
+{% block custom_js %}
+<script type="text/javascript" src="{{ MEDIA_URL }}js/jquery-1.2.6.min.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/downloads/rating.js"></script>
+{% endblock %}
+{% block content %}
+<h2>Downloads</h2>
+{% downloads_navigation %}
+<h3>Category: {{ category.title }}</h3>
+
+{% if page.object_list %}
+<ul class="tab-nav">
+   <li><a href="{% url downloads-category category=category.id,sort="title",page="1" %}"
+      {% ifequal s "title" %}class="active" {% endifequal %}>Title</a></li>
+   <li><a href="{% url downloads-category category=category.id,sort="date",page="1" %}"
+      {% ifequal s "date" %}class="active"{% endifequal %}>Date</a></li>
+   <li><a href="{% url downloads-category category=category.id,sort="rating",page="1" %}"
+      {% ifequal s "rating" %}class="active"{% endifequal %}>Rating</a></li>
+   <li><a href="{% url downloads-category category=category.id,sort="hits",page="1" %}"
+      {% ifequal s "hits" %}class="active"{% endifequal %}>Hits</a></li>
+</ul>
+
+{% include 'core/pagination.html' %}
+
+<dl>
+{% for download in page.object_list %}
+   {% include 'downloads/download.html' %}
+{% endfor %}
+</dl>
+
+{% include 'core/pagination.html' %}
+{% else %}
+<p>No downloads in this category at this time.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/downloads/download_summary.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,25 @@
+{% extends 'base.html' %}
+{% load downloads_tags %}
+{% block title %}{{ title }}{% endblock %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/downloads.css" />
+{% endblock %}
+{% block custom_js %}
+<script type="text/javascript" src="{{ MEDIA_URL }}js/jquery-1.2.6.min.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/downloads/rating.js"></script>
+{% endblock %}
+{% block content %}
+<h2>Downloads</h2>
+{% downloads_navigation %}
+<h3>{{ title }}</h3>
+
+{% if downloads %}
+<dl>
+{% for download in downloads %}
+   {% include 'downloads/download.html' %}
+{% endfor %}
+</dl>
+{% else %}
+<p>No downloads available at this time.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/downloads/index.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,23 @@
+{% extends 'base.html' %}
+{% load downloads_tags %}
+{% block title %}Downloads{% endblock %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/downloads.css" />
+{% endblock %}
+{% block content %}
+<h2>Downloads</h2>
+{% downloads_navigation %}
+<h3>Categories</h3>
+{% if categories %}
+<p>We have {{ total_dls }} download{{ total_dls|pluralize }} in {{ categories.count }} categories.</p>
+<dl>
+{% for category in categories %}
+<dt>
+<a href="{% url downloads-category category=category.pk,sort="title",page=1 %}">{{ category.title }}</a>
+({{ category.num_downloads }})
+</dt>
+<dd><p>{{ category.description }}</p></dd>
+{% endfor %}
+</dl>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/downloads/markdown.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,3 @@
+{% load markup %}
+{% load smiley_tags %}
+{{ data|markdown:"safe"|smilify }}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/downloads/navigation.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,11 @@
+<form id="downloads-search" action="{% url downloads-search page=1 %}" method="post">
+    <p>{{ search_form.text }} <input type="submit" value="Search" /></p>
+</form>
+<ul class="app-menu">
+    <li><a href="{% url downloads-index %}">Categories</a></li>
+    <li><a href="{% url downloads-new %}">New</a></li>
+    <li><a href="{% url downloads-popular %}">Popular</a></li>
+    <li><a href="{% url downloads-rating %}">Highest Rated</a></li>
+    <li><a href="{% url downloads-random %}">Random</a></li>
+    <li><a href="{% url downloads-add %}">Add</a></li>
+</ul>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/downloads/search_results.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,31 @@
+{% extends 'base.html' %}
+{% load downloads_tags %}
+{% block title %}Downloads: Search Results{% endblock %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/downloads.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/tab-nav.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/pagination.css" />
+{% endblock %}
+{% block custom_js %}
+<script type="text/javascript" src="{{ MEDIA_URL }}js/jquery-1.2.6.min.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/downloads/rating.js"></script>
+{% endblock %}
+{% block content %}
+<h2>Downloads</h2>
+{% downloads_navigation %}
+<h3>Search Results: {{ query }}</h3>
+
+{% if page.object_list %}
+{% include 'core/pagination_query.html' %}
+
+<dl>
+{% for download in page.object_list %}
+   {% include 'downloads/download.html' %}
+{% endfor %}
+</dl>
+
+{% include 'core/pagination_query.html' %}
+{% else %}
+<p>No results found for &quot;{{ query }}&quot;.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/downloads/thanks.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,14 @@
+{% extends 'base.html' %}
+{% load downloads_tags %}
+{% block title %}Thanks for the Download{% endblock %}
+{% block custom_css %}
+{{ add_form.media }}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/downloads.css" />
+{% endblock %}
+{% block content %}
+<h2>Downloads</h2>
+{% downloads_navigation %}
+<h3>Thanks for the Download</h3>
+<p>Thank you for sending in a download! Your file will be reviewed by the site staff and made
+available shortly.</p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/gcalendar/edit.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,29 @@
+{% extends 'base.html' %}
+{% block title %}Edit Calendar Events{% endblock %}
+{% block custom_js %}
+<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/gcalendar_edit.js"></script>
+{% endblock %}
+{% block content %}
+<div class="breadcrumbs"><a href="{% url gcalendar-index %}">Calendar</a> &raquo; Edit Events</div>
+<h2>Edit Calendar Events</h2>
+{% if events %}
+<p>You have the following events on our calendar. Click on the event title to edit it, or click the
+<img src="{{ MEDIA_URL }}icons/cross.png" alt="Cross" /> to delete it. 
+Your changes will be submitted to the site staff for approval, and won't be reflected on the Google
+calendar until then. The approval process usually takes less than 12 hours. Thanks for helping to 
+keep our calendar up to date!
+</p>
+<ul>
+{% for event in events %}
+<li>{{ event.start_date|date:"M d, Y"}} &bull;
+<a href="{% url gcalendar-edit_event event.id %}" title="Edit Event">{{ event.what }}</a>
+<a class="gcal-del" id="gcal-{{ event.id }}" href="#"><img src="{{ MEDIA_URL }}icons/cross.png" alt="Delete Event" title="Delete Event" /></a>
+</li>
+{% endfor %}
+</ul>
+{% else %}
+<p>You either have no events on our calendar, or all your events have pending changes 
+that require admin review.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/gcalendar/event.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,43 @@
+{% extends 'base.html' %}
+{% block title %}{{ title }}{% endblock %}
+{% block custom_js %}
+{{ form.media }}
+{% endblock %}
+{% block content %}
+<div class="breadcrumbs"><a href="{% url gcalendar-index %}">Calendar</a> &raquo; {{ title }}</div>
+<h2>{{ title }}</h2>
+<p>Use this form to add or change an event on our calendar. Please note the following:</p>
+<ul>
+   <li>If applicable, please fill out the <strong>Where</strong> field as completely as you can. 
+   Google will generate a Google map from this information.</li>
+   <li>Currently, images and smilies won't show up correctly on the Google calendar. If you would
+   like to include an image, add a link to it instead.</li>
+   <li>Once submitted, your event will be reviewed by the site staff for approval. Normally it will appear on
+   the calendar within 24 hours.</li>
+</ul>
+<form id="id_gcal_event_form" action="." method="post">
+<table>
+{% if form.non_field_errors %}
+<tr><td>&nbsp;</td><td>{{ form.non_field_errors }}</td></tr>
+{% endif %}
+<tr><th>What:</th><td>{{ form.what.errors }}{{ form.what }}</td></tr>
+<tr><th>When:</th><td>
+      {{ form.start_date.errors }}{{ form.start_date }}
+      {{ form.start_time.errors }}{{ form.start_time }} to
+      {{ form.end_date.errors }}{{ form.end_date }}
+      {{ form.end_time.errors }}{{ form.end_time }}
+      {{ form.all_day.errors }}{{ form.all_day }} <strong>All Day</strong><br />
+      <div id="id_tz_stuff">
+      {{ form.time_zone.errors }}
+      <strong>Time Zone:</strong> <select id="id_tz_area"></select> <select id="id_tz_location"></select>
+      {{ form.time_zone }}
+      </div>
+</td></tr>
+<tr><th>Where:</th><td>{{ form.where.errors }}{{ form.where }}</td></tr>
+<tr><th>Details:</th><td>{{ form.description.errors }}{{ form.description }}</td></tr>
+{# form.as_p #}
+<tr><td>&nbsp;</td><td><input type="submit" name="submit_button" value="Submit" /></td></tr>
+</table>
+</form>
+<p><a href="{% url gcalendar-index %}">&laquo; Back to the Event Calendar</a></p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/gcalendar/google_sync.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,35 @@
+{% extends 'admin/base_site.html' %}
+{% block title %}Sync Events w/Google Calendar{% endblock %}
+{% block breadcrumbs %}
+<div class="breadcrumbs">
+   <a href="../../../">Home</a> &rsaquo;
+   <a href="../../">Gcalendar</a> &rsaquo; 
+   <a href="../">Events</a> &rsaquo; Google Sync
+</div>
+{% endblock %}
+{% block content %}
+<h1>Synchronize Approved Events with Google Calendar</h1>
+{% if errors %}
+<ul class="errorlist">
+   {{ errors|unordered_list }}
+</ul>
+{% endif %}
+{% if events %}
+<p>To synchronize the following approved events with the Google calendar, please enter the password for the
+account and click submit.</p>
+<ol>
+{% for event in events %}
+{% if not event.on_calendar %}
+<li><a href="../../{{ event.id }}">{{ event.start_date|date:"M d, Y" }} - {{ event.what }}</a>
+&bull; Submitted by {{ event.user.username }} &bull; <strong>{{ event.get_status_display }}</strong></li>
+{% endif %}
+{% endfor %}
+</ol>
+<form action="." method="POST">
+   {{ form.as_p }}
+   <p><input type="submit" name="submit" value="Submit" /></p>
+</form>
+{% else %}
+<p>No events to synchronize at this time.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/gcalendar/index.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,28 @@
+{% extends 'base.html' %}
+{% block title %}Event Calendar{% endblock %}
+{% block content %}
+<h2>SurfGuitar101 Event Calendar</h2>
+<p>All times are shown in the US/Central time zone. You can add any event on our calendar to your own 
+<a href="http://www.google.com/googlecalendar/overview.html">Google calendar</a>. If you do, the event
+will be displayed in your time zone, and you can have Google send you email or text message reminders for
+the events. Click on any event below to see more details or copy it to your calendar.</p>
+<iframe src="//www.google.com/calendar/embed?showTitle=0&amp;showTz=0&amp;height=540&amp;wkst=1&amp;bgcolor=%23DFDFDF&amp;src=i81lu3fkh57sgqqenogefd9v78%40group.calendar.google.com&amp;color=%231B887A&amp;ctz=America%2FChicago" style=" border:solid 1px #777 " width="720" height="540" frameborder="0" scrolling="no"></iframe>
+
+<ul>
+{% if user.is_authenticated %}
+   <li><a href="{% url gcalendar-add %}"><img src="{{ MEDIA_URL}}icons/calendar_add.png" alt="Add" /></a>
+      <a href="{% url gcalendar-add %}">Add an event to our calendar</a>.</li>
+   <li><a href="{% url gcalendar-edit_events %}"><img src="{{ MEDIA_URL}}icons/calendar_edit.png" alt="Edit" /></a>
+      <a href="{% url gcalendar-edit_events %}">Change or delete an event you previously added</a>.</li>
+{% endif %}
+   <li>
+<a href="http://www.google.com/calendar/feeds/i81lu3fkh57sgqqenogefd9v78%40group.calendar.google.com/public/basic">
+<img src="{{ MEDIA_URL }}icons/feed.png" alt="feed" /></a>
+<a href="http://www.google.com/calendar/feeds/i81lu3fkh57sgqqenogefd9v78%40group.calendar.google.com/public/basic">
+RSS Feed</a>
+</li>
+<li>
+Want to embed this calendar on your website or blog? <a href="http://www.google.com/calendar/embedhelper?src=i81lu3fkh57sgqqenogefd9v78%40group.calendar.google.com&ctz=America/Chicago">Use this helper tool</a>.
+</li>
+</ul>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/gcalendar/markdown.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,2 @@
+{% load markup %}
+{{ data|markdown:"safe" }}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/gcalendar/thanks_add.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,9 @@
+{% extends 'base.html' %}
+{% block title %}Event Calendar - Thanks{% endblock %}
+{% block content %}
+<div class="breadcrumbs"><a href="{% url gcalendar-index %}">Calendar</a> &raquo; Thanks</div>
+<h2>Thanks for Submitting an Event!</h2>
+<p>Thanks for submitting an event to our calendar. Your event will be reviewed by the staff,
+and should appear on the calendar shortly. You may be contacted if we have any questions.</p>
+<p><a href="{% url gcalendar-index %}">&laquo; Back to the Event Calendar</a></p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/gcalendar/thanks_edit.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,9 @@
+{% extends 'base.html' %}
+{% block title %}Event Calendar Changes Received{% endblock %}
+{% block content %}
+<div class="breadcrumbs"><a href="{% url gcalendar-index %}">Calendar</a> &raquo; Edit Events</div>
+<h2>Event Calendar Changes Received</h2>
+<p>We've received the changes to your calendar event. Your changes will be reviewed by the staff,
+and should appear on the calendar shortly. You may be contacted if we have any questions.</p>
+<p><a href="{% url gcalendar-index %}">&laquo; Back to the Event Calendar</a></p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/irc/irc_block.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,14 @@
+{% extends 'side_block.html' %}
+{% block block_title %}IRC Status{% endblock %}
+{% block block_content %}
+{% if nicks %}
+<ul>
+   {% for nick in nicks %}
+      <li>{{ nick.name }}</li>
+   {% endfor %}
+</ul>
+<p>Join them in the <a href="irc://surfguitar101.com/ShallowEnd,needpass">#ShallowEnd</a>!</p>
+{% else %}
+<p><a href="irc://surfguitar101.com/ShallowEnd,needpass">#ShallowEnd</a> is empty.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/irc/view.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+{% block title %}IRC Channel Status{% endblock %}
+{% block content %}
+<h2>Who is Chatting in IRC?</h2>
+{% if nicks %}
+<ul>
+   {% for nick in nicks %}
+      <li>{{ nick.name }}</li>
+   {% endfor %}
+</ul>
+<p>Join them in the <a href="irc://surfguitar101.com/ShallowEnd,needpass">#ShallowEnd</a>!</p>
+{% else %}
+<p><a href="irc://surfguitar101.com/ShallowEnd,needpass">#ShallowEnd</a> is empty at this time.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/legal/view.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,9 @@
+{% extends 'base.html' %}
+{% block title %}{{ policy.title }}{% endblock %}
+{% block content %}
+<h2>{{ policy.title }}</h2>
+<div class="legal">
+   {{ policy.text|linebreaks }}
+</div>
+<p><em>Last Revised: {{ policy.revised_date }}</em></p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/membermap/index.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,77 @@
+{% extends 'base.html' %}
+{% block title %}Member Map{% endblock %}
+{% block custom_js %}
+<script type="text/javascript" src="{{ MEDIA_URL }}js/jquery-1.2.6.min.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/membermap.js"></script>
+<script src="http://maps.google.com/maps?file=api&amp;v=2&amp;key=ABQIAAAAql_1Xw9MGW3mOxzo8gLb3hStsp1UIpjYO7Py4hlJXjRzAdyQtBTt5uM4QCgXtTKcuwba8HA9TL9LgQ" type="text/javascript"></script>
+{{ form.media }}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/membermap.css" />
+<script type="text/javascript">
+//<![CDATA[
+var mmapUser = {
+   {% if user.is_authenticated %}
+   userName : "{{ user.username }}",
+   userId : "{{ user.id }}"
+   {% else %}
+   userName : null,
+   userId : null
+   {% endif %}
+};
+//]]>
+</script>
+{% endblock %}
+{% block content %}
+<h2>Member Map</h2>
+<div id="member_map_main">
+    {% if user.is_authenticated %}
+    <div id="member_map_top">
+        Members on the map: <span id="member_map_count">0</span> &bull; Recent updates:
+        <select id="member_map_recent"><option value="0" selected="selected">(select)</option></select>
+    </div>
+    {% endif %}
+    <div>
+        {% if user.is_authenticated %}
+        <div id="member_map_members_column">
+            Members:<br />
+            <select id="member_map_members" multiple="multiple">
+            </select>
+        </div>
+        {% endif %}
+        <div id="member_map_map">
+        </div>
+    </div>
+    <div id="member_map_info">
+        {% if user.is_authenticated %}
+        <p id="member_map_directions"></p>
+        <p>
+        The location you enter below will not be shown to others, but can be determined from the map. 
+        For privacy reasons, we don't recommend you enter your exact address. Use a nearby intersection, 
+        landmark, or just keep it city and state.
+        </p>
+        <p>
+        Example locations:
+        </p>
+        <ul>
+            <li>3rd and Main, Chicago, IL</li>
+            <li>Tucson, Arizona</li>
+            <li>Rome, Italy</li>
+            <li>5018EA, Tilburg, Netherlands</li>
+        </ul>
+        <form action="" method="post">
+            {{ form.as_p }}
+            <p>
+            <input type="submit" id="member_map_submit" name="submit" value="Submit" />
+            <input type="submit" id="member_map_delete" name="delete" value="Delete" />
+            </p>
+        </form>
+        {% else %}
+        <p>
+        The member map allows members to place themselves on a google map along with a short message.
+        This feature is only for registered users of SurfGuitar101.com. Please
+        <a href="{% url accounts-login %}">login</a> or
+        <a href="{% url accounts-register %}">register</a> to use the member map.
+        </p>
+        {% endif %}
+    </div>
+</div>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/membermap/markdown.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,4 @@
+{% load markup %}
+{% load smiley_tags %}
+{% load avatar_tags %}
+{% avatar entry.user "left" %}<a href="{% url bio-view_profile username=entry.user.username %}">{{ entry.user.username }}</a>:<br />{{ entry.message|markdown:"safe"|smilify }}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/messages/base.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,23 @@
+{% extends 'base.html' %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/tab-nav.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/messages.css" />
+{% endblock %}
+{% block content %}
+<h2>Your Private Messsages</h2>
+<ul class="tab-nav">
+<li><a {% block compose-class %}{% endblock %} 
+   href="{% url messages-compose %}">Compose</a></li>
+<li><a {% block inbox-class %}{% endblock %} 
+   href="{% url messages-inbox %}">Inbox</a></li>
+<li><a {% block outbox-class %}{% endblock %} 
+   href="{% url messages-outbox %}">Outbox</a></li>
+<li><a {% block trash-class %}{% endblock %} 
+   href="{% url messages-trash %}">Trash</a></li>
+<li><a {% block options-class %}{% endblock %} 
+   href="{% url messages-options %}">Options</a></li>
+</ul>
+{% block messages_content %}
+{% endblock %}
+<br />
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/messages/compose.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,22 @@
+{% extends 'messages/base.html' %}
+{% block title %}Messages: Compose{% endblock %}
+{% block custom_js %}
+   {{ compose_form.media }}
+{% endblock %}
+{% block compose-class %}class="active"{% endblock %}
+{% block messages_content %}
+<h3>Compose Message</h3>
+<form action="." method="post">
+<table>
+{{ compose_form.as_table }}
+<tr>
+   <td>&nbsp;</td>
+   <td>
+      <input type="submit" name="submit_button" value="Send" />
+      <input type="submit" name="submit_button" value="Cancel" />
+   </td>
+</tr>
+</table>
+</form>
+<br />
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/messages/inbox.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,49 @@
+{% extends 'messages/base.html' %}
+{% block title %}Messages: Inbox{% endblock %}
+{% block custom_js %}
+   <script type="text/javascript" src="{{ MEDIA_URL }}js/messages/box.js"></script>
+{% endblock %}
+{% block inbox-class %}class="active"{% endblock %}
+{% block messages_content %}
+<h3>Inbox</h3>
+{% if messages %}
+<ul class="user-messages">
+   {% for msg in messages %}
+      <li>{{ msg }}</li>
+   {% endfor %}
+</ul>
+{% endif %}
+{% if msgs %}
+   <form action="{% url messages-delete_bulk %}" method="post" name="messages_box_form"
+      onsubmit="return messages_confirm_delete();">
+   <table class="messages">
+   <tr>
+      <th>From</th>
+      <th>Subject</th>
+      <th>Date</th>
+      <th><input type="checkbox" id="master_select" onclick="messages_master_click();" /></th>
+   </tr>
+   {% for msg in msgs %}
+   <tr>
+      <td><a href="{% url bio.views.view_profile msg.sender.username %}">
+         {{ msg.sender.username }}</a></td>
+      <td>
+         {% if msg.unread %}<strong>{% endif %}
+         {% if msg.replied_to %}<em>{% endif %}
+         <a href="{{ msg.get_absolute_url }}">{{ msg.subject }}</a>
+         {% if msg.replied_to %}</em>{% endif %}
+         {% if msg.unread %}</strong>{% endif %}
+         </td>
+      <td>{{ msg.send_date|date:"M j, Y g:i A" }}</td>
+      <td><input type="checkbox" name="delete_ids" value="{{ msg.id }}" 
+         onclick="messages_set_master();" /></td>
+   </tr>
+   {% endfor %}
+   <tr><td colspan="4" align="center"><input type="submit" value="Delete Checked Messages" /></td></tr>
+   </table>
+   <input type="hidden" name="box" value="inbox" />
+   </form>
+{% else %}
+   <p><em>Your Inbox is empty.</em></p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/messages/markdown.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,3 @@
+{% load markup %}
+{% load smiley_tags %}
+{{ data|markdown:"safe"|smilify }}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/messages/notification_email.txt	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,24 @@
+Dear {{ msg.receiver.username }},
+
+You have just received a new private message at {{ site.name }}. The message
+details are as follows:
+
+From: {{ msg.sender.username }}
+Subject: {{ msg.subject }}
+
+You may read this message by visiting the following link:
+
+http://{{ site.domain }}{{ msg.get_absolute_url }}
+
+Thanks,
+
+The staff at {{ site.name }}.
+http://{{ site.domain }}
+
+P.S.
+You are receiving this email because you have elected to receive 
+notifications of new private messages. To stop receiving these emails,
+please update your private message options at this link:
+
+http://{{ site.domain }}{{ options_url }}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/messages/options.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,26 @@
+{% extends 'messages/base.html' %}
+{% block title %}Messages: Options{% endblock %}
+{% block options-class %}active{% endblock %}
+{% block messages_content %}
+<h3>Private Message Options</h3>
+{% if messages %}
+<ul class="user-messages">
+   {% for msg in messages %}
+      <li>{{ msg }}</li>
+   {% endfor %}
+</ul>
+{% endif %}
+<form action="." method="post">
+<table>
+{{ form.as_table }}
+<tr>
+   <td>&nbsp;</td>
+   <td>
+      <input type="submit" name="submit_button" value="Save" />
+      <input type="submit" name="submit_button" value="Cancel" />
+   </td>
+</tr>
+</table>
+</form>
+<br />
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/messages/outbox.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,51 @@
+{% extends 'messages/base.html' %}
+{% block title %}Messages: Outbox{% endblock %}
+{% block custom_js %}
+   <script type="text/javascript" src="{{ MEDIA_URL }}js/messages/box.js"></script>
+{% endblock %}
+{% block outbox-class %}class="active"{% endblock %}
+{% block messages_content %}
+<h3>Outbox</h3>
+{% if messages %}
+<ul class="user-messages">
+   {% for msg in messages %}
+      <li>{{ msg }}</li>
+   {% endfor %}
+</ul>
+{% endif %}
+{% if msgs %}
+   <form action="{% url messages-delete_bulk %}" method="post" name="messages_box_form"
+      onsubmit="return messages_confirm_delete();">
+   <table class="messages"> 
+   <tr>
+      <th>To</th>
+      <th>Subject</th>
+      <th>Sent</th>
+      <th>Received</th>
+      <th><input type="checkbox" id="master_select" onclick="messages_master_click();" /></th>
+   </tr>
+   {% for msg in msgs %}
+   <tr>
+      <td><a href="{% url bio.views.view_profile msg.receiver.username %}">
+         {{ msg.receiver.username }}</a></td>
+      <td>
+         {% if msg.unread %}<strong>{% endif %}
+         {% if msg.replied_to %}<em>{% endif %}
+         <a href="{{ msg.get_absolute_url }}?box=outbox">{{ msg.subject }}</a>
+         {% if msg.replied_to %}</em>{% endif %}
+         {% if msg.unread %}</strong>{% endif %}
+         </td>
+      <td>{{ msg.send_date|date:"M j, Y g:i A" }}</td>
+      <td>{% if msg.unread %}<em>Unread</em>{% else %}{{ msg.read_date|date:"M j, Y g:i A" }}{% endif %}</td>
+      <td><input type="checkbox" name="delete_ids" value="{{ msg.id }}"
+         onclick="messages_set_master();" /></td>
+   </tr>
+   {% endfor %}
+   <tr><td colspan="5" align="center"><input type="submit" value="Delete Checked Messages" /></td></tr>
+   </table>
+   <input type="hidden" name="box" value="outbox" />
+   </form>
+{% else %}
+   <p><em>Your Outbox is empty.</em></p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/messages/trash.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,51 @@
+{% extends 'messages/base.html' %}
+{% block title %}Messages: Trash{% endblock %}
+{% block custom_js %}
+   <script type="text/javascript" src="{{ MEDIA_URL }}js/messages/box.js"></script>
+{% endblock %}
+{% block trash-class %}class="active"{% endblock %}
+{% block messages_content %}
+<h3>Trash</h3>
+{% if messages %}
+<ul class="user-messages">
+   {% for msg in messages %}
+      <li>{{ msg }}</li>
+   {% endfor %}
+</ul>
+{% endif %}
+{% if msgs %}
+   <form action="{% url messages-undelete_bulk %}" method="post" name="messages_box_form"
+      onsubmit="return messages_confirm_undelete();">
+   <table class="messages">
+   <tr>
+      <th>From</th>
+      <th>To</th>
+      <th>Subject</th>
+      <th>Date</th>
+      <th><input type="checkbox" id="master_select" onclick="messages_master_click();" /></th>
+   </tr>
+   {% for msg in msgs %}
+   <tr>
+      <td><a href="{% url bio.views.view_profile msg.sender.username %}">
+         {{ msg.sender.username }}</a></td>
+      <td><a href="{% url bio.views.view_profile msg.receiver.username %}">
+         {{ msg.receiver.username }}</a></td>
+      <td>
+         {% if msg.unread %}<strong>{% endif %}
+         {% if msg.replied_to %}<em>{% endif %}
+         <a href="{{ msg.get_absolute_url }}?box=trash">{{ msg.subject }}</a>
+         {% if msg.replied_to %}</em>{% endif %}
+         {% if msg.unread %}</strong>{% endif %}
+         </td>
+      <td>{{ msg.send_date|date:"M j, Y g:i:s A T" }}</td>
+      <td><input type="checkbox" name="undelete_ids" value="{{ msg.id }}"
+         onclick="messages_set_master();" /></td>
+   </tr>
+   {% endfor %}
+   <tr><td colspan="5" align="center"><input type="submit" value="Undelete Checked Messages" /></td></tr>
+   </table>
+   </form>
+{% else %}
+   <p><em>Your Trash is empty.</em></p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/messages/view.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,31 @@
+{% extends 'messages/base.html' %}
+{% block title %}Messages: {{ msg.subject }}{% endblock %}
+{% block messages_content %}
+<h3>Viewing Message: {{ msg.subject }}</h3>
+<table class="message-header">
+   <tr><th>Subject:</th><td>{{ msg.subject }}</td></tr>
+   <tr><th>From:</th><td>{{ msg.sender }}</td></tr>
+   <tr><th>To:</th><td>{{ msg.receiver }}</td></tr>
+   <tr><th>Date Sent:</th><td>{{ msg.send_date|date:"F d, Y g:i:s A T" }}</td></tr>
+   <tr><th>Date Received:</th>
+      <td>{% if msg.unread %}<em>Unread</em>{% else %}{{ msg.read_date|date:"F d, Y g:i:s A T" }}{% endif %}</td></tr>
+</table>
+
+<div class="message-body">
+   {{ msg.html|safe }}
+</div>
+{% if msg.signature_attached %}
+<div class="message-hr"></div>
+<div class="message-signature">
+   {{ msg.sender.get_profile.signature_html|safe }}
+</div>
+{% endif %}
+<p>
+{% if is_deleted %}
+<a href="{% url messages-undelete msg.id %}{% if box %}?box={{ box }}{% endif %}">Undelete</a>
+{% else %}
+<a href="{% url messages-reply msg.id %}{% if box %}?box={{ box }}{% endif %}">Reply</a> |
+<a href="{% url messages-delete msg.id %}{% if box %}?box={{ box }}{% endif %}">Delete</a>
+{% endif %}
+</p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/news/archive_index.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,20 @@
+{% extends 'news/base.html' %}
+{% block title %}News Archive{% endblock %}
+{% block archive-class %}class="active"{% endblock %}
+{% block news_content %}
+<h3>News Archive</h3>
+<p>
+This is our news archive. Click on a link to view the list of stories we published for that month.
+</p>
+
+{% if dates %}
+<ul>
+{% for date in dates %}
+   <li><a href="{% url news-archive_page year=date.year,month=date.month,page=1 %}">
+      {{ date|date:"F, Y" }}</a></li>
+{% endfor %}
+</ul>
+{% else %}
+   <p>No archives at this time.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/news/base.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,27 @@
+{% extends 'base.html' %}
+{% block custom_head %}
+<link rel="alternate" type="application/rss+xml" title="SurfGuitar101 News" href="{% url feeds-news url="news" %}" />
+{% endblock %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/news.css" />
+{% block news_css %}{% endblock %}
+{% endblock %}
+{% block content %}
+<h2>SurfGuitar101 News &amp; Articles</h2>
+{% if search_form %}
+<div class="news-search">
+<form action="{% url news-search_page page=1 %}" method="post">
+   <p>{{ search_form.text }} {{ search_form.category }} <input type="submit" value="Search" /></p>
+</form>
+</div>
+{% endif %}
+<ul class="app-menu">
+<li><a href="{% url news-index_page page=1 %}">News Main</a></li>
+<li><a href="{% url news-archive_index %}">Archive</a></li>
+<li><a href="{% url news.views.category_index %}">Categories</a></li>
+<li><a href="{% url news-tag_index %}">Tags</a></li>
+<li><a href="{% url news.views.submit %}">Submit News</a></li>
+</ul>
+{% block news_content %}
+{% endblock %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/news/category_index.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,31 @@
+{% extends 'news/base.html' %}
+{% block title %}News: Categories{% endblock %}
+{% block categories-class %}class="active"{% endblock %}
+{% block news_content %}
+<h3>Categories</h3>
+<p>
+This page shows the list of news categories for the site. Under each category we show the 
+latest ten stories. To see all the stories in a category, click the icon for that category.
+Each story is also <strong>tagged</strong> with a set of tags. You may also wish to 
+<a href="{% url news-tag_index %}">view our stories by their tags</a>.
+</p>
+
+{% for category, story_set in cat_list %}
+   <h3>{{ category.title }}</h3>
+   <p><a href="{% url news.views.category category=category.id,page=1 %}">
+      <img src="{{ category.icon.url }}" alt="{{ category.title }}" title="{{ category.title }}" />
+      </a>
+   </p>
+   {% if story_set %}
+      <ul>
+      {% for story in story_set %}
+         <li><a href="{{ story.get_absolute_url }}">{{ story.title }}</a> 
+            - {{ story.date_published|date:"F d, Y" }}</li>
+      {% endfor %}
+      </ul>
+   {% else %}
+      <p>No news at this time.</p>
+   {% endif %}
+{% endfor %}
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/news/feed_description.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,1 @@
+{{ obj.short_text|safe }}{{ obj.long_text|safe }}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/news/feed_title.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,1 @@
+{{ obj.title|safe }}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/news/index.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,36 @@
+{% extends 'news/base.html' %}
+{% load tagging_tags %}
+{% block title %}News: {{ title }}{% endblock %}
+{% block news_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/pagination.css" />
+{% endblock %}
+{% block news_content %}
+<h3>{{ title }}</h3>
+
+{% if query %}
+{% include 'core/pagination_query.html' %}
+{% else %}
+{% include 'core/pagination.html' %}
+{% endif %}
+
+{% if page.object_list %}
+{% for story in page.object_list %}
+   {% tags_for_object story as story_tags %}
+   {% include 'news/story_summary.html' %}
+{% endfor %}
+<div style="clear:right;"></div>
+{% else %}
+   {% if query %}
+      <p>No results found.</p>
+   {% else %}
+      <p>No news at this time.</p>
+   {% endif %}
+{% endif %}
+
+{% if query %}
+{% include 'core/pagination_query.html' %}
+{% else %}
+{% include 'core/pagination.html' %}
+{% endif %}
+
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/news/send_story.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,24 @@
+{% extends 'base.html' %}
+{% block title %}News: Send Story to a Friend{% endblock %}
+{% block content %}
+<h3>Send Story to a Friend</h3>
+{% if send_form %}
+   <p>Would you like to send a link to the story titled <strong>{{ story.title }}</strong> to a friend?
+   Just fill out the form below and click Send.</p>
+   <ul>
+      <li>Your name: {{ user.get_full_name }}</li>
+      <li>Your email: {{ user.email }}</li>
+   </ul>
+   <form action="." method="post">
+      <table>
+         {{ send_form.as_table }}
+         <tr><td>&nbsp;</td><td><input type="submit" value="Send" />
+            <input type="button" value="Cancel" onclick="history.back(); return true;" /></td></tr>
+      </table>
+   </form>
+{% else %}
+   <p><strong>Thank you for letting your friend know about this story and our site!</strong></p>
+   <p>An email has been sent to your friend with a link to the story.</p>
+{% endif %}
+<br />
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/news/send_story_email.txt	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,10 @@
+Dear {{ to_name }},
+
+Your friend, {{ sender_name }}, wanted to send you a link to an 
+interesting story found on {{ site_name }}.
+
+{{ story_title|safe }}
+http://{{ site_url }}{{ story_link }}
+
+Thanks!
+http://{{ site_url}}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/news/story.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,57 @@
+{% extends 'news/base.html' %}
+{% load tagging_tags %}
+{% load comment_tags %}
+{% block title %}News: {{ story.title }}{% endblock %}
+{% block news_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}js/markitup/skins/markitup/style.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}js/markitup/sets/markdown/style.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/comments.css" />
+{% endblock %}
+{% block custom_js %}
+<script type="text/javascript" src="{{ MEDIA_URL }}js/jquery-1.2.6.min.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/comments.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/markitup/jquery.markitup.pack.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/markitup/sets/markdown/set.js"></script>
+{% endblock %}
+{% block news_content %}
+<h3>{{ story.title }}</h3>
+<div class="news-details">
+   Submitted by {{ story.submitter.username }} on {{ story.date_published|date:"F d, Y" }}.
+</div>
+<div class="news-content">
+   <a href="{% url news.views.category category=story.category.id,page=1 %}">
+   <img src="{{ story.category.icon.url }}" alt="{{ story.category.title }}" title="{{ story.category.title }}" 
+      class="news-icon" /></a>
+   {{ story.short_text|safe }}
+   {{ story.long_text|safe }}
+   <br clear="all" />
+   {% tags_for_object story as story_tags %}
+   {% if story_tags %}
+   <div class="news-tags">
+      <span>Tags:</span>
+      <ul>
+         {% for tag in story_tags %}
+            <li><a href="{% url news-tag_page tag_name=tag.name,page=1 %}">{{ tag.name }}</a></li>
+         {% endfor %}
+      </ul>
+   </div>
+   {% endif %}
+   <p>
+   <a href="{{ story.get_absolute_url }}"><img src="{{ MEDIA_URL }}icons/link.png"
+      alt="Story Permalink" title="Story Permalink" /></a>
+   <a href="{% url news.views.email_story story.id %}"><img src="{{ MEDIA_URL }}icons/email_go.png"
+      alt="Send this story to a friend" title="Send this story to a friend" /></a>
+   </p>
+</div>
+{% get_comment_count for story as comment_count %}
+<p>This story has <span id="comment-count">{{ comment_count }}</span> comment{{ comment_count|pluralize }}.</p>
+<hr />
+{% render_comment_list story %}
+{% if story.can_comment_on %}
+<p>Leave a comment?</p>
+{% render_comment_form for story %}
+{% else %}
+<p>Comments are closed for this story. If you'd like to share your thoughts on this story 
+with the site staff, you can <a href="{% url contact-form %}">contact us directly</a>.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/news/story_summary.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,30 @@
+{% load comment_tags %}
+{% get_comment_count for story as comment_count %}
+<div class="news-story-container">
+<h4><a href="{% url news.views.story story.id %}">{{ story.title }}</a></h4>
+<div class="news-details">
+   Submitted by {{ story.submitter.username }} on {{ story.date_published|date:"F d, Y" }}.
+</div>
+<a href="{% url news.views.category category=story.category.id,page=1 %}">
+<img src="{{ story.category.icon.url }}" alt="{{ story.category.title }}" title="{{ story.category.title }}" 
+   class="news-icon" /></a>
+<div class="news-content">
+   {{ story.short_text|safe }}
+</div>
+<p>
+{% if story.long_text or comment_count %}
+<a href="{% url news.views.story story.id %}">Read more...</a> |
+{% endif %}
+<a href="{% url news.views.story story.id %}">{{ comment_count }} comment{{ comment_count|pluralize }}</a>.
+</p>
+{% if story_tags %}
+<div class="news-tags">
+   <span>Tags:</span>
+   <ul>
+      {% for tag in story_tags %}
+         <li><a href="{% url news-tag_page tag_name=tag.name,page=1 %}">{{ tag.name }}</a></li>
+      {% endfor %}
+   </ul>
+</div>
+{% endif %}
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/news/submit_news.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,26 @@
+{% extends 'news/base.html' %}
+{% block title %}News: Submit News{% endblock %}
+{% if add_form %}
+   {% block custom_js %}
+      {{ add_form.media }}
+   {% endblock %}
+{% endif %}
+{% block submit-class %}class="active"{% endblock %}
+{% block news_content %}
+<h3>Submit News</h3>
+{% if add_form %}
+   <form action="." method="post">
+      <table>
+         {{ add_form.as_table }}
+         <tr><td>&nbsp;</td><td><input type="submit" value="Submit" />
+            <input type="button" value="Cancel" onclick="history.back(); return true;" /></td></tr>
+      </table>
+   </form>
+{% else %}
+   <p><strong>Thank you for submitting a news item!</strong></p>
+   <p>Your news item has been submitted for review to the site staff. Your item may be edited for content,
+   grammar, or spelling. If there are any problems or questions, you will receive an email or private message.
+   Thank you for contributing to the site!</p>
+{% endif %}
+<br />
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/news/tag_index.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,24 @@
+{% extends 'news/base.html' %}
+{% block title %}News: Tags{% endblock %}
+{% block tags-class %}class="active"{% endblock %}
+{% block news_content %}
+<h3>Tags</h3>
+<p>
+This page shows the list of tags for our news stories in &quot;cloud&quot; form. The
+bigger the tag, the more stories we have tagged with it. Click a tag to view a list
+of stories with that tag.
+</p>
+{% if tags %}
+   <div class="news-tag-cloud">
+   <ul>
+   {% for tag in tags %}
+      <li><a href="{% url news-tag_page tag_name=tag.name,page=1 %}">
+         <font size="{{ tag.font_size }}">{{ tag.name }}</font></a></li>
+   {% endfor %}
+   </ul>
+   </div>
+   <br />
+{% else %}
+<p>No tags available at this time.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/podcast/base.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,22 @@
+{% extends 'base.html' %}
+{% block content %}
+<h2>SurfGuitar101 Podcast</h2>
+<img src="{{ channel.image.url }}" alt="Podcast Logo" style="float: left; margin-right:10px;" />
+<p>
+Welcome to the <strong>Surfguitar101 Podcast</strong>! The podcast started as an experiment just to see what it took to create a podcast. The reaction was very positive so hopefully we will start seeing new episodes on a semi-regular basis. You can download the podcast episodes here and we are also listed in iTunes.
+</p>
+<p>
+Please discuss the podcasts in our Podcast forum. We'd love to hear your suggestions, and let us know if you would like
+to help in any way. We need producers, interviewers, artwork, etc. you name it. Thanks!
+</p>
+<p>
+In addition to the forum, you can contact us by email at 
+<a href="mailto:podcast@surfguitar101.com">podcast@surfguitar101.com</a>.
+</p>
+<p>
+Subscribe to the podcast via RSS: <a href="{% url podcast.views.feed %}">Feed</a>
+</p>
+<br clear="all" />
+<hr />
+{% block podcast-content %}{% endblock %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/podcast/detail.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,12 @@
+{% extends 'podcast/base.html' %}
+{% block title %}Podcast: {{ podcast.title }}{% endblock %}
+{% block podcast-content %}
+<div class="breadcrumb">
+   <a href="{% url podcast.views.index %}">Podcast Index</a> &gt;&gt; {{ podcast.title }}
+</div>
+<h3>{{ podcast.pubdate|date:"F d, Y" }} &bull; {{ podcast.title }}</h3>
+<h4>{{ podcast.subtitle }}</h4>
+{{ podcast.summary|linebreaks }}
+<p><a href="{{ podcast.enclosure_url }}">Download Now</a> &bull;
+{{ podcast.enclosure_length|filesizeformat }} &bull; {{ podcast.duration }}</p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/podcast/feed.xml	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<rss version="2.0"
+   xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
+   xmlns:atom="http://www.w3.org/2005/Atom">
+<channel>
+{% if channel %}
+<atom:link href="{{ request.build_absolute_uri }}" rel="self" type="application/rss+xml" />
+<title>{{ channel.title }}</title>
+<link>{{ channel.link }}</link>
+<language>{{ channel.language }}</language>
+<copyright>{{ channel.copyright }}</copyright>
+<itunes:subtitle>{{ channel.subtitle }}</itunes:subtitle>
+<itunes:author>{{ channel.author }}</itunes:author>
+<itunes:summary>{{ channel.description }}</itunes:summary>
+<description>{{ channel.description }}</description>
+<itunes:owner>
+<itunes:name>{{ channel.owner_name }}</itunes:name>
+<itunes:email>{{ channel.owner_email }}</itunes:email>
+</itunes:owner>
+<itunes:image href="{{ channel.image.url }}" />
+<itunes:category text="{{ channel.category }}" />
+<itunes:explicit>{{ channel.explicit }}</itunes:explicit>
+{% for item in channel.item_set.all %}
+<item>
+<title>{{ item.title }}</title>
+<itunes:author>{{ item.author }}</itunes:author>
+<itunes:subtitle>{{ item.subtitle }}</itunes:subtitle>
+<itunes:summary>{{ item.summary }}</itunes:summary>
+<enclosure url="{{ item.enclosure_url }}" length="{{ item.enclosure_length }}" type="{{ item.enclosure_type }}" />
+<guid>{{ item.enclosure_url }}</guid>
+<pubDate>{{ item.pubdate|date:"D, d M Y H:i:s O" }}</pubDate>
+<itunes:duration>{{ item.duration }}</itunes:duration>
+<itunes:keywords>{{ item.keywords }}</itunes:keywords>
+<itunes:explicit>{{ item.explicit }}</itunes:explicit>
+</item>
+{% endfor %}
+{% endif %}
+</channel>
+</rss>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/podcast/index.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,15 @@
+{% extends 'podcast/base.html' %}
+{% block title %}Podcast{% endblock %}
+{% block podcast-content %}
+{% if channel and channel.item_set %}
+   <h3>Podcast Index</h3>
+   <ul>
+   {% for item in channel.item_set.all %}
+      <li><a href="{{ item.get_absolute_url }}">{{ item.title }}</a> &bull;
+         {{ item.subtitle }} &bull; {{ item.pubdate|date:"F d, Y" }}</li>
+   {% endfor %}
+   </ul>
+{% else %}
+<p>No podcasts available at this time.</a>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/polls/index.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,37 @@
+{% extends 'base.html' %}
+{% load comment_tags %}
+{% block title %}Polls{% endblock %}
+{% block content %}
+   <h2>Polls</h2>
+   <h3>Current Polls</h3>
+   {% if current_polls %}
+   <ul>
+   {% for poll in current_polls %}
+      <li><a href="{{ poll.get_absolute_url }}">{{ poll.question }}</a> &bull; 
+      {% get_comment_count for poll as comment_count %}
+      {{ comment_count }} comment{{ comment_count|pluralize }} &bull;
+      {{ poll.start_date|date:"M d, Y" }}
+      {% if poll.end_date %}
+         - {{ poll.end_date|date:"M d, Y" }}
+      {% endif %}
+      </li>
+   {% endfor %}
+   </ul>
+   {% else %}
+   <p>No open polls at this time.</p>
+   {% endif %}
+   <h3>Closed Polls</h3>
+   {% if old_polls %}
+   <ul>
+   {% for poll in old_polls %}
+      <li><a href="{{ poll.get_absolute_url }}">{{ poll.question }}</a> &bull; 
+      {% get_comment_count for poll as comment_count %}
+      {{ comment_count }} comment{{ comment_count|pluralize }} &bull;
+      {{ poll.start_date|date:"M d, Y" }} -
+         {{ poll.end_date|date:"M d, Y" }}</li>
+   {% endfor %}
+   </ul>
+   {% else %}
+   <p>No closed polls at this time.</p>
+   {% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/polls/poll.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,31 @@
+{% extends 'base.html' %}
+{% load comment_tags %}
+{% block title %}Polls: {{ poll.question }}{% endblock %}
+{% block content %}
+<h2>Poll</h2>
+<h3>{{ poll.question }}</h3>
+<ul class="poll-detail">
+{% for choice in poll.choice_set.all %}
+   <li>{{ choice.choice }}</li>
+{% endfor %}
+</ul>
+{% get_comment_count for poll as comment_count %}
+<p>
+This poll has <a href="{% url polls.views.poll_results poll.id %}">{{ comment_count }} comment{{ comment_count|pluralize }}</a>.
+{% if poll.is_open %}
+Voting for this poll started on {{ poll.start_date|date:"F d, Y" }}. 
+   {% if poll.end_date %}
+      Voting will end on {{ poll.end_date|date:"F d, Y" }}.
+   {% endif %}
+{% else %}
+This poll ran from {{ poll.start_date|date:"F d, Y" }} to {{ poll.end_date|date:"F d, Y" }}.
+{% endif %}
+</p>
+<p class="poll-nav">
+<a href="{% url polls.views.poll_results poll.id %}">View Results &amp; Comments</a> 
+{% if poll.is_open and user.is_authenticated %}
+| <a href="{% url polls.views.poll_vote poll_id=poll.id %}">Vote</a>
+{% endif %}
+| <a href="{% url polls.views.poll_index %}">Poll Index</a>
+</p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/polls/poll_results.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,48 @@
+{% extends 'base.html' %}
+{% load comment_tags %}
+{% block title %}Poll Results: {{ poll.question }}{% endblock %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/polls.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/comments.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}js/markitup/skins/markitup/style.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}js/markitup/sets/markdown/style.css" />
+{% endblock %}
+{% block custom_js %}
+<script type="text/javascript" src="{{ MEDIA_URL }}js/jquery-1.2.6.min.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/comments.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/markitup/jquery.markitup.pack.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/markitup/sets/markdown/set.js"></script>
+{% endblock %}
+{% block content %}
+<h2>Polls</h2>
+<h3>{{ poll.question }}</h3>
+<dl class="poll-result">
+{% for choice in choices %}
+   <dt>{{ choice.choice }} - {{ choice.pct|floatformat }}% ({{ choice.votes }} vote{{ choice.votes|pluralize }})</dt>
+   <dd>
+      <div class="poll-percent" style="width: {{ choice.pct|floatformat:0 }}%; background-color: teal; color: white;">
+         <span>&nbsp;</span></div>
+   </dd>
+{% endfor %}
+</dl>
+<p><strong>{{ total_votes }} total vote{{ total_votes|pluralize }}.</strong></p>
+<p class="poll-nav">
+<a href="{{ poll.get_absolute_url }}">Poll Details</a>
+{% if poll.is_open and user.is_authenticated %}
+| <a href="{% url polls.views.poll_vote poll_id=poll.id %}">Vote</a> 
+{% endif %}
+| <a href="{% url polls.views.poll_index %}">Poll Index</a>
+</p>
+
+{% get_comment_count for poll as comment_count %}
+<p>This poll has <span id="comment-count">{{ comment_count }}</span> comment{{ comment_count|pluralize }}.</p>
+<hr />
+{% render_comment_list poll %}
+{% if poll.is_open %}
+<p>Leave a comment?</p>
+{% render_comment_form for poll %}
+{% else %}
+<p>Comments are closed for this poll. If you'd like to share your thoughts on this poll 
+with the site staff, you can <a href="{% url contact-form %}">contact us directly</a>.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/polls/poll_vote.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,21 @@
+{% extends 'base.html' %}
+{% block title %}Polls: {{ poll.question }}{% endblock %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/polls.css" />
+{% endblock %}
+{% block content %}
+<h2>Poll</h2>
+<h3>{{ poll.question }}</h3>
+<form action="." method="post">
+   <div class="poll-form">
+   {{ vote_form.as_p }}
+   <input type="submit" value="Vote" />
+   </div>
+</form>
+<p class="poll-nav">
+<a href="{{ poll.get_absolute_url }}">Poll Details</a>
+| <a href="{% url polls.views.poll_results poll.id %}">View Results</a>
+| <a href="{% url polls.views.poll_index %}">Poll Index</a>
+</p>
+<p>This poll was published on {{ poll.start_date|date:"F d, Y" }}.</p>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/potd/potd_block.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,12 @@
+{% extends 'side_block.html' %}
+{% block block_title %}Photo of the Day{% endblock %}
+{% block block_content %}
+{% if potd %}
+<center>
+<img src="{{ potd.thumb.url }}" alt="{{ potd.caption }}" title="{{ potd.caption }}" /><br />
+<a href="{% url potd-view %}">{{ potd.caption }}</a>
+</center>
+{% else %}
+<p>No photo at this time.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/potd/view.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,49 @@
+{% extends 'base.html' %}
+{% load comment_tags %}
+{% block title %}Photo Of The Day{% endblock %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/potd.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/comments.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}js/markitup/skins/markitup/style.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}js/markitup/sets/markdown/style.css" />
+{% endblock %}
+{% block custom_js %}
+<script type="text/javascript" src="{{ MEDIA_URL }}js/jquery-1.2.6.min.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/comments.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/markitup/jquery.markitup.pack.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/markitup/sets/markdown/set.js"></script>
+{% endblock %}
+{% block content %}
+<h2>Photo Of The Day</h2>
+<h3>{% now "l, F d, Y" %}</h3>
+<div class="potd-details">
+{% if potd %}
+<img src="{{ potd.photo.url }}" alt="{{ potd.caption }}" title="{{ potd.caption }}" />
+<p class="caption">{{ potd.caption }}</p>
+<p class="details">
+Submitted by 
+<a href="{% url bio-view_profile username=potd.user.username %}">{{ potd.user.username }}</a>
+on {{ potd.date_added|date:"d F Y" }}.<br />
+This photo has been Photo of the Day {{ potd.potd_count }} time{{ potd.potd_count|pluralize }}.
+</p>
+<p class="description">{{ potd.description }}</p>
+<p class="details">
+If you would like us to feature your photo, send it along with your username, a title, and 
+short description to <a href="mailto:admin@surfguitar101.com">admin@surfguitar101.com</a>.
+</p>
+{% get_comment_count for potd as comment_count %}
+<p>This photo has <span id="comment-count">{{ comment_count }}</span> comment{{ comment_count|pluralize }}.</p>
+<hr />
+</div>
+{% render_comment_list potd %}
+{% if potd.can_comment_on %}
+<p>Leave a comment?</p>
+{% render_comment_form for potd %}
+{% else %}
+<p>Comments are allowed only on today's photo of the day. If you'd like to share your thoughts on this photo 
+with the site staff, you can <a href="{% url contact-form %}">contact us directly</a>.</p>
+{% endif %}
+{% else %}
+<p>We're sorry, there doesn't seem to be a photo of the day right now.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/shoutbox/render_shout.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,2 @@
+{% load smiley_tags %}
+{{ shout.shout|smilify|urlize }}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/shoutbox/shoutbox.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,39 @@
+{% extends 'side_block.html' %}
+{% load smiley_tags %}
+{% block block_title %}Shoutbox{% endblock %}
+{% block block_content %}
+{% if shouts %}
+<!--<div id="marqueecontainer" onmouseover="copyspeed=pausespeed" onmouseout="copyspeed=marqueespeed">-->
+<div id="marqueecontainer">
+<!--<div id="vmarquee" style="position: absolute; width: 98%; overflow:auto;">-->
+   {# for shout in shouts reversed #}
+   {% for shout in shouts %}
+      <p>
+      <span class="shoutbox-user">{{ shout.user.username }}:</span>
+      <span class="shoutbox-shout">{{ shout.shout|smilify|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="13" name="msg" value="" id="shoutbox-smiley-input" />
+   <br />
+   <input type="submit" value="Shout" />
+   <input type="button" value="Smilies" onclick="sb_toggle_smilies();" />
+</form>
+<div id="shoutbox-smiley-frame" style="display:none;">
+{% smiley_farm %}
+</div>
+</center>
+{% else %}
+<p>
+Please <a href="{% url accounts-login %}">login</a> or
+<a href="{% url accounts-register %}">register</a> to shout.
+</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/shoutbox/view.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,45 @@
+{% extends 'base.html' %}
+{% load avatar_tags %}
+{% load smiley_tags %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/shoutbox_app.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/pagination.css" />
+{% endblock %}
+{% block custom_js %}
+<script type="text/javascript" src="{{ MEDIA_URL }}js/jquery-1.2.6.min.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/jquery.jeditable.mini.js"></script>
+<script type="text/javascript" src="{{ MEDIA_URL }}js/shoutbox_app.js"></script>
+{% endblock %}
+{% block title %}Shout History{% endblock %}
+{% block content %}
+<h2>Shout History</h2>
+{% if page.object_list %}
+{% include 'core/pagination.html' %}
+
+<div class="shoutbox-history">
+<table>
+{% for shout in page.object_list %}
+    <tr>
+    <th>
+    <a href="{% url bio-view_profile username=shout.user.username %}">{% avatar shout.user %}</a>
+    <a href="{% url bio-view_profile username=shout.user.username %}">{{ shout.user.username }}</a>
+    </th>
+    <td>
+<div {% ifequal user shout.user %}class="edit" id="shout-{{ shout.id }}"{% endifequal %}>{{ shout.shout|smilify|urlize }}</div>
+    </div>
+    <br />
+    <span class="date">{{ shout.shout_date|date:"D M d Y H:i:s" }}</span>
+    {% ifequal user shout.user %}
+        | <a href="#" class="shout-del" id="shout-del-{{ shout.id }}">Delete</a>
+    {% endifequal %}
+    </td>
+    </tr>
+{% endfor %}
+</table>
+</div>
+
+{% include 'core/pagination.html' %}
+{% else %}
+<p>No shouts at this time.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/side_block.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,8 @@
+<div class="side-block">
+<div class="side-block-title">
+{% block block_title %}{% endblock %}
+</div>
+<div class="side-block-content">
+{% block block_content %}{% endblock %}
+</div>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/smiley/smiley_farm.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,6 @@
+<div class="smiley_farm">
+{% for s in smilies %}
+    <img src="{{ s.image.url }}" alt="{{ s.code }}" title="{{ s.title }} {{ s.code }}"
+        onclick="sb_smiley_click(' {{ s.code }} ');" />
+{% endfor %}
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/weblinks/add_link.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,18 @@
+{% extends 'weblinks/base.html' %}
+{% block title %}Web Links: Add Link{% endblock %}
+{% block weblinks_content %}
+   <h3>Add Link</h3>
+   {% if add_form %}
+   <form action="." method="post">
+   <table>
+      {{ add_form.as_table }}
+      <tr><td>&nbsp;</td><td><input type="submit" value="Add Link" />
+         <input type="button" value="Cancel" onclick="history.back(); return true;" /></td></tr>
+   </table>
+   </form>
+   <br />
+   {% else %}
+   <p><strong>Thank you for submitting a link!</strong></p>
+   <p>Your link has been submitted for review to the site staff.</p>
+   {% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/weblinks/base.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,29 @@
+{% extends 'base.html' %}
+{% block custom_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/weblinks.css" />
+{% block weblinks_css %}{% endblock %}
+{% endblock %}
+{% block content %}
+<h2>Web Links</h2>
+
+<div class="weblinks-search">
+<form action="{% url weblinks-search page=1 %}" method="post">
+   <p>{{ search_form.text }} <input type="submit" value="Search" /></p>
+</form>
+</div>
+
+<ul class="app-menu">
+<li><a href="{% url weblinks.views.link_index %}">Categories</a></li>
+<li><a href="{% url weblinks.views.new_links %}">New</a></li>
+<li><a href="{% url weblinks.views.popular_links %}">Popular</a></li>
+<li><a href="{% url weblinks.views.random_link %}" target="_blank">Random</a></li>
+{% if user.is_authenticated %}
+<li><a href="{% url weblinks.views.add_link %}">Add</a></li>
+{% endif %}
+</ul>
+
+<div class="weblinks-content">
+   {% block weblinks_content %}
+   {% endblock %}
+</div>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/weblinks/index.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,15 @@
+{% extends 'weblinks/base.html' %}
+{% block title %}Web Links{% endblock %}
+{% block weblinks_content %}
+   <h3>Categories</h3>
+   {% if categories %}
+      <p>We have {{ total_links }} links in {{ categories.count }} categories.</p>
+      <dl>
+      {% for category in categories %}
+      <dt><a href="{% url weblinks-view_links category=category.id,sort="title",page=1 %}">{{ category.title }}</a>
+       ({{ category.num_links}})</dt>
+         <dd><p>{{ category.description }}</p></dd>
+      {% endfor %}
+      </dl>
+   {% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/weblinks/link.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,16 @@
+<dt>
+<a href="{% url weblinks.views.visit link.id %}" target="_blank">{{ link.title }}</a>
+</dt>
+<dd>
+<p>{{ link.description }}</p>
+<table class="link-stats">
+   <tr>
+      <th>Date:</th><td>{{ link.date_added|date:"M d, Y" }}</td>
+      <th>Hits:</th><td>{{ link.hits }}</td>
+   </tr>
+</table>
+<p>
+   <a href="{% url weblinks.views.report_link link.id %}">Report Broken Link</a>
+</p>
+<br />
+</dd>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/weblinks/link_summary.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,12 @@
+{% extends 'weblinks/base.html' %}
+{% block title %}Web Links: {{ title }}{% endblock %}
+{% block weblinks_content %}
+   <h3>{{ title }}</h3>
+   {% if links %}
+   <dl>
+   {% for link in links %}
+      {% include 'weblinks/link.html' %}
+   {% endfor %}
+   </dl>
+   {% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/weblinks/report_link.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,19 @@
+{% extends 'weblinks/base.html' %}
+{% block title %}Web Links: Report Broken Link{% endblock %}
+{% block weblinks_content %}
+   <h3>Report Broken Link</h3>
+   {% if report_thanks %}
+   <p><strong>Thank you for helping to keep our links database current</strong>. Your report has
+   been sent to the site staff for review.</p>
+   {% else %}
+   <p>
+   Do you wish to report <a href="{% url weblinks.views.visit link.id %}" target="_blank">{{ link.title }}</a>
+   as a broken link? This will notify the site staff that the link is now dead and may need to be deleted or
+   revised.</p>
+   <form action="." method="post">
+      <input type="submit" value="Yes, this link is broken" />
+      <input type="button" value="Oops, nevermind" onclick="history.back(); return true;" />
+   </form>
+   <br />
+   {% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/weblinks/search_results.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,23 @@
+{% extends 'weblinks/base.html' %}
+{% block title %}Web Links: Search Results{% endblock %}
+{% block weblinks_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/pagination.css" />
+{% endblock %}
+{% block weblinks_content %}
+<h3>Search Results: {{ query }}</h3>
+
+{% include 'core/pagination_query.html' %}
+
+{% if page.object_list %}
+<dl>
+{% for link in page.object_list %}
+   {% include 'weblinks/link.html' %}
+{% endfor %}
+</dl>
+
+{% include 'core/pagination_query.html' %}
+
+{% else %}
+<p>No results found.</p>
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/weblinks/view_links.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,30 @@
+{% extends 'weblinks/base.html' %}
+{% block title %}Web Links: {{ category.title }}{% endblock %}
+{% block weblinks_css %}
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/tab-nav.css" />
+<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/pagination.css" />
+{% endblock %}
+{% block weblinks_content %}
+<h3>Category: {{ category.title }}</h3>
+
+{% if page.object_list %}
+<ul class="tab-nav">
+   <li><a href="{% url weblinks-view_links category=category.id,sort="title",page="1" %}"
+      {% ifequal s "title" %}class="active" {% endifequal %}>Title</a></li>
+   <li><a href="{% url weblinks-view_links category=category.id,sort="date",page="1" %}"
+      {% ifequal s "date" %}class="active"{% endifequal %}>Date</a></li>
+   <li><a href="{% url weblinks-view_links category=category.id,sort="hits",page="1" %}"
+      {% ifequal s "hits" %}class="active"{% endifequal %}>Hits</a></li>
+</ul>
+
+{% include 'core/pagination.html' %}
+
+<dl>
+{% for link in page.object_list %}
+   {% include 'weblinks/link.html' %}
+{% endfor %}
+</dl>
+
+{% include 'core/pagination.html' %}
+{% endif %}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,41 @@
+from django.conf.urls.defaults import *
+
+import settings
+from django.contrib import admin
+from news.feeds import LatestNewsFeed
+
+admin.autodiscover()
+
+feeds = {
+   'news': LatestNewsFeed,
+}
+
+urlpatterns = patterns('',
+   (r'^admin/doc/', include('django.contrib.admindocs.urls')),
+   (r'^admin/', include(admin.site.urls)),
+   (r'^accounts/', include('accounts.urls')),
+   (r'^calendar/', include('gcalendar.urls')),
+   (r'^comments/', include('comments.urls')),
+   (r'^contact/', include('contact.urls')),
+   (r'^downloads/', include('downloads.urls')),
+   url(r'^feeds/(?P<url>.*)/$', 
+      'django.contrib.syndication.views.feed', 
+      {'feed_dict': feeds },
+      'feeds-news'),
+   (r'^irc/', include('irc.urls')),
+   (r'^legal/', include('legal.urls')),
+   (r'^links/', include('weblinks.urls')),
+   (r'^member_map/', include('membermap.urls')),
+   (r'^messages/', include('messages.urls')),
+   (r'^news/', include('news.urls')),
+   (r'^podcast/', include('podcast.urls')),
+   (r'^polls/', include('polls.urls')),
+   (r'^potd/', include('potd.urls')),
+   (r'^profile/', include('bio.urls')),
+   (r'^shout/', include('shoutbox.urls')),
+)
+
+if settings.DEBUG:
+   urlpatterns += patterns('',
+      (r'^static/(?P<path>.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}),
+   )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/weblinks/admin.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,24 @@
+"""This file contains the automatic admin site definitions for the weblinks models"""
+
+from django.contrib import admin
+from weblinks.models import Category
+from weblinks.models import Link
+from weblinks.models import FlaggedLink
+
+class LinkAdmin(admin.ModelAdmin):
+    list_display = ('title', 'url', 'category', 'date_added', 'hits', 'is_public')
+    list_filter = ('date_added', 'is_public', 'category')
+    date_hierarchy = 'date_added'
+    ordering = ('-date_added', )
+    search_fields = ('title', 'description', 'url', 'user__username')
+    raw_id_fields = ('user', )
+    save_on_top = True
+
+class FlaggedLinkAdmin(admin.ModelAdmin):
+    list_display = ('__unicode__', 'url', 'date_flagged')
+    date_hierarchy = 'date_flagged'
+    raw_id_fields = ('user', )
+
+admin.site.register(Category)
+admin.site.register(Link, LinkAdmin)
+admin.site.register(FlaggedLink, FlaggedLinkAdmin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/weblinks/forms.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,30 @@
+"""
+Forms for the weblinks application.
+"""
+
+from django import forms
+from weblinks.models import Link
+
+class SearchForm(forms.Form):
+   '''Weblinks search form'''
+   text = forms.CharField(max_length = 30)
+
+   def query(self):
+      return self.cleaned_data['text']
+
+
+class AddLinkForm(forms.ModelForm):
+   title = forms.CharField(widget = forms.TextInput(attrs = {'size': 52}))
+   url = forms.CharField(widget = forms.TextInput(attrs = {'size': 52}))
+
+   def clean_url(self):
+      new_url = self.cleaned_data['url']
+      try:
+         Link.objects.get(url__iexact = new_url)
+      except Link.DoesNotExist:
+         return new_url
+      raise forms.ValidationError('That link already exists in our database.')
+
+   class Meta:
+      model = Link
+      exclude = ('user', 'date_added', 'hits', 'is_public')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/weblinks/models.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,84 @@
+'''
+This module contains the models for the weblinks application.
+'''
+
+from django.db import models
+from django.contrib import auth
+
+
+class Category(models.Model):
+    '''Links belong to categories'''
+    title = models.CharField(max_length = 64)
+    description = models.TextField(blank = True)
+
+    def __unicode__(self):
+        return self.title
+
+    def num_links(self):
+        return Link.public_objects.filter(category = self.pk).count()
+
+    class Meta:
+        verbose_name_plural = 'Categories'
+        ordering = ('title', )
+
+
+class PublicLinkManager(models.Manager):
+    """The manager for all public links."""
+    def get_query_set(self):
+        return super(PublicLinkManager, self).get_query_set().filter(is_public=True)
+
+
+class Link(models.Model):
+    '''Model to represent a web link'''
+    category = models.ForeignKey(Category)
+    title = models.CharField(max_length=128)
+    url = models.URLField(verify_exists=False, db_index=True)
+    description = models.TextField(blank=True)
+    user = models.ForeignKey(auth.models.User)
+    date_added = models.DateField(auto_now_add=True)
+    hits = models.IntegerField(default=0)
+    is_public = models.BooleanField(default=False, db_index=True)
+
+    # Managers:
+    objects = models.Manager()
+    public_objects = PublicLinkManager()
+
+    def __unicode__(self):
+        return self.title
+
+    class Meta:
+        ordering = ('title', )
+
+
+class FlaggedLinkManager(models.Manager):
+
+    def create(self, link, user):
+        flagged_link = FlaggedLink(link = link, user = user, approved = False)
+        flagged_link.save()
+
+
+class FlaggedLink(models.Model):
+    '''Model to represent links that have been flagged as broken by users'''
+    link = models.ForeignKey(Link)
+    user = models.ForeignKey(auth.models.User)
+    date_flagged = models.DateField(auto_now_add = True)
+    approved = models.BooleanField(default = False, 
+        help_text = 'Check this and save to remove the referenced link from the database')
+
+    objects = FlaggedLinkManager()
+
+    def save(self, force_insert = False, force_update = False):
+        if self.approved:
+            self.link.delete()
+            self.delete()
+        else:
+            super(FlaggedLink, self).save(force_insert, force_update)
+
+    def url(self):
+        return self.link.url
+
+    def __unicode__(self):
+        return self.link.title
+
+    class Meta:
+        ordering = ('-date_flagged', )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/weblinks/urls.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,20 @@
+"""urls for the weblinks application"""
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('weblinks.views',
+   url(r'^$', 'link_index', name='weblinks-main'),
+   (r'^add/$', 'add_link'),
+   (r'^add/thanks/$', 'add_thanks'),
+   url(r'^category/(?P<category>\d+)/(?P<sort>title|date|rating|hits)/page/(?P<page>\d+)/$', 
+      'view_links',
+      name='weblinks-view_links'),
+   (r'^new/$', 'new_links'),
+   (r'^popular/$', 'popular_links'),
+   (r'^random/$', 'random_link'),
+   (r'^report/(\d+)/$', 'report_link'),
+   (r'^report/thanks/(\d+)$', 'report_thanks'),
+   url(r'^search/page/(?P<page>\d+)/$', 
+      'search_links',
+      name="weblinks-search"),
+   (r'^visit/(\d+)/$', 'visit'),
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/weblinks/views.py	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,226 @@
+"""
+Views for the weblinks application.
+"""
+
+import datetime
+import random
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.contrib import auth
+from django.http import HttpResponseRedirect
+from django.contrib.auth.decorators import login_required
+from django.shortcuts import get_object_or_404
+from django.core.urlresolvers import reverse
+from django.db.models import Q
+from django.http import Http404
+
+from core.paginator import DiggPaginator
+from core.functions import email_admins
+from weblinks.models import Category
+from weblinks.models import Link
+from weblinks.models import FlaggedLink
+from weblinks.forms import SearchForm
+from weblinks.forms import AddLinkForm
+
+#######################################################################
+
+LINKS_PER_PAGE = 10
+
+def create_paginator(links):
+   return DiggPaginator(links, LINKS_PER_PAGE, body=5, tail=3, margin=3, padding=2)
+
+#######################################################################
+
+def link_index(request):
+   categories = Category.objects.all()
+   total_links = Link.public_objects.all().count()
+   form = SearchForm()
+   return render_to_response('weblinks/index.html', {
+      'categories': categories, 
+      'total_links': total_links,
+      'search_form': form,
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def new_links(request):
+   links = Link.public_objects.order_by('-date_added')[:LINKS_PER_PAGE]
+   return render_to_response('weblinks/link_summary.html', {
+      'links': links, 
+      'title': 'Newest Links',
+      'search_form': SearchForm(),
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def popular_links(request):
+   links = Link.public_objects.order_by('-hits')[:LINKS_PER_PAGE]
+   return render_to_response('weblinks/link_summary.html', {
+      'links': links, 
+      'title': 'Popular Links',
+      'search_form': SearchForm(),
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def add_link(request):
+   if request.method == 'POST':
+      add_form = AddLinkForm(request.POST)
+      if add_form.is_valid():
+         new_link = add_form.save(commit=False)
+         new_link.user = request.user
+         new_link.is_public = False
+         new_link.save()
+         email_admins('New link for approval', """Hello,
+
+A user has added a new link for your approval.
+""")
+         return HttpResponseRedirect(reverse('weblinks.views.add_thanks'))
+   else:
+      add_form = AddLinkForm()
+
+   return render_to_response('weblinks/add_link.html', {
+      'search_form': SearchForm(),
+      'add_form': add_form,
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+@login_required
+def add_thanks(request):
+   return render_to_response('weblinks/add_link.html', {
+      'search_form': SearchForm(),
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+# Maps URL component to database field name for the links table:
+
+LINK_FIELD_MAP = {
+   'title': 'title', 
+   'date': '-date_added',
+   'hits': '-hits'
+}
+
+def view_links(request, category, sort='title', page='1'):
+   try:
+      cat = Category.objects.get(pk=category)
+   except Category.DoesNotExist:
+      raise Http404
+
+   if sort in LINK_FIELD_MAP:
+      order_by = LINK_FIELD_MAP[sort]
+   else:
+      sort = 'title'
+      order_by = LINK_FIELD_MAP['title']
+
+   links = Link.public_objects.filter(category = category).order_by(order_by)
+   paginator = create_paginator(links)
+   try:
+      the_page = paginator.page(int(page))
+   except InvalidPage:
+      raise Http404
+
+   return render_to_response('weblinks/view_links.html', {
+      's' : sort,
+      'category' : cat,
+      'page' : the_page, 
+      'search_form': SearchForm(),
+      }, 
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def _visit_link(request, link):
+   link.hits += 1
+   link.save()
+   return HttpResponseRedirect(link.url)
+
+#######################################################################
+
+def visit(request, link_id):
+   link = get_object_or_404(Link, pk = link_id)
+   return _visit_link(request, link)
+
+#######################################################################
+
+def random_link(request):
+   ids = Link.public_objects.values_list('id', flat=True)
+   if not ids:
+       raise Http404
+   id = random.choice(ids)
+   random_link = Link.public_objects.get(pk=id)
+   return _visit_link(request, random_link)
+
+#######################################################################
+
+@login_required
+def report_link(request, link_id):
+   link = get_object_or_404(Link, pk = link_id)
+   if request.method == "POST":
+      FlaggedLink.objects.create(link, request.user)
+      email_admins('A Link Has Been Flagged as Broken', """Hello,
+
+A user has flagged a link as broken.
+""")
+      return HttpResponseRedirect(reverse('weblinks.views.report_thanks', args = (link_id, )))
+
+   return render_to_response('weblinks/report_link.html', {
+      'link': link, 
+      'search_form': SearchForm(),
+      'report_thanks': False,
+      },
+      context_instance = RequestContext(request))
+
+
+#######################################################################
+
+@login_required
+def report_thanks(request, link_id):
+   link = get_object_or_404(Link, pk = link_id)
+   return render_to_response('weblinks/report_link.html', {
+      'link': link, 
+      'search_form': SearchForm(),
+      'report_thanks': True,
+      },
+      context_instance = RequestContext(request))
+
+#######################################################################
+
+def search_links(request, page=1):
+   if request.method == 'POST':
+      form = SearchForm(request.POST)
+      if form.is_valid():
+         query_text = form.query()
+         page = 1
+      else:
+         return HttpResponseRedirect(reverse('weblinks.views.link_index'))
+   else:
+      if 'query' in request.GET:
+         query_text = request.GET['query']
+      else:
+         return HttpResponseRedirect(reverse('weblinks.views.link_index'))
+
+   links = Link.public_objects.filter(
+         Q(title__icontains = query_text) |
+         Q(description__icontains = query_text)).order_by('title')
+   paginator = create_paginator(links)
+   try:
+      the_page = paginator.page(int(page))
+   except EmptyPage:
+      links = Link.public_objects.none()
+   except InvalidPage:
+      raise Http404
+
+   return render_to_response('weblinks/search_results.html', {
+      'query': query_text,
+      'page': the_page, 
+      'search_form': SearchForm(),
+      }, 
+      context_instance = RequestContext(request))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/base.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,100 @@
+body {
+ background-color: #eee;
+}
+#page {
+}
+#header {
+ background-color: teal;
+}
+#header h1 {
+ border-bottom: 1px solid #ccc;
+ font: normal italic 1.5em/18px Georgia, serif;
+ margin-bottom: 11px;
+ color: #57FEFF;
+ margin-left: -108px;
+ padding: 15px 0 21px 108px; 
+}
+#header p {
+ float: left;
+ padding: 60px 24px 24px 24px;
+ font-size: 1.5em;
+ line-height: 1em;
+}
+#header ul {
+ padding-top: 62px;
+ padding-right: 12px;
+ text-align: right;
+ list-style: none;
+}
+#header li {
+ display: inline;
+ margin: 0 12px;
+}
+#header li a {
+ font-size: 1.25em;
+ line-height: 1em;
+ text-decoration: none;
+ color: #57FEFF;
+}
+#header li a:hover {
+ color: #300;
+} 
+#content-primary {
+}
+#content-secondary {
+}
+#footer {
+ height: 192px;
+ background: #333;
+ font-size: 10px;
+ line-height: 1em;
+ text-shadow: 1px 1px 1px #333;
+ color: #fff; 
+}
+#footer p {
+ padding: 24px;
+} 
+ul.app-menu {
+   text-align: center;
+   list-style: none;
+}
+ul.app-menu li {
+   display: inline;
+   border: 1px solid black;
+   padding: 3px 1em;
+   background-color: teal;
+}
+ul.app-menu li a {
+   text-decoration: none;
+   background-color: teal;
+   color: #111;
+}
+ul.app-menu li a:hover {
+   color: #fff;
+}
+div.side-block {
+   margin: 0 0 0.5em 0;
+   padding: 2px 2px;
+   width: 100%;
+}
+div.side-block-title {
+   margin: 0;
+   background-color: teal;
+   color: white;
+   font-weight: bold;
+   text-align: center;
+}
+div.side-block-content {
+   margin: 0;
+   border: 1px solid teal;
+   padding: 2px 2px;
+}
+iframe {
+   margin-bottom: 1em;
+}
+img {
+   border-style: none;
+}
+.breadcrumbs {
+   font-size: x-small;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/bio.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,37 @@
+div.user_profile table tr th {
+   font-weight: bold;
+   text-align: left;
+   padding: 5px 5px;
+}
+div.user_profile table tr td {
+   font-weight: normal;
+   text-align: left;
+   padding: 5px 5px;
+}
+
+div.members-list table {
+   border-collapse: collapse;
+   width: 95%;
+   border: 1px solid black;
+   margin: 1em auto 1em auto;
+}
+
+div.members-list table tr th {
+   font-weight: bold;
+   text-align: center;
+   padding: 5px 5px;
+}
+
+div.members-list table tr {
+   border-top: 1px solid black;
+   border-bottom: 1px solid black;
+   text-align: center;
+}
+
+div.members-list table tr td {
+   padding: 5px 5px;
+}
+
+div.members-list table tr.odd {
+   background-color: #ddd;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/blueprint/ie.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,26 @@
+/* -----------------------------------------------------------------------
+
+
+ Blueprint CSS Framework 0.8
+ http://blueprintcss.org
+
+   * Copyright (c) 2007-Present. See LICENSE for more info.
+   * See README for instructions on how to use Blueprint.
+   * For credits and origins, see AUTHORS.
+   * This is a compressed file. See the sources in the 'src' directory.
+
+----------------------------------------------------------------------- */
+
+/* ie.css */
+body {text-align:center;}
+.container {text-align:left;}
+* html .column, * html div.span-1, * html div.span-2, * html div.span-3, * html div.span-4, * html div.span-5, * html div.span-6, * html div.span-7, * html div.span-8, * html div.span-9, * html div.span-10, * html div.span-11, * html div.span-12, * html div.span-13, * html div.span-14, * html div.span-15, * html div.span-16, * html div.span-17, * html div.span-18, * html div.span-19, * html div.span-20, * html div.span-21, * html div.span-22, * html div.span-23, * html div.span-24 {overflow-x:hidden;}
+* html legend {margin:0px -8px 16px 0;padding:0;}
+ol {margin-left:2em;}
+sup {vertical-align:text-top;}
+sub {vertical-align:text-bottom;}
+html>body p code {*white-space:normal;}
+hr {margin:-8px auto 11px;}
+.clearfix, .container {display:inline-block;}
+* html .clearfix, * html .container {height:1%;}
+fieldset {padding-top:0;}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/blueprint/print.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,30 @@
+/* -----------------------------------------------------------------------
+
+
+ Blueprint CSS Framework 0.8
+ http://blueprintcss.org
+
+   * Copyright (c) 2007-Present. See LICENSE for more info.
+   * See README for instructions on how to use Blueprint.
+   * For credits and origins, see AUTHORS.
+   * This is a compressed file. See the sources in the 'src' directory.
+
+----------------------------------------------------------------------- */
+
+/* print.css */
+body {line-height:1.5;font-family:"Helvetica Neue", Arial, Helvetica, sans-serif;color:#000;background:none;font-size:10pt;}
+.container {background:none;}
+hr {background:#ccc;color:#ccc;width:100%;height:2px;margin:2em 0;padding:0;border:none;}
+hr.space {background:#fff;color:#fff;}
+h1, h2, h3, h4, h5, h6 {font-family:"Helvetica Neue", Arial, "Lucida Grande", sans-serif;}
+code {font:.9em "Courier New", Monaco, Courier, monospace;}
+img {float:left;margin:1.5em 1.5em 1.5em 0;}
+a img {border:none;}
+p img.top {margin-top:0;}
+blockquote {margin:1.5em;padding:1em;font-style:italic;font-size:.9em;}
+.small {font-size:.9em;}
+.large {font-size:1.1em;}
+.quiet {color:#999;}
+.hide {display:none;}
+a:link, a:visited {background:transparent;font-weight:700;text-decoration:underline;}
+a:link:after, a:visited:after {content:" (" attr(href) ") ";font-size:90%;}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/blueprint/screen.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,251 @@
+/* -----------------------------------------------------------------------
+
+
+ Blueprint CSS Framework 0.8
+ http://blueprintcss.org
+
+   * Copyright (c) 2007-Present. See LICENSE for more info.
+   * See README for instructions on how to use Blueprint.
+   * For credits and origins, see AUTHORS.
+   * This is a compressed file. See the sources in the 'src' directory.
+
+----------------------------------------------------------------------- */
+
+/* reset.css */
+html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, code, del, dfn, em, img, q, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td {margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;}
+body {line-height:1.5;}
+table {border-collapse:separate;border-spacing:0;}
+caption, th, td {text-align:left;font-weight:normal;}
+table, td, th {vertical-align:middle;}
+blockquote:before, blockquote:after, q:before, q:after {content:"";}
+blockquote, q {quotes:"" "";}
+a img {border:none;}
+
+/* typography.css */
+body {font-size:75%;color:#222;background:#fff;font-family:"Helvetica Neue", Arial, Helvetica, sans-serif;}
+h1, h2, h3, h4, h5, h6 {font-weight:normal;color:#111;}
+h1 {font-size:3em;line-height:1;margin-bottom:0.5em;}
+h2 {font-size:2em;margin-bottom:0.75em;}
+h3 {font-size:1.5em;line-height:1;margin-bottom:1em;}
+h4 {font-size:1.2em;line-height:1.25;margin-bottom:1.25em;}
+h5 {font-size:1em;font-weight:bold;margin-bottom:1.5em;}
+h6 {font-size:1em;font-weight:bold;}
+h1 img, h2 img, h3 img, h4 img, h5 img, h6 img {margin:0;}
+p {margin:0 0 1.5em;}
+p img.left {float:left;margin:1.5em 1.5em 1.5em 0;padding:0;}
+p img.right {float:right;margin:1.5em 0 1.5em 1.5em;}
+a:focus, a:hover {color:#000;}
+a {color:#009;text-decoration:underline;}
+blockquote {margin:1.5em;color:#666;font-style:italic;}
+strong {font-weight:bold;}
+em, dfn {font-style:italic;}
+dfn {font-weight:bold;}
+sup, sub {line-height:0;}
+abbr, acronym {border-bottom:1px dotted #666;}
+address {margin:0 0 1.5em;font-style:italic;}
+del {color:#666;}
+pre {margin:1.5em 0;white-space:pre;}
+pre, code, tt {font:1em 'andale mono', 'lucida console', monospace;line-height:1.5;}
+li ul, li ol {margin:0 1.5em;}
+ul, ol {margin:0 1.5em 1.5em 1.5em;}
+ul {list-style-type:disc;}
+ol {list-style-type:decimal;}
+dl {margin:0 0 1.5em 0;}
+dl dt {font-weight:bold;}
+dd {margin-left:1.5em;}
+table {margin-bottom:1.4em;width:100%;}
+th {font-weight:bold;}
+thead th {background:#c3d9ff;}
+th, td, caption {padding:4px 10px 4px 5px;}
+tr.even td {background:#e5ecf9;}
+tfoot {font-style:italic;}
+caption {background:#eee;}
+.small {font-size:.8em;margin-bottom:1.875em;line-height:1.875em;}
+.large {font-size:1.2em;line-height:2.5em;margin-bottom:1.25em;}
+.hide {display:none;}
+.quiet {color:#666;}
+.loud {color:#000;}
+.highlight {background:#ff0;}
+.added {background:#060;color:#fff;}
+.removed {background:#900;color:#fff;}
+.first {margin-left:0;padding-left:0;}
+.last {margin-right:0;padding-right:0;}
+.top {margin-top:0;padding-top:0;}
+.bottom {margin-bottom:0;padding-bottom:0;}
+
+/* grid.css */
+.container {width:950px;margin:0 auto;}
+.showgrid {background:url(src/grid.png);}
+.column, div.span-1, div.span-2, div.span-3, div.span-4, div.span-5, div.span-6, div.span-7, div.span-8, div.span-9, div.span-10, div.span-11, div.span-12, div.span-13, div.span-14, div.span-15, div.span-16, div.span-17, div.span-18, div.span-19, div.span-20, div.span-21, div.span-22, div.span-23, div.span-24 {float:left;margin-right:10px;}
+.last, div.last {margin-right:0;}
+.span-1 {width:30px;}
+.span-2 {width:70px;}
+.span-3 {width:110px;}
+.span-4 {width:150px;}
+.span-5 {width:190px;}
+.span-6 {width:230px;}
+.span-7 {width:270px;}
+.span-8 {width:310px;}
+.span-9 {width:350px;}
+.span-10 {width:390px;}
+.span-11 {width:430px;}
+.span-12 {width:470px;}
+.span-13 {width:510px;}
+.span-14 {width:550px;}
+.span-15 {width:590px;}
+.span-16 {width:630px;}
+.span-17 {width:670px;}
+.span-18 {width:710px;}
+.span-19 {width:750px;}
+.span-20 {width:790px;}
+.span-21 {width:830px;}
+.span-22 {width:870px;}
+.span-23 {width:910px;}
+.span-24, div.span-24 {width:950px;margin:0;}
+input.span-1, textarea.span-1, select.span-1 {width:30px!important;}
+input.span-2, textarea.span-2, select.span-2 {width:50px!important;}
+input.span-3, textarea.span-3, select.span-3 {width:90px!important;}
+input.span-4, textarea.span-4, select.span-4 {width:130px!important;}
+input.span-5, textarea.span-5, select.span-5 {width:170px!important;}
+input.span-6, textarea.span-6, select.span-6 {width:210px!important;}
+input.span-7, textarea.span-7, select.span-7 {width:250px!important;}
+input.span-8, textarea.span-8, select.span-8 {width:290px!important;}
+input.span-9, textarea.span-9, select.span-9 {width:330px!important;}
+input.span-10, textarea.span-10, select.span-10 {width:370px!important;}
+input.span-11, textarea.span-11, select.span-11 {width:410px!important;}
+input.span-12, textarea.span-12, select.span-12 {width:450px!important;}
+input.span-13, textarea.span-13, select.span-13 {width:490px!important;}
+input.span-14, textarea.span-14, select.span-14 {width:530px!important;}
+input.span-15, textarea.span-15, select.span-15 {width:570px!important;}
+input.span-16, textarea.span-16, select.span-16 {width:610px!important;}
+input.span-17, textarea.span-17, select.span-17 {width:650px!important;}
+input.span-18, textarea.span-18, select.span-18 {width:690px!important;}
+input.span-19, textarea.span-19, select.span-19 {width:730px!important;}
+input.span-20, textarea.span-20, select.span-20 {width:770px!important;}
+input.span-21, textarea.span-21, select.span-21 {width:810px!important;}
+input.span-22, textarea.span-22, select.span-22 {width:850px!important;}
+input.span-23, textarea.span-23, select.span-23 {width:890px!important;}
+input.span-24, textarea.span-24, select.span-24 {width:940px!important;}
+.append-1 {padding-right:40px;}
+.append-2 {padding-right:80px;}
+.append-3 {padding-right:120px;}
+.append-4 {padding-right:160px;}
+.append-5 {padding-right:200px;}
+.append-6 {padding-right:240px;}
+.append-7 {padding-right:280px;}
+.append-8 {padding-right:320px;}
+.append-9 {padding-right:360px;}
+.append-10 {padding-right:400px;}
+.append-11 {padding-right:440px;}
+.append-12 {padding-right:480px;}
+.append-13 {padding-right:520px;}
+.append-14 {padding-right:560px;}
+.append-15 {padding-right:600px;}
+.append-16 {padding-right:640px;}
+.append-17 {padding-right:680px;}
+.append-18 {padding-right:720px;}
+.append-19 {padding-right:760px;}
+.append-20 {padding-right:800px;}
+.append-21 {padding-right:840px;}
+.append-22 {padding-right:880px;}
+.append-23 {padding-right:920px;}
+.prepend-1 {padding-left:40px;}
+.prepend-2 {padding-left:80px;}
+.prepend-3 {padding-left:120px;}
+.prepend-4 {padding-left:160px;}
+.prepend-5 {padding-left:200px;}
+.prepend-6 {padding-left:240px;}
+.prepend-7 {padding-left:280px;}
+.prepend-8 {padding-left:320px;}
+.prepend-9 {padding-left:360px;}
+.prepend-10 {padding-left:400px;}
+.prepend-11 {padding-left:440px;}
+.prepend-12 {padding-left:480px;}
+.prepend-13 {padding-left:520px;}
+.prepend-14 {padding-left:560px;}
+.prepend-15 {padding-left:600px;}
+.prepend-16 {padding-left:640px;}
+.prepend-17 {padding-left:680px;}
+.prepend-18 {padding-left:720px;}
+.prepend-19 {padding-left:760px;}
+.prepend-20 {padding-left:800px;}
+.prepend-21 {padding-left:840px;}
+.prepend-22 {padding-left:880px;}
+.prepend-23 {padding-left:920px;}
+div.border {padding-right:4px;margin-right:5px;border-right:1px solid #eee;}
+div.colborder {padding-right:24px;margin-right:25px;border-right:1px solid #eee;}
+.pull-1 {margin-left:-40px;}
+.pull-2 {margin-left:-80px;}
+.pull-3 {margin-left:-120px;}
+.pull-4 {margin-left:-160px;}
+.pull-5 {margin-left:-200px;}
+.pull-6 {margin-left:-240px;}
+.pull-7 {margin-left:-280px;}
+.pull-8 {margin-left:-320px;}
+.pull-9 {margin-left:-360px;}
+.pull-10 {margin-left:-400px;}
+.pull-11 {margin-left:-440px;}
+.pull-12 {margin-left:-480px;}
+.pull-13 {margin-left:-520px;}
+.pull-14 {margin-left:-560px;}
+.pull-15 {margin-left:-600px;}
+.pull-16 {margin-left:-640px;}
+.pull-17 {margin-left:-680px;}
+.pull-18 {margin-left:-720px;}
+.pull-19 {margin-left:-760px;}
+.pull-20 {margin-left:-800px;}
+.pull-21 {margin-left:-840px;}
+.pull-22 {margin-left:-880px;}
+.pull-23 {margin-left:-920px;}
+.pull-24 {margin-left:-960px;}
+.pull-1, .pull-2, .pull-3, .pull-4, .pull-5, .pull-6, .pull-7, .pull-8, .pull-9, .pull-10, .pull-11, .pull-12, .pull-13, .pull-14, .pull-15, .pull-16, .pull-17, .pull-18, .pull-19, .pull-20, .pull-21, .pull-22, .pull-23, .pull-24 {float:left;position:relative;}
+.push-1 {margin:0 -40px 1.5em 40px;}
+.push-2 {margin:0 -80px 1.5em 80px;}
+.push-3 {margin:0 -120px 1.5em 120px;}
+.push-4 {margin:0 -160px 1.5em 160px;}
+.push-5 {margin:0 -200px 1.5em 200px;}
+.push-6 {margin:0 -240px 1.5em 240px;}
+.push-7 {margin:0 -280px 1.5em 280px;}
+.push-8 {margin:0 -320px 1.5em 320px;}
+.push-9 {margin:0 -360px 1.5em 360px;}
+.push-10 {margin:0 -400px 1.5em 400px;}
+.push-11 {margin:0 -440px 1.5em 440px;}
+.push-12 {margin:0 -480px 1.5em 480px;}
+.push-13 {margin:0 -520px 1.5em 520px;}
+.push-14 {margin:0 -560px 1.5em 560px;}
+.push-15 {margin:0 -600px 1.5em 600px;}
+.push-16 {margin:0 -640px 1.5em 640px;}
+.push-17 {margin:0 -680px 1.5em 680px;}
+.push-18 {margin:0 -720px 1.5em 720px;}
+.push-19 {margin:0 -760px 1.5em 760px;}
+.push-20 {margin:0 -800px 1.5em 800px;}
+.push-21 {margin:0 -840px 1.5em 840px;}
+.push-22 {margin:0 -880px 1.5em 880px;}
+.push-23 {margin:0 -920px 1.5em 920px;}
+.push-24 {margin:0 -960px 1.5em 960px;}
+.push-1, .push-2, .push-3, .push-4, .push-5, .push-6, .push-7, .push-8, .push-9, .push-10, .push-11, .push-12, .push-13, .push-14, .push-15, .push-16, .push-17, .push-18, .push-19, .push-20, .push-21, .push-22, .push-23, .push-24 {float:right;position:relative;}
+.prepend-top {margin-top:1.5em;}
+.append-bottom {margin-bottom:1.5em;}
+.box {padding:1.5em;margin-bottom:1.5em;background:#E5ECF9;}
+hr {background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:none;}
+hr.space {background:#fff;color:#fff;}
+.clearfix:after, .container:after {content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden;}
+.clearfix, .container {display:block;}
+.clear {clear:both;}
+
+/* forms.css */
+label {font-weight:bold;}
+fieldset {padding:1.4em;margin:0 0 1.5em 0;border:1px solid #ccc;}
+legend {font-weight:bold;font-size:1.2em;}
+input.text, input.title, textarea, select {margin:0.5em 0;border:1px solid #bbb;}
+input.text:focus, input.title:focus, textarea:focus, select:focus {border:1px solid #666;}
+input.text, input.title {width:300px;padding:5px;}
+input.title {font-size:1.5em;}
+textarea {width:390px;height:250px;padding:5px;}
+.error, .notice, .success {padding:.8em;margin-bottom:1em;border:2px solid #ddd;}
+.error {background:#FBE3E4;color:#8a1f11;border-color:#FBC2C4;}
+.notice {background:#FFF6BF;color:#514721;border-color:#FFD324;}
+.success {background:#E6EFC2;color:#264409;border-color:#C6D880;}
+.error a {color:#8a1f11;}
+.notice a {color:#514721;}
+.success a {color:#264409;}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/comments.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,26 @@
+#comment-list {
+    font-size: 18px;
+    font-weight: bold;
+    color: #999;
+}
+div.comment {
+    padding: 0.5em;
+    border-bottom: 1px dashed black;
+    font: 12px/18px "Lucida Grande", Verdana, sans-serif; 
+    color: #333;
+}
+div.comment-avatar {
+    float: left;
+    padding-right: 1.5em;
+}
+div.comment-text {
+}
+div.comment-text-removed {
+    font-style: italic;
+}
+div.comment-details {
+    clear: both;
+    font-size: smaller;
+    font-style: italic;
+    padding-top: 0.5em;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/downloads.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,11 @@
+#downloads-search {
+   text-align: center;
+}
+#downloads-add td {
+    padding-bottom: 5px;
+}
+
+#downloads-add fieldset {
+    margin: 1em 0 1em;
+    padding: 0.5em;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/gcalendar.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,7 @@
+.markItUp {
+   width: 600px;
+}
+.markItUpEditor {
+	width:543px;
+	height:200px;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/membermap.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,20 @@
+#member_map_members_column {
+   float: left;
+}
+#member_map_map {
+   float: left;
+   width: 640px;
+   height: 480px;
+   border: 1px solid black;
+}
+#member_map_info {
+   padding-top: 1em;
+   clear: left;
+}
+.markItUp {
+   width: 600px;
+}
+.markItUpEditor {
+	width:543px;
+	height:200px;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/messages.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,63 @@
+blockquote {
+   margin-left: 2em;
+   border-left: 3px solid grey;
+   padding-left: 3px;
+}
+blockquote ul {
+    margin-left: 1em;
+}
+
+table.messages {
+   border: 1px solid black;
+   border-spacing: 0px;
+   border-collapse: collapse;
+   margin-left: 1em;
+}
+
+table.messages th {
+   color: black;
+   background: #bdd6d6;
+   padding: 3px 4px 3px 4px;
+   font-weight: bold;
+}
+
+table.messages td {
+   border: 1px solid black;
+   padding: 10px 8px;
+}
+
+table.message-header {
+   border-spacing: 0px;
+   border-collapse: collapse;
+   border-top: 1px solid black;
+   border-bottom: 1px solid black;
+}
+
+table.message-header tr {
+   border-bottom: 1px solid black;
+}
+
+table.message-header th {
+   text-align: left;
+   font-weight: bold;
+   padding: 3px;
+}
+
+table.message-header td {
+   text-align: left;
+   padding: 3px;
+}
+
+div.message-body {
+   margin: 1em 1em;
+   padding-top: 5px;
+}
+div.message-signature {
+   font-size: smaller;
+   margin-left: 1em;
+}
+div.message-hr {
+   width: 20%;
+   margin-left: 0;
+   border-top: 1px solid black;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/news.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,77 @@
+div.news-search {
+   text-align: center;
+}
+
+div.news-story-container {
+   border-top: 1px solid black;
+   margin: 1em 0.5em;
+   padding-top: 0;
+   padding-bottom: 1em;
+   clear: right;
+}
+div.news-story-container h4 {
+   margin: .5em 0;
+   padding: 0;
+}
+div.news-details {
+   font-style: italic;
+   font-size: small;
+   margin: 0;
+   padding: 0;
+}
+
+img.news-icon {
+   float: right;
+   margin-right: .5em;
+   padding-bottom: 1em;
+   padding-left: .5em;
+   border: 0;
+}
+div.news-content {
+    margin: 0.5em 0.5em;
+}
+
+div.news-tags {
+   font-size: small;
+   margin: 1em 0;
+}
+
+div.news-tags ul {
+   margin: 0;
+   padding-left: .5em;
+   list-style-type: none;
+   display: inline;
+}
+
+div.news-tags ul li {
+   margin-left: 0;
+   display: inline;
+}
+
+div.news-tag-cloud {
+   padding: 0;
+   width: 80%;
+   text-align: center;
+   margin: auto;
+}
+
+div.news-tag-cloud ul {
+   margin: 0;
+   list-style-type: none;
+}
+
+div.news-tag-cloud li {
+   display: inline;
+}
+
+div.news-tag-cloud a {
+   text-decoration: none;
+   vertical-align: middle;
+   background-color: white;
+   color: black;
+}
+
+div.news-tag-cloud a:hover {
+   background-color: #bdd6d6;
+   color: black;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/pagination.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,30 @@
+div.pagination {
+	padding: 3px;
+}
+div.pagination ul {
+	list-style-type: none;
+}
+div.pagination li {
+	float: left;
+	display: inline;
+	margin: 0 5px 0 0;
+	display: block;
+}
+div.pagination li a {
+	color: #333;
+	padding: 4px;
+	border: 1px solid #333;
+	text-decoration: none;
+	float: left;
+}
+div.pagination li a:hover {
+	color: #333;
+	background: #57FEFF;
+	border: 1px solid #333;
+}
+div.pagination li.current {
+	color: #fff;
+	border: 1px solid #333;
+	padding: 4px;
+	background: teal;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/polls.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,13 @@
+.poll-form {
+    padding-bottom: 1em;
+}
+.poll-form ul {
+    list-style: none;
+    padding-bottom: 0.5em;
+}
+.poll-form li {
+    padding: 0.5em 0 0.5em 0.5em;
+}
+dl.poll-result {
+    width: 80%;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/potd.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,10 @@
+div.potd-details {
+    text-align: center;
+}
+div.potd-details p.caption {
+    font-weight: bold;
+}
+div.potd-details p.details {
+    font-style: italic;
+    font-size: smaller;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/shoutbox.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,36 @@
+span.shoutbox-user {
+   font-weight: bold;
+   text-decoration: underline;
+}
+span.shoutbox-shout {
+}
+span.shoutbox-date {
+   font-style: italic;
+}
+
+div#shoutbox-smiley-frame {
+    margin: 0.5em 2px;
+}
+
+div#shoutbox-smiley-frame img {
+    padding: 1px 1px;
+}
+
+div.smiley_farm img {
+    border: 0;
+    cursor: pointer;
+}
+
+#marqueecontainer {
+position: relative;
+width: 142; /*marquee width */
+height: 200px; /*marquee height */
+background-color: #bdd6d6;
+overflow: hidden;
+overflow: auto;
+border: 1px solid teal;
+padding: 2px;
+padding-left: 4px;
+margin-bottom: 2px;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/shoutbox_app.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,38 @@
+div.shoutbox-history table {
+   border-collapse: collapse;
+   width: 95%;
+   margin: 1em auto 1em auto;
+   border: 1px solid black;
+}
+div.shoutbox-history table tr {
+   border-top: 1px solid black;
+   border-bottom: 1px solid black;
+}
+div.shoutbox-history table tr th {
+    border-left: 1px solid black;
+    border-right: 1px solid black;
+    padding: 5px 2px;
+    text-align: center;
+    width: 10%;
+}
+div.shoutbox-history table tr td {
+    border-left: 1px solid black;
+    border-right: 1px solid black;
+    padding: 5px 5px;
+    width: 90%;
+}
+div.shoutbox-history table tr.odd {
+    background-color: #ddd;
+}
+div.shoutbox-history .date {
+    font-style: italic;
+}
+
+div.shoutbox-history .edit {
+    padding: 5px 5px;
+}
+
+div.shoutbox-history .edit:hover {
+    background-color: #7fffd4;
+    cursor: pointer;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/tab-nav.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,33 @@
+ul.tab-nav {
+   border-bottom: 1px solid black;
+   padding-left: 10px;
+   margin: 12px 0px 1em 0px;
+}
+
+ul.tab-nav li {
+   display: inline;
+   overflow: hidden;
+   list-style-type: none;
+}
+
+ul.tab-nav li a, ul.tab-nav li a.active {
+   color: black;
+   background-color: teal;
+   border: 1px solid #300;
+   padding: 2px 5px 2px 5px;
+   text-decoration: none;
+}
+
+ul.tab-nav li a.active {
+   color: #000;
+   background-color: #eee;
+   border-bottom: 1px solid #eee;
+}
+
+ul.tab-nav li a:hover {
+    color: #fff;
+}
+
+ul.tab-nav li a.active:hover {
+    color: #000;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/css/weblinks.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,18 @@
+div.weblinks-search {
+   text-align: center;
+}
+
+div.weblinks-link-sort {
+   padding-bottom: .5em;
+}
+
+ul.weblinks-link-options {
+   margin: 0;
+   padding-left: 0;
+   list-style-type: none;
+}
+
+ul.weblinks-link-options li {
+   display: inline;
+   padding: 0 5px;
+}
Binary file media/downloads/stars/rating_half.gif has changed
Binary file media/downloads/stars/rating_off.gif has changed
Binary file media/downloads/stars/rating_on.gif has changed
Binary file media/downloads/stars/rating_over.gif has changed
Binary file media/icons/application_edit.png has changed
Binary file media/icons/calendar_add.png has changed
Binary file media/icons/calendar_delete.png has changed
Binary file media/icons/calendar_edit.png has changed
Binary file media/icons/cross.png has changed
Binary file media/icons/email.png has changed
Binary file media/icons/email_go.png has changed
Binary file media/icons/feed.png has changed
Binary file media/icons/flag_red.png has changed
Binary file media/icons/image_edit.png has changed
Binary file media/icons/key.png has changed
Binary file media/icons/link.png has changed
Binary file media/icons/note.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/bio.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,11 @@
+
+$(document).ready(function() {
+    $('#id_profile_text').markItUp(mySettings);
+    $('#id_signature').markItUp(mySettings);
+    $('#id_birthday').datepicker({changeMonth: true, 
+       changeYear: true,
+       dateFormat: 'yy-mm-dd',
+       minDate: new Date(1909, 0, 1),
+       maxDate: new Date(),
+       yearRange: '-100:+0'});
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/comments.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,40 @@
+$(document).ready(function() {
+    $('#comment-form-post').click(function () {
+        $(this).attr('disabled', 'disabled').val('Posting Comment...');
+        $.post('/comments/post/', { 
+            comment : $('#id_comment').val(), 
+            content_type : $('#id_content_type').val(), 
+            object_pk : $('#id_object_pk').val() 
+            }, 
+            function (data, textStatus) {
+                $('#id_comment').val(''); 
+                $('#comment-list').append(data);
+                var lastLi = $('#comment-list > li:last');
+                lastLi.hide();
+                lastLi.fadeIn(3000);
+                $('#comment-form-post').removeAttr('disabled').val('Post Comment');
+                var count = $('#comment-count');
+                if (count.length) {
+                    count.html(parseInt(count.html()) + 1);
+                }
+            }, 
+            'html');
+        return false;
+    });
+    $('a.comment-flag').click(function () {
+        var id = this.id;
+        if (id.match(/fc-(\d+)/)) {
+            id = RegExp.$1;
+            if (confirm('Only flag a comment if you feel it is spam, abuse, violates site rules, ' +
+                    'or is not appropriate. ' +
+                    'A moderator will be notified and will review the comment. ' +
+                    'Are you sure you want to flag this comment?')) {
+                $.post('/comments/flag/', { id : id }, function(response) {
+                    alert(response);
+                    }, 'text');
+            }
+        }
+        return false;
+    });
+    $('#id_comment').markItUp(mySettings);
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/downloads/add.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,3 @@
+$(document).ready(function() {
+    $('#id_description').markItUp(mySettings);
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/downloads/rating.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,107 @@
+function dlRatingOver(event)
+{
+    var div = $(this).parent('div');
+    var stars = $('img', div);
+    for (var i = 0; i <= event.data; ++i)
+    {
+        var star = $(stars[i]);
+        star.attr('src', '/static/downloads/stars/rating_over.gif');
+    }
+}
+
+function dlRatingOut(event)
+{
+    var div = $(this).parent('div');
+    var stars = $('img', div);
+    for (var i = 0; i <= event.data; ++i)
+    {
+        var star = $(stars[i]);
+        star.attr('src', '/static/downloads/stars/rating_' + star.attr('class') + '.gif');
+    }
+}
+
+function dlRatingClick(event)
+{
+    var star = $(this);
+    var id = star.attr('id');
+    if (id.match(/star-(\d+)-(\d+)/))
+    {
+        $.post('/downloads/rate/', { id: RegExp.$1, rating: parseInt(RegExp.$2)  + 1},
+            function(rating) {
+                rating = parseFloat(rating);
+                if (rating < 0)
+                {
+                    alert("You've already rated this download.");
+                    return;
+                }
+                alert('Thanks for rating this download!');
+                var div = star.parent('div');
+                var stars = $('img', div);
+                rating = parseFloat(rating);
+                for (var i = 0; i < 5; ++i)
+                {
+                    var s = $(stars[i]);
+                    s.removeClass(s.attr('class'));
+                    if (rating >= 1.0)
+                    {
+                        s.attr('src', '/static/downloads/stars/rating_on.gif');
+                        s.addClass('on')
+                        rating -= 1.0;
+                    }
+                    else if (rating >= 0.5)
+                    {
+                        s.attr('src', '/static/downloads/stars/rating_half.gif');
+                        s.addClass('half')
+                        rating = 0;
+                    }
+                    else
+                    {
+                        s.attr('src', '/static/downloads/stars/rating_off.gif');
+                        s.addClass('off')
+                    }
+                }
+            },
+            'text');
+    }
+}
+
+$(document).ready(function() {
+    $('.rating').each(function(n) {
+        var div = $(this);
+        var id = div.attr('id');
+        var numeric_id = -1;
+        if (id.match(/rating-(\d+)/))
+        {
+            numeric_id = RegExp.$1;
+        }
+        var rating = div.html();
+        div.html('');
+        for (var i = 0; i < 5; ++i)
+        {
+            var star = $('<img />');
+            if (rating >= 1)
+            {
+                star.attr('src', '/static/downloads/stars/rating_on.gif');
+                star.addClass('on')
+                --rating;
+            }
+            else if (rating >= 0.5)
+            {
+                star.attr('src', '/static/downloads/stars/rating_half.gif');
+                star.addClass('half')
+                rating = 0;
+            }
+            else
+            {
+                star.attr('src', '/static/downloads/stars/rating_off.gif');
+                star.addClass('off')
+            }
+            star.attr('alt', 'star');
+            star.attr('id', 'star-' + numeric_id + '-' + i);
+            star.bind('mouseover', i, dlRatingOver);
+            star.bind('mouseout', i, dlRatingOut);
+            star.click(dlRatingClick);
+            div.append(star);
+        }
+    });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/downloads_admin.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,3 @@
+$(document).ready(function() {
+    $('#id_description').markItUp(mySettings);
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/gcalendar.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,111 @@
+var gcalTzInfo = {
+    areas: ['Africa', 'America', 'Antarctica', 'Asia', 'Atlantic', 'Australia', 'Europe', 'Indian', 'Pacific', 'US'],
+    locations: [['Abidjan', 'Accra', 'Addis Ababa', 'Algiers', 'Asmara', 'Bamako', 'Bangui', 'Banjul', 'Bissau', 'Blantyre', 'Brazzaville', 'Bujumbura', 'Cairo', 'Casablanca', 'Ceuta', 'Conakry', 'Dakar', 'Dar es Salaam', 'Djibouti', 'Douala', 'El Aaiun', 'Freetown', 'Gaborone', 'Harare', 'Johannesburg', 'Kampala', 'Khartoum', 'Kigali', 'Kinshasa', 'Lagos', 'Libreville', 'Lome', 'Luanda', 'Lubumbashi', 'Lusaka', 'Malabo', 'Maputo', 'Maseru', 'Mbabane', 'Mogadishu', 'Monrovia', 'Nairobi', 'Ndjamena', 'Niamey', 'Nouakchott', 'Ouagadougou', 'Porto-Novo', 'Sao Tome', 'Tripoli', 'Tunis', 'Windhoek'],
+['Adak', 'Anchorage', 'Anguilla', 'Antigua', 'Araguaina', 'Argentina/Buenos Aires', 'Argentina/Catamarca', 'Argentina/Cordoba', 'Argentina/Jujuy', 'Argentina/La Rioja', 'Argentina/Mendoza', 'Argentina/Rio Gallegos', 'Argentina/Salta', 'Argentina/San Juan', 'Argentina/San Luis', 'Argentina/Tucuman', 'Argentina/Ushuaia', 'Aruba', 'Asuncion', 'Atikokan', 'Bahia', 'Barbados', 'Belem', 'Belize', 'Blanc-Sablon', 'Boa Vista', 'Bogota', 'Boise', 'Cambridge Bay', 'Campo Grande', 'Cancun', 'Caracas', 'Cayenne', 'Cayman', 'Chicago', 'Chihuahua', 'Costa Rica', 'Cuiaba', 'Curacao', 'Danmarkshavn', 'Dawson', 'Dawson Creek', 'Denver', 'Detroit', 'Dominica', 'Edmonton', 'Eirunepe', 'El Salvador', 'Fortaleza', 'Glace Bay', 'Godthab', 'Goose Bay', 'Grand Turk', 'Grenada', 'Guadeloupe', 'Guatemala', 'Guayaquil', 'Guyana', 'Halifax', 'Havana', 'Hermosillo', 'Indiana/Indianapolis', 'Indiana/Knox', 'Indiana/Marengo', 'Indiana/Petersburg', 'Indiana/Tell City', 'Indiana/Vevay', 'Indiana/Vincennes', 'Indiana/Winamac', 'Inuvik', 'Iqaluit', 'Jamaica', 'Juneau', 'Kentucky/Louisville', 'Kentucky/Monticello', 'La Paz', 'Lima', 'Los Angeles', 'Maceio', 'Managua', 'Manaus', 'Martinique', 'Mazatlan', 'Menominee', 'Merida', 'Mexico City', 'Miquelon', 'Moncton', 'Monterrey', 'Montevideo', 'Montreal', 'Montserrat', 'Nassau', 'New York', 'Nipigon', 'Nome', 'Noronha', 'North Dakota/Center', 'North Dakota/New Salem', 'Panama', 'Pangnirtung', 'Paramaribo', 'Phoenix', 'Port of Spain', 'Port-au-Prince', 'Porto Velho', 'Puerto Rico', 'Rainy River', 'Rankin Inlet', 'Recife', 'Regina', 'Resolute', 'Rio Branco', 'Santarem', 'Santiago', 'Santo Domingo', 'Sao Paulo', 'Scoresbysund', 'St Johns', 'St Kitts', 'St Lucia', 'St Thomas', 'St Vincent', 'Swift Current', 'Tegucigalpa', 'Thule', 'Thunder Bay', 'Tijuana', 'Toronto', 'Tortola', 'Vancouver', 'Whitehorse', 'Winnipeg', 'Yakutat', 'Yellowknife'],
+['Casey', 'Davis', 'DumontDUrville', 'Mawson', 'McMurdo', 'Palmer', 'Rothera', 'Syowa', 'Vostok'],
+['Aden', 'Almaty', 'Amman', 'Anadyr', 'Aqtau', 'Aqtobe', 'Ashgabat', 'Baghdad', 'Bahrain', 'Baku', 'Bangkok', 'Beirut', 'Bishkek', 'Brunei', 'Choibalsan', 'Chongqing', 'Colombo', 'Damascus', 'Dhaka', 'Dili', 'Dubai', 'Dushanbe', 'Gaza', 'Harbin', 'Ho Chi Minh', 'Hong Kong', 'Hovd', 'Irkutsk', 'Jakarta', 'Jayapura', 'Jerusalem', 'Kabul', 'Kamchatka', 'Karachi', 'Kashgar', 'Kathmandu', 'Kolkata', 'Krasnoyarsk', 'Kuala Lumpur', 'Kuching', 'Kuwait', 'Macau', 'Magadan', 'Makassar', 'Manila', 'Muscat', 'Nicosia', 'Novosibirsk', 'Omsk', 'Oral', 'Phnom Penh', 'Pontianak', 'Pyongyang', 'Qatar', 'Qyzylorda', 'Rangoon', 'Riyadh', 'Sakhalin', 'Samarkand', 'Seoul', 'Shanghai', 'Singapore', 'Taipei', 'Tashkent', 'Tbilisi', 'Tehran', 'Thimphu', 'Tokyo', 'Ulaanbaatar', 'Urumqi', 'Vientiane', 'Vladivostok', 'Yakutsk', 'Yekaterinburg', 'Yerevan'],
+['Azores', 'Bermuda', 'Canary', 'Cape Verde', 'Faroe', 'Madeira', 'Reykjavik', 'South Georgia', 'St Helena', 'Stanley'],
+['Adelaide', 'Brisbane', 'Broken Hill', 'Currie', 'Darwin', 'Eucla', 'Hobart', 'Lindeman', 'Lord Howe', 'Melbourne', 'Perth', 'Sydney'],
+['Amsterdam', 'Andorra', 'Athens', 'Belgrade', 'Berlin', 'Brussels', 'Bucharest', 'Budapest', 'Chisinau', 'Copenhagen', 'Dublin', 'Gibraltar', 'Helsinki', 'Istanbul', 'Kaliningrad', 'Kiev', 'Lisbon', 'London', 'Luxembourg', 'Madrid', 'Malta', 'Minsk', 'Monaco', 'Moscow', 'Oslo', 'Paris', 'Prague', 'Riga', 'Rome', 'Samara', 'Simferopol', 'Sofia', 'Stockholm', 'Tallinn', 'Tirane', 'Uzhgorod', 'Vaduz', 'Vienna', 'Vilnius', 'Volgograd', 'Warsaw', 'Zaporozhye', 'Zurich'],
+['Antananarivo', 'Chagos', 'Christmas', 'Cocos', 'Comoro', 'Kerguelen', 'Mahe', 'Maldives', 'Mauritius', 'Mayotte', 'Reunion'],
+['Apia', 'Auckland', 'Chatham', 'Easter', 'Efate', 'Enderbury', 'Fakaofo', 'Fiji', 'Funafuti', 'Galapagos', 'Gambier', 'Guadalcanal', 'Guam', 'Honolulu', 'Johnston', 'Kiritimati', 'Kosrae', 'Kwajalein', 'Majuro', 'Marquesas', 'Midway', 'Nauru', 'Niue', 'Norfolk', 'Noumea', 'Pago Pago', 'Palau', 'Pitcairn', 'Ponape', 'Port Moresby', 'Rarotonga', 'Saipan', 'Tahiti', 'Tarawa', 'Tongatapu', 'Truk', 'Wake', 'Wallis'],
+['Alaska', 'Arizona', 'Central', 'Eastern', 'Hawaii', 'Mountain', 'Pacific']],
+    default_area: 9,
+    default_location: 6
+};
+
+$(document).ready(function() {
+    $('#id_description').markItUp(mySettings);
+    $('#id_start_date').datepicker({constrainInput: true, 
+       onClose: function () {
+         var end = $('#id_end_date');
+         if (this.value > end.val())
+         {
+            end.val(this.value);
+         }
+       }
+       });
+    $('#id_end_date').datepicker({constrainInput: true,
+       onClose: function () {
+         var start = $('#id_start_date');
+         if (this.value < start.val())
+         {
+            start.val(this.value);
+         }
+       }
+       });
+    if ($('#id_all_day:checked').length)
+    {
+       $('#id_start_time').hide();
+       $('#id_end_time').hide();
+       $('#id_tz_stuff').hide();
+    }
+    $('#id_all_day').click(function () {
+       $('#id_start_time').toggle();
+       $('#id_end_time').toggle();
+       $('#id_tz_stuff').toggle();
+       });
+
+    var tz_area = $('#id_tz_area')[0];
+    var tz_loc = $('#id_tz_location')[0];
+    tz_area.options.length = 0;
+    for (var i = 0; i < gcalTzInfo.areas.length; ++i)
+    {
+       tz_area.options[i] = new Option(gcalTzInfo.areas[i]);     
+    }
+
+    var event_tz = $('#id_time_zone').val();
+    if (event_tz == '')
+    {
+       tz_area.options.selectedIndex = gcalTzInfo.default_area;
+    }
+    else
+    {
+       var area_match = event_tz.match(/^(\w+)\/.*$/);
+       if (area_match != null)
+       {
+          var i = gcalTzInfo.areas.indexOf(area_match[1]);
+          tz_area.options.selectedIndex = (i != -1) ? i : gcalTzInfo.default_area;
+       }
+       else
+       {
+          tz_area.options.selectedIndex = gcalTzInfo.default_area;
+       }
+    }
+
+    $('#id_tz_area').change(function () {
+       tz_loc.options.length = 0;
+       var area = tz_area.options.selectedIndex;
+       for (var i = 0; i < gcalTzInfo.locations[area].length; ++i)
+       {
+          tz_loc.options[i] = new Option(gcalTzInfo.locations[area][i]);
+       }
+    }).change();
+
+    if (event_tz == '')
+    {
+       tz_loc.options.selectedIndex = gcalTzInfo.default_location;
+    }
+    else
+    {
+       var loc_match = event_tz.match(/^[^\/]+\/(.*)$/);
+       if (loc_match != null)
+       {
+          var loc = loc_match[1].replace(/_/g, ' ');
+          var i = gcalTzInfo.locations[tz_area.options.selectedIndex].indexOf(loc);
+          tz_loc.options.selectedIndex = (i != -1) ? i : gcalTzInfo.default_location;
+       }
+       else
+       {
+          tz_loc.options.selectedIndex = gcalTzInfo.default_location;
+       }
+    }
+
+    $('#id_gcal_event_form').submit(function () {
+       var area = $('#id_tz_area').val();
+       var loc = $('#id_tz_location').val();
+       var tz = area + '/' + loc.replace(/ /g, '_');
+       $('#id_time_zone').val(tz);
+       return true;
+    });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/gcalendar_edit.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,14 @@
+$(document).ready(function() {
+   $('.gcal-del').click(function () {
+      if (confirm('Really delete this event?')) {
+         var id = this.id;
+         if (id.match(/gcal-(\d+)/)) {
+            $.post('/calendar/delete/', { id : RegExp.$1 }, function (id) {
+               var id = '#gcal-' + id;
+               $(id).parents('li').hide('normal');
+            }, 'text');
+         }
+      }
+      return false;
+   });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/markitup/jquery.markitup.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,553 @@
+// ----------------------------------------------------------------------------
+// markItUp! Universal MarkUp Engine, JQuery plugin
+// v 1.1.5
+// Dual licensed under the MIT and GPL licenses.
+// ----------------------------------------------------------------------------
+// Copyright (C) 2007-2008 Jay Salvat
+// http://markitup.jaysalvat.com/
+// ----------------------------------------------------------------------------
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+// ----------------------------------------------------------------------------
+(function($) {
+	$.fn.markItUp = function(settings, extraSettings) {
+		var options, ctrlKey, shiftKey, altKey;
+		ctrlKey = shiftKey = altKey = false;
+
+		options = {	id:						'',
+					nameSpace:				'',
+					root:					'',
+					previewInWindow:		'', // 'width=800, height=600, resizable=yes, scrollbars=yes'
+					previewAutoRefresh:		true,
+					previewPosition:		'after',
+					previewTemplatePath:	'~/templates/preview.html',
+					previewParserPath:		'',
+					previewParserVar:		'data',
+					resizeHandle:			true,
+					beforeInsert:			'',
+					afterInsert:			'',
+					onEnter:				{},
+					onShiftEnter:			{},
+					onCtrlEnter:			{},
+					onTab:					{},
+					markupSet:			[	{ /* set */ } ]
+				};
+		$.extend(options, settings, extraSettings);
+
+		// compute markItUp! path
+		if (!options.root) {
+			$('script').each(function(a, tag) {
+				miuScript = $(tag).get(0).src.match(/(.*)jquery\.markitup(\.pack)?\.js$/);
+				if (miuScript !== null) {
+					options.root = miuScript[1];
+				}
+			});
+		}
+
+		return this.each(function() {
+			var $$, textarea, levels, scrollPosition, caretPosition, caretOffset,
+				clicked, hash, header, footer, previewWindow, template, iFrame, abort;
+			$$ = $(this);
+			textarea = this;
+			levels = [];
+			abort = false;
+			scrollPosition = caretPosition = 0;
+			caretOffset = -1;
+
+			options.previewParserPath = localize(options.previewParserPath);
+			options.previewTemplatePath = localize(options.previewTemplatePath);
+
+			// apply the computed path to ~/
+			function localize(data, inText) {
+				if (inText) {
+					return 	data.replace(/("|')~\//g, "$1"+options.root);
+				}
+				return 	data.replace(/^~\//, options.root);
+			}
+
+			// init and build editor
+			function init() {
+				id = ''; nameSpace = '';
+				if (options.id) {
+					id = 'id="'+options.id+'"';
+				} else if ($$.attr("id")) {
+					id = 'id="markItUp'+($$.attr("id").substr(0, 1).toUpperCase())+($$.attr("id").substr(1))+'"';
+
+				}
+				if (options.nameSpace) {
+					nameSpace = 'class="'+options.nameSpace+'"';
+				}
+				$$.wrap('<div '+nameSpace+'></div>');
+				$$.wrap('<div '+id+' class="markItUp"></div>');
+				$$.wrap('<div class="markItUpContainer"></div>');
+				$$.addClass("markItUpEditor");
+
+				// add the header before the textarea
+				header = $('<div class="markItUpHeader"></div>').insertBefore($$);
+				$(dropMenus(options.markupSet)).appendTo(header);
+
+				// add the footer after the textarea
+				footer = $('<div class="markItUpFooter"></div>').insertAfter($$);
+
+				// add the resize handle after textarea
+				if (options.resizeHandle === true && $.browser.safari !== true) {
+					resizeHandle = $('<div class="markItUpResizeHandle"></div>')
+						.insertAfter($$)
+						.bind("mousedown", function(e) {
+							var h = $$.height(), y = e.clientY, mouseMove, mouseUp;
+							mouseMove = function(e) {
+								$$.css("height", Math.max(20, e.clientY+h-y)+"px");
+								return false;
+							};
+							mouseUp = function(e) {
+								$("html").unbind("mousemove", mouseMove).unbind("mouseup", mouseUp);
+								return false;
+							};
+							$("html").bind("mousemove", mouseMove).bind("mouseup", mouseUp);
+					});
+					footer.append(resizeHandle);
+				}
+
+				// listen key events
+				$$.keydown(keyPressed).keyup(keyPressed);
+				
+				// bind an event to catch external calls
+				$$.bind("insertion", function(e, settings) {
+					if (settings.target !== false) {
+						get();
+					}
+					if (textarea === $.markItUp.focused) {
+						markup(settings);
+					}
+				});
+
+				// remember the last focus
+				$$.focus(function() {
+					$.markItUp.focused = this;
+				});
+			}
+
+			// recursively build header with dropMenus from markupset
+			function dropMenus(markupSet) {
+				var ul = $('<ul></ul>'), i = 0;
+				$('li:hover > ul', ul).css('display', 'block');
+				$.each(markupSet, function() {
+					var button = this, t = '', title, li, j;
+					title = (button.key) ? (button.name||'')+' [Ctrl+'+button.key+']' : (button.name||'');
+					key   = (button.key) ? 'accesskey="'+button.key+'"' : '';
+					if (button.separator) {
+						li = $('<li class="markItUpSeparator">'+(button.separator||'')+'</li>').appendTo(ul);
+					} else {
+						i++;
+						for (j = levels.length -1; j >= 0; j--) {
+							t += levels[j]+"-";
+						}
+						li = $('<li class="markItUpButton markItUpButton'+t+(i)+' '+(button.className||'')+'"><a href="" '+key+' title="'+title+'">'+(button.name||'')+'</a></li>')
+						.bind("contextmenu", function() { // prevent contextmenu on mac and allow ctrl+click
+							return false;
+						}).click(function() {
+							return false;
+						}).mouseup(function() {
+							if (button.call) {
+								eval(button.call)();
+							}
+							markup(button);
+							return false;
+						}).hover(function() {
+								$('> ul', this).show();
+								$(document).one('click', function() { // close dropmenu if click outside
+										$('ul ul', header).hide();
+									}
+								);
+							}, function() {
+								$('> ul', this).hide();
+							}
+						).appendTo(ul);
+						if (button.dropMenu) {
+							levels.push(i);
+							$(li).addClass('markItUpDropMenu').append(dropMenus(button.dropMenu));
+						}
+					}
+				}); 
+				levels.pop();
+				return ul;
+			}
+
+			// markItUp! markups
+			function magicMarkups(string) {
+				if (string) {
+					string = string.toString();
+					string = string.replace(/\(\!\(([\s\S]*?)\)\!\)/g,
+						function(x, a) {
+							var b = a.split('|!|');
+							if (altKey === true) {
+								return (b[1] !== undefined) ? b[1] : b[0];
+							} else {
+								return (b[1] === undefined) ? "" : b[0];
+							}
+						}
+					);
+					// [![prompt]!], [![prompt:!:value]!]
+					string = string.replace(/\[\!\[([\s\S]*?)\]\!\]/g,
+						function(x, a) {
+							var b = a.split(':!:');
+							if (abort === true) {
+								return false;
+							}
+							value = prompt(b[0], (b[1]) ? b[1] : '');
+							if (value === null) {
+								abort = true;
+							}
+							return value;
+						}
+					);
+					return string;
+				}
+				return "";
+			}
+
+			// prepare action
+			function prepare(action) {
+				if ($.isFunction(action)) {
+					action = action(hash);
+				}
+				return magicMarkups(action);
+			}
+
+			// build block to insert
+			function build(string) {
+				openWith 	= prepare(clicked.openWith);
+				placeHolder = prepare(clicked.placeHolder);
+				replaceWith = prepare(clicked.replaceWith);
+				closeWith 	= prepare(clicked.closeWith);
+				if (replaceWith !== "") {
+					block = openWith + replaceWith + closeWith;
+				} else if (selection === '' && placeHolder !== '') {
+					block = openWith + placeHolder + closeWith;
+				} else {
+					block = openWith + (string||selection) + closeWith;
+				}
+				return {	block:block, 
+							openWith:openWith, 
+							replaceWith:replaceWith, 
+							placeHolder:placeHolder,
+							closeWith:closeWith
+					};
+			}
+
+			// define markup to insert
+			function markup(button) {
+				var len, j, n, i;
+				hash = clicked = button;
+				get();
+
+				$.extend(hash, {	line:"", 
+						 			root:options.root,
+									textarea:textarea, 
+									selection:(selection||''), 
+									caretPosition:caretPosition,
+									ctrlKey:ctrlKey, 
+									shiftKey:shiftKey, 
+									altKey:altKey
+								}
+							);
+				// callbacks before insertion
+				prepare(options.beforeInsert);
+				prepare(clicked.beforeInsert);
+				if (ctrlKey === true && shiftKey === true) {
+					prepare(clicked.beforeMultiInsert);
+				}			
+				$.extend(hash, { line:1 });
+				
+				if (ctrlKey === true && shiftKey === true) {
+					lines = selection.split(/\r?\n/);
+					for (j = 0, n = lines.length, i = 0; i < n; i++) {
+						if ($.trim(lines[i]) !== '') {
+							$.extend(hash, { line:++j, selection:lines[i] } );
+							lines[i] = build(lines[i]).block;
+						} else {
+							lines[i] = "";
+						}
+					}
+					string = { block:lines.join('\n')};
+					start = caretPosition;
+					len = string.block.length + (($.browser.opera) ? n : 0);
+				} else if (ctrlKey === true) {
+					string = build(selection);
+					start = caretPosition + string.openWith.length;
+					len = string.block.length - string.openWith.length - string.closeWith.length;
+					len -= fixIeBug(string.block);
+				} else if (shiftKey === true) {
+					string = build(selection);
+					start = caretPosition;
+					len = string.block.length;
+					len -= fixIeBug(string.block);
+				} else {
+					string = build(selection);
+					start = caretPosition + string.block.length ;
+					len = 0;
+					start -= fixIeBug(string.block);
+				}
+				if ((selection === '' && string.replaceWith === '')) {
+					caretOffset += fixOperaBug(string.block);
+					
+					start = caretPosition + string.openWith.length;
+					len = string.block.length - string.openWith.length - string.closeWith.length;
+
+					caretOffset = $$.val().substring(caretPosition,  $$.val().length).length;
+					caretOffset -= fixOperaBug($$.val().substring(0, caretPosition));
+				}
+				$.extend(hash, { caretPosition:caretPosition, scrollPosition:scrollPosition } );
+
+				if (string.block !== selection && abort === false) {
+					insert(string.block);
+					set(start, len);
+				} else {
+					caretOffset = -1;
+				}
+				get();
+
+				$.extend(hash, { line:'', selection:selection });
+
+				// callbacks after insertion
+				if (ctrlKey === true && shiftKey === true) {
+					prepare(clicked.afterMultiInsert);
+				}
+				prepare(clicked.afterInsert);
+				prepare(options.afterInsert);
+
+				// refresh preview if opened
+				if (previewWindow && options.previewAutoRefresh) {
+					refreshPreview(); 
+				}
+																									
+				// reinit keyevent
+				shiftKey = altKey = ctrlKey = abort = false;
+			}
+
+			// Substract linefeed in Opera
+			function fixOperaBug(string) {
+				if ($.browser.opera) {
+					return string.length - string.replace(/\n*/g, '').length;
+				}
+				return 0;
+			}
+			// Substract linefeed in IE
+			function fixIeBug(string) {
+				if ($.browser.msie) {
+					return string.length - string.replace(/\r*/g, '').length;
+				}
+				return 0;
+			}
+				
+			// add markup
+			function insert(block) {	
+				if (document.selection) {
+					var newSelection = document.selection.createRange();
+					newSelection.text = block;
+				} else {
+					$$.val($$.val().substring(0, caretPosition)	+ block + $$.val().substring(caretPosition + selection.length, $$.val().length));
+				}
+			}
+
+			// set a selection
+			function set(start, len) {
+				if (textarea.createTextRange){
+					// quick fix to make it work on Opera 9.5
+					if ($.browser.opera && $.browser.version >= 9.5 && len == 0) {
+						return false;
+					}
+					range = textarea.createTextRange();
+					range.collapse(true);
+					range.moveStart('character', start); 
+					range.moveEnd('character', len); 
+					range.select();
+				} else if (textarea.setSelectionRange ){
+					textarea.setSelectionRange(start, start + len);
+				}
+				textarea.scrollTop = scrollPosition;
+				textarea.focus();
+			}
+
+			// get the selection
+			function get() {
+				textarea.focus();
+
+				scrollPosition = textarea.scrollTop;
+				if (document.selection) {
+					selection = document.selection.createRange().text;
+					if ($.browser.msie) { // ie
+						var range = document.selection.createRange(), rangeCopy = range.duplicate();
+						rangeCopy.moveToElementText(textarea);
+						caretPosition = -1;
+						while(rangeCopy.inRange(range)) { // fix most of the ie bugs with linefeeds...
+							rangeCopy.moveStart('character');
+							caretPosition ++;
+						}
+					} else { // opera
+						caretPosition = textarea.selectionStart;
+					}
+				} else { // gecko
+					caretPosition = textarea.selectionStart;
+					selection = $$.val().substring(caretPosition, textarea.selectionEnd);
+				} 
+				return selection;
+			}
+
+			// open preview window
+			function preview() {
+				if (!previewWindow || previewWindow.closed) {
+					if (options.previewInWindow) {
+						previewWindow = window.open('', 'preview', options.previewInWindow);
+					} else {
+						iFrame = $('<iframe class="markItUpPreviewFrame"></iframe>');
+						if (options.previewPosition == 'after') {
+							iFrame.insertAfter(footer);
+						} else {
+							iFrame.insertBefore(header);
+						}	
+						previewWindow = iFrame[iFrame.length-1].contentWindow || frame[iFrame.length-1];
+					}
+				} else if (altKey === true) {
+					if (iFrame) {
+						iFrame.remove();
+					}
+					previewWindow.close();
+					previewWindow = iFrame = false;
+				}
+				if (!options.previewAutoRefresh) {
+					refreshPreview(); 
+				}
+			}
+
+			// refresh Preview window
+			function refreshPreview() {
+				if (previewWindow.document) {			
+					try {
+						sp = previewWindow.document.documentElement.scrollTop
+					} catch(e) {
+						sp = 0;
+					}					
+					previewWindow.document.open();
+					previewWindow.document.write(renderPreview());
+					previewWindow.document.close();
+					previewWindow.document.documentElement.scrollTop = sp;
+				}
+				if (options.previewInWindow) {
+					previewWindow.focus();
+				}
+			}
+
+			function renderPreview() {				
+				if (options.previewParserPath !== '') {
+					$.ajax( {
+						type: 'POST',
+						async: false,
+						url: options.previewParserPath,
+						data: options.previewParserVar+'='+encodeURIComponent($$.val()),
+						success: function(data) {
+							phtml = localize(data, 1); 
+						}
+					} );
+				} else {
+					if (!template) {
+						$.ajax( {
+							async: false,
+							url: options.previewTemplatePath,
+							success: function(data) {
+								template = localize(data, 1); 
+							}
+						} );
+					}
+					phtml = template.replace(/<!-- content -->/g, $$.val());
+				}
+				return phtml;
+			}
+			
+			// set keys pressed
+			function keyPressed(e) { 
+				shiftKey = e.shiftKey;
+				altKey = e.altKey;
+				ctrlKey = (!(e.altKey && e.ctrlKey)) ? e.ctrlKey : false;
+
+				if (e.type === 'keydown') {
+					if (ctrlKey === true) {
+						li = $("a[accesskey="+String.fromCharCode(e.keyCode)+"]", header).parent('li');
+						if (li.length !== 0) {
+							ctrlKey = false;
+							li.triggerHandler('mouseup');
+							return false;
+						}
+					}
+					if (e.keyCode === 13 || e.keyCode === 10) { // Enter key
+						if (ctrlKey === true) {  // Enter + Ctrl
+							ctrlKey = false;
+							markup(options.onCtrlEnter);
+							return options.onCtrlEnter.keepDefault;
+						} else if (shiftKey === true) { // Enter + Shift
+							shiftKey = false;
+							markup(options.onShiftEnter);
+							return options.onShiftEnter.keepDefault;
+						} else { // only Enter
+							markup(options.onEnter);
+							return options.onEnter.keepDefault;
+						}
+					}
+					if (e.keyCode === 9) { // Tab key
+						if (shiftKey == true || ctrlKey == true || altKey == true) { // Thx Dr Floob.
+							return false; 
+						}
+						if (caretOffset !== -1) {
+							get();
+							caretOffset = $$.val().length - caretOffset;
+							set(caretOffset, 0);
+							caretOffset = -1;
+							return false;
+						} else {
+							markup(options.onTab);
+							return options.onTab.keepDefault;
+						}
+					}
+				}
+			}
+
+			init();
+		});
+	};
+
+	$.fn.markItUpRemove = function() {
+		return this.each(function() {
+				$$ = $(this).unbind().removeClass('markItUpEditor');
+				$$.parent('div').parent('div.markItUp').parent('div').replaceWith($$);
+			}
+		);
+	};
+
+	$.markItUp = function(settings) {
+		var options = { target:false };
+		$.extend(options, settings);
+		if (options.target) {
+			return $(options.target).each(function() {
+				$(this).focus();
+				$(this).trigger('insertion', [options]);
+			});
+		} else {
+			$('textarea').trigger('insertion', [options]);
+		}
+	};
+})(jQuery);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/markitup/jquery.markitup.pack.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,9 @@
+// ----------------------------------------------------------------------------
+// markItUp! Universal MarkUp Engine, JQuery plugin
+// v 1.1.5
+// Dual licensed under the MIT and GPL licenses.
+// ----------------------------------------------------------------------------
+// Copyright (C) 2007-2008 Jay Salvat
+// http://markitup.jaysalvat.com/
+// ----------------------------------------------------------------------------
+eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('(3($){$.24.T=3(f,g){E k,v,A,F;v=A=F=7;k={C:\'\',12:\'\',U:\'\',1j:\'\',1A:8,25:\'26\',1k:\'~/2Q/1B.1C\',1b:\'\',27:\'28\',1l:8,1D:\'\',1E:\'\',1F:{},1G:{},1H:{},1I:{},29:[{}]};$.V(k,f,g);2(!k.U){$(\'2R\').1c(3(a,b){1J=$(b).14(0).2S.2T(/(.*)2U\\.2V(\\.2W)?\\.2X$/);2(1J!==2a){k.U=1J[1]}})}4 G.1c(3(){E d,u,15,16,p,H,L,P,17,1m,w,1n,M,18;d=$(G);u=G;15=[];18=7;16=p=0;H=-1;k.1b=1d(k.1b);k.1k=1d(k.1k);3 1d(a,b){2(b){4 a.W(/("|\')~\\//g,"$1"+k.U)}4 a.W(/^~\\//,k.U)}3 2b(){C=\'\';12=\'\';2(k.C){C=\'C="\'+k.C+\'"\'}l 2(d.1K("C")){C=\'C="T\'+(d.1K("C").2c(0,1).2Y())+(d.1K("C").2c(1))+\'"\'}2(k.12){12=\'N="\'+k.12+\'"\'}d.1L(\'<z \'+12+\'></z>\');d.1L(\'<z \'+C+\' N="T"></z>\');d.1L(\'<z N="2Z"></z>\');d.2d("2e");17=$(\'<z N="30"></z>\').2f(d);$(1M(k.29)).1N(17);1m=$(\'<z N="31"></z>\').1O(d);2(k.1l===8&&$.X.32!==8){1l=$(\'<z N="33"></z>\').1O(d).1e("34",3(e){E h=d.2g(),y=e.2h,1o,1p;1o=3(e){d.2i("2g",35.36(20,e.2h+h-y)+"37");4 7};1p=3(e){$("1C").1P("2j",1o).1P("1q",1p);4 7};$("1C").1e("2j",1o).1e("1q",1p)});1m.2k(1l)}d.2l(1Q).38(1Q);d.1e("1R",3(e,a){2(a.1r!==7){14()}2(u===$.T.2m){Y(a)}});d.1f(3(){$.T.2m=G})}3 1M(b){E c=$(\'<Z></Z>\'),i=0;$(\'B:2n > Z\',c).2i(\'39\',\'q\');$.1c(b,3(){E a=G,t=\'\',1s,B,j;1s=(a.19)?(a.1S||\'\')+\' [3a+\'+a.19+\']\':(a.1S||\'\');19=(a.19)?\'2o="\'+a.19+\'"\':\'\';2(a.2p){B=$(\'<B N="3b">\'+(a.2p||\'\')+\'</B>\').1N(c)}l{i++;2q(j=15.6-1;j>=0;j--){t+=15[j]+"-"}B=$(\'<B N="2r 2r\'+t+(i)+\' \'+(a.3c||\'\')+\'"><a 3d="" \'+19+\' 1s="\'+1s+\'">\'+(a.1S||\'\')+\'</a></B>\').1e("3e",3(){4 7}).2s(3(){4 7}).1q(3(){2(a.2t){3f(a.2t)()}Y(a);4 7}).2n(3(){$(\'> Z\',G).3g();$(D).3h(\'2s\',3(){$(\'Z Z\',17).2u()})},3(){$(\'> Z\',G).2u()}).1N(c);2(a.2v){15.3i(i);$(B).2d(\'3j\').2k(1M(a.2v))}}});15.3k();4 c}3 2w(c){2(c){c=c.3l();c=c.W(/\\(\\!\\(([\\s\\S]*?)\\)\\!\\)/g,3(x,a){E b=a.1T(\'|!|\');2(F===8){4(b[1]!==2x)?b[1]:b[0]}l{4(b[1]===2x)?"":b[0]}});c=c.W(/\\[\\!\\[([\\s\\S]*?)\\]\\!\\]/g,3(x,a){E b=a.1T(\':!:\');2(18===8){4 7}1U=3m(b[0],(b[1])?b[1]:\'\');2(1U===2a){18=8}4 1U});4 c}4""}3 I(a){2($.3n(a)){a=a(P)}4 2w(a)}3 1g(a){J=I(L.J);1a=I(L.1a);Q=I(L.Q);O=I(L.O);2(Q!==""){q=J+Q+O}l 2(m===\'\'&&1a!==\'\'){q=J+1a+O}l{q=J+(a||m)+O}4{q:q,J:J,Q:Q,1a:1a,O:O}}3 Y(a){E b,j,n,i;P=L=a;14();$.V(P,{1t:"",U:k.U,u:u,m:(m||\'\'),p:p,v:v,A:A,F:F});I(k.1D);I(L.1D);2(v===8&&A===8){I(L.3o)}$.V(P,{1t:1});2(v===8&&A===8){R=m.1T(/\\r?\\n/);2q(j=0,n=R.6,i=0;i<n;i++){2($.3p(R[i])!==\'\'){$.V(P,{1t:++j,m:R[i]});R[i]=1g(R[i]).q}l{R[i]=""}}o={q:R.3q(\'\\n\')};11=p;b=o.q.6+(($.X.1V)?n:0)}l 2(v===8){o=1g(m);11=p+o.J.6;b=o.q.6-o.J.6-o.O.6;b-=1u(o.q)}l 2(A===8){o=1g(m);11=p;b=o.q.6;b-=1u(o.q)}l{o=1g(m);11=p+o.q.6;b=0;11-=1u(o.q)}2((m===\'\'&&o.Q===\'\')){H+=1W(o.q);11=p+o.J.6;b=o.q.6-o.J.6-o.O.6;H=d.K().1h(p,d.K().6).6;H-=1W(d.K().1h(0,p))}$.V(P,{p:p,16:16});2(o.q!==m&&18===7){2y(o.q);1X(11,b)}l{H=-1}14();$.V(P,{1t:\'\',m:m});2(v===8&&A===8){I(L.3r)}I(L.1E);I(k.1E);2(w&&k.1A){1Y()}A=F=v=18=7}3 1W(a){2($.X.1V){4 a.6-a.W(/\\n*/g,\'\').6}4 0}3 1u(a){2($.X.2z){4 a.6-a.W(/\\r*/g,\'\').6}4 0}3 2y(a){2(D.m){E b=D.m.1Z();b.2A=a}l{d.K(d.K().1h(0,p)+a+d.K().1h(p+m.6,d.K().6))}}3 1X(a,b){2(u.2B){2($.X.1V&&$.X.3s>=9.5&&b==0){4 7}1i=u.2B();1i.3t(8);1i.2C(\'21\',a);1i.3u(\'21\',b);1i.3v()}l 2(u.2D){u.2D(a,a+b)}u.1v=16;u.1f()}3 14(){u.1f();16=u.1v;2(D.m){m=D.m.1Z().2A;2($.X.2z){E a=D.m.1Z(),1w=a.3w();1w.3x(u);p=-1;3y(1w.3z(a)){1w.2C(\'21\');p++}}l{p=u.2E}}l{p=u.2E;m=d.K().1h(p,u.3A)}4 m}3 1B(){2(!w||w.3B){2(k.1j){w=3C.2F(\'\',\'1B\',k.1j)}l{M=$(\'<2G N="3D"></2G>\');2(k.25==\'26\'){M.1O(1m)}l{M.2f(17)}w=M[M.6-1].3E||3F[M.6-1]}}l 2(F===8){2(M){M.3G()}w.2H();w=M=7}2(!k.1A){1Y()}}3 1Y(){2(w.D){3H{22=w.D.2I.1v}3I(e){22=0}w.D.2F();w.D.3J(2J());w.D.2H();w.D.2I.1v=22}2(k.1j){w.1f()}}3 2J(){2(k.1b!==\'\'){$.2K({2L:\'3K\',2M:7,2N:k.1b,28:k.27+\'=\'+3L(d.K()),2O:3(a){23=1d(a,1)}})}l{2(!1n){$.2K({2M:7,2N:k.1k,2O:3(a){1n=1d(a,1)}})}23=1n.W(/<!-- 3M -->/g,d.K())}4 23}3 1Q(e){A=e.A;F=e.F;v=(!(e.F&&e.v))?e.v:7;2(e.2L===\'2l\'){2(v===8){B=$("a[2o="+3N.3O(e.1x)+"]",17).1y(\'B\');2(B.6!==0){v=7;B.3P(\'1q\');4 7}}2(e.1x===13||e.1x===10){2(v===8){v=7;Y(k.1H);4 k.1H.1z}l 2(A===8){A=7;Y(k.1G);4 k.1G.1z}l{Y(k.1F);4 k.1F.1z}}2(e.1x===9){2(A==8||v==8||F==8){4 7}2(H!==-1){14();H=d.K().6-H;1X(H,0);H=-1;4 7}l{Y(k.1I);4 k.1I.1z}}}}2b()})};$.24.3Q=3(){4 G.1c(3(){$$=$(G).1P().3R(\'2e\');$$.1y(\'z\').1y(\'z.T\').1y(\'z\').Q($$)})};$.T=3(a){E b={1r:7};$.V(b,a);2(b.1r){4 $(b.1r).1c(3(){$(G).1f();$(G).2P(\'1R\',[b])})}l{$(\'u\').2P(\'1R\',[b])}}})(3S);',62,241,'||if|function|return||length|false|true|||||||||||||else|selection||string|caretPosition|block||||textarea|ctrlKey|previewWindow|||div|shiftKey|li|id|document|var|altKey|this|caretOffset|prepare|openWith|val|clicked|iFrame|class|closeWith|hash|replaceWith|lines||markItUp|root|extend|replace|browser|markup|ul||start|nameSpace||get|levels|scrollPosition|header|abort|key|placeHolder|previewParserPath|each|localize|bind|focus|build|substring|range|previewInWindow|previewTemplatePath|resizeHandle|footer|template|mouseMove|mouseUp|mouseup|target|title|line|fixIeBug|scrollTop|rangeCopy|keyCode|parent|keepDefault|previewAutoRefresh|preview|html|beforeInsert|afterInsert|onEnter|onShiftEnter|onCtrlEnter|onTab|miuScript|attr|wrap|dropMenus|appendTo|insertAfter|unbind|keyPressed|insertion|name|split|value|opera|fixOperaBug|set|refreshPreview|createRange||character|sp|phtml|fn|previewPosition|after|previewParserVar|data|markupSet|null|init|substr|addClass|markItUpEditor|insertBefore|height|clientY|css|mousemove|append|keydown|focused|hover|accesskey|separator|for|markItUpButton|click|call|hide|dropMenu|magicMarkups|undefined|insert|msie|text|createTextRange|moveStart|setSelectionRange|selectionStart|open|iframe|close|documentElement|renderPreview|ajax|type|async|url|success|trigger|templates|script|src|match|jquery|markitup|pack|js|toUpperCase|markItUpContainer|markItUpHeader|markItUpFooter|safari|markItUpResizeHandle|mousedown|Math|max|px|keyup|display|Ctrl|markItUpSeparator|className|href|contextmenu|eval|show|one|push|markItUpDropMenu|pop|toString|prompt|isFunction|beforeMultiInsert|trim|join|afterMultiInsert|version|collapse|moveEnd|select|duplicate|moveToElementText|while|inRange|selectionEnd|closed|window|markItUpPreviewFrame|contentWindow|frame|remove|try|catch|write|POST|encodeURIComponent|content|String|fromCharCode|triggerHandler|markItUpRemove|removeClass|jQuery'.split('|'),0,{}))
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/markitup/readme.txt	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,62 @@
+markItUp! 1.1.5
+
+CHANGE LOG
+markItUp! 1.1.5 2009-05-01
+- Modified: http://drupal.org/project/wysiwyg compatibility
+- Modified: Alt/Ctrl/Alt+Tab are now disabled
+
+markItUp! 1.1.4 2008-12-03
+- Fixed: Extra quote deleted line 95
+
+markItUp! 1.1.3 2008-09-12
+- Fixed: IE7 preview problem
+
+markItUp! 1.1.2 2008-07-17
+- Fixed: Quick fix for Opera 9.5 caret position problem after insertion
+
+markItUp! 1.1.1 2008-06-02
+- Fixed: Key events status are passed to callbacks properly
+- Improved: ScrollPosition is kept in the preview when its refreshed
+
+markItUp! 1.1.0 2008-05-04
+- Modified: Textarea's id is no more moved to the main container
+- Modified: NameSpace Span become a Div to remain strict
+- Added: Relative path to the script is computed
+- Added: Relative path to the script passed to callbacks
+- Added: Global instance ID property
+- Added: $(element).markItUpRemove() to remove markItUp!
+- Added: Resize handle is now optional with resizeHandle property
+- Added: Property previewInWindow is added and accept window parameter
+- Added: Property previewPosition is added
+- Modified: Resize handle is no more displayed in Safari to avoid repetition with the native handle
+- Modified: Property previewIframeRefresh become previewAutorefresh
+- Modified: Built-in Html Preview call a template file
+- Improved: Autorefreshing is now apply for preview in window too
+- Improved: Cancel button in prompt window cancel now the whole insertion process
+- Improved: Cleaner markItUp! code added to the DOM
+- Removed: Depreciated preview properties as previewBaseUrl, previewCharset, previewCssPath, previewBodyId, previewBodyClassName
+- Removed: Property previewIframe not longer exists
+- Fixed: "Magic markups" works with line feeds
+- Fixed: Key events are initialized after insertion
+- Fixed: Internet Explorer line feed offset bug
+- Fixed: Shortcut keys on Mac OS
+- Fixed: Ctrl+click works and doesn't open Mac context menu anymore
+- Fixed: Ctrl+click works and doesn't open the page in a new tab anymore
+- Fixed: Minor Css modifications
+
+markItUp! 1.0.3 2008-04-04
+- Fixed: IE7 Preview empty baseurl problem
+- Fixed: IE7 external targeted insertion
+- Added: Property scrollPosition is passed to callbacks functions
+
+markItUp! 1.0.2 2008-03-31
+- Fixed: IE7 Html preview problems
+- Fixed: Selection is kept if nothing is inserted
+- Improved: Code minified
+
+markItUp! 1.0.1 2008-03-21
+- Removed: Global PlaceHolder
+- Modified: Property previewCharset is setted to "utf-8" by default
+
+markItUp! 1.0.0 2008-03-01
+- First public release
Binary file media/js/markitup/sets/default/images/bold.png has changed
Binary file media/js/markitup/sets/default/images/clean.png has changed
Binary file media/js/markitup/sets/default/images/image.png has changed
Binary file media/js/markitup/sets/default/images/italic.png has changed
Binary file media/js/markitup/sets/default/images/link.png has changed
Binary file media/js/markitup/sets/default/images/picture.png has changed
Binary file media/js/markitup/sets/default/images/preview.png has changed
Binary file media/js/markitup/sets/default/images/stroke.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/markitup/sets/default/set.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,27 @@
+// ----------------------------------------------------------------------------
+// markItUp!
+// ----------------------------------------------------------------------------
+// Copyright (C) 2008 Jay Salvat
+// http://markitup.jaysalvat.com/
+// ----------------------------------------------------------------------------
+// Html tags
+// http://en.wikipedia.org/wiki/html
+// ----------------------------------------------------------------------------
+// Basic set. Feel free to add more tags
+// ----------------------------------------------------------------------------
+mySettings = {	
+	onShiftEnter:  	{keepDefault:false, replaceWith:'<br />\n'},
+	onCtrlEnter:  	{keepDefault:false, openWith:'\n<p>', closeWith:'</p>'},
+	onTab:    		{keepDefault:false, replaceWith:'    '},
+	markupSet:  [ 	
+		{name:'Bold', key:'B', openWith:'(!(<strong>|!|<b>)!)', closeWith:'(!(</strong>|!|</b>)!)' },
+		{name:'Italic', key:'I', openWith:'(!(<em>|!|<i>)!)', closeWith:'(!(</em>|!|</i>)!)'  },
+		{name:'Stroke through', key:'S', openWith:'<del>', closeWith:'</del>' },
+		{separator:'---------------' },
+		{name:'Picture', key:'P', replaceWith:'<img src="[![Source:!:http://]!]" alt="[![Alternative text]!]" />' },
+		{name:'Link', key:'L', openWith:'<a href="[![Link:!:http://]!]"(!( title="[![Title]!]")!)>', closeWith:'</a>', placeHolder:'Your text to link...' },
+		{separator:'---------------' },
+		{name:'Clean', className:'clean', replaceWith:function(markitup) { return markitup.selection.replace(/<(.*?)>/g, "") } },		
+		{name:'Preview', className:'preview',  call:'preview'}
+	]
+}
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/markitup/sets/default/style.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,27 @@
+/* -------------------------------------------------------------------
+// markItUp!
+// By Jay Salvat - http://markitup.jaysalvat.com/
+// ------------------------------------------------------------------*/
+.markItUp .markItUpButton1 a {
+	background-image:url(images/bold.png);
+}
+.markItUp .markItUpButton2 a {
+	background-image:url(images/italic.png);
+}
+.markItUp .markItUpButton3 a {
+	background-image:url(images/stroke.png);
+}
+
+.markItUp .markItUpButton4 a {
+	background-image:url(images/picture.png); 
+}
+.markItUp .markItUpButton5 a {
+	background-image:url(images/link.png);
+}
+
+.markItUp .markItUpButton6 a {
+	background-image:url(images/clean.png);
+}
+.markItUp .preview a {
+	background-image:url(images/preview.png);
+}
\ No newline at end of file
Binary file media/js/markitup/sets/markdown/images/bold.png has changed
Binary file media/js/markitup/sets/markdown/images/code.png has changed
Binary file media/js/markitup/sets/markdown/images/h1.png has changed
Binary file media/js/markitup/sets/markdown/images/h2.png has changed
Binary file media/js/markitup/sets/markdown/images/h3.png has changed
Binary file media/js/markitup/sets/markdown/images/h4.png has changed
Binary file media/js/markitup/sets/markdown/images/h5.png has changed
Binary file media/js/markitup/sets/markdown/images/h6.png has changed
Binary file media/js/markitup/sets/markdown/images/italic.png has changed
Binary file media/js/markitup/sets/markdown/images/link.png has changed
Binary file media/js/markitup/sets/markdown/images/list-bullet.png has changed
Binary file media/js/markitup/sets/markdown/images/list-numeric.png has changed
Binary file media/js/markitup/sets/markdown/images/picture.png has changed
Binary file media/js/markitup/sets/markdown/images/preview.png has changed
Binary file media/js/markitup/sets/markdown/images/quotes.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/markitup/sets/markdown/readme.txt	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,11 @@
+Markup language: 
+Markdown
+
+Description:
+A basic Markdown markup set with Headings, Bold, Italic, Picture, Link, List, Quotes, Code, Preview button.
+
+Install:
+- Download the zip file
+- Unzip it in your markItUp! sets folder
+- Modify your JS link to point at this set.js
+- Modify your CSS link to point at this style.css
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/markitup/sets/markdown/set.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,80 @@
+// -------------------------------------------------------------------
+// markItUp!
+// -------------------------------------------------------------------
+// Copyright (C) 2008 Jay Salvat
+// http://markitup.jaysalvat.com/
+// -------------------------------------------------------------------
+// MarkDown tags example
+// http://en.wikipedia.org/wiki/Markdown
+// http://daringfireball.net/projects/markdown/
+// -------------------------------------------------------------------
+// Feel free to add more tags
+// -------------------------------------------------------------------
+mySettings = {
+	previewParserPath: '/comments/markdown/',
+	previewParserVar: 'data',
+    previewInWindow: false,
+    previewAutoRefresh: false,
+    previewPosition: 'after',
+	onShiftEnter:		{keepDefault:false, openWith:'\n\n'},
+	markupSet: [
+		{name:'First Level Heading', key:'1', placeHolder:'Your title here...', closeWith:function(markItUp) { return miu.markdownTitle(markItUp, '=') } },
+		{name:'Second Level Heading', key:'2', placeHolder:'Your title here...', closeWith:function(markItUp) { return miu.markdownTitle(markItUp, '-') } },
+		{name:'Heading 3', key:'3', openWith:'### ', placeHolder:'Your title here...' },
+		{name:'Heading 4', key:'4', openWith:'#### ', placeHolder:'Your title here...' },
+		{name:'Heading 5', key:'5', openWith:'##### ', placeHolder:'Your title here...' },
+		{name:'Heading 6', key:'6', openWith:'###### ', placeHolder:'Your title here...' },
+		{separator:'---------------' },		
+		{name:'Bold', key:'B', openWith:'**', closeWith:'**'},
+		{name:'Italic', key:'I', openWith:'_', closeWith:'_'},
+		{separator:'---------------' },
+		{name:'Bulleted List', openWith:'- ' },
+		{name:'Numeric List', openWith:function(markItUp) {
+			return markItUp.line+'. ';
+		}},
+		{separator:'---------------' },
+		{name:'Picture', key:'P', replaceWith:'![[![Alternative text]!]]([![Url:!:http://]!] "[![Title]!]")'},
+		{name:'Link', key:'L', openWith:'[', closeWith:']([![Url:!:http://]!] "[![Title]!]")', placeHolder:'Your text to link here...' },
+		{separator:'---------------'},	
+		{name:'Quotes', openWith:'> '},
+		{name:'Code Block / Code', openWith:'(!(\t|!|`)!)', closeWith:'(!(`)!)'},
+		{separator:'---------------'},
+        {name:'Smilies', className:'smilies', dropMenu: [
+            {name:'Argh', replaceWith:' :argh: ', className:'col1-1' },
+            {name:'Grin', replaceWith:' :-D ', className:'col1-2' },
+            {name:'Razz', replaceWith:' :-P ', className:'col1-3' },
+            {name:'Confused', replaceWith:' o_O ', className:'col1-4' },
+            {name:'Cool', replaceWith:' 8^) ', className:'col1-5' },
+            {name:'Cry', replaceWith:' :-( ', className:'col2-1' },
+            {name:'Dead', replaceWith:' x_x ', className:'col2-2' },
+            {name:'Embarrassed', replaceWith:' :-# ', className:'col2-3' },
+            {name:'LOL', replaceWith:' :lol: ', className:'col2-4' },
+            {name:'Mad', replaceWith:' X-( ', className:'col2-5' },
+            {name:'No', replaceWith:' :no: ', className:'col3-1' },
+            {name:'None', replaceWith:' :-| ', className:'col3-2' },
+            {name:'Shock', replaceWith:' :shock: ', className:'col3-3' },
+            {name:'Sigh', replaceWith:' :sigh: ', className:'col3-4' },
+            {name:'Smile', replaceWith:' :-) ', className:'col3-5' },
+            {name:'Uh-oh', replaceWith:' :uh-oh: ', className:'col4-1' },
+            {name:'Whatever', replaceWith:' :whatever: ', className:'col4-2' },
+            {name:'Wink', replaceWith:' ;-) ', className:'col4-3' },
+            {name:'Yes', replaceWith:' :yes: ', className:'col4-4' },
+            {name:'Sleep', replaceWith:' :sleep: ', className:'col4-5' }
+            ]
+        },
+		{separator:'---------------'},
+		{name:'Preview', call:'preview', className:"preview"}
+	]
+}
+
+// mIu nameSpace to avoid conflict.
+miu = {
+	markdownTitle: function(markItUp, char) {
+		heading = '';
+		n = $.trim(markItUp.selection||markItUp.placeHolder).length;
+		for(i = 0; i < n; i++) {
+			heading += char;
+		}
+		return '\n'+heading;
+	}
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/markitup/sets/markdown/style.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,138 @@
+/* -------------------------------------------------------------------
+// markItUp!
+// By Jay Salvat - http://markitup.jaysalvat.com/
+// ------------------------------------------------------------------*/
+.markItUp .markItUpButton1 a {
+	background-image:url(images/h1.png); 
+}
+.markItUp .markItUpButton2 a {
+	background-image:url(images/h2.png); 
+}
+.markItUp .markItUpButton3 a {
+	background-image:url(images/h3.png); 
+}
+.markItUp .markItUpButton4 a {
+	background-image:url(images/h4.png); 
+}
+.markItUp .markItUpButton5 a {
+	background-image:url(images/h5.png); 
+}
+.markItUp .markItUpButton6 a {
+	background-image:url(images/h6.png); 
+}
+
+.markItUp .markItUpButton7 a {
+	background-image:url(images/bold.png);
+}
+.markItUp .markItUpButton8 a {
+	background-image:url(images/italic.png);
+}
+
+.markItUp .markItUpButton9 a {
+	background-image:url(images/list-bullet.png);
+}
+.markItUp .markItUpButton10 a {
+	background-image:url(images/list-numeric.png);
+}
+
+.markItUp .markItUpButton11 a {
+	background-image:url(images/picture.png); 
+}
+.markItUp .markItUpButton12 a {
+	background-image:url(images/link.png);
+}
+
+.markItUp .markItUpButton13 a	{
+	background-image:url(images/quotes.png);
+}
+.markItUp .markItUpButton14 a	{
+	background-image:url(images/code.png);
+}
+
+.markItUp .preview a {
+	background-image:url(images/preview.png);
+}
+
+.markItUp .smilies a {
+    background-image:url(../../../../smiley/images/smile.gif);
+}
+.markItUp .smilies ul {
+    width:155px;
+    padding:1px;	
+}
+.markItUp .smilies  li {
+    border:1px solid white;
+    width:20px;
+    height:26px;
+    overflow:hidden;
+    padding:4px; margin:0px;
+    float:left;
+}
+.markItUp .smilies ul a {
+    width:20px;
+    height:26px;
+}
+.markItUp .smilies ul li a:hover {
+    background-color:none;
+    border:1px solid black;
+}
+.markItUp .smilies .col1-1 a {
+    background-image:url(../../../../smiley/images/upset.gif);
+}
+.markItUp .smilies .col1-2 a {
+    background-image:url(../../../../smiley/images/biggrin.gif);
+}
+.markItUp .smilies .col1-3 a {
+    background-image:url(../../../../smiley/images/bigrazz.gif);
+}
+.markItUp .smilies .col1-4 a {
+    background-image:url(../../../../smiley/images/confused.gif);
+}
+.markItUp .smilies .col1-5 a {
+    background-image:url(../../../../smiley/images/cool.gif);
+}
+.markItUp .smilies .col2-1 a {
+    background-image:url(../../../../smiley/images/cry.gif);
+}
+.markItUp .smilies .col2-2 a {
+    background-image:url(../../../../smiley/images/dead.gif);
+}
+.markItUp .smilies .col2-3 a {
+    background-image:url(../../../../smiley/images/embarrassed.gif);
+}
+.markItUp .smilies .col2-4 a {
+    background-image:url(../../../../smiley/images/laugh.gif);
+}
+.markItUp .smilies .col2-5 a {
+    background-image:url(../../../../smiley/images/mad.gif);
+}
+.markItUp .smilies .col3-1 a {
+    background-image:url(../../../../smiley/images/no.gif);
+}
+.markItUp .smilies .col3-2 a {
+    background-image:url(../../../../smiley/images/none.gif);
+}
+.markItUp .smilies .col3-3 a {
+    background-image:url(../../../../smiley/images/bigeek.gif);
+}
+.markItUp .smilies .col3-4 a {
+    background-image:url(../../../../smiley/images/sigh.gif);
+}
+.markItUp .smilies .col3-5 a {
+    background-image:url(../../../../smiley/images/smile.gif);
+}
+.markItUp .smilies .col4-1 a {
+    background-image:url(../../../../smiley/images/uhoh.gif);
+}
+.markItUp .smilies .col4-2 a {
+    background-image:url(../../../../smiley/images/rolleyes.gif);
+}
+.markItUp .smilies .col4-3 a {
+    background-image:url(../../../../smiley/images/smilewinkgrin.gif);
+}
+.markItUp .smilies .col4-4 a {
+    background-image:url(../../../../smiley/images/yes.gif);
+}
+.markItUp .smilies .col4-5 a {
+    background-image:url(../../../../smiley/images/sleep.gif);
+}
Binary file media/js/markitup/skins/markitup/images/bg-container.png has changed
Binary file media/js/markitup/skins/markitup/images/bg-editor-bbcode.png has changed
Binary file media/js/markitup/skins/markitup/images/bg-editor-dotclear.png has changed
Binary file media/js/markitup/skins/markitup/images/bg-editor-html.png has changed
Binary file media/js/markitup/skins/markitup/images/bg-editor-json.png has changed
Binary file media/js/markitup/skins/markitup/images/bg-editor-markdown.png has changed
Binary file media/js/markitup/skins/markitup/images/bg-editor-textile.png has changed
Binary file media/js/markitup/skins/markitup/images/bg-editor-wiki.png has changed
Binary file media/js/markitup/skins/markitup/images/bg-editor-xml.png has changed
Binary file media/js/markitup/skins/markitup/images/bg-editor.png has changed
Binary file media/js/markitup/skins/markitup/images/handle.png has changed
Binary file media/js/markitup/skins/markitup/images/menu.png has changed
Binary file media/js/markitup/skins/markitup/images/submenu.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/markitup/skins/markitup/style.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,148 @@
+/* -------------------------------------------------------------------
+// markItUp! Universal MarkUp Engine, JQuery plugin
+// By Jay Salvat - http://markitup.jaysalvat.com/
+// ------------------------------------------------------------------*/
+.markItUp * {
+	margin:0px; padding:0px;
+	outline:none;
+}
+.markItUp a:link,
+.markItUp a:visited {
+	color:#000;
+	text-decoration:none;
+}
+.markItUp  {
+	width:600px;
+	margin:5px 0 5px 0;
+	border:5px solid #F5F5F5;	
+}
+.markItUpContainer  {
+	border:1px solid #3C769D;	
+	background:#FFF url(images/bg-container.png) repeat-x top left;
+	padding:5px 5px 2px 5px;
+	font:11px Verdana, Arial, Helvetica, sans-serif;
+}
+.markItUpEditor {
+	font:12px 'Courier New', Courier, monospace;
+	padding:5px 5px 5px 35px;
+	border:3px solid #3C769D;
+	width:543px;
+	height:160px;
+	background-image:url(images/bg-editor.png);
+	background-repeat:no-repeat;
+	clear:both; display:block;
+	line-height:18px;
+	overflow:auto;
+}
+.markItUpPreviewFrame	{
+	overflow:auto;
+	background-color:#FFFFFF;
+	border:1px solid #3C769D;
+	width:99.9%;
+	height:150px;
+	margin:5px 0;
+}
+.markItUpFooter {
+	width:100%;
+	cursor:n-resize;
+}
+.markItUpResizeHandle {
+	overflow:hidden;
+	width:22px; height:5px;
+	margin-left:auto;
+	margin-right:auto;
+	background-image:url(images/handle.png);
+	cursor:n-resize;
+}
+/***************************************************************************************/
+/* first row of buttons */
+.markItUpHeader ul li	{
+	list-style:none;
+	float:left;
+	position:relative;
+}
+.markItUpHeader ul li ul{
+	display:none;
+}
+.markItUpHeader ul li:hover > ul{
+	display:block;
+}
+.markItUpHeader ul .markItUpDropMenu {
+	background:transparent url(images/menu.png) no-repeat 115% 50%;
+	margin-right:5px;
+}
+.markItUpHeader ul .markItUpDropMenu li {
+	margin-right:0px;
+}
+.markItUpHeader ul .markItUpSeparator {
+	margin:0 10px;
+	width:1px;
+	height:16px;
+	overflow:hidden;
+	background-color:#CCC;
+}
+.markItUpHeader ul ul .markItUpSeparator {
+	width:auto; height:1px;
+	margin:0px;
+}
+/* next rows of buttons */
+.markItUpHeader ul ul {
+	display:none;
+	position:absolute;
+	top:18px; left:0px;	
+	background:#F5F5F5;
+	border:1px solid #3C769D;
+	height:inherit;
+}
+.markItUpHeader ul ul li {
+	float:none;
+	border-bottom:1px solid #3C769D;
+}
+.markItUpHeader ul ul .markItUpDropMenu {
+	background:#F5F5F5 url(images/submenu.png) no-repeat 100% 50%;
+}
+/* next rows of buttons */
+.markItUpHeader ul ul ul {
+	position:absolute;
+	top:-1px; left:150px;
+}
+.markItUpHeader ul ul ul li {
+	float:none;
+}
+.markItUpHeader ul a {
+	display:block;
+	width:16px; height:16px;
+	text-indent:-10000px;
+	background-repeat:no-repeat;
+	padding:3px;
+	margin:0px;
+}
+.markItUpHeader ul ul a {
+	display:block;
+	padding-left:0px;
+	text-indent:0;
+	width:120px; 
+	padding:5px 5px 5px 25px;
+	background-position:2px 50%;
+}
+.markItUpHeader ul ul a:hover  {
+	color:#FFF;
+	background-color:#3C769D;
+}
+/***************************************************************************************/
+.html .markItUpEditor {
+	background-image:url(images/bg-editor-html.png);
+}
+.markdown .markItUpEditor {
+	background-image:url(images/bg-editor-markdown.png);
+}
+.textile .markItUpEditor {
+	background-image:url(images/bg-editor-textile.png);
+}
+.bbcode .markItUpEditor {
+	background-image:url(images/bg-editor-bbcode.png);
+}
+.wiki .markItUpEditor,
+.dotclear .markItUpEditor {
+	background-image:url(images/bg-editor-wiki.png);
+}
Binary file media/js/markitup/skins/simple/images/handle.png has changed
Binary file media/js/markitup/skins/simple/images/menu.png has changed
Binary file media/js/markitup/skins/simple/images/submenu.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/markitup/skins/simple/style.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,118 @@
+/* -------------------------------------------------------------------
+// markItUp! Universal MarkUp Engine, JQuery plugin
+// By Jay Salvat - http://markitup.jaysalvat.com/
+// ------------------------------------------------------------------*/
+.markItUp * {
+	margin:0px; padding:0px;
+	outline:none;
+}
+.markItUp a:link,
+.markItUp a:visited {
+	color:#000;
+	text-decoration:none;
+}
+.markItUp  {
+	width:700px;
+	margin:5px 0 5px 0;
+}
+.markItUpContainer  {
+	font:11px Verdana, Arial, Helvetica, sans-serif;
+}
+.markItUpEditor {
+	font:12px 'Courier New', Courier, monospace;
+	padding:5px;
+	width:690px;
+	height:320px;
+	clear:both; display:block;
+	line-height:18px;
+	overflow:auto;
+}
+.markItUpPreviewFrame	{
+	overflow:auto;
+	background-color:#FFF;
+	width:99.9%;
+	height:300px;
+	margin:5px 0;
+}
+.markItUpFooter {
+	width:100%;
+}
+.markItUpResizeHandle {
+	overflow:hidden;
+	width:22px; height:5px;
+	margin-left:auto;
+	margin-right:auto;
+	background-image:url(images/handle.png);
+	cursor:n-resize;
+}
+/***************************************************************************************/
+/* first row of buttons */
+.markItUpHeader ul li	{
+	list-style:none;
+	float:left;
+	position:relative;
+}
+.markItUpHeader ul li:hover > ul{
+	display:block;
+}
+.markItUpHeader ul .markItUpDropMenu {
+	background:transparent url(images/menu.png) no-repeat 115% 50%;
+	margin-right:5px;
+}
+.markItUpHeader ul .markItUpDropMenu li {
+	margin-right:0px;
+}
+/* next rows of buttons */
+.markItUpHeader ul ul {
+	display:none;
+	position:absolute;
+	top:18px; left:0px;	
+	background:#FFF;
+	border:1px solid #000;
+}
+.markItUpHeader ul ul li {
+	float:none;
+	border-bottom:1px solid #000;
+}
+.markItUpHeader ul ul .markItUpDropMenu {
+	background:#FFF url(images/submenu.png) no-repeat 100% 50%;
+}
+.markItUpHeader ul .markItUpSeparator {
+	margin:0 10px;
+	width:1px;
+	height:16px;
+	overflow:hidden;
+	background-color:#CCC;
+}
+.markItUpHeader ul ul .markItUpSeparator {
+	width:auto; height:1px;
+	margin:0px;
+}
+/* next rows of buttons */
+.markItUpHeader ul ul ul {
+	position:absolute;
+	top:-1px; left:150px; 
+}
+.markItUpHeader ul ul ul li {
+	float:none;
+}
+.markItUpHeader ul a {
+	display:block;
+	width:16px; height:16px;
+	text-indent:-10000px;
+	background-repeat:no-repeat;
+	padding:3px;
+	margin:0px;
+}
+.markItUpHeader ul ul a {
+	display:block;
+	padding-left:0px;
+	text-indent:0;
+	width:120px; 
+	padding:5px 5px 5px 25px;
+	background-position:2px 50%;
+}
+.markItUpHeader ul ul a:hover  {
+	color:#FFF;
+	background-color:#000;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/markitup/templates/preview.css	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,5 @@
+/* preview style examples */
+body {
+	background-color:#EFEFEF;
+	font:70% Verdana, Arial, Helvetica, sans-serif;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/markitup/templates/preview.html	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,11 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>markItUp! preview template</title>
+<link rel="stylesheet" type="text/css" href="~/templates/preview.css" />
+</head>
+<body>
+<!-- content -->
+</body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/membermap.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,161 @@
+var mmap = {
+   map: null,
+   geocoder: null,
+   users: Object,
+   userOnMap: false,
+   userClick: function() {
+      var name = $('option:selected', this).text();
+      if (name != mmap.selectText)
+      {
+         mmap.clickUser(name);
+      }
+   },
+   clickUser: function(name) {
+      pt = new GLatLng(mmap.users[name].lat, mmap.users[name].lon);
+      mmap.map.setCenter(pt);
+      mmap.users[name].marker.openInfoWindowHtml(mmap.users[name].message);
+   },
+   clear: function() {
+      mmap.users.length = 0;
+   },
+   resizeUserList: function() {
+      var sel = $('#member_map_members');
+      sel[0].size = Math.min(29, sel[0].length);
+      $('#member_map_count').html(sel[0].length);
+   },
+   selectText: "(select)",
+   onMapDir: 'You have previously added yourself to the member map. Your information appears below. You may change ' +
+      'the information if you wish. To delete yourself from the map, click the Delete button.',
+   offMapDir: 'Your location is not on the map. If you would like to appear on the map, please fill out the form below ' +
+      'and click the Submit button.'
+};
+$(document).ready(function() {
+   $('#id_message').markItUp(mySettings);
+   if (GBrowserIsCompatible())
+   {
+      $(window).unload(GUnload);
+      mmap.map = new GMap2($('#member_map_map')[0]);
+      mmap.map.setCenter(new GLatLng(15.0, -30.0), 2);
+      mmap.map.enableScrollWheelZoom();
+      mmap.map.addControl(new GLargeMapControl());
+      mmap.map.addControl(new GMapTypeControl());
+      mmap.geocoder = new GClientGeocoder();
+
+      if (mmapUser.userName)
+      {
+         $.getJSON('/member_map/query/', 
+            function(data) {
+               mmap.map.clearOverlays();
+               var sel = $('#member_map_members');
+               sel[0].length = 0;
+               mmap.clear();
+               $.each(data.users, function(i, item) {
+                  sel.append($('<option />').html(item.name));
+                  var marker = new GMarker(new GLatLng(item.lat, item.lon));
+                  marker.bindInfoWindowHtml(item.message);
+                  mmap.map.addOverlay(marker);
+                  mmap.users[item.name] = item;
+                  mmap.users[item.name].marker = marker;
+                  if (mmapUser.userName == item.name)
+                  {
+                     mmap.userOnMap = true;
+                  }
+               });
+               sel[0].size = Math.min(29, data.users.length);
+               $('#member_map_count').html(data.users.length);
+
+               sel = $('#member_map_recent');
+               sel[0].length = 0;
+               sel.append($('<option />').html(mmap.selectText));
+               $.each(data.recent, function(i, item) {
+                  sel.append($('<option />').html(item));
+               });
+               var submitButton = $('#member_map_submit');
+               var deleteButton = $('#member_map_delete');
+
+               submitButton.click(function() {
+                  if (mmap.geocoder)
+                  {
+                     $(this).attr('disabled', 'disabled').val('Updating Map...');
+                     var address = $('#id_location').val();
+                     mmap.geocoder.getLatLng(address, 
+                        function(point) { 
+                           if (!point)
+                           {
+                              alert(address + ' could not be found on Google Maps.');
+                              return;
+                           }
+                           $.post('/member_map/add/', {
+                              loc : address,
+                              lat : point.lat(),
+                              lon : point.lng(),
+                              msg : $('#id_message').val()
+                              },
+                              function(data, textStatus) {
+                                 var wasOnMap = mmap.userOnMap;
+                                 if (mmap.userOnMap)
+                                 {
+                                    mmap.map.removeOverlay(mmap.users[mmapUser.userName].marker);
+                                 }
+                                 else
+                                 {
+                                    $('#member_map_members').append($('<option />').html(data.name));
+                                    mmap.userOnMap = true;
+                                    deleteButton.removeAttr('disabled').val('Delete');
+                                 }
+                                 var marker = new GMarker(new GLatLng(data.lat, data.lon));
+                                 marker.bindInfoWindowHtml(data.message);
+                                 mmap.map.addOverlay(marker);
+                                 mmap.users[mmapUser.userName] = data;
+                                 mmap.users[mmapUser.userName].marker = marker;
+                                 mmap.clickUser(mmapUser.userName);
+                                 submitButton.removeAttr('disabled').val('Update');
+                                 $('#member_map_directions').html(mmap.onMapDir);
+                                 mmap.resizeUserList();
+                                 alert(wasOnMap ? "Your location has been updated!" : 
+                                       "You've been added to the map!");
+                              },
+                              'json');
+                     });
+                  }
+                  return false;
+               });
+
+               deleteButton.click(function() {
+                  deleteButton.attr('disabled', 'disabled').val('Deleting...');
+                  $.post('/member_map/delete/', function(data, textStatus) {
+                        $('#id_location').val('');
+                        $('#id_message').val('');
+                        $("#member_map_members option[value='" + mmapUser.userName + "']").remove();
+                        $("#member_map_recent option[value='" + mmapUser.userName + "']").remove();
+                        mmap.map.removeOverlay(mmap.users[mmapUser.userName].marker);
+                        mmap.users[mmapUser.userName].marker = null;
+                        mmap.users[mmapUser.userName] = null;
+                        mmap.userOnMap = false;
+                        deleteButton.val('Delete');
+                        submitButton.removeAttr('disabled').val('Add');
+                        $('#member_map_directions').html(mmap.offMapDir);
+                        mmap.resizeUserList();
+                        alert("You've been removed from the map.");
+                     },
+                     'text');
+                  return false;
+               });
+               
+               if (mmap.userOnMap)
+               {     
+                  submitButton.val('Update');
+                  $('#member_map_directions').html(mmap.onMapDir);
+               }
+               else
+               {
+                  submitButton.val('Add');
+                  deleteButton.attr('disabled', 'disabled');
+                  $('#member_map_directions').html(mmap.offMapDir);
+               }
+         });
+         $('#member_map_members').change(mmap.userClick);
+         $('#member_map_recent').change(mmap.userClick);
+      }
+   }
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/messages/box.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,67 @@
+function messages_master_click()
+{
+   var state = document.getElementById('master_select').checked
+   for (i = 0; i < document.messages_box_form.length; ++i)
+   {
+      if (document.messages_box_form.elements[i].type == 'checkbox')
+      {
+         document.messages_box_form.elements[i].checked = state;
+      }
+   }
+}
+
+function messages_set_master()
+{
+   var count = 0;
+   var numChkBoxes = 0;
+   for (i = 0; i < document.messages_box_form.length; ++i)
+   {
+      if (document.messages_box_form.elements[i].type == 'checkbox' &&
+          document.messages_box_form.elements[i].id != 'master_select')
+      {
+         ++numChkBoxes;
+         if (document.messages_box_form.elements[i].checked)
+         {
+            ++count;
+         }
+      }
+   }
+   document.getElementById('master_select').checked = count == numChkBoxes;
+}
+
+function messages_count_selected()
+{
+   var count = 0;
+   for (i = 0; i < document.messages_box_form.length; ++i)
+   {
+      if (document.messages_box_form.elements[i].type == 'checkbox' &&
+          document.messages_box_form.elements[i].checked &&
+          document.messages_box_form.elements[i].id != 'master_select')
+      {
+         ++count;
+      }
+   }
+   return count;
+}
+
+function messages_confirm_delete()
+{
+   var count = messages_count_selected();
+   if (count == 0)
+   {
+      alert("No messages selected.");
+      return false;
+   }
+   return confirm('Really delete selected messages?');
+}
+
+function messages_confirm_undelete()
+{
+   var count = messages_count_selected();
+   if (count == 0)
+   {
+      alert("No messages selected.");
+      return false;
+   }
+   return confirm('Really undelete selected messages?');
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/messages/compose.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,3 @@
+$(document).ready(function() {
+    $('#id_message').markItUp(mySettings);
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/shoutbox.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,57 @@
+/***********************************************
+* Cross browser Marquee II- © Dynamic Drive (www.dynamicdrive.com)
+* This notice MUST stay intact for legal use
+* Visit http://www.dynamicdrive.com/ for this script and 100s more.
+***********************************************/
+
+var delayb4scroll=2000 //Specify initial delay before marquee starts to scroll on page (2000=2 seconds)
+var marqueespeed=1 //Specify marquee scroll speed (larger is faster 1-10)
+var pauseit=1 //Pause marquee onMousever (0=no. 1=yes)?
+
+////NO NEED TO EDIT BELOW THIS LINE////////////
+
+var copyspeed=marqueespeed
+var pausespeed=(pauseit==0)? copyspeed: 0
+var actualheight=''
+
+function scrollmarquee(){
+if (parseInt(cross_marquee.style.top)>(actualheight*(-1)+8))
+cross_marquee.style.top=parseInt(cross_marquee.style.top)-copyspeed+"px"
+else
+cross_marquee.style.top=parseInt(marqueeheight)+8+"px"
+}
+
+function initializemarquee(){
+cross_marquee=document.getElementById("vmarquee")
+cross_marquee.style.top=0
+marqueeheight=document.getElementById("marqueecontainer").offsetHeight
+actualheight=cross_marquee.offsetHeight
+if (window.opera || navigator.userAgent.indexOf("Netscape/7")!=-1){ //if Opera or Netscape 7x, add scrollbars to scroll and exit
+cross_marquee.style.height=marqueeheight+"px"
+cross_marquee.style.overflow="scroll"
+return
+}
+setTimeout('lefttime=setInterval("scrollmarquee()",30)', delayb4scroll)
+}
+
+if (window.addEventListener)
+window.addEventListener("load", initializemarquee, false)
+else if (window.attachEvent)
+window.attachEvent("onload", initializemarquee)
+else if (document.getElementById)
+window.onload=initializemarquee
+
+///////////////////////////////
+
+function sb_toggle_smilies()
+{
+    var d = document.getElementById("shoutbox-smiley-frame");
+    d.style.display = d.style.display == "none" ? "block" : "none";
+}
+
+function sb_smiley_click(code)
+{
+    var txt = document.getElementById("shoutbox-smiley-input");
+    txt.value += code;
+    txt.focus();
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/shoutbox_app.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,25 @@
+$(document).ready(function() {
+     $('div.shoutbox-history table tr:odd').addClass('odd');
+     $('.edit').editable('/shout/edit/', {
+         loadurl : '/shout/text/',
+         indicator : 'Saving...',
+         tooltip   : 'Click to edit your shout...',
+         submit : 'OK',
+         cancel : 'Cancel'
+     });
+     $('.shout-del').click(function () {
+         if (confirm('Really delete this shout?')) {
+             var id = this.id;
+             if (id.match(/shout-del-(\d+)/)) {
+                $.post('/shout/delete/', { id : RegExp.$1 }, function (id) {
+                    var id = '#shout-del-' + id;
+                    $(id).parents('tr').fadeOut(1500, function () {
+                        $('div.shoutbox-history table tr:visible:even').removeClass('odd');
+                        $('div.shoutbox-history table tr:visible:odd').addClass('odd');
+                        });
+                    }, 'text');
+             }            
+         }
+         return false;
+     });
+ });
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/tiny_mce_init_admin.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,20 @@
+tinyMCE.init({
+	mode : "textareas",
+	theme : "advanced",
+	content_css : "css/example.css",
+	theme_advanced_toolbar_location : "top",
+	theme_advanced_toolbar_align : "left",
+	theme_advanced_buttons1 : "fullscreen,|,preview,|,bold,italic,underline,strikethrough,|,bullist,numlist,outdent,indent,|,undo,redo,|,link,unlink,anchor,|,image,media,cleanup,help,|,code",
+	theme_advanced_buttons2 : "",
+	theme_advanced_buttons3 : "",
+	auto_cleanup_word : true,
+	plugins : "table,advhr,emotions,insertdatetime,preview,searchreplace,print,contextmenu,fullscreen,media",
+	plugin_insertdate_dateFormat : "%m/%d/%Y",
+	plugin_insertdate_timeFormat : "%H:%M:%S",
+	fullscreen_settings : {
+		theme_advanced_path_location : "top",
+		theme_advanced_buttons1 : "fullscreen,|,preview,|,cut,copy,paste,|,undo,redo,|,search,replace,|,code,|,cleanup,|,bold,italic,underline,strikethrough,|,forecolor,backcolor,|,justifyleft,justifycenter,justifyright,justifyfull,|,help",
+		theme_advanced_buttons2 : "removeformat,styleselect,formatselect,fontselect,fontsizeselect,|,bullist,numlist,outdent,indent,|,link,unlink,anchor",
+		theme_advanced_buttons3 : "sub,sup,|,image,insertdate,inserttime,|,tablecontrols,|,hr,advhr,visualaid,|,charmap,emotions,|,print"
+	}
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/tiny_mce_init_messages.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,20 @@
+tinyMCE.init({
+	mode : "textareas",
+	theme : "advanced",
+	content_css : "/static/css/messages.css",
+	theme_advanced_toolbar_location : "top",
+	theme_advanced_toolbar_align : "left",
+	theme_advanced_buttons1 : "fullscreen,|,preview,|,bold,italic,underline,strikethrough,|,bullist,numlist,outdent,indent,|,undo,redo,|,link,unlink,anchor,|,image,media,cleanup,help,|,code",
+	theme_advanced_buttons2 : "",
+	theme_advanced_buttons3 : "",
+	auto_cleanup_word : true,
+	plugins : "table,advhr,emotions,insertdatetime,preview,searchreplace,print,contextmenu,fullscreen,media",
+	plugin_insertdate_dateFormat : "%m/%d/%Y",
+	plugin_insertdate_timeFormat : "%H:%M:%S",
+	fullscreen_settings : {
+		theme_advanced_path_location : "top",
+		theme_advanced_buttons1 : "fullscreen,|,preview,|,cut,copy,paste,|,undo,redo,|,search,replace,|,code,|,cleanup,|,bold,italic,underline,strikethrough,|,forecolor,backcolor,|,justifyleft,justifycenter,justifyright,justifyfull,|,help",
+		theme_advanced_buttons2 : "removeformat,styleselect,formatselect,fontselect,fontsizeselect,|,bullist,numlist,outdent,indent,|,link,unlink,anchor",
+		theme_advanced_buttons3 : "sub,sup,|,image,insertdate,inserttime,|,tablecontrols,|,hr,advhr,visualaid,|,charmap,emotions,|,print"
+	}
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/media/js/tiny_mce_init_std.js	Mon Apr 06 02:43:12 2009 +0000
@@ -0,0 +1,20 @@
+tinyMCE.init({
+	mode : "textareas",
+	theme : "advanced",
+	content_css : "css/example.css",
+	theme_advanced_toolbar_location : "top",
+	theme_advanced_toolbar_align : "left",
+	theme_advanced_buttons1 : "fullscreen,|,preview,|,bold,italic,underline,strikethrough,|,bullist,numlist,outdent,indent,|,undo,redo,|,link,unlink,anchor,|,image,media,cleanup,help,|,code",
+	theme_advanced_buttons2 : "",
+	theme_advanced_buttons3 : "",
+	auto_cleanup_word : true,
+	plugins : "table,advhr,emotions,insertdatetime,preview,searchreplace,print,contextmenu,fullscreen,media",
+	plugin_insertdate_dateFormat : "%m/%d/%Y",
+	plugin_insertdate_timeFormat : "%H:%M:%S",
+	fullscreen_settings : {
+		theme_advanced_path_location : "top",
+		theme_advanced_buttons1 : "fullscreen,|,preview,|,cut,copy,paste,|,undo,redo,|,search,replace,|,code,|,cleanup,|,bold,italic,underline,strikethrough,|,forecolor,backcolor,|,justifyleft,justifycenter,justifyright,justifyfull,|,help",
+		theme_advanced_buttons2 : "removeformat,styleselect,formatselect,fontselect,fontsizeselect,|,bullist,numlist,outdent,indent,|,link,unlink,anchor",
+		theme_advanced_buttons3 : "sub,sup,|,image,insertdate,inserttime,|,tablecontrols,|,hr,advhr,visualaid,|,charmap,emotions,|,print"
+	}
+});