# HG changeset patch # User Bryan O'Sullivan # Date 1236746539 25200 # Node ID ad304b606163e5ba8c9706f5101473e74daed7c5 # Parent 40025381bded808bbe502067ae95f96380ffee90 Initial cut at web comment system import diff -r 40025381bded -r ad304b606163 .hgignore --- a/.hgignore Mon Mar 09 23:42:31 2009 -0700 +++ b/.hgignore Tue Mar 10 21:42:19 2009 -0700 @@ -26,6 +26,7 @@ *.pdf *.png *.ps +*.pyc *.rej *.run *.tmp @@ -37,3 +38,4 @@ .run xsl/system-xsl .validated-00book.xml +web/hgbook/secrets.py diff -r 40025381bded -r ad304b606163 web/README --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/README Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,5 @@ +This directory contains web-related files. Surprise! + +javascript - files used by the comment system, based on jQuery +rwh - Django app that acts as the comment back end +styles.css - style file diff -r 40025381bded -r ad304b606163 web/hgbook.conf --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook.conf Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,78 @@ +# -*- apache -*- + + + ServerName www.hgbook.org + ServerAdmin bos@serpentine.com + ErrorLog logs/hgbook-error_log + # Debian: + # CustomLog logs/hgbook-access_log full + # Fedora: + CustomLog logs/hgbook-access_log combined + Options +MultiViews + DirectoryIndex index.html.var index.html + DocumentRoot "/home/bos/hg/hgbook/en/html" + + # Actively redirect requests via a ServerAlias to the canonical hostname. + RewriteEngine On + RewriteCond %{HTTP_HOST} !=www.hgbook.org + RewriteRule ^(.*) http://www.hgbook.org$1 [R] + + + SetHandler python-program + # hg clone http://bitbucket.org/mirror/django-trunk/ + PythonPath "['/home/bos/hg/django-trunk', '/home/bos/hg/hgbook/web'] + sys.path" + PythonHandler django.core.handlers.modpython + PythonAutoReload Off + SetEnv DJANGO_SETTINGS_MODULE hgbook.settings + PythonDebug Off + + + + SetHandler None + DirectoryIndex index.html + + + + SetHandler None + + + + SetHandler None + + + + SetHandler None + + + + SetHandler None + + + + SetHandler None + + + Alias /media /home/bos/hg/django-trunk/django/contrib/admin/media + + + Options Indexes FollowSymlinks + AllowOverride None + Order allow,deny + Allow from all + + + + AllowOverride AuthConfig + + + + Options None + + + + + Options None + AllowOverride None + Order allow,deny + Allow from all + diff -r 40025381bded -r ad304b606163 web/hgbook/__init__.py diff -r 40025381bded -r ad304b606163 web/hgbook/comments/__init__.py diff -r 40025381bded -r ad304b606163 web/hgbook/comments/feeds.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/comments/feeds.py Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,35 @@ +from django.core.exceptions import ObjectDoesNotExist +from django.utils.feedgenerator import Atom1Feed +from django.contrib.syndication.feeds import Feed +from hgbook.comments.models import Comment, Element + +class MyAtomFeed(Atom1Feed): + title_type = u'html' + +class Comments(Feed): + feed_type = MyAtomFeed + title = 'Real World Haskell: recent comments' + subtitle = ('Recent comments on the text of “Real World ' + 'Haskell”, from our readers') + link = '/feeds/comments/' + author_name = 'Our readers' + + def feedfilter(self, queryset): + return queryset.order_by('-date')[:20] + + def items(self): + return self.feedfilter(Comment.objects) + + def item_author_name(self, obj): + return obj.submitter_name + + def item_pubdate(self, obj): + return obj.date + + def get_object(self, bits): + if len(bits) == 0: + return self.items() + elif len(bits) > 1: + raise ObjectDoesNotExist + return self.feedfilter(Comment.objects.filter(element__chapter=bits[0], + hidden=False)) diff -r 40025381bded -r ad304b606163 web/hgbook/comments/models.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/comments/models.py Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,62 @@ +from django.db import models +import sha + +mutable = True + +class Element(models.Model): + class Admin: + search_fields = ['id', 'chapter'] + list_filter = ['chapter', 'title'] + + id = models.CharField('ID attribute', max_length=64, editable=False, + primary_key=True) + chapter = models.CharField('Chapter ID', max_length=64, editable=False, + db_index=True) + title = models.CharField('Section title', max_length=256, editable=False) + + def __unicode__(self): + return self.id + +class Comment(models.Model): + class Admin: + list_display = ['element', 'submitter_name', 'comment', 'reviewed', + 'hidden', 'date'] + search_fields = ['comment'] + date_hierarchy = 'date' + list_filter = ['date', 'submitter_name'] + search_fields = ['title', 'submitter_name', 'submitter_url'] + fields = ( + (None, {'fields': ('submitter_name', 'element', 'comment')}), + ('Review and presentation state', + {'fields': ('reviewed', 'hidden')}), + ('Other info', {'fields': ('date', 'submitter_url', 'ip')}), + ) + + element = models.ForeignKey(Element, + help_text='ID of paragraph that was commented on') + comment = models.TextField(editable=mutable, + help_text='Text of submitted comment (please do not modify)') + submitter_name = models.CharField('Submitter', max_length=64, + help_text='Self-reported name of submitter (may be bogus)') + submitter_url = models.URLField('URL', blank=True, editable=mutable, + help_text='Self-reported URL of submitter (may be empty or bogus)') + ip = models.IPAddressField('IP address', editable=mutable, + help_text='IP address from which comment was submitted') + date = models.DateTimeField('date submitted', auto_now=True, + auto_now_add=True) + reviewed = models.BooleanField(default=False, db_index=True, + help_text='Has this comment been reviewed by an author?') + hidden = models.BooleanField(default=False, db_index=True, + help_text='Has this comment been hidden from public display?') + + def __unicode__(self): + return self.comment[:32] + + def get_absolute_url(self): + s = sha.new() + s.update(repr(self.comment)) + s.update(repr(self.submitter_name)) + s.update(str(self.date)) + return '/read/%s.html#%s?comment=%s&uuid=%s' % ( + self.element.chapter, self.element.id, self.id, s.hexdigest()[:20] + ) diff -r 40025381bded -r ad304b606163 web/hgbook/comments/sql/comment.mysql.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/comments/sql/comment.mysql.sql Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,2 @@ +alter table comments_comment convert to character set utf8 collate utf8_bin; +alter table comments_comment default character set utf8 collate utf8_bin; diff -r 40025381bded -r ad304b606163 web/hgbook/comments/sql/element.mysql.sql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/comments/sql/element.mysql.sql Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,2 @@ +alter table comments_element convert to character set utf8 collate utf8_bin; +alter table comments_element default character set utf8 collate utf8_bin; diff -r 40025381bded -r ad304b606163 web/hgbook/comments/urls.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/comments/urls.py Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('', + (r'chapter/(?P[^/]+)/?$', 'hgbook.comments.views.chapter'), + (r'chapter/(?P[^/]+)/count/?$', 'hgbook.comments.views.chapter_count'), + (r'single/(?P[^/]+)/?$', 'hgbook.comments.views.single'), + (r'submit/(?P[^/]+)/?$', 'hgbook.comments.views.submit') +) diff -r 40025381bded -r ad304b606163 web/hgbook/comments/views.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/comments/views.py Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,101 @@ +import django.newforms as forms +from django.db import connection +from django.http import HttpResponse +from hgbook.comments.models import Comment, Element +from django.shortcuts import get_object_or_404, render_to_response +from django.template import Context +from django.template.loader import get_template +from django.utils.simplejson import dumps + +def dump_queries(): + # requires settings.DEBUG to be set to True in order to work + if len(connection.queries) == 1: + print connection.queries + else: + qs = {} + for q in connection.queries: + qs[q['sql']] = qs.setdefault(q['sql'], 0) + 1 + for q in sorted(qs.items(), key=lambda x: x[1], reverse=True): + print q + print len(connection.queries) + +class CommentForm(forms.Form): + id = forms.CharField(widget=forms.HiddenInput) + name = forms.CharField(max_length=64) + url = forms.URLField(max_length=128, required=False) + comment = forms.CharField(widget=forms.Textarea(attrs={ + 'rows': 8, 'cols': 60 + })) + remember = forms.BooleanField(initial=True, required=False) + +def comments_by_chapter(id): + objs = {} + for c in Comment.objects.filter(element__chapter=id, hidden=False).order_by('date'): + objs.setdefault(c.element_id, []).append(c) + return objs + +def chapter(request, id): + template = get_template('comment.html') + resp = {} + for elt, comments in comments_by_chapter(id).iteritems(): + form = CommentForm(initial={ + 'id': elt, + 'name': request.session.get('name', ''), + }) + resp[elt] = template.render(Context({ + 'id': elt, + 'form': form, + 'length': len(comments), + 'query': comments, + })) + return HttpResponse(dumps(resp), mimetype='application/json') + +def chapter_count(request, id): + resp = comments_by_chapter(id) + for elt, comments in resp.iteritems(): + resp[elt] = len(comments) + return HttpResponse(dumps(resp), mimetype='application/json') + +def single(request, id, form=None, newid=None): + queryset = Comment.objects.filter(element=id, hidden=False).order_by('date') + if form is None: + form = CommentForm(initial={ + 'id': id, + 'name': request.session.get('name', ''), + }) + try: + error = form.errors[0] + except: + error = '' + return render_to_response('comment.html', { + 'id': id, + 'form': form, + 'length': len(queryset), + 'query': queryset, + 'newid': newid or True, + 'error': error, + }) + +def submit(request, id): + element = get_object_or_404(Element, id=id) + form = None + newid = None + if request.method == 'POST': + form = CommentForm(request.POST) + if form.is_valid(): + data = form.cleaned_data + if data.get('remember'): + request.session['name'] = data['name'] + request.session['url'] = data['url'] + else: + request.session.pop('name', None) + request.session.pop('url', None) + c = Comment(element=element, + comment=data['comment'], + submitter_name=data['name'], + submitter_url=data['url'], + ip=request.META.get('REMOTE_ADDR')) + c.save() + newid = c.id + form = None + return single(request, id, form, newid) diff -r 40025381bded -r ad304b606163 web/hgbook/dbutil.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/dbutil.py Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,31 @@ +import MySQLdb as mysql + +def connect(): + try: + import secrets + except ImportError: + print >> sys.stderr, 'Decrypt secrets.py.gpg or create a new copy!' + sys.exit(1) + + if secrets.DATABASE_ENGINE != 'mysql': + print >> sys.stderr, ('You are using a %s database' % + secrets.DATABASE_ENGINE) + sys.exit(1) + + kwargs = { + 'charset': 'utf8', + 'use_unicode': True, + } + if secrets.DATABASE_USER: + kwargs['user'] = secrets.DATABASE_USER + if secrets.DATABASE_NAME: + kwargs['db'] = secrets.DATABASE_NAME + if secrets.DATABASE_PASSWORD: + kwargs['passwd'] = secrets.DATABASE_PASSWORD + if secrets.DATABASE_HOST.startswith('/'): + kwargs['unix_socket'] = secrets.DATABASE_HOST + elif secrets.DATABASE_HOST: + kwargs['host'] = secrets.DATABASE_HOST + if secrets.DATABASE_PORT: + kwargs['port'] = int(secrets.DATABASE_PORT) + return mysql.connect(**kwargs) diff -r 40025381bded -r ad304b606163 web/hgbook/load_elements.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/load_elements.py Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# +# This script updates the contents of the comments_element table. +# It's fugly, but a lot less painful than trying to use Django's +# fixtures system. + +import os, sys +sys.path.append(os.path.dirname(__file__)) +import dbutil + +os.system('make -C ../../en ids') + +conn = dbutil.connect() +c = conn.cursor() +c.execute('''load data local infile "../../en/all-ids.dat" replace + into table comments_element + fields terminated by "|"''') +print 'Database updated' diff -r 40025381bded -r ad304b606163 web/hgbook/manage.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/manage.py Tue Mar 10 21:42:19 2009 -0700 @@ -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) diff -r 40025381bded -r ad304b606163 web/hgbook/reviewers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/reviewers.py Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os, sys +sys.path.append(os.path.dirname(__file__)) +import dbutil + +conn = dbutil.connect() +c = conn.cursor() + +c.execute('''select submitter_name from comments_comment''') + +reviewers = {} + +mappings = { + u'alejandro "tab-lover" dubrovsky': u'Alejandro Dubrovsky', + u'alex hirzel ': u'Alex Hirzel', + u'anonymous coward': u'Anonymous', + u'arthur van leeuwen': u'Arthur van Leeuwen', + u'augustss': u'Lennart Augustsson', + u'ed t': u'Anonymous', + u'geogre moschovitis': u'George Moschovitis', + u'george m': u'George Moschovitis', + u'haskell newb': u'Anonymous', + u'j. pablo fernandez': u'J. Pablo Fernández', + u'kamal al-marhoobi': u'Kamal Al-Marhubi', + u'kevin w.': u'Kevin Watters', + u'max cantor (#haskell - mxc)': u'Max Cantor', + u'michael campbell': u'Michael Campbell', + u'mike btauwerman': u'Mike Brauwerman', + u'no credit necessary': u'Anonymous', + u'nykänen, matti': u'Matti Nykänen', + u'omar antolin camarena': u'Omar Antolín Camarena', + u'ryan t mulligan': u'Ryan T. Mulligan', + u'sengan baring-gould': u'Sengan Baring-Gould', + u'some guy': u'Anonymous', + u'tomas janousek': u'Tomáš Janoušek', + u'william halchin': u'William N. Halchin', + } + +def fixup(s): + try: + return s.encode('ascii') + except UnicodeEncodeError: + def f(c): + o = ord(c) + if o < 128: + return c + return '&#%d;' % o + return ''.join(map(f, s)) + +total = 0 +for r in c.fetchall(): + r = r[0].decode('utf-8') + if r in ("Bryan O'Sullivan", 'John Goerzen', 'Don Stewart'): + continue + total += 1 + m = mappings.get(r.lower()) + if m: + r = m + elif len(r) < 2 or ' ' not in r: + r = 'Anonymous' + reviewers.setdefault(r, 0) + reviewers[r] += 1 + +reviewers = sorted(reviewers.iteritems(), key=lambda x: x[0]) + +cohorts = [(.01,1),(.002,.01)] + +for (lo,hi) in cohorts: + lo = total * lo + hi = total * hi + for r in [n for n in reviewers if lo <= n[1] < hi]: + if r[1] > 3: + print '%s,' % fixup(r[0]) + print + +lo = total * .002 +for n in reviewers: + if n[1] < lo: + print '%s,' % fixup(n[0]) diff -r 40025381bded -r ad304b606163 web/hgbook/secrets.py.gpg Binary file web/hgbook/secrets.py.gpg has changed diff -r 40025381bded -r ad304b606163 web/hgbook/settings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/settings.py Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,87 @@ +# Django settings for hgbook project. + +import os, sys + +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + ("Bryan O'Sullivan", 'bos@serpentine.com'), +) + +MANAGERS = ADMINS + +ROOT = os.path.dirname(sys.modules[__name__].__file__) + +try: + from secrets import DATABASE_ENGINE, DATABASE_NAME, DATABASE_USER, \ + DATABASE_PASSWORD, DATABASE_HOST, DATABASE_PORT, SECRET_KEY +except ImportError: + print >> sys.stderr, 'Faking up some database configuration for you' + DATABASE_ENGINE = 'sqlite3' + DATABASE_NAME = os.path.join(ROOT, '.database.sqlite3') + DATABASE_USER = '' + DATABASE_PASSWORD = '' + DATABASE_HOST = '' + DATABASE_PORT = '' + SECRET_KEY = '' + +# 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 avilable on all operating systems. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Los_Angeles' + +# Language code for this installation. All choices can be found here: +# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# Absolute path to the directory that holds media. +# Example: "/home/media/media.lawrence.com/" +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 = '' + +# 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 = '/media/' + +# 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', + 'django.middleware.doc.XViewMiddleware', +) + +ROOT_URLCONF = 'hgbook.urls' + +TEMPLATE_DIRS = ( + os.path.join(ROOT, 'templates') +) + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'hgbook.comments', +) diff -r 40025381bded -r ad304b606163 web/hgbook/templates/404.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/templates/404.html Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,8 @@ +{% extends "simple.html" %} + +{% block title %}Page Not Found{% endblock %} + +{% block body %} +

