# HG changeset patch # User k0s # Date 1262457383 18000 # Node ID c904249afb042e6193c2350263621e12dda61aef initial commit of discussions diff -r 000000000000 -r c904249afb04 discusssions.ini --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions.ini Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,22 @@ +#!/usr/bin/env paster + +[DEFAULT] +debug = true +email_to = +smtp_server = localhost +error_email_from = paste@localhost + +[server:main] +use = egg:Paste#http +host = 0.0.0.0 +port = 1800 + +[composite:main] +use = egg:Paste#urlmap +/ = discusssions + +set debug = false + +[app:discusssions] +paste.app_factory = discusssions.factory:factory +discusssions.name = discussions diff -r 000000000000 -r c904249afb04 discusssions/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions/__init__.py Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,1 @@ +# diff -r 000000000000 -r c904249afb04 discusssions/dispatcher.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions/dispatcher.py Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,64 @@ +""" +request dispatcher: +data persisting across requests should go here +""" + +import os + +from handlers import Index + +from genshi.template import TemplateLoader +from paste.fileapp import FileApp +from pkg_resources import resource_filename +from webob import Request, Response, exc + +class Dispatcher(object): + + ### class level variables + defaults = { 'auto_reload': 'False', + } + + def __init__(self, **kw): + + # set instance parameters from kw and defaults + for key in self.defaults: + setattr(self, key, kw.get(key, self.defaults[key])) + self.auto_reload = self.auto_reload.lower() == 'true' + + # request handlers + self.handlers = [ Index ] + + # template loader + templates_dir = resource_filename(__name__, 'templates') + self.loader = TemplateLoader(templates_dir, + auto_reload=self.auto_reload) + + def __call__(self, environ, start_response): + + # get a request object + request = Request(environ) + + # get the path + path = request.path_info.strip('/').split('/') + if path == ['']: + path = [] + request.environ['path'] = path + + # match the request to a handler + for h in self.handlers: + handler = h.match(self, request) + if handler is not None: + break + else: + handler = exc.HTTPNotFound + + # add navigation links to handler [example] + if hasattr(handler, 'data'): + handler.data.setdefault('links', []) + for h in self.handlers: + handler.data['links'].append((handler.link(h.handler_path), + h.__name__)) + + # get response + res = handler() + return res(environ, start_response) diff -r 000000000000 -r c904249afb04 discusssions/events.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions/events.py Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,13 @@ +""" +events: + add_message(message) + add_member(member, discussion) + digest(name, discussion) # name: id token of digest, e.g. 'daily' +""" + +def notify_subscribers(message): + """notify subscribers"" + +def notify_digest_subscribers(message, group): + """notify digest subscribers""" + diff -r 000000000000 -r c904249afb04 discusssions/factory.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions/factory.py Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,31 @@ +import os + +from dispatcher import Dispatcher +from paste.httpexceptions import HTTPExceptionHandler +from paste.urlparser import StaticURLParser +from pkg_resources import resource_filename + +class PassthroughFileserver(object): + """serve files if they exist""" + + def __init__(self, app, directory): + self.app = app + self.directory = directory + self.fileserver = StaticURLParser(self.directory) + + def __call__(self, environ, start_response): + path = environ['PATH_INFO'].strip('/') + if path and os.path.exists(os.path.join(self.directory, path)): + return self.fileserver(environ, start_response) + return self.app(environ, start_response) + +def factory(global_conf, **app_conf): + """create a webob view and wrap it in middleware""" + + keystr = 'discusssions.' + args = dict([(key.split(keystr, 1)[-1], value) + for key, value in app_conf.items() + if key.startswith(keystr) ]) + app = Dispatcher(**args) + return HTTPExceptionHandler(PassthroughFileserver(app, resource_filename(__name__, 'static'))) + diff -r 000000000000 -r c904249afb04 discusssions/handlers.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions/handlers.py Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,85 @@ +""" +request handlers: +these are instantiated for every request, then called +""" + +from urlparse import urlparse +from webob import Response, exc + +class HandlerMatchException(Exception): + """the handler doesn't match the request""" + +class Handler(object): + + methods = set(['GET']) # methods to listen to + handler_path = [] # path elements to match + + @classmethod + def match(cls, app, request): + + # check the method + if request.method not in cls.methods: + return None + + # check the path + if request.environ['path'][:len(cls.handler_path)] != cls.handler_path: + return None + + try: + return cls(app, request) + except HandlerMatchException: + return None + + def __init__(self, app, request): + self.app = app + self.request = request + self.application_path = urlparse(request.application_url)[2] + + def link(self, path=(), permanant=False): + if isinstance(path, basestring): + path = [ path ] + path = [ i.strip('/') for i in path ] + if permanant: + application_url = [ self.request.application_url ] + else: + application_url = [ self.application_path ] + path = application_url + path + return '/'.join(path) + + def redirect(self, location): + raise exc.HTTPSeeOther(location=location) + +class GenshiHandler(Handler): + + def __init__(self, app, request): + Handler.__init__(self, app, request) + self.data = { 'request': request, + 'link': self.link } + + def __call__(self): + return getattr(self, self.request.method.title())() + + def Get(self): + # needs to have self.template set + template = self.app.loader.load(self.template) + return Response(content_type='text/html', + body=template.generate(**self.data).render('html')) + +class Discussions(GenshiHandler): + """ + ?format=json,mbox,rss,text,snippet + """ + + template = 'index.html' + methods=set(['GET', 'POST']) + + def __init__(self, app, request): + GenshiHandler.__init__(self, app, request) + + def Get(self): + self.data['name'] = self.request.remote_user or self.app.name + return GenshiHandler.Get(self) + + def Post(self): + self.app.name = self.request.POST.get('name', self.app.name) + self.redirect(self.link(self.handler_path)) diff -r 000000000000 -r c904249afb04 discusssions/main.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions/main.py Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +import sys + +from optparse import OptionParser + +def main(args=sys.argv[1:]): + parser = OptionParser() + options, args = parser.parse_args(args) + +if __name__ == '__main__': + main() diff -r 000000000000 -r c904249afb04 discusssions/member.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions/member.py Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,23 @@ +{ 'email': , + + 'password': 'secret', + +} + +# you sign in with an email address +# email addresses should be multiple + +def Member(object): + + def role(self, discussion): + """ + member's role with respect to discussion or conference; + returns 'admin', 'member', or None + """ + + def add(self, discussion): + """adds a member to a discussion""" + + + + diff -r 000000000000 -r c904249afb04 discusssions/model.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions/model.py Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,149 @@ +import datetime +from discussions.utils import is_reply + +class MessageContainer(object): + """ + something that can contain messages + JSON representation: + { + 'name': 'name of discussion', + 'subject': 'what its about', + 'subscribers': [ list of subsribers ], + 'unsubscribers': [ list of unsubscribers ], + 'moderated': False, + 'options': { dict of options }, 'attachments': True # archive attachments? + 'discussions': [ ordered list of discussions ids (either conferences or discussions) + 'date': creation date (format?), + + } + """ + + options = { 'archive': True, + 'web': True, + 'moderated': True, + 'type': 'public', + 'post': 'web', 'email', + 'digest': (), + } + + def __init__(self, parent, name, subject, subscribers=(), locked=None): + self.parent = parent + + ### traverse to message + next = parent + self.path = [ name ] + while next: + self.path[:0] = next + next = next.parent + + ### set options + if locked is not None: + # lock the options + + ### set subscribers + + ### methods for messages + + def message(self, id): + """ + retrieves a submessage given the id + """ + + def role(self, member, message): + + + def messages(self): # future arguments == search criteria + """returns a list of top-level messages""" + + def add_message(self, message): + """ + add a message to the container + message: an email message + """ + for m in self.messages(): + replied_to = m.reply_to(message): + if replied_to: + replied_to._add_message(message) + # TODO: trigger + self._add_message(message) + + def search(self, **query): + """ + e.g. from='k0scist@example.com' + """ + + ### methods for members + + def members(self): + """ + members of this discussion + """ + return self.parent.members() + + def subscribers(self, unsubscribers=()): + """ + return people watching these messages + walks up the chain + """ + _subscribers = self['subscribers'] + + + + def subscribe(self, subscriber): + """subscribe someone to the messages in the container""" + + def unsubscribe(self, subscriber): + """removes a member from this discussion""" + + ### database functions + + def _add_message(self, message): + """add a message to the database""" + date = message.date() + if not date: + date = datetime.datetime.now() + +class Forum(MessageContainer): + """ + a mailing list and a forum + JSON same as above, but also with + { 'address': 'thisforum@example.org', + 'import': True # whether importing from an e.g. mbox is allowed or not + } + """ + def __init__(self, parent, name, subject, subscribers=(), options=None): + MessageContainer(self, member, parent, name, subject, subscribers, options) + + # if name is None, it is the metacontainer + self.conferences = [] + + + def reply_to(self, reply): + if not is_reply(self.address, reply): + return None + for _message in messages: + + + def add_conference(self, options): + """add a sub-forum to this forum""" + + def members(self): + """return members of this mailing list""" + +class Discussion(MessageContainer): + """ + a mailing list thread + JSON representation: as for message, but also with + { 'headers': { dictionary of headers } + } + """ + + def reply_to(self, message): + """TODO""" + +def site(name): + """ + return the top-level site for the database 'name' + """ + + diff -r 000000000000 -r c904249afb04 discusssions/static/jquery.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions/static/jquery.js Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,4376 @@ +/*! + * jQuery JavaScript Library v1.3.2 + * http://jquery.com/ + * + * Copyright (c) 2009 John Resig + * Dual licensed under the MIT and GPL licenses. + * http://docs.jquery.com/License + * + * Date: 2009-02-19 17:34:21 -0500 (Thu, 19 Feb 2009) + * Revision: 6246 + */ +(function(){ + +var + // Will speed up references to window, and allows munging its name. + window = this, + // Will speed up references to undefined, and allows munging its name. + undefined, + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + // Map over the $ in case of overwrite + _$ = window.$, + + jQuery = window.jQuery = window.$ = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context ); + }, + + // A simple way to check for HTML strings or ID strings + // (both of which we optimize for) + quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#([\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 + + + +
    +
  • + +
  • + + diff -r 000000000000 -r c904249afb04 discusssions/templates/navigation.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions/templates/navigation.html Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,18 @@ + + + + + + + diff -r 000000000000 -r c904249afb04 discusssions/templates/start.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions/templates/start.html Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,54 @@ + + + + + + + + +

    Start a Discussion

    + +
    + +
    + + + + + +
    + +
    + +
    + +
    + + + + + + + + + + \ No newline at end of file diff -r 000000000000 -r c904249afb04 discusssions/utils.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/discusssions/utils.py Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,6 @@ +""" +utilities for dealing with email +""" + +def is_reply(address, message): + """returns if the message is a reply to the address""" diff -r 000000000000 -r c904249afb04 setup.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/setup.py Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,31 @@ +from setuptools import setup, find_packages + +version = "0.0" + +setup(name='discusssions', + version=version, + description="mailing lists, forums are all part of discussions: a many to many conversation", + long_description=""" +""", + classifiers=[], # Get strings from http://www.python.org/pypi?%3Aaction=list_classifiers + author='Jeff Hammel', + author_email='', + url='', + license="", + packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), + include_package_data=True, + zip_safe=False, + install_requires=[ + # -*- Extra requirements: -*- + 'WebOb', + 'Paste', + 'PasteScript', + 'genshi' + ], + entry_points=""" + # -*- Entry points: -*- + [paste.app_factory] + discusssions = discusssions.factory:factory + """, + ) + diff -r 000000000000 -r c904249afb04 whitepaper.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/whitepaper.txt Sat Jan 02 13:36:23 2010 -0500 @@ -0,0 +1,35 @@ += Discussions = + +All Discussions objects are basically the same. You get a set of +options which are inherited from the parent or provided. + +== Options == + +Options can be in one of three states: unset, set, and locked. In +the case where the option is locked, all children inherit the setting +as well: + +archive.lock = false + +Locking an option prohibits any but a higher-level admin from +unlocking it. The higher-level admin is free to change the setting: + +archive.lock = false +archive = true + +This locks the archive of subchildren into the false state, but sets +the archive for this discussion in the current discussion. + +This prevents archiving on any sublist. Messages to a list inherit +the list settings. + +If an option is unset, it will use the settings of its parent, the +site defaults, or the application default, based on its type +(Forum or Discussion). + + += Members = + +== Preferences == + +Member preferences can also be set from the site level. \ No newline at end of file