From 7bf34c27ad5be5edfcf6f63a8c4d5fa931c005ef Mon Sep 17 00:00:00 2001 From: Andrew Badr Date: Sun, 25 Jul 2010 16:27:14 -0700 Subject: [PATCH] initial open-source commit --- .gitignore | 3 + __init__.py | 0 apache/wsgi.py | 8 + apache/yourworldoftext | 7 + helpers.py | 19 + lib/__init__.py | 0 lib/jsonfield.py | 26 + lib/log.py | 41 + manage.py | 11 + requirements.txt | 2 + settings.py | 83 + static/Icon_External_Link.png | Bin 0 -> 172 bytes static/css/blueprint/ie.css | 35 + static/css/blueprint/print.css | 29 + static/css/blueprint/screen.css | 258 + static/css/site.css | 117 + static/favicon.png | Bin 0 -> 842 bytes static/jquery-1.3.2.js | 4376 +++++++++++++++++ static/jquery-1.3.2.min.js | 19 + static/jquery-ui-1.7.2.custom.js | 1112 +++++ static/jquery-ui-1.7.2.custom.min.js | 31 + static/jquery.autocomplete.css | 50 + static/jquery.autocomplete.min.js | 13 + static/jquery.droppy.js | 59 + static/jquery.scrollview.js | 67 + static/jquery.simplemodal-1.3.3.js | 612 +++ static/jquery.simplemodal-1.3.3.min.js | 8 + static/jquery.simplemodal-1.3.3.mod.js | 615 +++ static/json2.js | 476 ++ static/profile.js | 3 + static/twitterfeed.js | 8 + static/yourworld.css | 192 + static/yourworld.js | 1333 +++++ templates/404.html | 8 + templates/500.html | 9 + templates/base.html | 70 + templates/configure.html | 118 + templates/home.html | 26 + templates/private.html | 23 + templates/profile.html | 67 + templates/registration/activate.html | 13 + templates/registration/activation_email.txt | 5 + .../registration/activation_email_subject.txt | 1 + templates/registration/login.html | 27 + .../registration/registration_complete.html | 14 + templates/registration/registration_form.html | 56 + templates/yourworld.html | 62 + urls.py | 47 + ywot/__init__.py | 0 ywot/models.py | 96 + ywot/permissions.py | 71 + ywot/views.py | 387 ++ 52 files changed, 10713 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 apache/wsgi.py create mode 100644 apache/yourworldoftext create mode 100644 helpers.py create mode 100644 lib/__init__.py create mode 100644 lib/jsonfield.py create mode 100644 lib/log.py create mode 100644 manage.py create mode 100644 requirements.txt create mode 100644 settings.py create mode 100644 static/Icon_External_Link.png create mode 100644 static/css/blueprint/ie.css create mode 100644 static/css/blueprint/print.css create mode 100644 static/css/blueprint/screen.css create mode 100644 static/css/site.css create mode 100644 static/favicon.png create mode 100644 static/jquery-1.3.2.js create mode 100644 static/jquery-1.3.2.min.js create mode 100644 static/jquery-ui-1.7.2.custom.js create mode 100644 static/jquery-ui-1.7.2.custom.min.js create mode 100644 static/jquery.autocomplete.css create mode 100644 static/jquery.autocomplete.min.js create mode 100644 static/jquery.droppy.js create mode 100644 static/jquery.scrollview.js create mode 100644 static/jquery.simplemodal-1.3.3.js create mode 100644 static/jquery.simplemodal-1.3.3.min.js create mode 100644 static/jquery.simplemodal-1.3.3.mod.js create mode 100644 static/json2.js create mode 100644 static/profile.js create mode 100644 static/twitterfeed.js create mode 100644 static/yourworld.css create mode 100644 static/yourworld.js create mode 100644 templates/404.html create mode 100644 templates/500.html create mode 100644 templates/base.html create mode 100644 templates/configure.html create mode 100644 templates/home.html create mode 100644 templates/private.html create mode 100644 templates/profile.html create mode 100644 templates/registration/activate.html create mode 100644 templates/registration/activation_email.txt create mode 100644 templates/registration/activation_email_subject.txt create mode 100644 templates/registration/login.html create mode 100644 templates/registration/registration_complete.html create mode 100644 templates/registration/registration_form.html create mode 100644 templates/yourworld.html create mode 100644 urls.py create mode 100644 ywot/__init__.py create mode 100644 ywot/models.py create mode 100644 ywot/permissions.py create mode 100644 ywot/views.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..684f257 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pyc +*.swp +localsettings.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apache/wsgi.py b/apache/wsgi.py new file mode 100644 index 0000000..f54335e --- /dev/null +++ b/apache/wsgi.py @@ -0,0 +1,8 @@ +import os, sys +sys.path.append('/var/www/yourworld/') +sys.path.append('/var/www/') +os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' + +import django.core.handlers.wsgi + +application = django.core.handlers.wsgi.WSGIHandler() diff --git a/apache/yourworldoftext b/apache/yourworldoftext new file mode 100644 index 0000000..5e1c5cc --- /dev/null +++ b/apache/yourworldoftext @@ -0,0 +1,7 @@ + + ServerName yourserver.example.com + ServerAdmin yourname@example.com + Alias /static/ /var/www/yourworldoftext/static/ + Alias /media/ /usr/lib/python2.6/site-packages/django/contrib/admin/media/ + WSGIScriptAlias / /var/www/yourworldoftext/apache/wsgi.py + diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..9714d17 --- /dev/null +++ b/helpers.py @@ -0,0 +1,19 @@ +import os, re + +def here(*args): + return os.path.join(os.path.abspath(os.path.dirname(__file__)), *args) + +def req_render_to_response(request, template, context=None): + from django.shortcuts import render_to_response + from django.template import RequestContext + context = context or {} + rc = RequestContext(request, context) + return render_to_response(template, context_instance=rc) + +# This block is from http://stackoverflow.com/questions/92438/ +control_chars = ''.join(map(unichr, range(0,32) + range(127,160))) +control_chars_set = set(control_chars) +control_char_re = re.compile('[%s]' % re.escape(control_chars)) +def remove_control_chars(s): + return control_char_re.sub('', s) + diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/jsonfield.py b/lib/jsonfield.py new file mode 100644 index 0000000..ffe7065 --- /dev/null +++ b/lib/jsonfield.py @@ -0,0 +1,26 @@ +#http://www.djangosnippets.org/snippets/1478/ + +from django.db import models +from django.core.serializers.json import DjangoJSONEncoder +from django.utils import simplejson as json + +class DictField(models.TextField): + """DictField is a textfield that contains JSON-serialized dictionaries.""" + + # Used so to_python() is called + __metaclass__ = models.SubfieldBase + + def to_python(self, value): + """Convert our string value to JSON after we load it from the DB""" + if isinstance(value, dict): + return value + value = json.loads(value) + assert isinstance(value, dict) + return value + + def get_db_prep_save(self, value): + """Convert our JSON object to a string before we save""" + assert isinstance(value, dict) + value = json.dumps(value, cls=DjangoJSONEncoder) + return super(DictField, self).get_db_prep_save(value) + diff --git a/lib/log.py b/lib/log.py new file mode 100644 index 0000000..3a9b17a --- /dev/null +++ b/lib/log.py @@ -0,0 +1,41 @@ +import os, logging, logging.handlers, time + +from django.conf import settings + +def _mkdir(newdir): + # Copied from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/82465 + """works the way a good mkdir should :) + - already exists, silently complete + - regular file in the way, raise an exception + - parent directory(ies) does not exist, make them as well + """ + if os.path.isdir(newdir): + pass + elif os.path.isfile(newdir): + raise OSError("a file with the same name as the desired " \ + "dir, '%s', already exists." % newdir) + else: + head, tail = os.path.split(newdir) + if head and not os.path.isdir(head): + _mkdir(head) + if tail: + os.mkdir(newdir) + +_mkdir(settings.LOG_DIRECTORY) + +filename = settings.LOG_DIRECTORY + '/application.log' + +logger = logging.getLogger('default') +handler = logging.handlers.RotatingFileHandler(filename, maxBytes=10*1024*1024, backupCount=10) +formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") +handler.setFormatter(formatter) +logger.addHandler(handler) + +logger.setLevel(1) # 0 seems to skip DEBUG messages, contrary to the docs + +debug = logger.debug +info = logger.info +warning = logger.warning +error = logger.error +critical = logger.critical +exception = logger.exception diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..5e78ea9 --- /dev/null +++ b/manage.py @@ -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 --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ac972d0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Django==1.2.1 +django-registration==0.7 diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..802075a --- /dev/null +++ b/settings.py @@ -0,0 +1,83 @@ +from helpers import here + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( +) + +MANAGERS = ADMINS + +DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. +DATABASE_NAME = 'ywot.sqlite' # Or path to database file if using sqlite3. +DATABASE_USER = '' # Not used with sqlite3. +DATABASE_PASSWORD = '' # Not used with sqlite3. +DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. +DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. + +TIME_ZONE = 'America/Los_Angeles' + +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +USE_I18N = False + +# 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/' + +SECRET_KEY = 'default' + +# 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.csrf.CsrfViewMiddleware', +) + +ROOT_URLCONF = 'yourworld.urls' + +TEMPLATE_DIRS = ( + here('templates') +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'registration', + 'ywot', +) + +DEFAULT_FROM_EMAIL = SERVER_EMAIL = '"Your World of Text" ' + +# You should change this +LOG_DIRECTORY = './log/' + +ACCOUNT_ACTIVATION_DAYS = 3 + +try: + from localsettings import * +except: + pass + +assert DEBUG or (SECRET_KEY != 'default'), "Change the secret key." diff --git a/static/Icon_External_Link.png b/static/Icon_External_Link.png new file mode 100644 index 0000000000000000000000000000000000000000..ba4f2059d64b625143dfc3e604aaf2ce6147efae GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&k&H|6fVg?4j!ywFfJby(BP%zHZ z#WAE}&e{nFc@G%yFl`Pl`{Z}8bM6b}O)|mD=d*QAw-huwCMtPz*X5E6G9j{!88etd z4;;t}@{T)D*Rer)c9g?m!)r3O4)@al?#Y%84Y8y5e6#b{RlqVAse RV`ZRC44$rjF6*2UngCpdJq-W= literal 0 HcmV?d00001 diff --git a/static/css/blueprint/ie.css b/static/css/blueprint/ie.css new file mode 100644 index 0000000..3dddda9 --- /dev/null +++ b/static/css/blueprint/ie.css @@ -0,0 +1,35 @@ +/* ----------------------------------------------------------------------- + + + Blueprint CSS Framework 0.9 + 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 .span-1, * html .span-2, * html .span-3, * html .span-4, * html .span-5, * html .span-6, * html .span-7, * html .span-8, * html .span-9, * html .span-10, * html .span-11, * html .span-12, * html .span-13, * html .span-14, * html .span-15, * html .span-16, * html .span-17, * html .span-18, * html .span-19, * html .span-20, * html .span-21, * html .span-22, * html .span-23, * html .span-24 {display:inline;overflow-x:hidden;} +* html legend {margin:0px -8px 16px 0;padding:0;} +sup {vertical-align:text-top;} +sub {vertical-align:text-bottom;} +html>body p code {*white-space:normal;} +hr {margin:-8px auto 11px;} +img {-ms-interpolation-mode:bicubic;} +.clearfix, .container {display:inline-block;} +* html .clearfix, * html .container {height:1%;} +fieldset {padding-top:0;} +textarea {overflow:auto;} +input.text, input.title, textarea {background-color:#fff;border:1px solid #bbb;} +input.text:focus, input.title:focus {border-color:#666;} +input.text, input.title, textarea, select {margin:0.5em 0;} +input.checkbox, input.radio {position:relative;top:.25em;} +form.inline div, form.inline p {vertical-align:middle;} +form.inline label {position:relative;top:-0.25em;} +form.inline input.checkbox, form.inline input.radio, form.inline input.button, form.inline button {margin:0.5em 0;} +button, input.button {position:relative;top:0.25em;} \ No newline at end of file diff --git a/static/css/blueprint/print.css b/static/css/blueprint/print.css new file mode 100644 index 0000000..fdb8220 --- /dev/null +++ b/static/css/blueprint/print.css @@ -0,0 +1,29 @@ +/* ----------------------------------------------------------------------- + + + Blueprint CSS Framework 0.9 + 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;visibility:hidden;} +h1, h2, h3, h4, h5, h6 {font-family:"Helvetica Neue", Arial, "Lucida Grande", sans-serif;} +code {font:.9em "Courier New", Monaco, Courier, monospace;} +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 diff --git a/static/css/blueprint/screen.css b/static/css/blueprint/screen.css new file mode 100644 index 0000000..22a07b6 --- /dev/null +++ b/static/css/blueprint/screen.css @@ -0,0 +1,258 @@ +/* ----------------------------------------------------------------------- + + + Blueprint CSS Framework 0.9 + 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, article, aside, dialog, figure, footer, header, hgroup, nav, section {margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;} +article, aside, dialog, figure, footer, header, hgroup, nav, section {display:block;} +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 */ +html {font-size:100.01%;} +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;} +ul, ol {margin:0 1.5em 1.5em 0;padding-left:3.333em;} +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;} + +/* 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[type=text], input[type=password], input.text, input.title, textarea, select {background-color:#fff;border:1px solid #bbb;} +input[type=text]:focus, input[type=password]:focus, input.text:focus, input.title:focus, textarea:focus, select:focus {border-color:#666;} +input[type=text], input[type=password], input.text, input.title, textarea, select {margin:0.5em 0;} +input.text, input.title {width:300px;padding:5px;} +input.title {font-size:1.5em;} +textarea {width:390px;height:250px;padding:5px;} +input[type=checkbox], input[type=radio], input.checkbox, input.radio {position:relative;top:.25em;} +form.inline {line-height:3;} +form.inline p {margin-bottom:0;} +.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;} + +/* grid.css */ +.container {width:950px;margin:0 auto;} +.showgrid {background:url(src/grid.png);} +.column, .span-1, .span-2, .span-3, .span-4, .span-5, .span-6, .span-7, .span-8, .span-9, .span-10, .span-11, .span-12, .span-13, .span-14, .span-15, .span-16, .span-17, .span-18, .span-19, .span-20, .span-21, .span-22, .span-23, .span-24 {float:left;margin-right:10px;} +.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 {width:950px;margin-right:0;} +input.span-1, textarea.span-1, input.span-2, textarea.span-2, input.span-3, textarea.span-3, input.span-4, textarea.span-4, input.span-5, textarea.span-5, input.span-6, textarea.span-6, input.span-7, textarea.span-7, input.span-8, textarea.span-8, input.span-9, textarea.span-9, input.span-10, textarea.span-10, input.span-11, textarea.span-11, input.span-12, textarea.span-12, input.span-13, textarea.span-13, input.span-14, textarea.span-14, input.span-15, textarea.span-15, input.span-16, textarea.span-16, input.span-17, textarea.span-17, input.span-18, textarea.span-18, input.span-19, textarea.span-19, input.span-20, textarea.span-20, input.span-21, textarea.span-21, input.span-22, textarea.span-22, input.span-23, textarea.span-23, input.span-24, textarea.span-24 {border-left-width:1px!important;border-right-width:1px!important;padding-left:5px!important;padding-right:5px!important;} +input.span-1, textarea.span-1 {width:18px!important;} +input.span-2, textarea.span-2 {width:58px!important;} +input.span-3, textarea.span-3 {width:98px!important;} +input.span-4, textarea.span-4 {width:138px!important;} +input.span-5, textarea.span-5 {width:178px!important;} +input.span-6, textarea.span-6 {width:218px!important;} +input.span-7, textarea.span-7 {width:258px!important;} +input.span-8, textarea.span-8 {width:298px!important;} +input.span-9, textarea.span-9 {width:338px!important;} +input.span-10, textarea.span-10 {width:378px!important;} +input.span-11, textarea.span-11 {width:418px!important;} +input.span-12, textarea.span-12 {width:458px!important;} +input.span-13, textarea.span-13 {width:498px!important;} +input.span-14, textarea.span-14 {width:538px!important;} +input.span-15, textarea.span-15 {width:578px!important;} +input.span-16, textarea.span-16 {width:618px!important;} +input.span-17, textarea.span-17 {width:658px!important;} +input.span-18, textarea.span-18 {width:698px!important;} +input.span-19, textarea.span-19 {width:738px!important;} +input.span-20, textarea.span-20 {width:778px!important;} +input.span-21, textarea.span-21 {width:818px!important;} +input.span-22, textarea.span-22 {width:858px!important;} +input.span-23, textarea.span-23 {width:898px!important;} +input.span-24, textarea.span-24 {width:938px!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;} +.border {padding-right:4px;margin-right:5px;border-right:1px solid #eee;} +.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;visibility:hidden;} +.clearfix:after, .container:after {content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden;} +.clearfix, .container {display:block;} +.clear {clear:both;} diff --git a/static/css/site.css b/static/css/site.css new file mode 100644 index 0000000..5fe49cb --- /dev/null +++ b/static/css/site.css @@ -0,0 +1,117 @@ +/* Blueprint overrides: */ +body { + font-family: Verdana, "Helvetica Neue", Arial, Helvetica, sans-serif; +} +.container { + margin: 0; +} +ul, ol { + padding-left: 1em; +} + +/* end blueprint overrides */ + +body { + padding-left: 2em; + background: #eee; +} + +#topbar { + text-align: right; +} + +p { + margin-bottom: 1em; +} + +ul { + margin-top: .2em; +} + +li { + margin-left: 2em; +} + +#updates { + background: #ddf; + overflow: hidden; +} + +#updates ul, #updates ol { + list-style-type: none; + padding: 0; + margin-bottom:0; +} + +#updates li { + padding-top: .5em; + padding-bottom: .5em; +} + +#updates_title { + font-style: italic; + text-align: center; + font-size: 80%; +} + +#updates_container { + margin-top: .5em; +} + +.text { + text-align: left; + padding-left: 2em; + max-width: 360px; /* 9 cols, 30em */ +} + +.date { +color: #444; +font-size: 80%; +} + +.loading { + color: #666; +} + +.content_set { + background-color:#DDDDDD; + border:1px solid #AAAAAA; + margin-bottom:1em; + padding-bottom:0.3em; +} + +.content_set_content { + clear:both; + padding:0.2em 0.5em; +} + +.content_set_title { + background-color: #eee; + float:left; + font-size:120%; + margin-left:1px; + margin-top:1px; + padding:0.1em 0.5em; + color: #222; +} +.set_description { + font-size: 80%; + max-width: 80%; + color: #444; +} + +#feature_choices td { + padding-top:0.5em; + padding-bottom:0.5em; +} + +.feature_name { + padding-right:0.5em; +} + +.feature_description { + padding-left: .5em; + font-size: 80%; + color: #444; + width: 30%; +} diff --git a/static/favicon.png b/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..3e3e6e2183c66efa6300f77b00a499421b4d3306 GIT binary patch literal 842 zcmV-Q1GW5#P)j6N5ouFo+Qt1meHY$;>jdvuy3v&MsF6 zRVkQG7`{&abUJbH4_?j*eL{Z+^xPuwJP6;1?+f^0f%#gu^oWF~?LLIibuz)Q097pE zt-p1lyNuB-_MJ;Z|F_!L{}lQ;%)4)f)?l&p;5ZJIAXiv!btAHX**c~j$}wtlok+N3 z^AH3QevpzgSLjemva_{~%Mn-oYvj=;LPNO7+WF2t6ahg;&1E}0NSFf_tF8B#=lRWH z{giUuCQ`4|^oWS9y(iy>daO)u$zlUzfO6r$4eOhwv?C(R!CEGAj3@1DC*soQ^kGIY zx~II=2>~^~Z>wUihdzSo{~m#r*UdpaSG;wE)b#r3p%SBSPjI@2)%aXascz@Sn8?}8 zyyG!)l16G);O}JhvXFO`i7tNrAd8KvRjk`^DBYmX1j$Nu1-+N7)MG)Dx1N3|PESol zld|0Dq{%|J%ZzN5kto*CK{8o&Ja_RALo4&aX77V@Lz6UXDhPe|3o`-h(1pViwB&ez z0Y`$K474BmW>m)QDCw9kw7!|q{Et8{ZF1A=$JYfhx7ueB#y7E3)F8XaFuxZ1f~G7{ zRu!_)MvkmRrEPW7Kbep^4qIpzH+Kt9CXAgY>PrM)ht?Y9*B*K-Ivzh8=E^!3Kg8lb zmj?RInKL#0ub`d3EGP5{eL{b}8jcE)&+p>22vc;^L7}HMvwJ@mK~kV;YUHuB@Eql^ z91MD@&5G#iy!as{o%_Ppg`2GH0MJduC_5|1z06NbCGCy^-9xU4PgL&>g-MXSYv%Ub z&^9-Yn*HpzqkZ9ZGyB`nb{CSi``Pc(t$B86bLXS<^e5Z>p^!)FX>&&57yHLoh3{;( zP`^GA7X#!-e29H9r%fzsjuZ0cE~Sv>%KXW`Za~D%0#jcOwjwGzgw$|6K-CR&pg6WA zq&>U|%Hf!~M_0ynDc=^3r0x%%)[^>]*$|^#([\w-]+)$/, + // Is it a simple selector + isSimple = /^.[^:#\[\.,]*$/; + +jQuery.fn = jQuery.prototype = { + init: function( selector, context ) { + // Make sure that a selection was provided + selector = selector || document; + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this[0] = selector; + this.length = 1; + this.context = selector; + return this; + } + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + var match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) + selector = jQuery.clean( [ match[1] ], context ); + + // HANDLE: $("#id") + else { + var elem = document.getElementById( match[3] ); + + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem && elem.id != match[3] ) + return jQuery().find( selector ); + + // Otherwise, we inject the element directly into the jQuery object + var ret = jQuery( elem || [] ); + ret.context = document; + ret.selector = selector; + return ret; + } + + // HANDLE: $(expr, [context]) + // (which is just equivalent to: $(content).find(expr) + } else + return jQuery( context ).find( selector ); + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) + return jQuery( document ).ready( selector ); + + // Make sure that old selector state is passed along + if ( selector.selector && selector.context ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return this.setArray(jQuery.isArray( selector ) ? + selector : + jQuery.makeArray(selector)); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.3.2", + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num === undefined ? + + // Return a 'clean' array + Array.prototype.slice.call( this ) : + + // Return just the object + this[ num ]; + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = jQuery( elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) + ret.selector = this.selector + (this.selector ? " " : "") + selector; + else if ( name ) + ret.selector = this.selector + "." + name + "(" + selector + ")"; + + // Return the newly-formed element set + return ret; + }, + + // Force the current matched set of elements to become + // the specified array of elements (destroying the stack in the process) + // You should use pushStack() in order to do this, but maintain the stack + setArray: function( elems ) { + // Resetting the length to 0, then using the native Array push + // is a super-fast way to populate an object with array-like properties + this.length = 0; + Array.prototype.push.apply( this, elems ); + + return this; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem && elem.jquery ? elem[0] : elem + , this ); + }, + + attr: function( name, value, type ) { + var options = name; + + // Look for the case where we're accessing a style value + if ( typeof name === "string" ) + if ( value === undefined ) + return this[0] && jQuery[ type || "attr" ]( this[0], name ); + + else { + options = {}; + options[ name ] = value; + } + + // Check to see if we're setting style values + return this.each(function(i){ + // Set all the styles + for ( name in options ) + jQuery.attr( + type ? + this.style : + this, + name, jQuery.prop( this, options[ name ], type, i, name ) + ); + }); + }, + + css: function( key, value ) { + // ignore negative width and height values + if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 ) + value = undefined; + return this.attr( key, value, "curCSS" ); + }, + + text: function( text ) { + if ( typeof text !== "object" && text != null ) + return this.empty().append( (this[0] && this[0].ownerDocument || document).createTextNode( text ) ); + + var ret = ""; + + jQuery.each( text || this, function(){ + jQuery.each( this.childNodes, function(){ + if ( this.nodeType != 8 ) + ret += this.nodeType != 1 ? + this.nodeValue : + jQuery.fn.text( [ this ] ); + }); + }); + + return ret; + }, + + wrapAll: function( html ) { + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).clone(); + + if ( this[0].parentNode ) + wrap.insertBefore( this[0] ); + + wrap.map(function(){ + var elem = this; + + while ( elem.firstChild ) + elem = elem.firstChild; + + return elem; + }).append(this); + } + + return this; + }, + + wrapInner: function( html ) { + return this.each(function(){ + jQuery( this ).contents().wrapAll( html ); + }); + }, + + wrap: function( html ) { + return this.each(function(){ + jQuery( this ).wrapAll( html ); + }); + }, + + append: function() { + return this.domManip(arguments, true, function(elem){ + if (this.nodeType == 1) + this.appendChild( elem ); + }); + }, + + prepend: function() { + return this.domManip(arguments, true, function(elem){ + if (this.nodeType == 1) + this.insertBefore( elem, this.firstChild ); + }); + }, + + before: function() { + return this.domManip(arguments, false, function(elem){ + this.parentNode.insertBefore( elem, this ); + }); + }, + + after: function() { + return this.domManip(arguments, false, function(elem){ + this.parentNode.insertBefore( elem, this.nextSibling ); + }); + }, + + end: function() { + return this.prevObject || jQuery( [] ); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: [].push, + sort: [].sort, + splice: [].splice, + + find: function( selector ) { + if ( this.length === 1 ) { + var ret = this.pushStack( [], "find", selector ); + ret.length = 0; + jQuery.find( selector, this[0], ret ); + return ret; + } else { + return this.pushStack( jQuery.unique(jQuery.map(this, function(elem){ + return jQuery.find( selector, elem ); + })), "find", selector ); + } + }, + + clone: function( events ) { + // Do the clone + var ret = this.map(function(){ + if ( !jQuery.support.noCloneEvent && !jQuery.isXMLDoc(this) ) { + // IE copies events bound via attachEvent when + // using cloneNode. Calling detachEvent on the + // clone will also remove the events from the orignal + // In order to get around this, we use innerHTML. + // Unfortunately, this means some modifications to + // attributes in IE that are actually only stored + // as properties will not be copied (such as the + // the name attribute on an input). + var html = this.outerHTML; + if ( !html ) { + var div = this.ownerDocument.createElement("div"); + div.appendChild( this.cloneNode(true) ); + html = div.innerHTML; + } + + return jQuery.clean([html.replace(/ jQuery\d+="(?:\d+|null)"/g, "").replace(/^\s*/, "")])[0]; + } else + return this.cloneNode(true); + }); + + // Copy the events from the original to the clone + if ( events === true ) { + var orig = this.find("*").andSelf(), i = 0; + + ret.find("*").andSelf().each(function(){ + if ( this.nodeName !== orig[i].nodeName ) + return; + + var events = jQuery.data( orig[i], "events" ); + + for ( var type in events ) { + for ( var handler in events[ type ] ) { + jQuery.event.add( this, type, events[ type ][ handler ], events[ type ][ handler ].data ); + } + } + + i++; + }); + } + + // Return the cloned set + return ret; + }, + + filter: function( selector ) { + return this.pushStack( + jQuery.isFunction( selector ) && + jQuery.grep(this, function(elem, i){ + return selector.call( elem, i ); + }) || + + jQuery.multiFilter( selector, jQuery.grep(this, function(elem){ + return elem.nodeType === 1; + }) ), "filter", selector ); + }, + + closest: function( selector ) { + var pos = jQuery.expr.match.POS.test( selector ) ? jQuery(selector) : null, + closer = 0; + + return this.map(function(){ + var cur = this; + while ( cur && cur.ownerDocument ) { + if ( pos ? pos.index(cur) > -1 : jQuery(cur).is(selector) ) { + jQuery.data(cur, "closest", closer); + return cur; + } + cur = cur.parentNode; + closer++; + } + }); + }, + + not: function( selector ) { + if ( typeof selector === "string" ) + // test special case where just one selector is passed in + if ( isSimple.test( selector ) ) + return this.pushStack( jQuery.multiFilter( selector, this, true ), "not", selector ); + else + selector = jQuery.multiFilter( selector, this ); + + var isArrayLike = selector.length && selector[selector.length - 1] !== undefined && !selector.nodeType; + return this.filter(function() { + return isArrayLike ? jQuery.inArray( this, selector ) < 0 : this != selector; + }); + }, + + add: function( selector ) { + return this.pushStack( jQuery.unique( jQuery.merge( + this.get(), + typeof selector === "string" ? + jQuery( selector ) : + jQuery.makeArray( selector ) + ))); + }, + + is: function( selector ) { + return !!selector && jQuery.multiFilter( selector, this ).length > 0; + }, + + hasClass: function( selector ) { + return !!selector && this.is( "." + selector ); + }, + + val: function( value ) { + if ( value === undefined ) { + var elem = this[0]; + + if ( elem ) { + if( jQuery.nodeName( elem, 'option' ) ) + return (elem.attributes.value || {}).specified ? elem.value : elem.text; + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type == "select-one"; + + // Nothing was selected + if ( index < 0 ) + return null; + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + if ( option.selected ) { + // Get the specifc value for the option + value = jQuery(option).val(); + + // We don't need an array for one selects + if ( one ) + return value; + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + } + + // Everything else, we just grab the value + return (elem.value || "").replace(/\r/g, ""); + + } + + return undefined; + } + + if ( typeof value === "number" ) + value += ''; + + return this.each(function(){ + if ( this.nodeType != 1 ) + return; + + if ( jQuery.isArray(value) && /radio|checkbox/.test( this.type ) ) + this.checked = (jQuery.inArray(this.value, value) >= 0 || + jQuery.inArray(this.name, value) >= 0); + + else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(value); + + jQuery( "option", this ).each(function(){ + this.selected = (jQuery.inArray( this.value, values ) >= 0 || + jQuery.inArray( this.text, values ) >= 0); + }); + + if ( !values.length ) + this.selectedIndex = -1; + + } else + this.value = value; + }); + }, + + html: function( value ) { + return value === undefined ? + (this[0] ? + this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") : + null) : + this.empty().append( value ); + }, + + replaceWith: function( value ) { + return this.after( value ).remove(); + }, + + eq: function( i ) { + return this.slice( i, +i + 1 ); + }, + + slice: function() { + return this.pushStack( Array.prototype.slice.apply( this, arguments ), + "slice", Array.prototype.slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function(elem, i){ + return callback.call( elem, i, elem ); + })); + }, + + andSelf: function() { + return this.add( this.prevObject ); + }, + + domManip: function( args, table, callback ) { + if ( this[0] ) { + var fragment = (this[0].ownerDocument || this[0]).createDocumentFragment(), + scripts = jQuery.clean( args, (this[0].ownerDocument || this[0]), fragment ), + first = fragment.firstChild; + + if ( first ) + for ( var i = 0, l = this.length; i < l; i++ ) + callback.call( root(this[i], first), this.length > 1 || i > 0 ? + fragment.cloneNode(true) : fragment ); + + if ( scripts ) + jQuery.each( scripts, evalScript ); + } + + return this; + + function root( elem, cur ) { + return table && jQuery.nodeName(elem, "table") && jQuery.nodeName(cur, "tr") ? + (elem.getElementsByTagName("tbody")[0] || + elem.appendChild(elem.ownerDocument.createElement("tbody"))) : + elem; + } + } +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +function evalScript( i, elem ) { + if ( elem.src ) + jQuery.ajax({ + url: elem.src, + async: false, + dataType: "script" + }); + + else + jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" ); + + if ( elem.parentNode ) + elem.parentNode.removeChild( elem ); +} + +function now(){ + return +new Date; +} + +jQuery.extend = jQuery.fn.extend = function() { + // copy reference to target object + var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) + target = {}; + + // extend jQuery itself if only one argument is passed + if ( length == i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) + // Extend the base object + for ( var name in options ) { + var src = target[ name ], copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) + continue; + + // Recurse if we're merging object values + if ( deep && copy && typeof copy === "object" && !copy.nodeType ) + target[ name ] = jQuery.extend( deep, + // Never move original objects, clone them + src || ( copy.length != null ? [ ] : { } ) + , copy ); + + // Don't bring in undefined values + else if ( copy !== undefined ) + target[ name ] = copy; + + } + + // Return the modified object + return target; +}; + +// exclude the following css properties to add px +var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i, + // cache defaultView + defaultView = document.defaultView || {}, + toString = Object.prototype.toString; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) + window.jQuery = _jQuery; + + return jQuery; + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return toString.call(obj) === "[object Function]"; + }, + + isArray: function( obj ) { + return toString.call(obj) === "[object Array]"; + }, + + // check if an element is in a (or is an) XML document + isXMLDoc: function( elem ) { + return elem.nodeType === 9 && elem.documentElement.nodeName !== "HTML" || + !!elem.ownerDocument && jQuery.isXMLDoc( elem.ownerDocument ); + }, + + // Evalulates a script in a global context + globalEval: function( data ) { + if ( data && /\S/.test(data) ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + if ( jQuery.support.scriptEval ) + script.appendChild( document.createTextNode( data ) ); + else + script.text = data; + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, length = object.length; + + if ( args ) { + if ( length === undefined ) { + for ( name in object ) + if ( callback.apply( object[ name ], args ) === false ) + break; + } else + for ( ; i < length; ) + if ( callback.apply( object[ i++ ], args ) === false ) + break; + + // A special, fast, case for the most common use of each + } else { + if ( length === undefined ) { + for ( name in object ) + if ( callback.call( object[ name ], name, object[ name ] ) === false ) + break; + } else + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ){} + } + + return object; + }, + + prop: function( elem, value, type, i, name ) { + // Handle executable functions + if ( jQuery.isFunction( value ) ) + value = value.call( elem, i ); + + // Handle passing in a number to a CSS property + return typeof value === "number" && type == "curCSS" && !exclude.test( name ) ? + value + "px" : + value; + }, + + className: { + // internal only, use addClass("class") + add: function( elem, classNames ) { + jQuery.each((classNames || "").split(/\s+/), function(i, className){ + if ( elem.nodeType == 1 && !jQuery.className.has( elem.className, className ) ) + elem.className += (elem.className ? " " : "") + className; + }); + }, + + // internal only, use removeClass("class") + remove: function( elem, classNames ) { + if (elem.nodeType == 1) + elem.className = classNames !== undefined ? + jQuery.grep(elem.className.split(/\s+/), function(className){ + return !jQuery.className.has( classNames, className ); + }).join(" ") : + ""; + }, + + // internal only, use hasClass("class") + has: function( elem, className ) { + return elem && jQuery.inArray( className, (elem.className || elem).toString().split(/\s+/) ) > -1; + } + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations + swap: function( elem, options, callback ) { + var old = {}; + // Remember the old values, and insert the new ones + for ( var name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + callback.call( elem ); + + // Revert the old values + for ( var name in options ) + elem.style[ name ] = old[ name ]; + }, + + css: function( elem, name, force, extra ) { + if ( name == "width" || name == "height" ) { + var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ]; + + function getWH() { + val = name == "width" ? elem.offsetWidth : elem.offsetHeight; + + if ( extra === "border" ) + return; + + jQuery.each( which, function() { + if ( !extra ) + val -= parseFloat(jQuery.curCSS( elem, "padding" + this, true)) || 0; + if ( extra === "margin" ) + val += parseFloat(jQuery.curCSS( elem, "margin" + this, true)) || 0; + else + val -= parseFloat(jQuery.curCSS( elem, "border" + this + "Width", true)) || 0; + }); + } + + if ( elem.offsetWidth !== 0 ) + getWH(); + else + jQuery.swap( elem, props, getWH ); + + return Math.max(0, Math.round(val)); + } + + return jQuery.curCSS( elem, name, force ); + }, + + curCSS: function( elem, name, force ) { + var ret, style = elem.style; + + // We need to handle opacity special in IE + if ( name == "opacity" && !jQuery.support.opacity ) { + ret = jQuery.attr( style, "opacity" ); + + return ret == "" ? + "1" : + ret; + } + + // Make sure we're using the right name for getting the float value + if ( name.match( /float/i ) ) + name = styleFloat; + + if ( !force && style && style[ name ] ) + ret = style[ name ]; + + else if ( defaultView.getComputedStyle ) { + + // Only "float" is needed here + if ( name.match( /float/i ) ) + name = "float"; + + name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase(); + + var computedStyle = defaultView.getComputedStyle( elem, null ); + + if ( computedStyle ) + ret = computedStyle.getPropertyValue( name ); + + // We should always get a number back from opacity + if ( name == "opacity" && ret == "" ) + ret = "1"; + + } else if ( elem.currentStyle ) { + var camelCase = name.replace(/\-(\w)/g, function(all, letter){ + return letter.toUpperCase(); + }); + + ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ]; + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) { + // Remember the original values + var left = style.left, rsLeft = elem.runtimeStyle.left; + + // Put in the new values to get a computed value out + elem.runtimeStyle.left = elem.currentStyle.left; + style.left = ret || 0; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + elem.runtimeStyle.left = rsLeft; + } + } + + return ret; + }, + + clean: function( elems, context, fragment ) { + context = context || document; + + // !context.createElement fails in IE with an error but returns typeof 'object' + if ( typeof context.createElement === "undefined" ) + context = context.ownerDocument || context[0] && context[0].ownerDocument || document; + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + if ( !fragment && elems.length === 1 && typeof elems[0] === "string" ) { + var match = /^<(\w+)\s*\/?>$/.exec(elems[0]); + if ( match ) + return [ context.createElement( match[1] ) ]; + } + + var ret = [], scripts = [], div = context.createElement("div"); + + jQuery.each(elems, function(i, elem){ + if ( typeof elem === "number" ) + elem += ''; + + if ( !elem ) + return; + + // Convert html string into DOM nodes + if ( typeof elem === "string" ) { + // Fix "XHTML"-style tags in all browsers + elem = elem.replace(/(<(\w+)[^>]*?)\/>/g, function(all, front, tag){ + return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i) ? + all : + front + ">"; + }); + + // Trim whitespace, otherwise indexOf won't work as expected + var tags = elem.replace(/^\s+/, "").substring(0, 10).toLowerCase(); + + var wrap = + // option or optgroup + !tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + tags.match(/^<(thead|tbody|tfoot|colg|cap)/) && + [ 1, "", "
" ] || + + !tags.indexOf("", "" ] || + + // matched above + (!tags.indexOf("", "" ] || + + !tags.indexOf("", "" ] || + + // IE can't serialize and + + + {% block endbody %}{% endblock %} + + + + + diff --git a/templates/configure.html b/templates/configure.html new file mode 100644 index 0000000..37c8384 --- /dev/null +++ b/templates/configure.html @@ -0,0 +1,118 @@ +{% extends "base.html" %} + +{% block title %} + Options for "{{ world }}" | {{ block.super }} +{% endblock %} + +{% block endhead %} + +{% endblock %} + +{% block header %} +

