Commits

Brantley Harris committed a5e3f5f

Big changes, refactored javascript models, allowed drag and drop, also changed the README to include info on the fixtures.json.

  • Participants
  • Parent commits 763cd0a

Comments (0)

Files changed (13)

 ln -s /path/to/django-trunk/django django
 cd squash
 ./manager syncdb
+./manager loaddata fixtures.json
 ./manager runserver
 
 And scene.

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
+context_levels = ('ref', 'short', 'summary', 'full')
+
+def ref(object):
+    if object is None: return None
+    return {
+        '_pk': object._get_pk_val(),
+        '_model': object._meta.app_label + '.' + object.__class__.__name__
+    }
+
+def details(object, context='full', request=None):
+    details = ref(object)
+    for level in context_levels:
+        if hasattr(object, level): details.update(getattr(object, level)(request))
+        if context == level: return details
+    return details
 
 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))
+        model.details = details
     
     def handler(self):
         def handle(request):
             assert method in ('get', 'post'), "GET or POST to this resource."
             if (method == 'get'):
                 pk = request.GET['_pk']
-                return self.details(self.get(pk=pk), request)
+                return self.get(pk=pk).details(request.GET.get('context', 'summary'), request)
             if (method == 'post'):
                 pk = request.POST.get('_pk', None)
                 if (pk):
                 else:
                     instance = self.model()
                 instance.update(request.POST, request)
-                print instance.__dict__
                 instance.save()
-                return self.details(instance, request)
+                return instance.details('full', request)
         return handle
-        
-    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 resolve(self, pk):
+        """
+            TODO: Problem here, we can't have a pk named 'null', this has rather bad implications.
+                  Especially since we're not even handling serialization from posts.  Up until now
+                  I've considered it a good, conservative idea until now, but this makes me think
+                  otherwise.
+        """
+        if (pk == 'null'):
+            return None
+        return self.get(pk=pk)
+    
     def searcher(self):
         def search(request):
             if hasattr(self.model, 'query'):
                 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)
+            context = request.REQUEST.get('context', 'summary')
+            return [i.details(context, request) for i in query]
         return search
 
 ### Classes ###
         
     def get_url(self):
         return "/%s/" % self.slug
