Commits

Brantley Harris committed 5f4e635

Added the shelf, fixed up a bunch of stuff.

  • Participants
  • Parent commits 5e09a37

Comments (0)

Files changed (18)

File squash/base/models.py

 from django.contrib.auth import models as auth
 
 ### Manager ###
+def level_getter(model, level):
+    def getter(self, request=None):
+        return getattr(model._default_manager, level)(self, request)
+    return getter
+
 class JsonManager(models.Manager):
+    def contribute_to_class(self, model, name):
+        super(JsonManager, self).contribute_to_class(model, name)
+        for level in ('ref', 'short', 'summary', 'details'):
+            if hasattr(model, level):
+                setattr(model, '_' + level, getattr(model, level))
+            setattr(model, level, level_getter(model, level))
+    
     def handler(self):
         def handle(request):
             method = request.method.lower()
                 instance.save()
                 return self.details(instance, request)
         return handle
-    
-    def details(self, instance, request):
-        details = instance.details(request)
-        details.update({
-            '_pk': instance._get_pk_val(),
-            '_class': self.model.__name__
-        })
-        return details
-    
+        
+    def ref(self, i, request):    
+        fields = {
+            '_pk': i._get_pk_val(),
+            '_model': self.model._meta.app_label + '.' + self.model.__name__
+        }
+        if hasattr(i, '_ref'):
+            fields.update( i._ref(request) )
+        return fields
+
+    def short(self, i, request):
+        fields = self.ref(i, request)
+        if hasattr(i, '_short'):
+            fields.update( i._short(request) )
+        return fields
+
+    def summary(self, i, request):
+        fields = self.short(i, request)
+        print fields
+        if hasattr(i, '_summary'):
+            fields.update( i._summary(request) )
+        return fields
+        
+    def details(self, i, request):
+        fields = self.summary(i, request)
+        if hasattr(i, '_details'):
+            fields.update( i._details(request) )
+        return fields
+        
     def searcher(self):
         def search(request):
             if hasattr(self.model, 'query'):
-                return [i.details(request) for i in self.model.query(request)]
-            term = request.REQUEST.get('term', '').strip()
-            q = self.all()
-            if (term):
-                q = q.filter(term=term)
-            return [i.details(request) for i in q]
+                query = self.model.query(request)
+            else:
+                term = request.REQUEST.get('term', '').strip()
+                query = self.all()
+                if (term):
+                    query = query.filter(term=term)
+            
+            level = request.REQUEST.get('level', 'summary')
+            if level in ('ref', 'short', 'summary', 'details'):
+                return [getattr(self, level)(i, request) for i in query]
+            else:
+                raise RuntimeError("Unknown 'level' of detail requested in search: %r" % level)
         return search
 
 ### Classes ###
     def get_url(self):
         return "/%s/" % self.slug
     
-    objects = JsonManager()
+    def short(self, request=None):
+        return {
+            'slug': self.slug,
+            'name': self.name
+        }
     
-    def details(self, request=None):
+    def summary(self, request=None):
         return {
-            '_pk': self.slug,
-            'slug': self.slug,
-            'name': self.name,
             'owner': self.owner.username,
             'description': self.description,
         }
-        
-    def ref(self, request=None):
-        return {'_pk': self.slug, 'slug': self.slug, 'name': self.name}
+
+    objects = JsonManager()
         
     def update(self, details, request=None):
         self.slug = details.get('slug', self.slug)
         self.description = details.get('description', self.description)
         
     class Admin:
-        pass
-    
+        pass 
+
 class Tag(models.Model):
     slug = models.SlugField(primary_key = True)
     name = models.CharField(max_length=255)
     
     objects = JsonManager()
         
-    def details(self, request=None):
-        return {
-            '_pk': self.slug,
-            'slug': self.slug,
-            'name': self.name,
-        }
-    ref = details
+    def short(self, request=None):
+        return { 'slug': self.slug, 'name': self.name }
         
     def update(self, details, request=None):
         self.slug = details.get('slug', self.slug)
         self.name = details.get('name', self.name)