Options for "{{ world }}"

+ « Back to profile +{% endblock %} + +{% block content %} +
+
Access
+
+
{% csrf_token %} + Visitors who aren't members: + + +
+
+
+ +
+
Features
+
+
+ Control what features are usable on your world. + When a feature is set to "enabled", any visitor to the world can use it. Otherwise, it is only available for the owner. +
+
{% csrf_token %} + + + + + + + + + + + + + + + + + + +
Go to coordinates + + + Transports you to any given coordinates on a world. +
Create link to coordinates + + + Make a letter link to a different part of the world. +
Create link to URL + + + Make a letter link to a URL. +
+ + + + +
+
+
+ +
+
Members
+
+ {% if members %} +
    +
    {% csrf_token %} + + {% for member in members %} +
  • {{ member }} + {% endfor %} +
  • +
+ {% else %} + None + {% endif %} + {% if add_member_message %}
{{ add_member_message }}
{% endif %} +
+
{% csrf_token %} + + + + +
+ +
+
+{% endblock %} +{% block endbody %} + + +{% endblock %} diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..088eb8e --- /dev/null +++ b/templates/home.html @@ -0,0 +1,26 @@ +{% extends "base.html" %} + +{% block title %} + Homepage | {{ block.super }} +{% endblock %} + +{% block header %} +