Sorry, we hit when trying to find the +page you requested.

+{% endblock %} diff -r 40025381bded -r ad304b606163 web/hgbook/templates/500.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/templates/500.html Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,11 @@ +{% extends "simple.html" %} + +{% block title %}Internal Server Error{% endblock %} + +{% block body %} +

Sorry, we hit when +trying to process your request. If possible, please let Bryan know that this problem happened, +and what you were doing when it occurred.

+{% endblock %} diff -r 40025381bded -r ad304b606163 web/hgbook/templates/boilerplate.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/templates/boilerplate.html Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,34 @@ + + + + {% block pagetitle %}Real World Haskell{% endblock %} + + + + + + + + + + {% block bodycontent %}{% endblock %} + +

Want to stay + up to date? Subscribe to comment feeds for any chapter, or + the entire book.

Copyright + 2007, 2008 Bryan O'Sullivan, Don Stewart, and John Goerzen. This + work is licensed under a Creative + Commons Attribution-Noncommercial 3.0 License. Icons by + Paul Davey aka Mattahan.

+
+ + + + + diff -r 40025381bded -r ad304b606163 web/hgbook/templates/comment.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/templates/comment.html Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,62 @@ +{% ifequal length 1 %} + One comment +{% else %} + {% if length %} + {{ length }} comments + {% else %} + No comments + + {% endif %} +{% endifequal %} +{% for c in query %} + +{% endfor %} + diff -r 40025381bded -r ad304b606163 web/hgbook/templates/feeds/comments_description.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/templates/feeds/comments_description.html Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,12 @@ +