-        if ('project' in details):
-            self.project = Project.objects.get(pk=details['project'])
 
 """
 class Workflow(models.Model):

File squash/base/templates/application.js

  *
  */
 
-$.include('/js/jquery.template.js');
-$.include('/media/skin.css', '/js/skin.js', '/js/Stack.js', '/js/Project.js', '/js/Trash.js');
+$.include('/js/jquery.template.js', '/js/jquery.json.js');
+$.include('/media/skin.css', '/js/skin.js', '/js/Stack.js', '/js/Project.js', '/js/Shelf.js', '/js/Trash.js');
 
-TrashDrop = new DropBehavior({types: ['project', 'sprint', 'ticket']});
+UniversalDrop = new DropBehavior({types: ['project', 'sprint', 'ticket', 'document']});
 
 //// App //
 SquashApplication = Application.subclass({
             ]
         });
         
-        this.trash = new Trash({drop: TrashDrop});
-        $('#wrapper').append(this.trash.render());
+        this.trash = new Trash({drop: UniversalDrop});
+        this.shelf = new Shelf({drop: UniversalDrop, drag: new DragBehavior({snapToCursor: true, type: 'shelf'})});
+        this.shelf.add(this.trash);
+        
+        $('#wrapper').append(this.shelf.render());
         
         if (!App.user.authenticated)
             this.show_login();

File squash/base/views.py

 
 ### Ajax ###
 project = ajax( Project.objects.handler() )
-project_search = ajax( Project.objects.searcher() )
+project_search = ajax( Project.objects.searcher() )
+
+from squash.util import get_object_list
+
+@ajax
+def store(request, name):
+    if request.POST:
+        print request.POST['contents']
+        request.session[name + '-store'] = unjson(request.POST['contents'])
+        return 'ok'
+    if name == 'trash' and request.GET.get('empty', None):
+        return empty_trash(request)
+    else:
+        return [x._default_manager.details(x, request) for x in get_object_list( request.session.get(name + '-store', []) )]
+
+def empty_trash(request):
+    for o in get_object_list( request.session.get('trash-store', []) ):
+        o.delete()
+    request.session['trash-store'] = []
+    return 'ok'

File squash/document/models.py

     @property    
     def author(self):
         return self.content_set.latest().author
+    
+    def short(self, request=None):
+        return { 'slug': self.slug }
         
-    def details(self, request=None):
+    def summary(self, request=None):
         return {
-            '_pk': self.id,
-            'slug': self.slug,
-            'project': self.project.ref(),
+            'project': self.project.ref(request),
             'project_pk': self.project.slug,
             'author': self.author.username,
             'modified': DateFormat(self.modified).format("F jS \\a\\t P"),
             'src': self.src,
         }
 
-    def ref(self, request=None):
-        return { '_pk': self.id, 'slug': self.slug }
-
     def update(self, details, request=None):
         self.slug = details.get('slug', self.slug)
         if ('project_pk' in details):

File squash/js/Document.js

 Document = Model.subclass('Document', {
     options: {
         url : '/document/',
-        pk : '_pk'
+        model : 'document.Document'
     },
+    template: '<div class="x-icon DocumentIcon"></div><div class="x-header">{{slug}}</div><div class="x-description">{{author}} - {{modified}}</div>',
     getFields : function()
     {
         return [
         var store = Document.manager.create_store({autoLoad: true, params: {project: project.getPk()}});
         this.list = new List({
             store: store,
-            template: '<div class="x-icon DocumentIcon"></div><div class="x-header">{{slug}}</div><div class="x-description">{{author}} - {{modified}}</div>',
+            template: Document.prototype.template,
             scope: this,
             empty: '<div class="x-warning x-small"><div class="WarningIcon x-icon"></div><em>No documents found.</em></div>',
+            drag: new DragBehavior({snapToCursor: true, type: 'document'}),
             select: function() {
                 App.stack.pushAfter(new DocumentPanel(this.list.selected.object), this);
             }

File squash/js/LinkStore.js

+LinkStore = Component.subclass('LinkStore', {
+    options: {
+        url: null,
+    },
+    onInit : function()
+    {
+        this.contents = {};
+    },
+    add : function( object )
+    {
+        var id = object.getHash();
+        if (this.contents[id])
+            return false;
+        this.contents[object.getHash()] = object;
+        return true;
+    },
+    remove : function( object )
+    {
+        delete this.contents[object.getHash()];
+    },
+    clear : function()
+    {
+        this.contents = {};
+    },
+    save : function()
+    {
+        var contents = [];
+        for (var i in this.contents) 
+            contents.push(this.contents[i].getRef());
+        contents = $.toJSON(contents);
+        console.log(contents);
+        $.post(this.options.url, {'contents': contents}, function(r)
+        {
+            if (r.__exception__)
+                console.error(r.msg);
+        }, 'json');
+    },
+    load : function()
+    {
+        var self = this;
+        
+        $.get(this.options.url, null, function(response)
+        {
+            if (response.__exception__)
+                console.error(response.msg);
+                   
+            for(var i in response)
+            {
+                var o = Model.manifest(response[i]);
+                self.add( o, true );
+            }
+            self.trigger('load', self.contents)
+        }, 'json');
+    },
+    special : function(params, callback)
+    {
+        var self = this;
+        
+        $.get(this.options.url, params, function(response)
+        {
+            if (response.__exception__)
+                console.error(response.msg);
+            
+            if (callback)
+                callback.call(self, response);
+        });
+    }
+})

File squash/js/Project.js

 Project = Model.subclass('Project', {
     options: {
         url : '/project/',
-        pk : 'slug'
+        model: 'base.Project'
     },
+    template: '<div class="x-icon ProjectIcon"></div><div class="x-header">{{name}}</div><div class="x-description">{{description}}</div>',
     getFields : function()
     {
         return [
         var store = Project.manager.create_store({autoLoad: true});
         this.list = new List({
             store: store,
-            template: 
-                '<div class="x-icon ProjectIcon"></div><div class="x-header">{{name}}</div><div class="description">{{description}}</div>',
+            template: Project.prototype.template,
             scope: this,
             select: function() {
                 App.stack.pushAfter(new ProjectContentsPanel(this.list.selected.object), this);
     __init__ : function(project, options)
     {
         ProjectContentsPanel.uber('__init__').call(this, options);
-        
+        console.log(project);
         this.list = new List({
             store: new DataStore({
                 data: [

File squash/js/Shelf.js

+$.include('/js/LinkStore.js')
+
+Shelf = Container.subclass({
+    options: {
+        skin: 'Shelf.Skin',
+        drag: null,
+        url: '/store/shelf/'
+    },
+    onInit : function()
+    {
+        this.map = {};
+        this.store = new LinkStore({url: this.options.url});
+        this.store.load();
+        
+        var self = this;
+        this.store.bind('load', function(e, objects)
+        {
+            for(var i in objects)
+                self.addObject(objects[i]);
+        })
+        
+        this.bind('child-deleted', function(e, child)
+        {
+            self.store.remove(child.getValue());
+            self.store.save();
+        })
+    },
+    addObject : function( object )
+    {
+        var id = object.getHash();
+        if (this.map[id])
+            return this.map[id].source.fadeOut('fast').fadeIn();
+        
+        this.store.add(object);
+        
+        var item = new ModelItem();
+        item.setTemplate($.template(object.template));
+        item.setValue( object );
+        this.add( item );
+        this.options.drag.attach(item);
+        
+        this.map[id] = item;
+    }
+})
+
+Container.Skin.subclass('Shelf.Skin', {
+    attach : function(shelf)
+    {
+        Container.Skin.prototype.attach.call(this, shelf);
+        shelf.source.addClass('app-shelf');
+        shelf.source.addClass('x-medium');
+        
+        shelf.bind('drop', function(e, dropped)
+        {
+            shelf.addObject(dropped.object);
+            shelf.store.save();
+        })
+    },
+    append : function(element)
+    {
+        Container.Skin.prototype.append.call(this, element);
+        element.source.hide();
+        element.source.fadeIn();
+    }
+})

File squash/js/Sprint.js

 Sprint = Model.subclass('Sprint', {
     options: {
         url : '/sprint/',
-        pk : '_pk'
+        model: 'ticket.Sprint'
     },
+    template: '<div class="x-icon SprintIcon"></div><div class="x-header">{{name}}</div><div class="x-description">{{dates}}</div>',
     getFields : function()
     {
         return [
         var store = Sprint.manager.create_store({autoLoad: true, params: {project: project.getPk()}});
         this.list = new List({
             store: store,
-            template: '<div class="x-icon SprintIcon"></div><div class="x-header">{{name}}</div><div class="x-description">{{dates}}</div>',
+            template: Sprint.prototype.template,
             scope: this,
             empty: '<div class="x-warning x-small"><div class="WarningIcon x-icon"></div><em>No sprints found.</em></div>',
             drag: new DragBehavior({snapToCursor: true, type: 'sprint'}),

File squash/js/Ticket.js

 Ticket = Model.subclass('Ticket', {
     options: {
         url : '/ticket/',
-        pk : 'id'
+        model: 'ticket.Ticket'
     },
+    template: '<div class="x-icon TicketIcon"></div><div class="Ticket x-header">{{name}}</div><div>{{state_name}}</div>',
     getFields : function()
     {
         return [
-            new Field({name: 'name', type: 'text', label: 'Name'}),
+            new Field({name: 'name', type: 'text', label: 'Short Description'}),
             new Field({name: 'state_pk', type: 'select', label: 'State', choices: App.states}),
+            new Field({name: 'description', type: 'textarea', cls: 'long', label: 'Details'}),
             new Field({name: 'project_pk', type: 'hidden'}),
             new Field({name: 'sprint_pk', type: 'hidden'}),
             new Field({name: '_pk', type: 'hidden'})
         var store = Ticket.manager.create_store({autoLoad: true, params:params});
         this.list = new List({
             store: store,
-            template: '<div class="x-icon TicketIcon"></div><div class="x-header">{{name}}</div><div>{{state_name}}</div>',
+            template: Ticket.prototype.template,
             scope: this,
             empty: '<div class="x-warning x-small"><div class="WarningIcon x-icon"></div><em>No sprints found.</em></div>',
             drag: new DragBehavior({snapToCursor: true, type: 'ticket'}),

File squash/js/Trash.js

-Element.Skin.subclass('Trash.Skin', {
-    attach : function(element)
+$.include('/js/Shelf.js')
+
+Trash = Container.subclass({
+    options: {
+        skin: 'Trash.Skin',
+        drag_locked: true,
+        url: '/store/trash/'
+    },
+    onInit : function()
     {
-        Element.Skin.prototype.attach.call(this, element);
-        element.source.addClass('app-trash');
+        this.store = new LinkStore({url: this.options.url});
+        this.store.load();
         
-        element.bind('drop', function(e, dropped)
+        var self = this;
+        this.store.bind('load', function(e, objects)
         {
-            dropped.remove();
-            element.add(dropped.object);
+            for(var i in objects)
+                self.addObject(objects[i]);
+        })
+        
+        this.bind('child-deleted', function(e, child)
+        {
+            self.store.remove(child.getValue());
+            self.store.save();
         })
     },
-    markFilled : function()
+    addObject : function(object)
     {
-        this.element.source.addClass('app-trash-full');
+        this.store.add(object);
+        this.skin.markFilled(true);
+    },
+    clear : function()
+    {
+        this.store.clear();
+        this.store.save();
+        this.skin.markFilled(false);
+    },
+    empty : function()
+    {
+        var objects = this.store.contents;
+        
+        var self = this;
+        this.store.special({empty: true}, function()
+        {
+            self.skin.markFilled(false);
+        })
+        
+        this.store.clear();
+    
+        for(var i in objects)
+            objects[i].trigger('delete');
     }
 })
 
-Trash = Element.subclass({
-    options: {
-        skin: 'Trash.Skin'
+Container.Skin.subclass('Trash.Skin', {
+    attach : function(trash)
+    {
+        Container.Skin.prototype.attach.call(this, trash);
+        trash.source.addClass('app-trash');
+        trash.source.addClass('x-medium');
+        
+        trash.bind('drop', function(e, dropped)
+        {
+            var parent = dropped.parent;
+            
+            if (parent)
+                dropped.parent.trigger('child-deleted', dropped);
+                
+            dropped.remove();
+            
+            if (parent && parent.__class__ == Shelf)
+                return;
+                
+            trash.addObject(dropped.object);
+            trash.store.save();
+        })
     },
-    add : function(object)
+    markFilled : function(bool)
     {
-        this.skin.markFilled();
+        if (bool)
+            this.source.addClass('app-trash-full');
+        else
+            this.source.removeClass('app-trash-full');
     }
-})
+})
+

File squash/js/component.js

 /* Component
  */
 function Component() { this.__init__.apply(this, arguments); }
-Component.prototype.__init__ = function(options) { this.setOptions(options) };
+Component.prototype.__init__ = function(options) { 
+    this.setOptions(options)
+    if (this.onInit)
+        this.onInit.apply(this, arguments);
+};
 
 // Options
 Component.prototype.setOptions = function(options) { this.options = jQuery.extend({}, this.constructor.prototype.options, options) };
 Element = Component.subclass('Element', {
     options: {
         source: '<div/>',               // Source of the element.
-        skin: 'Element.Skin'            // The element skin.
+        skin: 'Element.Skin',           // The element skin.
+        drag_locked: false              // Can be dragged.
     },
     __init__ : function() {
         Element.uber('__init__').apply(this, arguments);
         this.refresh();
         
         var self = this;
-        this.object.bind('set', function() { self.refresh() });
+        object.bind('set', function() { self.refresh() });
     },
     getValue : function()
     {
             this.source.addClass('x-even');
         else
             this.source.addClass('x-odd');
-        this.source.click(function()
-        {
-            element.list.select(element);
-        })
         element.refresh();
     },
     setValue : function(v)
         if (this.template)
             item.setTemplate(this.template);
         
+        var self = this;
+        item.bind('click', function()
+        {
+            self.select(item);
+        });
+        
         item._index = this.items.length;
         
         item.setValue( value );
         url_post: null,
         url_del: null,
         url_search: null,
-        pk: null,
+        pk: '_pk',
+        mk: '_model',
         manager: null,
-        fields: null
+        fields: null,
+        ref: {}
     },
     __init__ : function(value, options)
     {
     },
     getPk : function()
     {
-        return this.get(this.options.pk || '_pk');
+        return this.get(this.options.pk);
+    },
+    getHash : function()
+    {
+        return this.value[this.options.model] + ":" + this.value[this.options.pk];
     },
     getRef : function()
     {
         var ref = {};
-        ref[this.options.pk || '_pk'] = this.getPk();
+        ref[this.options.pk] = this.value[this.options.pk];
+        ref[this.options.mk] = this.value[this.options.mk];
+        console.log(ref, this.value)
         return ref;
     },
     getValue : function()
     }
 })
 
+Model.types = {}
+
 Model.__subclass__ = function()
 {
     var cls = Component.__subclass__.apply(cls, arguments);
         cls.prototype.manager = cls.manager = new cls.prototype.manager(cls);
     else
         cls.prototype.manager = cls.manager = new Manager(cls);
+    if (cls.prototype.options.model)
+        Model.types[cls.prototype.options.model] = cls
     return cls;
 }
 
+Model.manifest = function(value)
+{
+    var Cls = Model.types[value._model];
+    if (Cls == undefined)
+        throw value._model + " is an unknown model type.";
+    return new Cls(value);
+}
+
 Manager = Component.subclass('Manager', {
     __init__ : function(model)
     {
         
         if ($('#x-overlay').length == 0)
         {
-            $('<div id="x-overlay" class="x-medium"/>').appendTo(document.body).css('opacity', .5);
+            $('<div id="x-overlay" class="x-medium"/>').appendTo(document.body);
         }
     },
     attach : function(element)
         var self = this;
         
         element.source.bind('mousedown', function(e) {
-            console.log('mousedown');
+            if (element.options.drag_locked)
+                return;
+                
             self.origin = {left: e.clientX, top: e.clientY, element: element};
             
             // No idea why this works, but it allows events to continue durring the drag.
     {
         var source = element.source;
         var clone = source.clone();
-        clone.css('opacity', .35);
+        clone.css('opacity', .5);
         clone.appendTo(document.body);
         clone.css('position', 'absolute');
         clone.width(source.width());
             this.drag.target.trigger('drop', this.drag.element);
         DragBehavior.drag = this.drag = null;
         
-        console.log("Done.");
         $('#x-overlay').hide();
     }
 })
                     cursor = element.source.css('cursor');
                     element.source.css('cursor', 'move');
                     DragBehavior.drag.target = element;
+                    if (e.stopPropagation) e.stopPropagation();
+            		if (e.preventDefault) e.preventDefault();
                 }
             },
             function(e) 

File squash/js/jquery.json.js

+(function($){function toIntegersAtLease(n)
+{return n<10?'0'+n:n;}
+Date.prototype.toJSON=function(date)
+{return date.getUTCFullYear()+'-'+
+toIntegersAtLease(date.getUTCMonth()+1)+'-'+
+toIntegersAtLease(date.getUTCDate());};var escapeable=/["\\\x00-\x1f\x7f-\x9f]/g;var meta={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'}
+$.quoteString=function(string)
+{if(escapeable.test(string))
+{return'"'+string.replace(escapeable,function(a)
+{var c=meta[a];if(typeof c==='string'){return c;}
+c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+(c%16).toString(16);})+'"'}
+return'"'+string+'"';}
+$.toJSON=function(o)
+{var type=typeof(o);if(type=="undefined")
+return"undefined";else if(type=="number"||type=="boolean")
+return o+"";else if(o===null)
+return"null";if(type=="string")
+{return $.quoteString(o);}
+if(type=="object"&&typeof o.toJSON=="function")
+return o.toJSON();if(type!="function"&&typeof(o.length)=="number")
+{var ret=[];for(var i=0;i<o.length;i++){ret.push($.toJSON(o[i]));}
+return"["+ret.join(", ")+"]";}
+if(type=="function"){throw new TypeError("Unable to convert object of type 'function' to json.");}
+ret=[];for(var k in o){var name;var type=typeof(k);if(type=="number")
+name='"'+k+'"';else if(type=="string")
+name=$.quoteString(k);else
+continue;val=$.toJSON(o[k]);if(typeof(val)!="string"){continue;}
+ret.push(name+": "+val);}
+return"{"+ret.join(", ")+"}";}
+$.evalJSON=function(src)
+{return eval("("+src+")");}
+$.secureEvalJSON=function(src)
+{var filtered=src;filtered=filtered.replace(/\\["\\\/bfnrtu]/g,'@');filtered=filtered.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,']');filtered=filtered.replace(/(?:^|:|,)(?:\s*\[)+/g,'');if(/^[\],:{}\s]*$/.test(filtered))
+return eval("("+src+")");else
+throw new SyntaxError("Error parsing JSON, source is not valid.");}})(jQuery);

File squash/media/base.css

 	margin-top: 0;
 }
 
+select { padding: 0px; margin: 0px }
+select option { padding: 0px 4px }
+
 /** Elements **/
 #wrapper { margin-left: auto; 
            margin-right: auto;
 
 /** Elements **/
 .app-trash {
-    position: absolute;
-    right: 5px;
-    top: -3px;
     width: 50px;
     height: 50px;
+    margin-top: -2px;
     background-image: url(/media/icons/trash-empty.png);
     background-position: bottom-right;
     background-repeat: no-repeat;
     background-image: url(/media/icons/trash-full.png);
 }
 
+.app-shelf {
+    position: absolute;
+    right: 5px;
+    top: 0px;
+    width: 860px;
+    height: 50px;
+    overflow: hidden;
+}
+
+.app-shelf .x-description {
+    display: none;
+}
+
+.app-shelf .x-item {
+    margin-top: 10px;
+    height: 48px;
+    margin-left: 11px;
+}
+
+.app-shelf .x-header {
+    margin-top: 8px;
+}
+
+.app-shelf .Ticket {
+    margin-top: 0px;
+}
+
+.app-shelf .x-icon {
+    margin-left: 0px;
+    width: 32px;
+    height: 32px;
+    margin-right: 6px;
+    background-position: left;
+    background-repeat: no-repeat;
+    float: left;
+}
+
+.app-shelf .x-icon {
+    width: 32px;
+    height: 32px;
+    margin-right: 6px;
+    margin-left: 0px !important;
+}
+
+.app-shelf > * {
+    float: right;
+}
+
+.long textarea {
+    height: 240px;
+}
+
 /** Icons **/
 .AddIcon        {  background-image: url(/media/icons/health.png)   }
 .EditIcon       {  background-image: url(/media/icons/tool.png)     }

File squash/media/skin.css

     float: left;
 }
 
+.x-drag .x-description {
+    display: none;
+}
+
 .x-medium .x-icon {
     width: 32px;
     height: 32px;

File squash/ticket/models.py

 from django.db import models
 from django.contrib.auth import models as auth
 from squash.base import models as base
+from squash.document import models as document
 
 from django.utils.dateformat import DateFormat
 
 
     def get_parent_ref(self, request=None):
         if (self.parent):
-            return self.parent.get_ref()
+            return self.parent.ref(request)
         else:
             return None
-
-    def details(self, request=None):
+        
+    def short(self, request=None):
+        return { 'slug': self.slug, 'name': self.name }
+    
+    def summary(self, request=None):
         if (self.start and self.end):
             dates = "Starts: %s | Ends: %s" % (format_date(self.start), format_date(self.end))
         elif (self.start):
         else:
             dates = "Persistant"
         return {
-            '_pk': self.id,
-            'slug': self.slug,
-            'name': self.name,
-            'project': self.project.ref(),
+            'project': self.project.ref(request),
             'project_pk': self.project.slug,
             'creator': self.creator.username,
-            'parent': self.get_parent_ref(),
+            'parent': self.get_parent_ref(request),
             'start': format_date(self.start),
             'end': format_date(self.end),
             'dates': dates
         }
 
-    def ref(self, request=None):
-        return { '_pk': self.id, 'slug': self.slug, 'name': self.name}
-
     def update(self, details, request=None):
         self.slug = details.get('slug', self.slug)
         self.name = details.get('name', self.name)
     created = models.DateTimeField(auto_now_add=True)
     modified = models.DateTimeField(auto_now=True)
     
+    description = models.TextField(blank=True)
+    
     def __str__(self):
         return self.name
         
     
     def get_parent_ref(self, request=None):
         if (self.parent):
-            return self.parent.ref()
+            return self.parent.ref(request)
         else:
             return None
     
-    def details(self, request=None):
+    def short(self, request=None):
+        return {'name': self.name}
+            
+    def summary(self, request=None):
         return {
-            '_pk': self.id,
-            'id': self.id,
-            'name': self.name,
-            
-            'project': self.project.ref(),
+            'project': self.project.ref(request),
             'project_pk': self.project._get_pk_val(),
-            'sprint': self.sprint.ref(),
+            'sprint': self.sprint.ref(request),
             'sprint_pk': self.sprint._get_pk_val(),
-            'state': self.state.ref(),
+            'state': self.state.ref(request),
             'state_pk': self.state._get_pk_val(),
             'state_name': self.state.name,
             
             'creator': self.creator.username,
-            'parent': self.get_parent_ref(),
+            'parent': self.get_parent_ref(request),
             'created': self.created,
             'modified': self.modified,
+            
+            'description': self.description,
         }
-        
-    def ref(self, request=None):
-        return {'_pk': self.id, 'id': self.id, 'name': self.name}
 
     def update(self, details, request=None):
         self.name = details.get('name', self.name)
+        self.description = details.get('description', self.description)
         if ('project_pk' in details):
             self.project = base.Project.objects.get(pk=details['project_pk'])
         if ('sprint_pk' in details and details['sprint_pk']):
             q = q.filter(project=params.get('project', None))        
         if ('sprint' in params):
             q = q.filter(sprint=params.get('sprint', None))
-        return q
+        return q.order_by('-state')
 
     objects = base.JsonManager()
     
             self.sprint = self.project.sprint_set.get(slug='default')
         models.Model.save(self)
 
+class Comment(models.Model):
+    ticket = models.ForeignKey(Ticket)
+    author = models.ForeignKey(auth.User)
+    text = models.TextField()
+    
+    created = models.DateTimeField(auto_now_add=True)
+    objects = base.JsonManager()
+    
+    def short(self, request):
+        return {
+            'ticket': self.ticket.ref(request),
+            'ticket_pk': self.ticket._get_pk_val(),
+            'author': self.author.username,
+            'created': format_date(self.created),
+            'text': self.text,
+        }
+    
+    def update(self, details, request=None):    
+        self.author = request.user
+        self.text = details.get('text', '')
+        if ('ticket_pk' in details and details['ticket_pk']):
+            self.ticket = Ticket.objects.get(pk=details['ticket_pk'])
+    
+    @classmethod
+    def query(cls, request):
+        params = request.REQUEST
+        return cls.objects.filter(ticket=params.get('ticket'))
+
+    class Meta:
+        ordering = ('-created',)
 
 ### Hookups ###
 from django.db.models import signals

File squash/urls.py

     (r'^document/search/', 'squash.document.views.document_search'),
     (r'^document/', 'squash.document.views.document'),
     
+    (r'^store/([\w-]*)/', 'squash.base.views.store'),
+    
     (r'^application\.js', 'squash.base.views.application'),
     (r'^$', 'squash.base.views.index'),
 )

File squash/util.py

     """
     __metaclass__ = ContinueMeta
 
+### Object Dereference ###
+from django.db.models import get_model
+
+def get_object(ref):
+    model, pk = ref['_model'], ref['_pk']
+    Model = get_model(*model.split('.', 1))
+    return Model._default_manager.get(pk=pk)
+
+def get_object_list(refs):
+    groups = {}
+    objects = []
+    for ref in refs:
+        model, pk = ref['_model'], ref['_pk']
+        if model not in groups:
+            groups[model] = []
+        groups[model].append(pk)
+    for model in groups:
+        Model = get_model(*model.split('.', 1))
+        pks = groups[model]
+        objects.extend(Model._default_manager.filter(pk__in=pks))
+    return objects
+
 ### JSON ###
 import datetime
 from django.utils import simplejson