About Your World of Text

+{% endblock %} + +{% block content %} +
+

+ Your World of Text is an infinite + grid of text editable by any visitor. The changes made by other people + appear on your screen as they happen. Everyone starts in the same place, but + you can scroll through the world using your mouse. +

+

+ Put any letters at the end of the URL to go to a new world. For + example, http://yoursitehere.com/forexample. + They all start off blank. You can also create a custom + world for you and your friends. +

+
+{% endblock %} diff --git a/templates/private.html b/templates/private.html new file mode 100644 index 0000000..0a2aa71 --- /dev/null +++ b/templates/private.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block title %} + Profile | {{ block.super }} +{% endblock %} + +{% block header %} +

Private

+{% endblock %} + +{% block content %} +
+

Sorry, the owner of that world has made it only visible to members. + {% if user.is_authenticated %} + Go back to the main world, perhaps. + {% else %} + Perhaps login or create an account. +

+ Or, go back to the main world. + {% endif %} +

+
+{% endblock %} diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..9b46c4a --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% block title %} + Profile | {{ block.super }} +{% endblock %} + +{% block header %} +

Your account

+{% endblock %} + +{% block content %} +
+
Claim a World
+
+ {% if message %} + {{ message }} + {% endif %} +
{% csrf_token %} + World name: + + +
+
+
+ + {% if worlds_owned %} +
+
Worlds you own
+
+
    + {% for world in worlds_owned %} +
  • {{ world }}, + {% if world.public_writable %} + publicly editable, + {% else %} + {% if world.public_readable %} + publicly visible (but not editable), + {% else %} + not publicly visible, + {% endif %} + {% endif %} + {% with world.whitelist_set.count as num_wl %} + {{ num_wl }} member{{ num_wl|pluralize }}. + {% endwith %} + + options +
  • + {% endfor %} +