-    
+            
     def short(self, request=None):
         return {
             'slug': self.slug,

File squash/base/views.py

     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', []) )]
+        return [x.details('summary', 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', []) ):

File squash/document/models.py

 from django.db import models
 from django.contrib.auth import models as auth
-from squash.base import models as base
+from squash.base import models as base; ref = base.ref
 from datetime import datetime
 from django.utils.dateformat import DateFormat
 
         
     def summary(self, request=None):
         return {
-            'project': self.project.ref(request),
+            'project': ref( self.project ),
             'project_pk': self.project.slug,
             'author': self.author.username,
             'modified': DateFormat(self.modified).format("F jS \\a\\t P"),

File squash/js/Document.js

 Document = Model.subclass('Document', {
     options: {
         url : '/document/',
-        model : 'document.Document'
+        model : 'document.Document',
+        template: '<div class="x-icon DocumentIcon"></div><div class="x-header">{{slug}}</div><div class="x-description">{{author}} - {{modified}}</div>'
     },
-    template: '<div class="x-icon DocumentIcon"></div><div class="x-header">{{slug}}</div><div class="x-description">{{author}} - {{modified}}</div>',
     getFields : function()
     {
         return [
         this.project = project;
         
         var store = Document.manager.create_store({autoLoad: true, params: {project: project.getPk()}});
-        this.list = new List({
+        this.list = new ModelList({
             store: store,
             template: Document.prototype.template,
             scope: this,

File squash/js/Project.js

 
 Project = Model.subclass('Project', {
     options: {
-        url : '/project/',
-        model: 'base.Project'
+        url: '/project/',
+        model: 'base.Project',
+        drop: new DropBehavior({types: ['sprint', 'document', 'ticket']}),
+        template: '<div class="x-icon ProjectIcon"></div><div class="x-header">{{name}}</div><div class="x-description">{{description}}</div>'
     },
-    template: '<div class="x-icon ProjectIcon"></div><div class="x-header">{{name}}</div><div class="x-description">{{description}}</div>',
     getFields : function()
     {
         return [
             new Field({name: 'description', type: 'textarea', label: 'Description'}),
             new Field({name: 'owner', type: 'hidden'})
         ]
+    },
+    onDrop : function(element)
+    {
+        var object = element.object;
+        var parent = element.parent;
+        
+        if (object.get('project_pk') == this.getPk())
+            return;
+        
+        object.set('project_pk', this.getPk());
+        if (object.has('sprint_pk'))
+            object.set('sprint_pk', null);
+            
+        object.save();
+        
+        object.trigger('moved');
+        this.trigger('child-moved');
     }
 })
 
         ProjectListPanel.uber('__init__').apply(this, arguments);
         
         var store = Project.manager.create_store({autoLoad: true});
-        this.list = new List({
+        this.list = new ModelList({
             store: store,
-            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

         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.remove(child.object);
             self.store.save();
         })
     },
         
         this.store.add(object);
         
-        var item = new ModelItem();
-        item.setTemplate($.template(object.template));
-        item.setValue( object );
+        var item = object.getItem({link: true});
         this.add( item );
         this.options.drag.attach(item);
         

File squash/js/Sprint.js

 Sprint = Model.subclass('Sprint', {
     options: {
         url : '/sprint/',
-        model: 'ticket.Sprint'
+        model: 'ticket.Sprint',
+        drop: new DropBehavior({types: ['ticket']}),
+        template: '<div class="x-icon SprintIcon"></div><div class="x-header">{{name}}</div><div class="x-description">{{dates}}</div>'
     },
-    template: '<div class="x-icon SprintIcon"></div><div class="x-header">{{name}}</div><div class="x-description">{{dates}}</div>',
     getFields : function()
     {
         return [
             new Field({name: 'project_pk', type: 'hidden'}),
             new Field({name: '_pk', type: 'hidden'})
         ]
+    },
+    onDrop : function(element)
+    {
+        var ticket = element.object;
+        var parent = element.parent;
+        
+        if (ticket.get('sprint_pk') == this.getPk())
+            return;
+        
+        ticket.set('sprint_pk', this.getPk());
+        ticket.save();
+        
+        ticket.trigger('moved');
+        this.trigger('child-moved', ticket);
     }
 })
 
         this.project = project;
         
         var store = Sprint.manager.create_store({autoLoad: true, params: {project: project.getPk()}});
-        this.list = new List({
+        this.list = new ModelList({
             store: store,
             template: Sprint.prototype.template,
             scope: this,

File squash/js/Ticket.js

 Ticket = Model.subclass('Ticket', {
     options: {
         url : '/ticket/',
-        model: 'ticket.Ticket'
+        model: 'ticket.Ticket',
+        template: '<div class="x-icon TicketIcon"></div><div class="Ticket x-header">{{name}}</div><div>{{state_name}}</div>'
     },
-    template: '<div class="x-icon TicketIcon"></div><div class="Ticket x-header">{{name}}</div><div>{{state_name}}</div>',
     getFields : function()
     {
         return [
         if (sprint)
             params.sprint = sprint.getPk();
         var store = Ticket.manager.create_store({autoLoad: true, params:params});
-        this.list = new List({
+        this.list = new ModelList({
             store: store,
             template: Ticket.prototype.template,
             scope: this,

File squash/js/component.js

     }
 })
 
+DataStore = Component.subclass('DataStore', {
+    options: {
+        url: null,
+        params: {},
+        autoLoad: false,
+        data: null
+    },
+    __init__ : function()
+    {
+        DataStore.uber('__init__').apply(this, arguments);
+        this.data = this.options.data || [];
+        this._preload = (this.options.data || this.options.autoLoad) ? true : false;
+    },
+    load : function(params)
+    {
+        var self = this;
+        var callback = function(response)
+        {
+            if (response.__exception__)
+                console.error(response.msg);
+
+            self.setValue(response);
+            self.trigger('load', self.data);
+        }
+        
+        params = jQuery.extend({}, this.options.params, params);
+        if (this.options.url)
+            $.get(this.options.url, params, callback, 'json');
+        else if (this.options.data)
+            setTimeout(callback, 10, this.options.data);
+    },
+    setValue : function(data)
+    {
+        self.data = data;
+    },
+    getValue : function()
+    {
+        return self.data;
+    },
+    autoLoad : function()
+    {
+        if (this._preload)
+            this.load();
+        this._preload = false;
+    }
+})
+
 ListItem = Element.subclass('ListItem', {
     options: {
-        skin: 'ListItem.Skin'
+        skin: 'ListItem.Skin',
+        drop: null,
+        template: null
+    },
+    __init__ : function(value, options)
+    {
+        ListItem.uber('__init__').call(this, options);
+        if (this.options.template)
+            this.setTemplate(this.options.template);
+        this.setValue( value );
     },
     setTemplate : function(t)
     {
+        if (typeof t == 'string') t = $.template(t);
         this.template = t;
     },
     setValue : function(v)
     {
         return this.value || null;
     },
-    setList : function(list)
-    {
-        this.list = list;
-    },
     refresh : function()
     {
         this.setValue(this.getValue());
     }
 })
 
-ModelItem = ListItem.subclass('ModelItem', {
-    setValue : function( object )
-    {
-        this.object = object;
-        this.refresh();
-        
-        var self = this;
-        object.bind('set', function() { self.refresh() });
-    },
-    getValue : function()
-    {
-        return this.object;
-    },
-    refresh : function( )
-    {
-        if (!this.skin) return;
-        
-        if (this.template)
-            return this.skin.setValue( this.template.apply(this.object.getValue()) );
-        if (this.object.getDisplay)
-            return this.skin.setValue( this.object.getDisplay() );
-        this.skin.setValue( this.object.getValue() );
-    }
-});
-
 ListItem.Skin = Container.Skin.subclass('ListItem.Skin', {
     attach : function(element)
     {
         else
             this.source.addClass('x-odd');
         element.refresh();
+        
+        if (element.options.drop)
+            element.options.drop.attach(element);
     },
     setValue : function(v)
     {
     options: {
         skin: 'List.Skin',
         template: null,
-        itemClass: null,
+        itemClass: ListItem,
         store: null,
         select: null,
         scope: null
         this.selected = null;
         
         if (this.options.template)
-            this.template = $.template(this.options.template);
+            if (typeof this.options.template == 'string')
+                this.template = $.template(this.options.template);
+            else
+                this.template = this.options.template.apply;
         else
             this.template = null;
-        
-        if (this.options.itemClass)
-            this.itemClass = this.options.itemClass;
-        else if (this.store && this.store.options.type)
-            this.itemClass = ModelItem;
-        else    
-            this.itemClass = ListItem;
-
     },
     select : function( item )
     {
         if (this.options.select)
             this.options.select.call(this.options.scope || this, item);
     },
-    addValue : function( value )
+    addValue : function( value, options )
     {
-        var item = new this.itemClass();
+        var item = new this.options.itemClass( value, options );
         if (this.template)
             item.setTemplate(this.template);
         
         });
         
         item._index = this.items.length;
-        
-        item.setValue( value );
-        item.setList( this );
         this.add( item );
         return item;
     },
         if (this.store)
         {
             var self = this;
-            this.store.bind('load', function(element, data) {
-                self.data = data;
+            this.store.bind('load', function() {
                 self.refresh();
             })
             this.store.autoLoad();
         if (this.store)
         {
             this.clear();
-            for(var i in this.data)
+            for(var i in this.store.data)
             {
-                this.addValue(this.data[i]);
+                this.addValue(this.store.data[i]);
+            }
+        }
+        else
+        {
+            for(var i in this.options.data)
+            {
+                this.addValue(this.options.data[i]);
             }
         }
         /*
-        if (this.data.length == 0 && this.options.empty)
+        if (this.items.length == 0 && this.options.empty)
         {
             var empty = new Element({source: this.options.empty});
             List.ubertype.add.call(this, empty);
         this.content.append(element.render());
         if (this.element.options.drag)
         {
-            console.log('drag');
             this.element.options.drag.attach(element);
         }
     }
     }
 })
 
-DataStore = Component.subclass('DataStore', {
-    options: {
-        url: null,
-        params: {},
-        autoLoad: false,
-        data: null,
-        type: null
-    },
-    __init__ : function()
-    {
-        DataStore.uber('__init__').apply(this, arguments);
-        this.data = [];
-        this._preload = (this.options.data || this.options.autoLoad)
-    },
-    load : function(params)
-    {
-        var self = this;
-        var callback = function(response)
-        {
-            if (response.__exception__)
-            {
-                console.error("== Server Exception ==\n", response.msg);
-            }
-            
-            self.data = response;
-            
-            if (self.options.type)
-            {
-                var collect = [];
-                var type = self.options.type;
-                for(var i=0;i<self.data.length; i++)
-                {
-                    var o = new type(self.data[i]);
-                    collect.push( o );
-                    o.bind('save', function() { self.load() });
-                }
-                self.data = collect;
-            }
-            
-            self.trigger('load', self.data);
-        }
-        
-        params = jQuery.extend({}, this.options.params, params);
-        if (this.options.url)
-            $.get(this.options.url, params, callback, 'json');
-        else if (this.options.data)
-            setTimeout(callback, 10, this.options.data);
-    },
-    autoLoad : function()
-    {
-        if (this._preload)
-            this.load();
-        this._preload = false;
-    }
-})
-
-Model = Component.subclass('Model', {
-    options: {
-        url: null,
-        url_get: null,
-        url_post: null,
-        url_del: null,
-        url_search: null,
-        pk: '_pk',
-        mk: '_model',
-        manager: null,
-        fields: null,
-        ref: {}
-    },
-    __init__ : function(value, options)
-    {
-        Model.uber('__init__').call(this, options)
-        this.setValue(value || {});
-    },
-    getPk : function()
-    {
-        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] = this.value[this.options.pk];
-        ref[this.options.mk] = this.value[this.options.mk];
-        console.log(ref, this.value)
-        return ref;
-    },
-    getValue : function()
-    {
-        return this.value;
-    },
-    setValue : function( v )
-    {
-        this.value = v;
-        this.trigger('set', v);
-    },
-    get : function(name)
-    {
-        return this.value[name];
-    },
-    set : function(name, v)
-    {
-        this.value[name] = v;
-    },
-    save : function( options )
-    {
-        var options = jQuery.extend({
-            url: this.options.url_post,
-            post: this.getValue(),
-            success: function() { this.trigger('save') },
-        }, options);
-        this.manager.interact(this, options);
-    },
-    load : function( options )
-    {
-        var options = jQuery.extend({
-            url: this.options.url_get,
-            get: this.getRef(),
-            success: function(value) { this.setValue(value); this.trigger('load') },
-        }, options);
-        this.manager.interact(this, options);
-    },
-    remove : function( options )
-    {
-        throw "Not implimented.";
-    },
-    getFields : function()
-    {
-        return this.manager.getFields();
-    }
-})
-
-Model.types = {}
-
-Model.__subclass__ = function()
-{
-    var cls = Component.__subclass__.apply(cls, arguments);
-    if (cls.prototype.manager)
-        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)
-    {
-        Manager.uber('__init__').call(this);
-        this.model = model;
-        this.fields = model.prototype.fields || [];
-    },
-    interact : function(object, options)
-    {
-        var url = options.url || object.options.url;
-        var scope = options.scope || object;
-        var success = options.success;
-        var failure = options.failure;
-
-        var callback = function(response)
-        {
-            if (response.__exception__)
-            {
-                if (failure)
-                    failure.call(scope, response.msg);
-                else
-                    console.error("== Server Exception ==\n", response.msg);
-            }
-            
-            if (success)
-                success.call(scope, response);
-        }
-        
-        if (options.post)
-            return $.post(url, options.post, callback, 'json');
-        else
-            return $.get(url, options.get, callback, 'json')
-    },
-    get : function(ref, options)
-    {
-        var o = new this.model(ref);
-        o.load(options);
-        return o;
-    },
-    search : function(options)
-    {
-        if (!options.url)
-            options.url = this.options.url_search || (this.options.url + 'search/');
-        return this.interact(options);
-    },
-    create_store : function(options)
-    {
-        var options = $.extend({}, {
-            url: this.model.prototype.url_search || (this.model.prototype.options.url + 'search/'),
-            type: this.model
-        }, options)
-        return new DataStore(options);
-    },
-    getFields : function()
-    {
-        return this.fields;
-    }
-})
-
 Application = Component.subclass('Application',
 {
     __init__ : function()

File squash/js/model.js

+ModelStore = DataStore.subclass('ModelStore', {
+    setValue : function(data)
+    {
+        this.data = [];
+        for(var i=0;i<data.length; i++)
+        {
+            var o = Model.manifest(data[i]);
+            this.data.push(o);
+        }
+    }
+})
+
+Model = Component.subclass('Model', {
+    options: {
+        url: null,
+        url_get: null,
+        url_post: null,
+        url_del: null,
+        url_search: null,
+        pk: '_pk',
+        mk: '_model',
+        manager: null,
+        fields: null,
+        ref: {},
+        template: null
+    },
+    __init__ : function(value, options)
+    {
+        Model.uber('__init__').call(this, options)
+        this.setValue(value || {});
+    },
+    getPk : function()
+    {
+        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] = this.value[this.options.pk];
+        ref[this.options.mk] = this.value[this.options.mk];
+        return ref;
+    },
+    getValue : function()
+    {
+        return this.value;
+    },
+    setValue : function( v )
+    {
+        this.value = v;
+        this.trigger('set', v);
+    },
+    get : function(name)
+    {
+        return this.value[name];
+    },
+    set : function(name, v)
+    {
+        this.value[name] = v;
+    },
+    has : function(name)
+    {
+        return this.value[name] != undefined;
+    },
+    save : function( options )
+    {
+        var options = jQuery.extend({
+            url: this.options.url_post,
+            post: this.getValue(),
+            success: function() { this.trigger('save') },
+        }, options);
+        this.manager.interact(this, options);
+    },
+    load : function( options )
+    {
+        var options = jQuery.extend({
+            url: this.options.url_get,
+            get: this.getRef(),
+            success: function(value) { this.setValue(value); this.trigger('load'); },
+        }, options);
+        this.manager.interact(this, options);
+    },
+    remove : function( options )
+    {
+        throw "Not implimented.";
+    },
+    getFields : function()
+    {
+        return this.manager.getFields();
+    },
+    getItem : function(options)
+    {
+        if (!options)
+            options = {};
+        
+        if (!options.template)
+            options.template = this.options.template;
+            
+        if (!options.drop)
+            options.drop = this.options.drop;
+        
+        var item = new ListItem( this.getValue(), options );
+        item.object = this;
+        
+        this.bind('delete', function() { item.remove() });
+        this.bind('set', function() { item.refresh() });
+        
+        if (options.drop || this.options.drop)
+        {
+            var self = this;
+            item.bind('drop', function(e, dropped) { self.onDrop.call(self, dropped) });
+        }
+        
+        if (options.link)
+        { item.link = true; }
+        
+        return item;
+    }
+})
+
+Model.types = {}
+
+Model.__subclass__ = function()
+{
+    var cls = Component.__subclass__.apply(cls, arguments);
+    if (cls.prototype.manager)
+        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)
+    {
+        Manager.uber('__init__').call(this);
+        this.model = model;
+        this.fields = model.prototype.fields || [];
+    },
+    interact : function(object, options)
+    {
+        var url = options.url || object.options.url;
+        var scope = options.scope || object;
+        var success = options.success;
+        var failure = options.failure;
+
+        var callback = function(response)
+        {
+            if (response.__exception__)
+            {
+                if (failure)
+                    failure.call(scope, response.msg);
+                else
+                    console.error("== Server Exception ==\n", response.msg);
+            }
+            
+            if (success)
+                success.call(scope, response);
+        }
+        
+        if (options.post)
+            return $.post(url, options.post, callback, 'json');
+        else
+            return $.get(url, options.get, callback, 'json')
+    },
+    get : function(ref, options)
+    {
+        var o = new this.model(ref);
+        o.load(options);
+        return o;
+    },
+    search : function(options)
+    {
+        if (!options.url)
+            options.url = this.options.url_search || (this.options.url + 'search/');
+        return this.interact(options);
+    },
+    create_store : function(options)
+    {
+        var options = $.extend({}, {
+            url: this.model.prototype.url_search || (this.model.prototype.options.url + 'search/'),
+            type: this.model
+        }, options)
+        return new ModelStore(options);
+    },
+    getFields : function()
+    {
+        return this.fields;
+    }
+})
+
+ModelList = List.subclass('ModelList', {
+    options: {
+        url: null,
+        params: {},
+        data: null
+    },
+    __init__ : function()
+    {
+        ModelList.uber('__init__').apply(this, arguments);
+        if (!this.store)
+        {
+            this.store = new ModelStore({
+                url: this.options.url,
+                params: this.options.params,
+                data: this.options.data
+            })
+        }
+    },
+    addValue : function( object )
+    {
+        var item = object.getItem();
+        
+        var self = this;
+        item.bind('click', function()
+        {
+            self.select(item);
+        });
+        
+        object.bind('moved', function()
+        {
+            self.store.load();
+        });
+        
+        item._index = this.items.length;
+        this.add( item );
+        return item;
+    }
+})

File squash/templates/base.html

 	<link rel="stylesheet" href="/media/base.css" type="text/css"/>
 	<script type='text/javascript' src='/js/jquery.js'></script>
 	<script type='text/javascript' src='/js/component.js'></script>
+	<script type='text/javascript' src='/js/model.js'></script>
 	<script type='text/javascript' src='/application.js'></script>
     {% block head %}
     {% endblock %}

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.base import models as base; ref = base.ref
 from squash.document import models as document
 
 from django.utils.dateformat import DateFormat
 
     def get_url(self):
         return "/%s/" % self.slug
-
-    def get_parent_ref(self, request=None):
-        if (self.parent):
-            return self.parent.ref(request)
-        else:
-            return None
         
     def short(self, request=None):
-        return { 'slug': self.slug, 'name': self.name }
+        return { 'slug': self.slug, 'name': self.name, 'locked': self.is_locked() }
     
     def summary(self, request=None):
         if (self.start and self.end):
         else:
             dates = "Persistant"
         return {
-            'project': self.project.ref(request),
+            'project': ref( self.project ),
             'project_pk': self.project.slug,
             'creator': self.creator.username,
-            'parent': self.get_parent_ref(request),
+            'parent': ref( self.parent ),
             'start': format_date(self.start),
             'end': format_date(self.end),
             'dates': dates
         }
 
+    def is_locked(self):
+        return self.slug in ('default', 'backlog')
+        
+    def change_project(self, project):    
+        if (project != self.project):
+            self.project = project
+            for ticket in self.ticket_set.all():
+                ticket.project = project
+                ticket.save()
+
     def update(self, details, request=None):
+        self.name = details.get('name', self.name)
+        if self.is_locked():
+            return
+            
         self.slug = details.get('slug', self.slug)
-        self.name = details.get('name', self.name)
         if ('project_pk' in details):
-            self.project = base.Project.objects.get(pk=details['project_pk'])
+            self.change_project(base.Project.objects.get(pk=details['project_pk']))
         self.creator = request.user
 
     @classmethod
             
     def summary(self, request=None):
         return {
-            'project': self.project.ref(request),
+            'project': ref( self.project ),
             'project_pk': self.project._get_pk_val(),
-            'sprint': self.sprint.ref(request),
+            'sprint': ref( self.sprint ),
             'sprint_pk': self.sprint._get_pk_val(),
-            'state': self.state.ref(request),
+            'state': ref( self.state ),
             'state_pk': self.state._get_pk_val(),
             'state_name': self.state.name,
             
             'creator': self.creator.username,
-            'parent': self.get_parent_ref(request),
+            'parent': ref( self.parent ),
             'created': self.created,
             'modified': self.modified,
             
             'description': self.description,
         }
-
+    
+    def change_sprint(self, sprint):
+        if (sprint == None):
+            sprint = self.project.sprint_set.get(slug='default')
+        self.sprint = sprint
+        self.project = self.sprint.project
+    
     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'])
+            self.project = base.Project.objects.resolve(details['project_pk'])
         if ('sprint_pk' in details and details['sprint_pk']):
-            self.sprint = Sprint.objects.get(pk=details['sprint_pk'])
+            self.change_sprint( Sprint.objects.resolve(details['sprint_pk']) )
         if ('state_pk' in details and details['state_pk']):
-            self.state = base.Tag.objects.get(slug=details['state_pk'])
+            self.state = base.Tag.objects.resolve(details['state_pk'])
         self.creator = request.user
 
     @classmethod
     
     def short(self, request):
         return {
-            'ticket': self.ticket.ref(request),
+            'ticket': ref( self.ticket ),
             'ticket_pk': self.ticket._get_pk_val(),
             'author': self.author.username,
             'created': format_date(self.created),