On {{ obj.date|date:"Y-m-d" }}, + {% if obj.submitter_url %} + {{ obj.submitter_name|escape }} + {% else %} + {{ obj.submitter_name|escape }} + {% endif %} +commented on “{{ obj.element.title|escape }}”:

+
+{{ obj.comment|escape|linebreaks }} +
+

To see this comment in context or to respond, visit {{ site.domain }}

diff -r 40025381bded -r ad304b606163 web/hgbook/templates/feeds/comments_title.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/templates/feeds/comments_title.html Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,1 @@ +Comment on “{{ obj.element.title|escape }}” diff -r 40025381bded -r ad304b606163 web/hgbook/templates/simple.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/templates/simple.html Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,7 @@ +{% extends "boilerplate.html" %} + +{% block bodycontent %} + + +
{% block body %}{% endblock %}
+{% endblock %} diff -r 40025381bded -r ad304b606163 web/hgbook/urls.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/hgbook/urls.py Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,22 @@ +import os +from django.conf.urls.defaults import * +import hgbook.comments.feeds as feeds + +feeds = { + 'comments': feeds.Comments, + } + +urlpatterns = patterns('', + (r'^comments/', include('hgbook.comments.urls')), + + (r'^feeds/(?P.*)/$', 'django.contrib.syndication.views.feed', + {'feed_dict': feeds}), + + # Only uncomment this for local testing without Apache. + # (r'^html/(?P.*)$', 'django.views.static.serve', + # {'document_root': os.path.realpath(os.path.dirname( + # sys.modules[__name__].__file__) + '/../../en/html'), + + # Uncomment this for admin: + (r'^admin/', include('django.contrib.admin.urls')), +) diff -r 40025381bded -r ad304b606163 web/icons/caution.png Binary file web/icons/caution.png has changed diff -r 40025381bded -r ad304b606163 web/icons/favicon.png Binary file web/icons/favicon.png has changed diff -r 40025381bded -r ad304b606163 web/icons/important.png Binary file web/icons/important.png has changed diff -r 40025381bded -r ad304b606163 web/icons/note.png Binary file web/icons/note.png has changed diff -r 40025381bded -r ad304b606163 web/icons/remark.png Binary file web/icons/remark.png has changed diff -r 40025381bded -r ad304b606163 web/icons/rss.png Binary file web/icons/rss.png has changed diff -r 40025381bded -r ad304b606163 web/icons/shell.png Binary file web/icons/shell.png has changed diff -r 40025381bded -r ad304b606163 web/icons/source.png Binary file web/icons/source.png has changed diff -r 40025381bded -r ad304b606163 web/icons/tip.png Binary file web/icons/tip.png has changed diff -r 40025381bded -r ad304b606163 web/icons/warning.png Binary file web/icons/warning.png has changed diff -r 40025381bded -r ad304b606163 web/javascript/form-min.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/javascript/form-min.js Tue Mar 10 21:42:19 2009 -0700 @@ -0,0 +1,1 @@ +(function($){$.fn.ajaxSubmit=function(_2){if(typeof _2=="function"){_2={success:_2};}_2=$.extend({url:this.attr("action")||window.location,type:this.attr("method")||"GET"},_2||{});var _3={};$.event.trigger("form.pre.serialize",[this,_2,_3]);if(_3.veto){return this;}var a=this.formToArray(_2.semantic);if(_2.data){for(var n in _2.data){a.push({name:n,value:_2.data[n]});}}if(_2.beforeSubmit&&_2.beforeSubmit(a,this,_2)===false){return this;}$.event.trigger("form.submit.validate",[a,this,_2,_3]);if(_3.veto){return this;}var q=$.param(a);if(_2.type.toUpperCase()=="GET"){_2.url+=(_2.url.indexOf("?")>=0?"&":"?")+q;_2.data=null;}else{_2.data=q;}var _7=this,callbacks=[];if(_2.resetForm){callbacks.push(function(){_7.resetForm();});}if(_2.clearForm){callbacks.push(function(){_7.clearForm();});}if(!_2.dataType&&_2.target){var _8=_2.success||function(){};callbacks.push(function(_9){if(this.evalScripts){$(_2.target).attr("innerHTML",_9).evalScripts().each(_8,arguments);}else{$(_2.target).html(_9).each(_8,arguments);}});}else{if(_2.success){callbacks.push(_2.success);}}_2.success=function(_a,_b){for(var i=0,max=callbacks.length;i");var io=$io[0];var op8=$.browser.opera&&window.opera.version()<9;if($.browser.msie||op8){io.src="javascript:false;document.write(\"\");";}$io.css({position:"absolute",top:"-1000px",left:"-1000px"});var xhr={responseText:null,responseXML:null,status:0,statusText:"n/a",getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){}};var g=_11.global;if(g&&!$.active++){$.event.trigger("ajaxStart");}if(g){$.event.trigger("ajaxSend",[xhr,_11]);}var _18=0;var _19=0;setTimeout(function(){$io.appendTo("body");io.attachEvent?io.attachEvent("onload",cb):io.addEventListener("load",cb,false);var _1a=_10.encoding?"encoding":"enctype";var t=_7.attr("target");_7.attr({target:id,method:"POST",action:_11.url});_10[_1a]="multipart/form-data";if(_11.timeout){setTimeout(function(){_19=true;cb();},_11.timeout);}_10.submit();_7.attr("target",t);},10);function cb(){if(_18++){return;}io.detachEvent?io.detachEvent("onload",cb):io.removeEventListener("load",cb,false);var ok=true;try{if(_19){throw "timeout";}var _1d,doc;doc=io.contentWindow?io.contentWindow.document:io.contentDocument?io.contentDocument:io.document;xhr.responseText=doc.body?doc.body.innerHTML:null;xhr.responseXML=doc.XMLDocument?doc.XMLDocument:doc;if(_11.dataType=="json"||_11.dataType=="script"){var ta=doc.getElementsByTagName("textarea")[0];_1d=ta?ta.value:xhr.responseText;if(_11.dataType=="json"){eval("data = "+_1d);}else{$.globalEval(_1d);}}else{if(_11.dataType=="xml"){_1d=xhr.responseXML;if(!_1d&&xhr.responseText!=null){_1d=toXml(xhr.responseText);}}else{_1d=xhr.responseText;}}}catch(e){ok=false;$.handleError(_11,xhr,"error",e);}if(ok){_11.success(_1d,"success");if(g){$.event.trigger("ajaxSuccess",[xhr,_11]);}}if(g){$.event.trigger("ajaxComplete",[xhr,_11]);}if(g&&!--$.active){$.event.trigger("ajaxStop");}if(_11.complete){_11.complete(xhr,ok?"success":"error");}setTimeout(function(){$io.remove();xhr.responseXML=null;},100);}function toXml(s,doc){if(window.ActiveXObject){doc=new ActiveXObject("Microsoft.XMLDOM");doc.async="false";doc.loadXML(s);}else{doc=(new DOMParser()).parseFromString(s,"text/xml");}return (doc&&doc.documentElement&&doc.documentElement.tagName!="parsererror")?doc:null;}}};$.fn.ajaxSubmit.counter=0;$.fn.ajaxForm=function(_21){return this.ajaxFormUnbind().submit(submitHandler).each(function(){this.formPluginId=$.fn.ajaxForm.counter++;$.fn.ajaxForm.optionHash[this.formPluginId]=_21;$(":submit,input:image",this).click(clickHandler);});};$.fn.ajaxForm.counter=1;$.fn.ajaxForm.optionHash={};function clickHandler(e){var _23=this.form;_23.clk=this;if(this.type=="image"){if(e.offsetX!=undefined){_23.clk_x=e.offsetX;_23.clk_y=e.offsetY;}else{if(typeof $.fn.offset=="function"){var _24=$(this).offset();_23.clk_x=e.pageX-_24.left;_23.clk_y=e.pageY-_24.top;}else{_23.clk_x=e.pageX-this.offsetLeft;_23.clk_y=e.pageY-this.offsetTop;}}}setTimeout(function(){_23.clk=_23.clk_x=_23.clk_y=null;},10);}function submitHandler(){var id=this.formPluginId;var _26=$.fn.ajaxForm.optionHash[id];$(this).ajaxSubmit(_26);return false;}$.fn.ajaxFormUnbind=function(){this.unbind("submit",submitHandler);return this.each(function(){$(":submit,input:image",this).unbind("click",clickHandler);});};$.fn.formToArray=function(_27){var a=[];if(this.length==0){return a;}var _29=this[0];var els=_27?_29.getElementsByTagName("*"):_29.elements;if(!els){return a;}for(var i=0,max=els.length;i= 0 ? '&' : '?') + q; + options.data = null; // data is null for 'get' + } + else + options.data = q; // data is the query string for 'post' + + var $form = this, callbacks = []; + if (options.resetForm) callbacks.push(function() { $form.resetForm(); }); + if (options.clearForm) callbacks.push(function() { $form.clearForm(); }); + + // perform a load on the target only if dataType is not provided + if (!options.dataType && options.target) { + var oldSuccess = options.success || function(){}; + callbacks.push(function(data) { + if (this.evalScripts) + $(options.target).attr("innerHTML", data).evalScripts().each(oldSuccess, arguments); + else // jQuery v1.1.4 + $(options.target).html(data).each(oldSuccess, arguments); + }); + } + else if (options.success) + callbacks.push(options.success); + + options.success = function(data, status) { + for (var i=0, max=callbacks.length; i < max; i++) + callbacks[i](data, status, $form); + }; + + // are there files to upload? + var files = $('input:file', this).fieldValue(); + var found = false; + for (var j=0; j < files.length; j++) + if (files[j]) + found = true; + + if (options.iframe || found) // options.iframe allows user to force iframe mode + fileUpload(); + else + $.ajax(options); + + // fire 'notify' event + $.event.trigger('form.submit.notify', [this, options]); + return this; + + + // private function for handling file uploads (hat tip to YAHOO!) + function fileUpload() { + var form = $form[0]; + var opts = $.extend({}, $.ajaxSettings, options); + + var id = 'jqFormIO' + $.fn.ajaxSubmit.counter++; + var $io = $('