+
+
+ {% endif %} + {% if memberships %} +
+
World you belong to
+
+
    + {% for world in memberships %} +
  • {{ world }} + {% endfor %} +
+
+
+ {% endif %} + +{% endblock %} + diff --git a/templates/registration/activate.html b/templates/registration/activate.html new file mode 100644 index 0000000..4eb5526 --- /dev/null +++ b/templates/registration/activate.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block title %} +Activated | {{ block.super }} +{% endblock %} + +{% block header %} +

Activation Successful

+{% endblock %} + +{% block content %} +Thanks for activating your account. You may now log in. +{% endblock %} diff --git a/templates/registration/activation_email.txt b/templates/registration/activation_email.txt new file mode 100644 index 0000000..eddfa99 --- /dev/null +++ b/templates/registration/activation_email.txt @@ -0,0 +1,5 @@ +Thanks for signing up at Your World of Text. Please activate your account by +visiting http://{{ site }}{% url registration_activate activation_key %} + +Andrew Badr +andrewbadr@gmail.com diff --git a/templates/registration/activation_email_subject.txt b/templates/registration/activation_email_subject.txt new file mode 100644 index 0000000..57df4ac --- /dev/null +++ b/templates/registration/activation_email_subject.txt @@ -0,0 +1 @@ +Your World of Text: Activate your account diff --git a/templates/registration/login.html b/templates/registration/login.html new file mode 100644 index 0000000..8e72ba0 --- /dev/null +++ b/templates/registration/login.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% block header %} +

Log in to Your World of Text

+{% endblock %} +{% block content %} +{% if form.errors %} +

Your username and password didn't match. Please try again.

+{% endif %} +
{% csrf_token %} + + + + + + + + + +
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
+ + +Don't have an account? + +
+ +{% endblock %} + diff --git a/templates/registration/registration_complete.html b/templates/registration/registration_complete.html new file mode 100644 index 0000000..04245e1 --- /dev/null +++ b/templates/registration/registration_complete.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} + +{% block title %} +Activate Account | {{ block.super }} +{% endblock %} + +{% block header %} +

Activate your account

+{% endblock %} + +{% block content %} +Thank you for signing up. An email with the activation code has been send to your inbox. +After clicking it, you can log in here. +{% endblock %} \ No newline at end of file diff --git a/templates/registration/registration_form.html b/templates/registration/registration_form.html new file mode 100644 index 0000000..3285a0e --- /dev/null +++ b/templates/registration/registration_form.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} + +{% block title %} +Registration | {{ block.super }} +{% endblock %} + +{% block header %} +

Sign up for a Your World of Text account

+{% endblock %} + +{% block content %} +
{% csrf_token %} + + + + + + + + + + + + + + + + + + + + + +
Username: + {{ form.username }}
+ {% for error in form.username.errors %} + {{ error }} + {% endfor %} +
Email: + {{ form.email }}
+ {% for error in form.email.errors %} + {{ error }} + {% endfor %} +
Password: + {{ form.password1 }}
+ {% for error in form.password1.errors %} + {{ error }} + {% endfor %} +
Password (again): + {{ form.password2 }}
+ {% for error in form.password2.errors %} + {{ error }} + {% endfor %} +
 
+
+{% endblock %} diff --git a/templates/yourworld.html b/templates/yourworld.html new file mode 100644 index 0000000..224c41c --- /dev/null +++ b/templates/yourworld.html @@ -0,0 +1,62 @@ + + + + Your World of Text + + + + + +
+
+ Paused +
+ Menu +
+
+ +
+ X: + Y: +
+ +

Loading...

+
+ + + + + + + +{% if not settings.DEBUG %} + + + +{% endif %} + + diff --git a/urls.py b/urls.py new file mode 100644 index 0000000..dea8e7c --- /dev/null +++ b/urls.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from django.conf import settings +from django.conf.urls.defaults import * +from django.views.generic.simple import redirect_to + +from helpers import here + +urlpatterns = patterns('yourworld.ywot.views', + ### Web page: + # Main + url(r'^home/$', 'home', name='home'), + + # Accounts + (r'^accounts/$', redirect_to, {'url': '/accounts/profile/'}), + url(r'^accounts/profile/', 'profile', name='profile'), + url(r'^accounts/logout/$', 'logout', name='logout'), + (r'^accounts/private/', 'private'), + (r'^accounts/configure/$', redirect_to, {'url': '/accounts/profile/'}), + url(r'^accounts/configure/(.*)/$', 'configure', name='configure'), + url(r'^accounts/configure/(beta/\w+)/$', 'configure', name='configure'), + url(r'^accounts/member_autocomplete/$', 'member_autocomplete'), + + (r'^accounts/', include('registration.urls')), + + ### Worlds: + # World management + url(r'^ajax/protect/$', 'protect', name='protect'), + url(r'^ajax/unprotect/$', 'unprotect', name='unprotect'), + url(r'^ajax/coordlink/$', 'coordlink', name='coordlink'), + url(r'^ajax/urllink/$', 'urllink', name='urllink'), + + # Worldviews + ('^(\w*)$', 'yourworld'), + ('^(beta/\w*)$', 'yourworld'), + ('^(frontpage/\w*)$', 'yourworld'), +) + +urlpatterns += patterns('', + (r'^favicon\.ico$', redirect_to, {'url': '/static/favicon.png'}), +) + +if settings.DEBUG: + urlpatterns += patterns('', + ('^static/(?P.*)$', 'django.views.static.serve', + {'document_root': here('static')}), + ) + diff --git a/ywot/__init__.py b/ywot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ywot/models.py b/ywot/models.py new file mode 100644 index 0000000..9ddf484 --- /dev/null +++ b/ywot/models.py @@ -0,0 +1,96 @@ +from django.contrib.auth.models import User +from django.db import models +from django.http import Http404 + +from yourworld.lib.jsonfield import DictField + +class World(models.Model): + name = models.TextField(unique=True) + # Creating this index for much faster world lookups from World.get_or_create, + # which are very common. + # CREATE INDEX CONCURRENTLY world_name_upper ON ywot_world(UPPER(name)); + owner = models.ForeignKey(User, null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + public_readable = models.BooleanField(default=True) # Otherwise whitelist + public_writable = models.BooleanField(default=True) # Otherwise whitelist + properties = DictField(default={}) + # properties: + # - features: {} + # - 'go_to_coord' true/false + + @staticmethod + def get_or_create(name): + """Same interface as Model.get_or_create.""" + if '/' in name: + # These are only created manually + try: + return (World.objects.get(name__iexact=name), False) + except World.DoesNotExist: + raise Http404 + + # GAE worlds were case-sensitive. Until we figure out what to do about that, just return + # the first one: + worlds = World.objects.filter(name__iexact=name) + if not len(worlds): + return (World.objects.create(name=name), True) + return (worlds[0], False) + + class Meta: + ordering = ['name'] + + def __unicode__(self): + return self.name + + def get_absolute_url(self): + return '/' + self.name + +class Tile(models.Model): + ROWS = 8 + COLS = 16 + LEN = ROWS*COLS + + world = models.ForeignKey(World) + content = models.CharField(default=' '*LEN, max_length=LEN) + tileY = models.IntegerField() + tileX = models.IntegerField() + properties = DictField(default={}) + # properties: + # - protected (bool) + # - cell_props[charY][charX] = {} + + created_at = models.DateTimeField(auto_now_add=True) + + def set_char(self, charY, charX, char): + from helpers import control_chars_set + if char in control_chars_set: + # TODO: log these guys again at some point + char = ' ' + assert len(self.content) == self.ROWS*self.COLS + charY, charX = int(charY), int(charX) + index = charY*self.COLS+charX + self.content = self.content[:index] + char + self.content[index+1:] + assert len(self.content) == self.ROWS*self.COLS + + class Meta: + unique_together=[['world', 'tileY', 'tileX']] + +class Edit(models.Model): + user = models.ForeignKey(User, null=True) + ip = models.IPAddressField(null=True) + world = models.ForeignKey(World) + time = models.DateTimeField(auto_now_add=True) # ADD INDEX: + #CREATE INDEX CONCURRENTLY ywot_edit_time ON ywot_edit(time); + content = models.TextField() + + class Meta: + ordering = ['time'] + +class Whitelist(models.Model): + user = models.ForeignKey(User) + world = models.ForeignKey(World) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + class Meta: + unique_together=[['user', 'world']] + diff --git a/ywot/permissions.py b/ywot/permissions.py new file mode 100644 index 0000000..ff09c9e --- /dev/null +++ b/ywot/permissions.py @@ -0,0 +1,71 @@ +def can_read(user, world): + from yourworld.ywot.models import Whitelist + if world.public_readable: + return True + if not user.is_authenticated(): + return False + if world.owner_id == user.id: + return True + if user.is_superuser: + return True + try: + Whitelist.objects.get(user=user, world=world) + return True + except Whitelist.DoesNotExist: + return False + +def can_write(user, world): + from yourworld.ywot.models import Whitelist + if world.public_writable: + return True + if not user.is_authenticated(): + return False + if world.owner_id == user.id: + return True + # Not allowing superuser to write. Be clean. + try: + Whitelist.objects.get(user=user, world=world) + return True + except Whitelist.DoesNotExist: + return False + +def can_admin(user, world): + return bool(world.owner_id and (world.owner_id == user.id)) + +def can_coordlink(user, world): + if not can_write(user, world): + return False + if can_admin(user, world): + return True + if world.properties.get('features', {}).get('coordLink', False): + return True + return False + +def is_superuser(user): + return user.is_authenticated() and user.is_superuser + +def can_urllink(user, world): + if not can_write(user, world): + return False + if can_admin(user, world): + return True + if world.properties.get('features', {}).get('coordLink', False): + return True + return False + +def get_available_features(user, world): + features = world.properties.get('features', {}) + if can_admin(user, world): + coordLink = True + go_to_coord = True + urlLink = True + else: + coordLink = features.get('coordLink', False) and can_write(user, world) + urlLink = features.get('urlLink', False) and can_write(user, world) + go_to_coord = features.get('go_to_coord', False) or is_superuser(user) + return { + 'coordLink': coordLink, + 'urlLink': urlLink, + 'go_to_coord': go_to_coord + } + diff --git a/ywot/views.py b/ywot/views.py new file mode 100644 index 0000000..ded8837 --- /dev/null +++ b/ywot/views.py @@ -0,0 +1,387 @@ +import collections, datetime, itertools, re, urlparse + +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.shortcuts import render_to_response, redirect +from django.utils import simplejson + +from yourworld.helpers import req_render_to_response +from yourworld.lib import log +from yourworld.ywot.models import Tile, World, Edit, Whitelist +from yourworld.ywot import permissions + +# +# Helpers +# + +class ClaimException(Exception): + pass + +def do_claim(user, world): + assert not world.owner + world.owner = user + world.save() + +def claim(user, worldname): + # TODO: write tests for this + if not re.match('\w+$', worldname): + raise ClaimException, "Invalid world name." + world, new = World.get_or_create(worldname) + if new: + return do_claim(user, world) + if world.owner: + raise ClaimException, "That world already has an owner." + editors = set(world.edit_set.all().values_list('user', flat=True)) + if not editors: + return do_claim(user, world) + if len(editors) > 2: + raise ClaimException, "Too many people have edited that world." + if len(editors) == 2: + if editors == set([user, None]): + return do_claim(user, world) + else: + raise ClaimException, "Too many people have edited that world." + assert len(editors) == 1 + obj = editors.pop() + if obj == user: + return do_claim(user, world) + if obj is not None: + raise ClaimException, "Too many people have edited that world." + if world.created_at > datetime.datetime.now() - datetime.timedelta(minutes=5): + raise ClaimException, "That world has been around too long to claim." + return do_claim(user, world) + +def try_add_member(world, username): + """Return success/error message.""" + try: + u = User.objects.get(username__iexact=username) + except User.DoesNotExist: + return 'User not found' + if u == world.owner: + return 'User is already the owner of "%s"' % world.name + Whitelist.objects.get_or_create(world=world, user=u) + return '%s is now a member of the "%s" world' % (username, world.name) + + +def date_range(from_date, to_date, step=datetime.timedelta(days=1)): + # from http://www.ianlewis.org/en/python-date-range-iterator + while from_date <= to_date: + yield from_date + from_date = from_date + step + return + +def get_counts(obj_iter, key_getter): + """ + Assumes iterable is sorted by key. Returns a defaultdict + of count of iterables with each key. + """ + result = collections.defaultdict(int) + for key, group in itertools.groupby(obj_iter, key_getter): + result[key] = len(list(group)) + return result + +def response_403(): + # TODO: returns JS content type here and elsewhere + response = HttpResponse(simplejson.dumps('No permission')) + response.status_code = 403 + return response + +# +# World Views +# + +def yourworld(request, namespace): + """Check permissions and route request.""" + world, _ = World.get_or_create(namespace) + if not permissions.can_read(request.user, world): + return HttpResponseRedirect('/accounts/private/') + if 'fetch' in request.GET: + return fetch_updates(request, world) + can_write = permissions.can_write(request.user, world) + if request.method == 'POST': + if not can_write: + return response_403() + return send_edits(request, world) + state = { + 'canWrite': can_write, + 'canAdmin': permissions.can_admin(request.user, world), + 'worldName': world.name, + 'features': permissions.get_available_features(request.user, world), + } + if 'MSIE' in request.META.get('HTTP_USER_AGENT', ''): + state['announce'] = "Sorry, your World of Text doesn't work well with Internet Explorer." + return req_render_to_response(request, 'yourworld.html', { + 'settings': settings, + 'state': simplejson.dumps(state), + }) + +def fetch_updates(request, world): + response = {} + min_tileY = int(request.GET['min_tileY']) + min_tileX = int(request.GET['min_tileX']) + max_tileY = int(request.GET['max_tileY']) + max_tileX = int(request.GET['max_tileX']) + response = {} + + assert min_tileY < max_tileY + assert min_tileX < max_tileX + assert ((max_tileY - min_tileY)*(max_tileX - min_tileX)) < 400 + + # Set default info to null + for tileY in xrange(min_tileY, max_tileY + 1): #+1 b/c of range bounds + for tileX in xrange(min_tileX, max_tileX + 1): + response["%d,%d" % (tileY, tileX)] = None + + tiles = Tile.objects.filter(world=world, + tileY__gte=min_tileY, tileY__lte=max_tileY, + tileX__gte=min_tileX, tileX__lte=max_tileX) + for t in tiles: + tile_key = "%s,%s" % (t.tileY, t.tileX) + if (int(request.GET.get('v', 0)) == 2): + d = {'content': t.content.replace('\n', ' ')} + if 'protected' in t.properties: # We want to send *any* set value (case: reset to false) + d['protected'] = t.properties['protected'] + response[tile_key] = d + elif (int(request.GET.get('v', 0)) == 3): + d = {'content': t.content.replace('\n', ' ')} + if t.properties: + d['properties'] = t.properties + response[tile_key] = d + else: + raise ValueError, 'Unknown JS version' + return HttpResponse(simplejson.dumps(response)) + +def send_edits(request, world): + assert permissions.can_write(request.user, world) # Checked by router + response = [] + tiles = {} # a simple cache + edits = [e.split(',', 5) for e in request.POST.getlist('edits')] + for edit in edits: + char = edit[5] + tileY, tileX, charY, charX, timestamp = map(int, edit[:5]) + assert len(char) == 1 # TODO: investigate these tracebacks + keyname = "%d,%d" % (tileY, tileX) + if keyname in tiles: + tile = tiles[keyname] + else: + # TODO: select for update + tile, _ = Tile.objects.get_or_create(world=world, tileY=tileY, tileX=tileX) + tiles[keyname] = tile + if tile.properties.get('protected'): + if not permissions.can_admin(request.user, world): + continue + tile.set_char(charY, charX, char) + # TODO: anything, please. + if tile.properties: + if 'cell_props' in tile.properties: + if str(charY) in tile.properties['cell_props']: #must be str because that's how JSON interprets int keys + if str(charX) in tile.properties['cell_props'][str(charY)]: + del tile.properties['cell_props'][str(charY)][str(charX)] + if not tile.properties['cell_props'][str(charY)]: + del tile.properties['cell_props'][str(charY)] + if not tile.properties['cell_props']: + del tile.properties['cell_props'] + response.append([tileY, tileX, charY, charX, timestamp, char]) + if len(edits) < 200: + for tile in tiles.values(): + tile.save() + Edit.objects.create(world=world, + user=request.user if request.user.is_authenticated() else None, + content=repr(edits), + ip=request.META['REMOTE_ADDR'], + ) + return HttpResponse(simplejson.dumps(response)) + +# +# Account Views +# + +def home(request): + """The main front-page other than a world.""" + return req_render_to_response(request, 'home.html') + +@login_required +def profile(request): + worlds_owned = World.objects.filter(owner=request.user) + memberships = World.objects.filter(whitelist__user=request.user) + context = {'worlds_owned': worlds_owned, 'memberships': memberships} + if request.method == 'POST': + worldname = request.POST['worldname'] + try: + claim(request.user, worldname) + context['claimed'] = True + context['message'] = 'World "%s" successfully claimed.' % worldname + except ClaimException, msg: + context['claimed'] = False + context['message'] = msg + return req_render_to_response(request, 'profile.html', context) + +@login_required +def configure(request, worldname): + try: + world = World.objects.get(name__iexact=worldname, owner=request.user) + except World.DoesNotExist: + # TODO: log security? + return redirect('profile') + add_member_message = None + if request.method == 'POST': + if request.POST['form'] == 'public_perm': + pp = request.POST['public_perm'] + if pp == 'none': + world.public_readable = False + world.public_writable = False + elif pp == 'read': + world.public_readable = True + world.public_writable = False + else: + assert pp == 'write' + world.public_readable = True + world.public_writable = True + world.save() + elif request.POST['form'] == 'add_member': + add_member_message = try_add_member(world, request.POST['add_member']) + elif request.POST['form'] == 'remove_member': + to_remove = [key for key in request.POST.keys() if key.startswith('remove_')] + assert len(to_remove) == 1 + username_to_remove = to_remove[0].split('_')[1] + wl = Whitelist.objects.get(world=world, user__username=username_to_remove) + wl.delete() + elif request.POST['form'] == 'features': + # TODO: move this crap into JSONField so I can do world.properties.features.go_to_coordinates = ... + features = world.properties.get('features', {}) + features['go_to_coord'] = bool(int(request.POST['go_to_coord'])) + features['coordLink'] = bool(int(request.POST['coordLink'])) + features['urlLink'] = bool(int(request.POST['urlLink'])) + world.properties['features'] = features + world.save() + else: + raise ValueError, "Unknown form type" + + if world.public_writable: + public_perm = 'write' + elif world.public_readable: + public_perm = 'read' + else: + public_perm = 'none' + return req_render_to_response(request, 'configure.html', { + 'world': world, + 'public_perm': public_perm, + 'members': User.objects.filter(whitelist__world=world).order_by('username'), + 'add_member_message': add_member_message + }) + +def logout(request): + from django.contrib.auth import logout + logout(request) + return HttpResponseRedirect(reverse('home')) + +def private(request): + return req_render_to_response(request, 'private.html') + +def member_autocomplete(request): + if not request.user.is_authenticated(): + return response_403() + q = request.GET['q'] + assert q + # TODO: filter by is_active? only if we aren't going to accept those as input... + users = (User.objects + .filter(username__istartswith=q) + .order_by('username') + .values_list('username', flat=True))[:10] + return HttpResponse('\n'.join(users)) + +def protect(request): + world = World.objects.get(name=request.POST['namespace']) + if not permissions.can_admin(request.user, world): + return response_403() + tileY, tileX = request.POST['tileY'], request.POST['tileX'] + # TODO: select for update + tile, _ = Tile.objects.get_or_create(world=world, tileY=tileY, tileX=tileX) + tile.properties['protected'] = True + tile.save() + log.info('ACTION:PROTECT %s %s %s' % (world.id, tileY, tileX)) + return HttpResponse('') + +def unprotect(request): + # TODO: factor out w/above + # TODO: make return javascript + world = World.objects.get(name=request.POST['namespace']) + if not permissions.can_admin(request.user, world): + return response_403() + tileY, tileX = request.POST['tileY'], request.POST['tileX'] + # TODO: select for update + tile, _ = Tile.objects.get_or_create(world=world, tileY=tileY, tileX=tileX) + tile.properties['protected'] = False + tile.save() + log.info('ACTION:UNPROTECT %s %s %s' % (world.id, tileY, tileX)) + return HttpResponse('') + +def coordlink(request): + world = World.objects.get(name=request.POST['namespace']) + if not permissions.can_coordlink(request.user, world): + return response_403() + tileY, tileX = int(request.POST['tileY']), int(request.POST['tileX']) + tile, _ = Tile.objects.get_or_create(world=world, tileY=tileY, tileX=tileX) + if tile.properties.get('protected'): + if not permissions.can_admin(request.user, world): + # TODO: log? + return HttpResponse('') + # Must convert to str because that's how JsonField reads the existing keys + charY = int(request.POST['charY']) + charX = int(request.POST['charX']) + assert charY < Tile.ROWS + assert charX < Tile.COLS + charY, charX = str(charY), str(charX) + link_tileY = str(int(request.POST['link_tileY'])) + link_tileX = str(int(request.POST['link_tileX'])) + if 'cell_props' not in tile.properties: + tile.properties['cell_props'] = {} + if charY not in tile.properties['cell_props']: + tile.properties['cell_props'][charY] = {} + if charX not in tile.properties['cell_props'][charY]: + tile.properties['cell_props'][charY][charX] = {} + tile.properties['cell_props'][charY][charX]['link'] = { + 'type': 'coord', + 'link_tileY': link_tileY, + 'link_tileX': link_tileX, + } + tile.save() + log.info('ACTION:COORDLINK %s %s %s %s %s %s %s' % (world.id, tileY, tileX, charY, charX, link_tileY, link_tileX)) + return HttpResponse('') + +def urllink(request): + # TODO: factor out w/above + world = World.objects.get(name=request.POST['namespace']) + if not permissions.can_urllink(request.user, world): + return response_403() + tileY, tileX = int(request.POST['tileY']), int(request.POST['tileX']) + tile, _ = Tile.objects.get_or_create(world=world, tileY=tileY, tileX=tileX) + if tile.properties.get('protected'): + if not permissions.can_admin(request.user, world): + # TODO: log? + return HttpResponse('') + # Must convert to str because that's how JsonField reads the existing keys + charY = int(request.POST['charY']) + charX = int(request.POST['charX']) + assert charY < Tile.ROWS + assert charX < Tile.COLS + charY, charX = str(charY), str(charX) + url = request.POST['url'].strip() + if not urlparse.urlparse(url)[0]: # no scheme + url = 'http://' + url + if 'cell_props' not in tile.properties: + tile.properties['cell_props'] = {} + if charY not in tile.properties['cell_props']: + tile.properties['cell_props'][charY] = {} + if charX not in tile.properties['cell_props'][charY]: + tile.properties['cell_props'][charY][charX] = {} + tile.properties['cell_props'][charY][charX]['link'] = { + 'type': 'url', + 'url': url, + } + tile.save() + log.info('ACTION:URLLINK %s %s %s %s %s %s' % (world.id, tileY, tileX, charY, charX, url)) + return HttpResponse('')