All of my work from commits: dd4a194, 692644a, 4a60203, 5de46bc, 152042d, 64a2d4e...
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Mon, 21 Mar 2011 15:39:05 +0000 (11:39 -0400)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Mon, 21 Mar 2011 15:39:05 +0000 (11:39 -0400)
45 files changed:
.gitmodules
README
README.markdown
contrib/gilbert/__init__.py
contrib/gilbert/admin.py [new file with mode: 0644]
contrib/gilbert/exceptions.py
contrib/gilbert/extdirect/__init__.py [new file with mode: 0644]
contrib/gilbert/extdirect/core.py [new file with mode: 0644]
contrib/gilbert/extdirect/forms.py [new file with mode: 0644]
contrib/gilbert/gilbert.py [new file with mode: 0644]
contrib/gilbert/media/gilbert/Gilbert.api.auth.js [deleted file]
contrib/gilbert/media/gilbert/Gilbert.lib.js [deleted file]
contrib/gilbert/media/gilbert/extjs
contrib/gilbert/media/gilbert/gilbert.js [new file with mode: 0644]
contrib/gilbert/media/gilbert/lib/app.js [new file with mode: 0644]
contrib/gilbert/media/gilbert/lib/models.js [new file with mode: 0644]
contrib/gilbert/media/gilbert/lib/plugins.js [new file with mode: 0644]
contrib/gilbert/media/gilbert/lib/ui/forms.js [new file with mode: 0644]
contrib/gilbert/media/gilbert/lib/ui/ui.js [new file with mode: 0644]
contrib/gilbert/media/gilbert/lib/ui/windows.js [new file with mode: 0644]
contrib/gilbert/media/gilbert/plugins/auth.js [new file with mode: 0644]
contrib/gilbert/media/gilbert/plugins/models.js [new file with mode: 0644]
contrib/gilbert/media/gilbert/superboxselect/SuperBoxSelect.js [new file with mode: 0755]
contrib/gilbert/media/gilbert/superboxselect/clear.png [new file with mode: 0755]
contrib/gilbert/media/gilbert/superboxselect/close.png [new file with mode: 0755]
contrib/gilbert/media/gilbert/superboxselect/expand.png [new file with mode: 0755]
contrib/gilbert/media/gilbert/superboxselect/superboxselect-gray-extend.css [new file with mode: 0755]
contrib/gilbert/media/gilbert/superboxselect/superboxselect.css [new file with mode: 0755]
contrib/gilbert/models.py [new file with mode: 0644]
contrib/gilbert/plugins.py [deleted file]
contrib/gilbert/plugins/__init__.py [new file with mode: 0644]
contrib/gilbert/plugins/auth.py [new file with mode: 0644]
contrib/gilbert/plugins/base.py [new file with mode: 0644]
contrib/gilbert/plugins/models.py [new file with mode: 0644]
contrib/gilbert/sites.py
contrib/gilbert/templates/gilbert/api.js [new file with mode: 0644]
contrib/gilbert/templates/gilbert/base.html [new file with mode: 0644]
contrib/gilbert/templates/gilbert/icons.css [new file with mode: 0644]
contrib/gilbert/templates/gilbert/index.html
contrib/gilbert/templates/gilbert/login.html [new file with mode: 0644]
contrib/gilbert/templates/gilbert/styles.css [deleted file]
contrib/penfield/gilbert.py [new file with mode: 0644]
forms/entities.py
gilbert.py [new file with mode: 0644]
hacks.py [new file with mode: 0644]

index d518e31..2eab002 100644 (file)
@@ -1,6 +1,6 @@
 [submodule "contrib/gilbert/media/gilbert/extjs"]
        path = contrib/gilbert/media/gilbert/extjs
-       url = git://github.com/probonogeek/extjs.git
+       url = git://git.ithinksw.org/extjs.git
 [submodule "contrib/gilbert/media/gilbert/fugue-icons"]
        path = contrib/gilbert/media/gilbert/fugue-icons
        url = git://git.ithinksw.org/fugue-icons.git
diff --git a/README b/README
index aec0c9c..6e067ca 100644 (file)
--- a/README
+++ b/README
@@ -3,11 +3,11 @@ Philo is a foundation for developing web content management systems.
 Prerequisites:
        * Python 2.5.4+ <http://www.python.org/>
        * Django 1.2+ <http://www.djangoproject.com/>
+       * django-mptt e734079+ <https://github.com/django-mptt/django-mptt/>
        * django-staticmedia 0.2+ <http://pypi.python.org/pypi/django-staticmedia/>
-       * django-mptt e734079+ <https://github.com/django-mptt/django-mptt/> 
        * (Optional) django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>
-       * (Optional) recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>
        * (Optional) south 0.7.2+ <http://south.aeracode.org/>
+       * (Optional) recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>
 
 To contribute, please visit the project website <http://philo.ithinksw.org/>. Feel free to join us on IRC at irc://irc.oftc.net/#philo.
 
@@ -22,3 +22,7 @@ After installing philo and mptt on your python path, make sure to complete the f
 4. Optionally add a root node to your current Site.
 
 Philo should be ready to go!
+
+If you are using philo.contrib.gilbert, you will additionally need to complete the following steps:
+
+1. add 'django.core.context_processors.request' to settings.TEMPLATE_CONTEXT_PROCESSORS
\ No newline at end of file
index 8060db8..ca9d00a 100644 (file)
@@ -5,11 +5,12 @@ Prerequisites:
  * [Python 2.5.4+ &lt;http://www.python.org&gt;](http://www.python.org/)
  * [Django 1.2+ &lt;http://www.djangoproject.com/&gt;](http://www.djangoproject.com/)
  * [django-mptt e734079+ &lt;https://github.com/django-mptt/django-mptt/&gt;](https://github.com/django-mptt/django-mptt/)
+ * [django-staticmedia 0.2+ &lt;http://pypi.python.org/pypi/django-staticmedia/&gt;](http://pypi.python.org/pypi/django-staticmedia/)
  * (Optional) [django-grappelli 2.0+ &lt;http://code.google.com/p/django-grappelli/&gt;](http://code.google.com/p/django-grappelli/)
  * (Optional) [south 0.7.2+ &lt;http://south.aeracode.org/)](http://south.aeracode.org/)
  * (Optional) [recaptcha-django r6 &lt;http://code.google.com/p/recaptcha-django/&gt;](http://code.google.com/p/recaptcha-django/)
 
-To contribute, please visit the [project website](http://philo.ithinksw.org/). Feel free to join us on IRC at [irc://irc.oftc.net/#philo](irc://irc.oftc.net/#philo>).
+To contribute, please visit the [project website &lt;http://philo.ithinksw.org/&lt;](http://philo.ithinksw.org/). Feel free to join us on IRC at [irc://irc.oftc.net/#philo](irc://irc.oftc.net/#philo).
 
 Using philo
 ===========
@@ -22,3 +23,7 @@ After installing philo and mptt on your python path, make sure to complete the f
 4. Optionally add a root node to your current Site.
 
 Philo should be ready to go!
+
+If you are using philo.contrib.gilbert, you will additionally need to complete the following steps:
+
+1. add 'django.core.context_processors.request' to settings.TEMPLATE_CONTEXT_PROCESSORS
\ No newline at end of file
index c55f250..de7dbc1 100644 (file)
@@ -13,12 +13,12 @@ def autodiscover():
        for app in settings.INSTALLED_APPS:
                mod = import_module(app)
                try:
-                       before_import_model_registry = copy.copy(site.model_registry)
-                       before_import_plugin_registry = copy.copy(site.plugin_registry)
+                       before_import_model_routers = copy.copy(site.model_routers)
+                       before_import_core_router = copy.copy(site.core_router)
                        import_module('%s.gilbert' % app)
                except:
-                       site.model_registry = before_import_model_registry
-                       site.plugin_registry = before_import_plugin_registry
+                       site.model_routers = before_import_model_routers
+                       site.core_router = before_import_core_router
                        
                        if module_has_submodule(mod, 'gilbert'):
                                raise
\ No newline at end of file
diff --git a/contrib/gilbert/admin.py b/contrib/gilbert/admin.py
new file mode 100644 (file)
index 0000000..af46cb8
--- /dev/null
@@ -0,0 +1,4 @@
+from django.contrib.admin import site
+from .models import UserPreferences
+
+site.register(UserPreferences)
\ No newline at end of file
index e96ba25..6c4fe17 100644 (file)
@@ -3,4 +3,24 @@ class AlreadyRegistered(Exception):
 
 
 class NotRegistered(Exception):
+       pass
+
+
+class ExtException(Exception):
+       """ Base class for all Ext.Direct-related exceptions. """
+       pass
+
+
+class InvalidExtMethod(ExtException):
+       """ Indicate that a function cannot be an Ext.Direct method. """
+       pass
+
+
+class NotExtAction(ExtException):
+       """ Indicate that an object is not an Ext.Direct action. """
+       pass
+
+
+class NotExtMethod(ExtException):
+       """ Indicate that a function is not an Ext.Direct method. """
        pass
\ No newline at end of file
diff --git a/contrib/gilbert/extdirect/__init__.py b/contrib/gilbert/extdirect/__init__.py
new file mode 100644 (file)
index 0000000..ff48f94
--- /dev/null
@@ -0,0 +1,2 @@
+from .core import *
+from .forms import *
\ No newline at end of file
diff --git a/contrib/gilbert/extdirect/core.py b/contrib/gilbert/extdirect/core.py
new file mode 100644 (file)
index 0000000..42c24c0
--- /dev/null
@@ -0,0 +1,637 @@
+import sys
+import datetime
+from inspect import isclass, ismethod, isfunction, getmembers, getargspec
+from traceback import format_tb
+from abc import ABCMeta, abstractproperty
+from collections import Callable, Sized, Mapping
+from django.utils import simplejson as json
+from django.views.debug import ExceptionReporter
+from django.http import HttpResponse
+from django.db.models import Q
+
+
+# __all__ = ('ext_action', 'ext_method', 'is_ext_action', 'is_ext_method', 'ExtAction', 'ExtMethod')
+
+
+class ExtRequest(object):
+       """
+       Represents a single Ext.Direct request along with the :class:`django.http.HttpRequest` it originates from.
+       
+       .. note::
+               
+               Passes undefined attribute accesses through to the underlying :class:`django.http.HttpRequest`.
+       
+       """
+       
+       @classmethod
+       def parse(cls, request, object_hook=None):
+               """
+               Parses Ext.Direct request(s) from the originating HTTP request.
+               
+               :arg request: the originating HTTP request
+               :type request: :class:`django.http.HttpRequest`
+               :returns: list of :class:`ExtRequest` instances
+               
+               """
+               
+               requests = []
+               
+               if request.META['CONTENT_TYPE'].startswith('application/x-www-form-urlencoded') or request.META['CONTENT_TYPE'].startswith('multipart/form-data'):
+                       requests.append(cls(request,
+                               type = request.POST.get('extType'),
+                               tid = request.POST.get('extTID'),
+                               action = request.POST.get('extAction'),
+                               method = request.POST.get('extMethod'),
+                               data = request.POST.get('extData', None),
+                               upload = True if request.POST.get('extUpload', False) in (True, 'true', 'True') else False,
+                               form_request = True,
+                       ))
+               else:
+                       decoded_requests = json.loads(request.raw_post_data, object_hook=object_hook)
+                       if type(decoded_requests) is dict:
+                               decoded_requests = [decoded_requests]
+                       for inner_request in decoded_requests:
+                               requests.append(cls(request,
+                                       type = inner_request.get('type'),
+                                       tid = inner_request.get('tid'),
+                                       action = inner_request.get('action'),
+                                       method = inner_request.get('method'),
+                                       data = inner_request.get('data', None),
+                               ))
+               
+               return requests
+       
+       def __init__(self, request, type, tid, action, method, data, upload=False, form_request=False):
+               """
+               :arg request: the originating HTTP request
+               :type request: :class:`django.http.HttpRequest`
+               :arg type: Ext.Direct request type
+               :type type: str
+               :arg tid: Ext.Direct transaction identifier
+               :type tid: str
+               :arg action: Ext.Direct action name
+               :type action: str
+               :arg method: Ext.Direct method name
+               :type method: str
+               :arg data: Ext.Direct method arguments
+               :type data: list
+               :arg upload: request includes uploaded file(s)
+               :type upload: bool
+               :arg form_request: request made by form submission
+               :type form_request: bool
+               
+               """
+               
+               self.type = type
+               self.tid = tid
+               self.request = request
+               self.action = action
+               self.method = method
+               self.data = data if data is not None else []
+               self.upload = upload
+               self.form_request = form_request
+       
+       def __getattr__(self, key):
+               try:
+                       return getattr(self.request, key)
+               except:
+                       raise AttributeError
+
+
+class ExtMethod(Callable, Sized):
+       """
+       Wraps a (previously :meth:`decorated <ExtMethod.decorate>`) function as an Ext.Direct method.
+       
+       """
+       
+       @classmethod
+       def decorate(cls, function=None, name=None, form_handler=False):
+               """
+               Applies metadata to function identifying it as wrappable, or returns a decorator for doing the same::
+               
+                       @ExtMethod.decorate
+                       def method(self, request):
+                               pass
+                       
+                       @ExtMethod.decorate(name='custom_name', form_handler=True)
+                       def form_handler_with_custom_name(self, request):
+                               pass
+               
+               Intended for use on methods of classes already decorated by :meth:`ExtAction.decorate`.
+               
+               :arg name: custom Ext.Direct method name
+               :type name: str
+               :arg form_handler: function handles form submissions
+               :type form_handler: bool
+               :returns: function with metadata applied
+               
+               """
+               
+               def setter(function):
+                       setattr(function, '_ext_method', True)
+                       setattr(function, '_ext_method_form_handler', form_handler)
+                       if name is not None:
+                               setattr(function, '_ext_method_name', name)
+                       return function
+               if function is not None:
+                       return setter(function)
+               return setter
+       
+       @classmethod
+       def validate(cls, function):
+               """
+               Validates that function has been :meth:`decorated <ExtMethod.decorate>` and is therefore wrappable.
+               
+               """
+               
+               return getattr(function, '_ext_method', False)
+       
+       def __init__(self, function):
+               """
+               :arg function: function to wrap
+               :type function: callable
+               
+               If the function accepts variable positional arguments, the Ext.Direct method argument count will be increased by one for acceptance of a list.
+               
+               Similarly, if the function accepts variable keyword arguments, the argument count will be increased by one for acceptance of a dictionary.
+               
+               .. warning::
+                       
+                       Wrapped functions **must** accept at least one positional argument (in addition to self if function is a method): the :class:`ExtRequest` that caused the invocation.
+               
+               .. warning::
+                       
+                       Wrapped functions identified as handling form submissions **must** return a tuple containing:
+                       
+                       * a boolean indicating success or failure
+                       * a dictionary of fields mapped to errors, if any, or None
+               
+               """
+               self.function = function
+               self.form_handler = getattr(function, '_ext_method_form_handler', False)
+               self.name = getattr(function, '_ext_method_name', function.__name__)
+               
+               argspec = getargspec(self.function)
+               len_ = len(argspec.args)
+               
+               if len_ >= 2 and ismethod(self.function):
+                       len_ -= 2
+               elif len_ >= 1 and not ismethod(self.function):
+                       len_ -= 1
+               else:
+                       raise TypeError('%s cannot be wrapped as an Ext.Direct method as it does not take an ExtRequest as its first positional argument')
+               
+               if argspec.varargs is not None:
+                       len_ += 1
+                       self.accepts_varargs = True
+               else:
+                       self.accepts_varargs = False
+               
+               if argspec.keywords is not None:
+                       len_ += 1
+                       self.accepts_keywords = True
+               else:
+                       self.accepts_keywords = False
+               
+               self.len = len_
+       
+       @property
+       def spec(self):
+               return {
+                       'name': self.name,
+                       'len': self.len,
+                       'formHandler': self.form_handler
+               }
+       
+       def __len__(self):
+               return self.len
+       
+       def __call__(self, request):
+               """
+               Invoke the wrapped function using the provided :class:`ExtRequest` and return the raw result.
+               
+               :arg request: the :class:`ExtRequest`
+               
+               :raises TypeError: the request did not provide the required number of arguments
+               :raises Exception: the (form handling) function did not return a valid result for a form submission request
+               
+               """
+               
+               args = request.data
+               args_len = len(args)
+               
+               if args_len != self.len:
+                       raise TypeError('%s takes exactly %i arguments (%i given)' % (self.name, self.len, args_len))
+               
+               keywords = {}
+               if self.accepts_keywords:
+                       keywords = args.pop()
+               
+               varargs = []
+               if self.accepts_varargs:
+                       varargs = args.pop()
+               
+               result = self.function(request, *(args + varargs), **keywords)
+               
+               if self.form_handler:
+                       try:
+                               new_result = {
+                                       'success': result[0],
+                                       'errors': result[1],
+                               }
+                               if len(result) > 2:
+                                       new_result['pk'] = result[2]
+                               
+                               if new_result['success']:
+                                       del new_result['errors']
+                               
+                               result = new_result
+                       except:
+                               raise Exception # pick a better one
+               
+               return result
+
+
+ext_method = ExtMethod.decorate
+"""
+Convenience alias for :meth:`ExtMethod.decorate`.
+
+"""
+
+
+is_ext_method = ExtMethod.validate
+"""
+Convenience alias for :meth:`ExtMethod.validate`.
+
+"""
+
+
+class ExtAction(Callable, Mapping):
+       """
+       Wraps a (previously :meth:`decorated <ExtAction.decorate>`) object as an Ext.Direct action.
+       
+       """
+       
+       method_class = ExtMethod
+       """
+       The :class:`ExtMethod` subclass used when wrapping the wrapped object's members.
+       
+       """
+       
+       @classmethod
+       def decorate(cls, obj=None, name=None):
+               """
+               Applies metadata to obj identifying it as wrappable, or returns a decorator for doing the same::
+               
+                       @ExtAction.decorate
+                       class MyAction(object):
+                               pass
+                       
+                       @ExtAction.decorate(name='GoodAction')
+                       class BadAction(object):
+                               pass
+               
+               Intended for use on classes with member functions (methods) already decorated with :meth:`ExtMethod.decorate`.
+               
+               :arg name: custom Ext.Direct action name
+               :type name: str
+               :returns: obj with metadata applied
+               
+               """
+               
+               def setter(obj):
+                       setattr(obj, '_ext_action', True)
+                       if name is not None:
+                               setattr(obj, '_ext_action_name', name)
+                       return obj
+               if obj is not None:
+                       return setter(obj)
+               return setter
+       
+       @classmethod
+       def validate(cls, obj):
+               """
+               Validates that obj has been :meth:`decorated <ExtAction.decorate>` and is therefore wrappable.
+               
+               """
+               
+               return getattr(obj, '_ext_action', False)
+       
+       def __init__(self, obj):
+               self.obj = obj
+               self.name = getattr(obj, '_ext_action_name', obj.__name__ if isclass(obj) else obj.__class__.__name__)
+               self._methods = None
+       
+       @property
+       def methods(self):
+               if not self._methods:
+                       self._methods = dict((method.name, method) for method in (self.method_class(member) for name, member in getmembers(self.obj, self.method_class.validate)))
+               return self._methods
+       
+       def __len__(self):
+               return len(self.methods)
+       
+       def __iter__(self):
+               return iter(self.methods)
+       
+       def __getitem__(self, name):
+               return self.methods[name]
+               
+       @property
+       def spec(self):
+               """
+               Returns a tuple containing:
+                       
+                       * the action name
+                       * a list of :class:`method specs <ExtMethod.spec>`
+               
+               Used internally by :class:`providers <ExtProvider>` to construct an Ext.Direct provider spec.
+               
+               """
+               
+               return self.name, list(method.spec for method in self.itervalues())
+       
+       def __call__(self, request):
+               return self[request.method](request)
+
+
+ext_action = ExtAction.decorate
+"""
+Convenience alias for :meth:`ExtAction.decorate`.
+
+"""
+
+
+is_ext_action = ExtAction.validate
+"""
+Convenience alias for :meth:`ExtAction.validate`.
+
+"""
+
+
+class ExtResponse(object):
+       """
+       Abstract base class for responses to :class:`requests <ExtRequest>`.
+       
+       """
+       __metaclass__ = ABCMeta
+       
+       @abstractproperty
+       def as_ext(self):
+               raise NotImplementedError
+
+
+class ExtResult(ExtResponse):
+       """
+       Represents a successful response to a :class:`request <ExtRequest>`.
+       
+       """
+       
+       def __init__(self, request, result):
+               """
+               :arg request: the originating Ext.Direct request
+               :type request: :class:`ExtRequest`
+               :arg result: the raw result
+               
+               """
+               self.request = request
+               self.result = result
+       
+       @property
+       def as_ext(self):
+               return {
+                       'type': self.request.type,
+                       'tid': self.request.tid,
+                       'action': self.request.action,
+                       'method': self.request.method,
+                       'result': self.result
+               }
+
+
+class ExtException(ExtResponse):
+       """
+       Represents an exception raised by an unsuccessful response to a :class:`request <ExtRequest>`.
+       
+       .. warning::
+               
+               If :data:`django.conf.settings.DEBUG` is True, information about which exception was raised and where it was raised (including a traceback in both plain text and HTML) will be provided to the client.
+       
+       """
+       
+       def __init__(self, request, exc_info):
+               """
+               
+               """
+               self.request = request
+               self.exc_info = exc_info
+       
+       @property
+       def as_ext(self):
+               from django.conf import settings
+               if settings.DEBUG:
+                       reporter = ExceptionReporter(self.request.request, *self.exc_info)
+                       return {
+                               'type': 'exception',
+                               'tid': self.request.tid,
+                               'message': '%s: %s' % (self.exc_info[0], self.exc_info[1]),
+                               'where': format_tb(self.exc_info[2])[0],
+                               'identifier': '%s.%s' % (self.exc_info[0].__module__, self.exc_info[0].__name__),
+                               'html': reporter.get_traceback_html()
+                       }
+               else:
+                       return {
+                               'type': 'exception',
+                               'tid': self.request.tid
+                       }
+
+
+class ExtProvider(Callable, Mapping):
+       """
+       Abstract base class for Ext.Direct provider implementations.
+       
+       """
+       
+       __metaclass__ = ABCMeta
+       
+       result_class = ExtResult
+       """
+       The :class:`ExtResponse` subclass used to represent the results of a successful Ext.Direct method invocation.
+       
+       """
+       
+       exception_class = ExtException
+       """
+       The :class:`ExtResponse` subclass used to represent the exception raised during an unsuccessful Ext.Direct method invocation.
+       
+       """
+       
+       @abstractproperty
+       def namespace(self):
+               """
+               The Ext.Direct provider namespace.
+               
+               """
+               raise NotImplementedError
+       
+       @abstractproperty
+       def url(self):
+               """
+               The Ext.Direct provider url.
+               
+               """
+               raise NotImplementedError
+       
+       @abstractproperty
+       def type(self):
+               """
+               The Ext.Direct provider type.
+               
+               """
+               raise NotImplementedError
+       
+       @abstractproperty
+       def actions(self):
+               """
+               The dictionary of action names and :class:`ExtAction` instances handled by the provider.
+               
+               """
+               raise NotImplementedError
+       
+       def __len__(self):
+               return len(self.actions)
+       
+       def __iter__(self):
+               return iter(self.actions)
+       
+       def __getitem__(self, name):
+               return self.actions[name]
+       
+       @property
+       def spec(self):
+               return {
+                       'namespace': self.namespace,
+                       'url': self.url,
+                       'type': self.type,
+                       'actions': dict(action.spec for action in self.itervalues())
+               }
+       
+       def __call__(self, request):
+               """
+               Returns a :class:`response <ExtResponse>` to the :class:`request <ExtRequest>`.
+               
+               """
+               try:
+                       return self.result_class(request=request, result=self[request.action](request))
+               except Exception:
+                       return self.exception_class(request=request, exc_info=sys.exc_info())
+
+
+class ExtRouter(ExtProvider):
+       """
+       A :class:`provider <ExtProvider>` base class with an implementation capable of handling the complete round-trip from a :class:`django.http.HttpRequest` to a :class:`django.http.HttpResponse`.
+       
+       """
+       
+       __metaclass__ = ABCMeta
+       
+       request_class = ExtRequest
+       """
+       The :class:`ExtRequest` subclass used parse and to represent the individual Ext.Direct requests within a :class:`django.http.HttpRequest`.
+       
+       """
+       
+       @classmethod
+       def json_object_hook(cls, obj):
+               if obj.get('q_object', False):
+                       return Q._new_instance(obj['children'], obj['connector'], obj['negated'])
+               return obj
+       
+       @classmethod
+       def json_default(cls, obj):
+               from django.forms.models import ModelChoiceIterator
+               from django.db.models.query import ValuesListQuerySet
+               from django.utils.functional import Promise
+               
+               if isinstance(obj, ExtResponse):
+                       return obj.as_ext
+               elif isinstance(obj, datetime.datetime):
+                       obj = obj.replace(microsecond=0)
+                       return obj.isoformat(' ')
+               elif isinstance(obj, ModelChoiceIterator) or isinstance(obj, ValuesListQuerySet):
+                       return list(obj)
+               elif isinstance(obj, Promise):
+                       return unicode(obj)
+               elif isinstance(obj, Q):
+                       return {
+                               'q_object': True,
+                               'connector': obj.connector,
+                               'negated': obj.negated,
+                               'children': obj.children
+                       }
+               else:
+                       raise TypeError, 'Object of type %s with value of %s is not JSON serializable' % (type(obj), repr(obj))
+       
+       def render_to_response(self, request):
+               """
+               Returns a :class:`django.http.HttpResponse` containing the :class:`response(s) <ExtResponse>` to the :class:`request(s) <ExtRequest>` in the provided :class:`django.http.HttpRequest`.
+               
+               """
+               
+               from django.conf import settings
+               
+               requests = self.request_class.parse(request, object_hook=self.json_object_hook)
+               responses = []
+               html_response = False
+               
+               for request in requests:
+                       if request.form_request and request.upload:
+                               html_response = True
+                       responses.append(self(request))
+               
+               response = responses[0] if len(responses) == 1 else responses
+               json_response = json.dumps(responses, default=self.json_default)
+               
+               if html_response:
+                       return HttpResponse('<html><body><textarea>%s</textarea></body></html>' % json_response)
+               return HttpResponse(json_response, content_type='application/json; charset=%s' % settings.DEFAULT_CHARSET)
+
+
+class SimpleExtRouter(ExtRouter):
+       """
+       A simple concrete :class:`router <ExtRouter>` implementation.
+       
+       """
+       
+       def __init__(self, namespace, url, actions=None, type='remoting'):
+               """
+               :arg namespace: the Ext.Direct provider namespace
+               :type namespace: str
+               :arg url: the Ext.Direct provider url
+               :type url: str
+               :arg actions: the dictionary of action names and :class:`ExtAction` instances handled by the provider
+               :type actions: dict
+               :arg type: the Ext.Direct provider type
+               :type type: str
+               
+               """
+               
+               self._type = type
+               self._namespace = namespace
+               self._url = url
+               self._actions = actions if actions is not None else {}
+       
+       @property
+       def namespace(self):
+               return self._namespace
+       
+       @property
+       def url(self):
+               return self._url
+       
+       @property
+       def type(self):
+               return self._type
+       
+       @property
+       def actions(self):
+               return self._actions
\ No newline at end of file
diff --git a/contrib/gilbert/extdirect/forms.py b/contrib/gilbert/extdirect/forms.py
new file mode 100644 (file)
index 0000000..ca757c3
--- /dev/null
@@ -0,0 +1,305 @@
+import os.path
+from django.forms.widgets import Widget, TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, Textarea, DateInput, DateTimeInput, TimeInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, RadioSelect, CheckboxSelectMultiple, MultiWidget, SplitHiddenDateTimeWidget
+from django.forms.forms import BaseForm, BoundField
+from django.forms.fields import FileField
+from django.forms.models import ModelForm, ModelChoiceField, ModelMultipleChoiceField
+from django.db.models import ForeignKey, ManyToManyField, Q
+from django.utils.encoding import force_unicode
+from philo.utils import ContentTypeLimiter
+from philo.hacks import Category
+
+
+# The "categories" in this module are listed in reverse order, because I wasn't able to ensure that they'd all take effect otherwise...
+
+
+#still to do: SplitHiddenDateTimeWidget
+
+
+class MultiWidget(MultiWidget):
+       __metaclass__ = Category
+       
+       def render_extdirect(self, name, data):
+               if not isinstance(data, list):
+                       data = self.decompress(data)
+               
+               specs = []
+               for i, widget in enumerate(self.widgets):
+                       try:
+                               widget_data = data[i]
+                       except IndexError:
+                               widget_data = None
+                       specs.extend(widget.render_extdirect(name + '_%s' % i, widget_data))
+               return specs
+
+
+#still to do: RadioSelect, CheckboxSelectMultiple
+
+
+class SelectMultiple(SelectMultiple):
+       __metaclass__ = Category
+       extdirect_xtype = 'superboxselect'
+       
+       def render_extdirect(self, name, data):
+               if self.choices:
+                       store = [choice for choice in self.choices if choice[0]]
+               else:
+                       store = []
+               spec = {
+                       'name': name,
+                       'value': data,
+                       'store': store,
+                       'xtype': self.extdirect_xtype,
+                       'forceFormValue': False
+               }
+               if not spec['value']:
+                       del spec['value']
+               return [spec]
+
+
+class NullBooleanSelect(NullBooleanSelect):
+       __metaclass__ = Category
+       
+       def render_extdirect(self, name, data):
+               try:
+                       data = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[data]
+               except KeyError:
+                       data = u'1'
+               return super(NullBooleanSelect, self).render_extdirect(name, data)
+
+
+class Select(Select):
+       __metaclass__ = Category
+       extdirect_xtype = 'combo'
+       
+       def render_extdirect(self, name, data):
+               if self.choices:
+                       store = [choice for choice in self.choices if choice[0]]
+               else:
+                       store = []
+               spec = {
+                       'hiddenName': name,
+                       'hiddenValue': data,
+                       'value': data,
+                       'xtype': self.extdirect_xtype,
+                       'store': store,
+                       'editable': False,
+                       'disableKeyFilter': True,
+                       'forceSelection': True,
+                       'triggerAction': 'all',
+               }
+               if not spec['value']:
+                       del spec['value']
+               return [spec]
+
+
+class CheckboxInput(CheckboxInput):
+       __metaclass__ = Category
+       extdirect_xtype = 'checkbox'
+       
+       def render_extdirect(self, name, data):
+               data = bool(data)
+               specs = super(CheckboxInput, self).render_extdirect(name, data)
+               specs[0]['checked'] = data
+               return specs
+
+
+class DateTimeInput(DateTimeInput):
+       __metaclass__ = Category
+       extdirect_xtype = 'gilbertdatetimefield'
+
+
+class TimeInput(TimeInput):
+       __metaclass__ = Category
+       extdirect_xtype = 'timefield'
+
+
+class DateInput(DateInput):
+       __metaclass__ = Category
+       extdirect_xtype = 'datefield'
+
+
+class Textarea(Textarea):
+       __metaclass__ = Category
+       extdirect_xtype = 'textarea'
+
+
+class FileInput(FileInput):
+       __metaclass__ = Category
+       extdirect_xtype = 'fileuploadfield'
+       
+       def render_extdirect(self, name, data):
+               if data is not None:
+                       data = os.path.split(data.name)[1]
+               return super(FileInput, self).render_extdirect(name, data)
+
+
+class MultipleHiddenInput(MultipleHiddenInput):
+       __metaclass__ = Category
+       extdirect_xtype = 'hidden'
+       
+       def render_extdirect(self, name, data):
+               if data is None:
+                       data = []
+               return [specs.extend(super(MultipleHiddenInput, self).render_extdirect(name, data)) for datum in data]
+
+
+class HiddenInput(HiddenInput):
+       __metaclass__ = Category
+       extdirect_xtype = 'hidden'
+
+
+class PasswordInput(PasswordInput):
+       __metaclass__ = Category
+       extdirect_xtype = 'textfield'
+       
+       def render_extdirect(self, name, data):
+               specs = super(PasswordInput, self).render_extdirect(name, data)
+               specs[0]['inputType'] = self.input_type
+               return specs
+
+
+class TextInput(TextInput):
+       __metaclass__ = Category
+       extdirect_xtype = 'textfield'
+
+
+class Widget(Widget):
+       __metaclass__ = Category
+       extdirect_xtype = None
+       
+       def render_extdirect(self, name, data):
+               if not self.extdirect_xtype:
+                       raise NotImplementedError
+               spec = {
+                       'name': name,
+                       'value': data,
+                       'xtype': self.extdirect_xtype
+               }
+               if not spec['value']:
+                       del spec['value']
+               return [spec]
+
+
+class BoundField(BoundField):
+       __metaclass__ = Category
+       
+       def as_hidden_extdirect(self, only_initial=False):
+               return self.as_widget_extdirect(self.field.hidden_widget(), only_initial)
+       
+       def as_widget_extdirect(self, widget=None, only_initial=False):
+               if not widget:
+                       widget = self.field.widget
+                       standard_widget = True
+               else:
+                       standard_widget = False
+               
+               if not self.form.is_bound:
+                       data = self.form.initial.get(self.name, self.field.initial)
+                       if callable(data):
+                               data = data()
+               else:
+                       if isinstance(self.field, FileField) and self.data is None:
+                               data = self.form.initial.get(self.name, self.field.initial)
+                       else:
+                               data = self.data
+               data = self.field.prepare_value(data)
+               
+               if not only_initial:
+                       name = self.html_name
+               else:
+                       name = self.html_initial_name
+               
+               specs = widget.render_extdirect(name, data)
+               
+               if standard_widget and isinstance(self.field, ModelChoiceField):
+                       limit_choices_to = None
+                       
+                       if isinstance(self.form, ModelForm):
+                               model = self.form._meta.model
+                               model_fields = model._meta.fields + model._meta.many_to_many
+                               
+                               for model_field in model_fields:
+                                       if model_field.name == self.name and (isinstance(model_field, ForeignKey) or isinstance(model_field, ManyToManyField)):
+                                               limit_choices_to = model_field.rel.limit_choices_to
+                                               if limit_choices_to is None:
+                                                       limit_choices_to = {}
+                                               elif isinstance(limit_choices_to, ContentTypeLimiter):
+                                                       limit_choices_to = limit_choices_to.q_object()
+                                               elif not isinstance(limit_choices_to, dict):
+                                                       limit_choices_to = None # can't support other objects with add_to_query methods
+                                               break
+                       
+                       if limit_choices_to is not None:
+                               specs[0]['model_filters'] = limit_choices_to
+                       else:
+                               specs[0]['model_filters'] = {
+                                       'pk__in': self.field.queryset.values_list('pk', flat=True)
+                               }
+                               
+                       specs[0]['model_app_label'] = self.field.queryset.model._meta.app_label
+                       specs[0]['model_name'] = self.field.queryset.model._meta.object_name
+                       
+                       if isinstance(self.field, ModelMultipleChoiceField):
+                               specs[0]['xtype'] = 'gilbertmodelmultiplechoicefield'
+                       else:
+                               specs[0]['xtype'] = 'gilbertmodelchoicefield'
+                               specs[0]['backup_store'] = specs[0]['store']
+                               del specs[0]['store']
+               
+               return specs
+                       
+       def as_extdirect(self):
+               if self.field.show_hidden_initial:
+                       return self.as_widget_extdirect() + self.as_hidden_extdirect(only_initial=True)
+               return self.as_widget_extdirect()
+
+
+class BaseForm(BaseForm):
+       __metaclass__ = Category
+       
+       def as_extdirect(self):
+               fields = []
+               
+               for bound_field in self:
+                       if bound_field.label:
+                               label = bound_field.label
+                       else:
+                               label = ''
+                       
+                       if bound_field.field.help_text:
+                               help_text = bound_field.field.help_text
+                       else:
+                               help_text = ''
+                       
+                       specs = bound_field.as_extdirect()
+                       
+                       if len(specs) < 1:
+                               continue
+                       
+                       if len(specs) > 1:
+                               specs = [{
+                                       'xtype': 'compositefield',
+                                       'items': specs
+                               }]
+                       
+                       if label:
+                               specs[0]['fieldLabel'] = label
+                       if help_text:
+                               specs[0]['help_text'] = help_text
+                       
+                       fields.extend(specs)
+               
+               if isinstance(self, ModelForm):
+                       pk = self.instance.pk
+                       if pk is not None:
+                               fields.append({
+                                       'name': 'pk',
+                                       'value': pk,
+                                       'xtype': 'hidden'
+                               })
+               
+               return {
+                       'items': fields,
+                       'labelSeparator': self.label_suffix,
+                       'fileUpload': self.is_multipart()
+               }
\ No newline at end of file
diff --git a/contrib/gilbert/gilbert.py b/contrib/gilbert/gilbert.py
new file mode 100644 (file)
index 0000000..402289f
--- /dev/null
@@ -0,0 +1,12 @@
+from . import site
+from django.contrib.auth.models import User, Group
+
+
+site.register_model(User, icon_name='user')
+site.register_model(Group, icon_name='users')
+
+
+from django.contrib.contenttypes.models import ContentType
+
+
+site.register_model(ContentType)
\ No newline at end of file
diff --git a/contrib/gilbert/media/gilbert/Gilbert.api.auth.js b/contrib/gilbert/media/gilbert/Gilbert.api.auth.js
deleted file mode 100644 (file)
index dc0a829..0000000
+++ /dev/null
@@ -1,128 +0,0 @@
-GILBERT_PLUGINS.push(new (function() {
-       return {
-               init: function(application) {
-                       if (GILBERT_LOGGED_IN) {
-                               application.on('ready', this.addUserMenu, this, {
-                                       single: true,
-                               });
-                       } else {
-                               application.on('ready', this.showLoginWindow, this, {
-                                       single: true,
-                               });
-                       }
-               },
-               addUserMenu: function(application) {
-                       Gilbert.api.auth.whoami(function(result) {
-                               application.mainmenu.add({
-                                       xtype: 'tbfill',
-                               },{
-                                       xtype: 'tbseparator',
-                               },{
-                                       xtype: 'button',
-                                       iconCls: 'user-silhouette',
-                                       text: '<span style="font-weight: bolder;">' + result + '</span>',
-                                       menu: [{
-                                               text: 'Change password',
-                                               iconCls: 'key--pencil',
-                                               handler: function(button, event) {
-                                                       Gilbert.api.auth.get_passwd_form(function(formspec) {
-                                                               var change_password_window = application.createWindow({
-                                                                       layout: 'fit',
-                                                                       resizable: false,
-                                                                       title: 'Change password',
-                                                                       iconCls: 'key--pencil',
-                                                                       width: 266,
-                                                                       height: 200,
-                                                                       items: change_password_form = new Ext.FormPanel(Ext.applyIf({
-                                                                               frame: true,
-                                                                               bodyStyle: 'padding: 5px 5px 0',
-                                                                               buttons: [{
-                                                                                       text: 'Change password',
-                                                                                       iconCls: 'key--pencil',
-                                                                                       handler: function(button, event) {
-                                                                                               change_password_form.getForm().submit({
-                                                                                                       success: function(form, action) {
-                                                                                                               Ext.MessageBox.alert('Password changed', 'Your password has been changed.');
-                                                                                                       },
-                                                                                               });
-                                                                                       },
-                                                                               }],
-                                                                               api: {
-                                                                                       submit: Gilbert.api.auth.submit_passwd_form,
-                                                                               },
-                                                                       }, formspec))
-                                                               });
-                                                               change_password_window.show();
-                                                       });
-                                                       
-                                               },
-                                       },{
-                                               text: 'Log out',
-                                               iconCls: 'door-open-out',
-                                               handler: function(button, event) {
-                                                       Gilbert.api.auth.logout(function(result) {
-                                                               if (result) {
-                                                                       document.location.reload();
-                                                               } else {
-                                                                       Ext.MessageBox.alert('Log out failed', 'You have <strong>not</strong> been logged out. This could mean that your connection with the server has been severed. Please try again.');
-                                                               }
-                                                       })
-                                               }
-                                       }],
-                               });
-                               application.doLayout();
-                       });
-               },
-               showLoginWindow: function(application) {
-                       application.mainmenu.hide();
-                       application.doLayout();
-                       var login_window = application.createWindow({
-                               header: false,
-                               closable: false,
-                               resizable: false,
-                               draggable: false,
-                               width: 266,
-                               height: 135,
-                               layout: 'fit',
-                               items: login_form = new Ext.FormPanel({
-                                       frame: true,
-                                       bodyStyle: 'padding: 5px 5px 0',
-                                       items: [
-                                               {
-                                                       fieldLabel: 'Username',
-                                                       name: 'username',
-                                                       xtype: 'textfield',
-                                               },
-                                               {
-                                                       fieldLabel: 'Password',
-                                                       name: 'password',
-                                                       xtype: 'textfield',
-                                                       inputType: 'password',
-                                               }
-                                       ],
-                                       buttons: [
-                                               {
-                                                       text: 'Log in',
-                                                       iconCls: 'door-open-in',
-                                                       handler: function(button, event) {
-                                                               var the_form = login_form.getForm().el.dom;
-                                                               var username = the_form[0].value;
-                                                               var password = the_form[1].value;
-                                                               Gilbert.api.auth.login(username, password, function(result) {
-                                                                       if (result) {
-                                                                               document.location.reload();
-                                                                       } else {
-                                                                               Ext.MessageBox.alert('Log in failed', 'Unable to authenticate using the credentials provided. Please try again.', function() {
-                                                                                       login_form.getForm().reset();
-                                                                               });
-                                                                       }
-                                                               });
-                                                       }
-                                               }
-                                       ],
-                               }),
-                       });
-                       login_window.show();
-               },
-       }
-})());
\ No newline at end of file
diff --git a/contrib/gilbert/media/gilbert/Gilbert.lib.js b/contrib/gilbert/media/gilbert/Gilbert.lib.js
deleted file mode 100644 (file)
index 02bdb7b..0000000
+++ /dev/null
@@ -1,83 +0,0 @@
-Ext.ns('Gilbert.lib');
-
-Gilbert.lib.Desktop = Ext.extend(Ext.Panel, {
-       constructor: function(config) {
-               Gilbert.lib.Desktop.superclass.constructor.call(this, Ext.applyIf(config||{}, {
-                       region: 'center',
-                       border: false,
-                       padding: '5',
-                       bodyStyle: 'background: none;',
-               }));
-       },
-});
-
-Gilbert.lib.MainMenu = Ext.extend(Ext.Toolbar, {
-       constructor: function(application) {
-               var application = this.application = application;
-               Gilbert.lib.MainMenu.superclass.constructor.call(this, {
-                       region: 'north',
-                       autoHeight: true,
-               });
-       },
-});
-
-Gilbert.lib.Application = Ext.extend(Ext.util.Observable, {
-       constructor: function(config) {
-               Ext.apply(this, config, {
-                       renderTo: Ext.getBody(),
-                       plugins: [],
-                       
-               });
-               Gilbert.lib.Application.superclass.constructor.call(this);
-               this.addEvents({
-                       'ready': true,
-               });
-               this.init();
-       },
-       init: function() {
-               Ext.QuickTips.init();
-               
-               var desktop = this.desktop = new Gilbert.lib.Desktop();
-               var mainmenu = this.mainmenu = new Gilbert.lib.MainMenu(this);
-               var viewport = this.viewport = new Ext.Viewport({
-                       renderTo: this.renderTo,
-                       layout: 'border',
-                       items: [
-                               this.mainmenu,
-                               this.desktop,
-                       ],
-               });
-               var windows = this.windows = new Ext.WindowGroup();
-               
-               if (this.plugins) {
-                       if (Ext.isArray(this.plugins)) {
-                               for (var i = 0; i < this.plugins.length; i++) {
-                                       this.plugins[i] = this.initPlugin(this.plugins[i]);
-                               }
-                       } else {
-                               this.plugins = this.initPlugin(this.plugins);
-                       }
-               }
-               
-               this.doLayout();
-               
-               this.fireEvent('ready', this);
-       },
-       initPlugin: function(plugin) {
-               plugin.init(this);
-               return plugin;
-       },
-       createWindow: function(config, cls) {
-               var win = new(cls||Ext.Window)(Ext.applyIf(config||{},{
-                       renderTo: this.desktop.el,
-                       manager: this.windows,
-                       constrainHeader: true,
-               }));
-               win.render(this.desktop.el);
-               return win;
-       },
-       doLayout: function() {
-               this.mainmenu.doLayout();
-               this.viewport.doLayout();
-       }
-});
\ No newline at end of file
index 530ef4b..e9397f9 160000 (submodule)
@@ -1 +1 @@
-Subproject commit 530ef4b6c5b943cfa68b779d11cf7de29aa878bf
+Subproject commit e9397f91ede62d446aed37d23256e8938fc4c335
diff --git a/contrib/gilbert/media/gilbert/gilbert.js b/contrib/gilbert/media/gilbert/gilbert.js
new file mode 100644 (file)
index 0000000..663776c
--- /dev/null
@@ -0,0 +1,10 @@
+Ext.override(String, {
+       
+       capfirst: function () {
+               return this.substr(0, 1).toUpperCase() + this.substr(1);
+       },
+       
+});
+
+
+Gilbert = new Gilbert.lib.app.Application(Gilbert);
\ No newline at end of file
diff --git a/contrib/gilbert/media/gilbert/lib/app.js b/contrib/gilbert/media/gilbert/lib/app.js
new file mode 100644 (file)
index 0000000..2c3f59c
--- /dev/null
@@ -0,0 +1,346 @@
+Ext.ns('Gilbert.lib.app')
+
+
+Gilbert.lib.app.Desktop = Ext.extend(Ext.Panel, {
+       
+       constructor: function(application, config) {
+               var application = this.application = application;
+               Gilbert.lib.app.Desktop.superclass.constructor.call(this, Ext.applyIf(config||{}, {
+                       region: 'center',
+                       border: false,
+                       padding: '5px',
+                       bodyStyle: 'background: none;',
+               }));
+       },
+       
+});
+
+
+Gilbert.lib.app.MainMenu = Ext.extend(Ext.Toolbar, {
+       
+       constructor: function(application, config) {
+               var application = this.application = application;
+               Gilbert.lib.app.MainMenu.superclass.constructor.call(this, Ext.applyIf(config||{}, {
+                       region: 'north',
+                       autoHeight: true,
+                       items: [
+                               {
+                                       xtype: 'tbtext',
+                                       text: '<span style="font-weight: bolder; text-transform: uppercase;">Gilbert</span>',
+                               },
+                               {
+                                       xtype: 'tbseparator',
+                               },
+                       ],
+               }));
+       },
+       
+});
+
+
+Gilbert.lib.app.TaskBar = Ext.extend(Ext.Toolbar, {
+       
+       constructor: function(application, config) {
+               var application = this.application = application;
+               Gilbert.lib.app.TaskBar.superclass.constructor.call(this, Ext.applyIf(config||{}, {
+                       region: 'south',
+                       enableOverflow: true,
+                       autoHeight: true,
+                       items: [],
+                       plugins: [
+                               new Ext.ux.ToolbarReorderer({
+                                       defaultReorderable: true,
+                               }),
+                       ],
+               }));
+       },
+       
+       get_button_for_window: function(win) {
+               return this.find('represented_window', win)[0];
+       },
+       
+       default_button_handler: function(button) {
+               var win = button.represented_window;
+               if (this.active_window === win) {
+                       win.minimize();
+               } else {
+                       win.show();
+               }
+       },
+       
+       register_window: function(win) {
+               win.on('show', this.window_shown, this);
+               win.on('hide', this.window_hidden, this);
+               win.on('minimize', this.window_minimize, this);
+               win.on('deactivate', this.window_deactivated, this);
+               win.on('activate', this.window_activated, this);
+               win.on('titlechange', this.window_titlechanged, this);
+               win.on('iconchange', this.window_iconchanged, this);
+               
+               var button = new Ext.Button({
+                       text: win.title,
+                       iconCls: win.iconCls,
+                       enableToggle: true,
+                       allowDepress: false,
+                       width: 200,
+                       hidden: true,
+               });
+               button.represented_window = win;
+               button.setHandler(this.default_button_handler, this);
+               
+               this.add(button);
+               
+               win.on('destroy', this.window_destroyed, this);
+       },
+       
+       window_destroyed: function(win) {
+               this.remove(this.get_button_for_window(win));
+               this.application.do_layout();
+       },
+       
+       window_shown: function(win) {
+               if (this.minimizing_window !== win) {
+                       this.get_button_for_window(win).show();
+                       this.application.do_layout();
+               }
+       },
+       
+       window_hidden: function(win) {
+               if (this.minimizing_window !== win) {
+                       this.get_button_for_window(win).hide();
+                       this.application.do_layout();
+               }
+       },
+       
+       window_minimize: function(win) {
+               var button = this.get_button_for_window(win);
+               
+               this.minimizing_window = win;
+               win.hide(button.el, function () {
+                       this.minimizing_window = undefined;
+                       
+                       win.minimized = true;
+                       button.setText('<i>'+win.title+'</i>');
+                       button.setHandler(function (button) {
+                               var win = button.represented_window;
+                               
+                               win.minimized = false;
+                               button.setText(win.title);
+                               button.setHandler(this.default_button_handler, this);
+                               
+                               this.minimizing_window = win;
+                               win.show(button.el, function () {
+                                       this.minimizing_window = undefined;
+                               }, this);
+                       }, this);
+               }, this);
+       },
+       
+       window_deactivated: function(win) {
+               var button = this.get_button_for_window(win);
+               button.toggle(false);
+               button.setText(win.title);
+               
+               if (this.active_window === win) {
+                       this.active_window = undefined;
+               }
+       },
+       
+       window_activated: function(win) {
+               var button = this.get_button_for_window(win);
+               button.toggle(true);
+               button.setText('<b>'+win.title+'</b>');
+               
+               this.active_window = win;
+       },
+       
+       window_titlechanged: function(win) {
+               var button = this.get_button_for_window(win);
+               if (win.minimized) {
+                       button.setText('<i>'+win.title+'</i>');
+               } else {
+                       button.setText(win.title);
+               }
+       },
+       
+       window_iconchanged: function(win) {
+               var button = this.get_button_for_window(win);
+               button.setIconClass(win.iconCls);
+       },
+       
+});
+
+
+Gilbert.lib.app.Application = Ext.extend(Ext.util.Observable, {
+       
+       constructor: function (config) {
+               
+               this.models = {};
+               this.plugins = {};
+               
+               Ext.apply(this, config, {
+                       renderTo: Ext.getBody(),
+               });
+               
+               Gilbert.lib.app.Application.superclass.constructor.call(this);
+               
+               this.addEvents({
+                       'ready': true,
+                       'model_registered': true,
+                       'plugin_registered': true,
+                       'window_created': true,
+               });
+               
+               Ext.onReady(this.pre_init.createDelegate(this));
+       },
+       
+       pre_init: function () {
+               var outer = this;
+               
+               Gilbert.api.plugins.auth.get_preference('gilbert.background', function (background) {
+                       if (background) {
+                               outer.renderTo.setStyle('background', background);
+                       }
+               });
+               Gilbert.api.plugins.auth.get_preference('gilbert.theme', function (theme) {
+                       if (theme) {
+                               var link_element = document.getElementById('gilbert.theme.' + theme);
+                               if (link_element) {
+                                       Ext.each(document.getElementsByClassName('gilbert.theme'), function (theme_element) {
+                                               if (theme_element != link_element) {
+                                                       theme_element.disabled = true;
+                                               } else {
+                                                       theme_element.disabled = false;
+                                               }
+                                       });
+                               }
+                       }
+                       outer.init();
+               });
+       },
+       
+       init: function () {
+               Ext.QuickTips.init();
+               
+               var desktop = this.desktop = new Gilbert.lib.app.Desktop();
+               var mainmenu = this.mainmenu = new Gilbert.lib.app.MainMenu(this);
+               var taskbar = this.taskbar = new Gilbert.lib.app.TaskBar(this);
+               var viewport = this.viewport = new Ext.Viewport({
+                       renderTo: this.renderTo,
+                       layout: 'border',
+                       items: [
+                               this.mainmenu,
+                               this.desktop,
+                               this.taskbar,
+                       ],
+               });
+               
+               var windows = this.windows = new Ext.WindowGroup();
+               
+               Ext.Direct.on('exception', function (exception) {
+                       if (exception.code == Ext.Direct.exceptions.TRANSPORT) {
+                               if (exception.xhr.status == 403) {
+                                       window.alert('You have been unexpectedly logged out.');
+                                       window.location.reload(true);
+                               }
+                       }
+                       if (exception.html) {
+                               var win = this.create_window({
+                                       width: 400,
+                                       height: 300,
+                                       maximizable: true,
+                                       minimizable: false,
+                                       modal: true,
+                                       html_source: exception.html,
+                               }, Gilbert.lib.ui.HTMLWindow);
+                               win.show();
+                       }
+               }, this);
+               
+               var initial_plugins = this.plugins;
+               this.plugins = {};
+               
+               Ext.iterate(initial_plugins, function (name, plugin, plugins) {
+                       this.register_plugin(name, plugin);
+               }, this);
+               
+               this.do_layout();
+               
+               this.renderTo.on('contextmenu', Ext.emptyFn, null, {preventDefault: true});
+               
+               window.onbeforeunload = function (event) {
+                       var notice = 'You will lose all unsaved changes and windows.';
+                       var event = event || window.event;
+                       if (event) {
+                               event.returnValue = notice;
+                       }
+               
+                       return notice;
+               };
+               
+               this.fireEvent('ready', this);
+       },
+       
+       create_window: function(config, cls) {
+               var win = new(cls||Ext.Window)(Ext.applyIf(config||{},{
+                       renderTo: this.desktop.el,
+                       manager: this.windows,
+                       minimizable: true,
+                       constrainHeader: true,
+               }));
+               win.render(this.desktop.el);
+               if (win.modal) {
+                       win.on('show', function () {
+                               this.mainmenu.hide();
+                               this.taskbar.hide();
+                               this.do_layout();
+                       }, this);
+                       win.on('hide', function () {
+                               this.taskbar.show();
+                               this.mainmenu.show();
+                               this.do_layout();
+                       }, this);
+                       win.on('close', function () {
+                               this.taskbar.show();
+                               this.mainmenu.show();
+                               this.do_layout();
+                       }, this);
+               }
+               this.taskbar.register_window(win);
+               this.fireEvent('window_created', win);
+               return win;
+       },
+       
+       do_layout: function() {
+               this.mainmenu.doLayout();
+               this.taskbar.doLayout();
+               this.viewport.doLayout();
+       },
+       
+       register_plugin: function (name, plugin) {
+               if (plugin.init(this) != false) {
+                       this.plugins[name] = plugin;
+                       this.fireEvent('plugin_registered', name, plugin, this);
+               }
+       },
+       
+       get_plugin: function (name) {
+               return this.plugins[name];
+       },
+       
+       register_model: function (app_label, name, model) {
+               if (!this.models[app_label]) {
+                       this.models[app_label] = {};
+               }
+               this.models[app_label][name] = model;
+               this.fireEvent('model_registered', name, model, this);
+       },
+       
+       get_model: function (app_label, name) {
+               if (!this.models[app_label]) {
+                       return undefined;
+               }
+               return this.models[app_label][name];
+       },
+       
+});
\ No newline at end of file
diff --git a/contrib/gilbert/media/gilbert/lib/models.js b/contrib/gilbert/media/gilbert/lib/models.js
new file mode 100644 (file)
index 0000000..8d33dca
--- /dev/null
@@ -0,0 +1,86 @@
+Ext.ns('Gilbert.lib.models');
+
+
+Gilbert.lib.models.Model = Ext.extend(Object, {
+       
+       constructor: function (config) {
+               Ext.apply(this, config);
+               this.drag_drop_group = 'Gilbert.lib.models.Model(' + this.app_label + ',' + this.name + ') ';
+       },
+       
+       create_reader: function (config) {
+               return new Ext.data.JsonReader(Ext.applyIf(config||{}, {}));
+       },
+       
+       create_writer: function (config) {
+               return new Ext.data.JsonWriter(Ext.applyIf(config||{}, {
+                       encode: false,
+               }));
+       },
+       
+       create_proxy: function (config) {
+               return new Ext.data.DirectProxy(Ext.applyIf(config||{},{
+                       paramsAsHash: true,
+                       api: {
+                               read: this.api.data_read,
+                               create: this.api.data_create,
+                               update: this.api.data_update,
+                               destroy: this.api.data_destroy,
+                       },
+               }));
+       },
+       
+       create_store: function (config) {
+               return new Ext.data.Store(Ext.applyIf(config||{},{
+                       proxy: this.create_proxy(),
+                       reader: this.create_reader(),
+                       writer: this.create_writer(),
+                       remoteSort: true,
+               }));
+       },
+       
+});
+
+
+Gilbert.lib.models.ModelInstance = Ext.extend(Object, {
+       
+       constructor: function (model, pk) {
+               this.model = model;
+               this.pk = pk;
+       },
+       
+});
+
+
+Ext.data.Types.GILBERTMODELFOREIGNKEY = {
+       
+       convert: function (v, data) {
+               if (v) {
+                       return new Gilbert.lib.models.ModelInstance(Gilbert.get_model(v.app_label, v.name), v.pk);
+               } else {
+                       return null;
+               }
+       },
+       
+       sortType: Ext.data.SortTypes.none,
+       
+       type: 'gilbertmodelforeignkey',
+       
+}
+
+
+Ext.data.Types.GILBERTMODELFILEFIELD = {
+       
+       convert: function (v, data) {
+               if (v) {
+                       return v.url;
+               } else {
+                       return null;
+               }
+       },
+       
+       sortType: Ext.data.SortTypes.none,
+       
+       type: 'gilbertmodelfilefield',
+       
+}
\ No newline at end of file
diff --git a/contrib/gilbert/media/gilbert/lib/plugins.js b/contrib/gilbert/media/gilbert/lib/plugins.js
new file mode 100644 (file)
index 0000000..66ff46f
--- /dev/null
@@ -0,0 +1,14 @@
+Ext.ns('Gilbert.lib.plugins');
+
+
+Gilbert.lib.plugins.Plugin = Ext.extend(Object, {
+       
+       constructor: function (config) {
+               Ext.apply(this, config);
+       },
+       
+       init: function (application) {
+               var application = this.application = application;
+       }
+       
+});
\ No newline at end of file
diff --git a/contrib/gilbert/media/gilbert/lib/ui/forms.js b/contrib/gilbert/media/gilbert/lib/ui/forms.js
new file mode 100644 (file)
index 0000000..719b7b1
--- /dev/null
@@ -0,0 +1,946 @@
+Ext.ns('Gilbert.lib.ui.forms');
+
+
+Gilbert.lib.ui.forms.ClearableComboBox = Ext.extend(Ext.form.ComboBox, {
+       
+       initComponent: function () {
+               Gilbert.lib.ui.forms.ClearableComboBox.superclass.initComponent.call(this);
+               
+               this.triggerConfig = {
+                       tag: 'span',
+                       cls: 'x-form-twin-triggers',
+                       cn: [
+                               {
+                                       tag: 'img',
+                                       src: Ext.BLANK_IMAGE_URL,
+                                       alt: '',
+                                       cls: 'x-form-trigger x-form-clear-trigger', 
+                               },
+                               {
+                                       tag: 'img',
+                                       src: Ext.BLANK_IMAGE_URL,
+                                       alt: '',
+                                       cls: 'x-form-trigger ' + this.triggerClass,
+                               },
+                       ],
+               };
+       },
+       
+       afterRender: function () {
+               Gilbert.lib.ui.forms.ClearableComboBox.superclass.afterRender.call(this);
+               
+               if (this.value && this.allowBlank) {
+                       this.triggers[0].show();
+               } else {
+                       this.triggers[0].hide();
+               }
+       },
+       
+       initTrigger: function () {
+               Ext.form.TwinTriggerField.prototype.initTrigger.call(this);
+       },
+       
+       getTriggerWidth: function () {
+               Ext.form.TwinTriggerField.prototype.getTriggerWidth.call(this);
+       },
+       
+       onTrigger2Click: function () {
+               this.onTriggerClick();
+       },
+       
+       onTrigger1Click: function () {
+               this.clearValue();
+               this.triggers[0].hide();
+       },
+       
+       setValue: function (v) {
+               Gilbert.lib.ui.forms.ClearableComboBox.superclass.setValue.call(this, v);
+               
+               if (this.value && this.allowBlank) {
+                       this.triggers[0].show();
+               } else {
+                       this.triggers[0].hide();
+               }
+       },
+       
+       onDestroy: function () {
+               Ext.destroy(this.triggers);
+               
+               Gilbert.lib.ui.forms.ClearableComboBox.superclass.onDestroy.call(this);
+       },
+       
+});
+Ext.reg('gilbertclearablecombo', Gilbert.lib.ui.forms.ClearableComboBox);
+
+
+Gilbert.lib.ui.forms.ModelChoiceField = Ext.extend(Gilbert.lib.ui.forms.ClearableComboBox, {
+       
+       model_app_label: undefined,
+       
+       model_name: undefined,
+       
+       model_filters: {},
+       
+       initComponent: function () {
+               if (!this.model) {
+                       this.model = Gilbert.get_model(this.model_app_label, this.model_name);
+               }
+               if (!this.store) {
+                       if (!this.model && this.backup_store) {
+                               this.store = this.backup_store;
+                       } else if (this.model) {
+                               this.store = this.model.create_store({
+                                       baseParams: {
+                                               filters: this.model_filters,
+                                       },
+                               });
+                               this.valueField = 'pk';
+                               this.displayField = '__unicode__';
+                       
+                               this.on('beforequery', function () {
+                                       delete this.lastQuery;
+                               }, this);
+                               this.store.on('load', function (store, records, options) {
+                                       this.store_loaded = true;
+                               }, this, {single: true});
+                               this.store.load();
+                               
+                               this.on('render', function () {
+                                       var outer = this;
+                                       this.dropTarget = new Ext.dd.DropTarget(this.el, {
+                                               ddGroup: outer.model.drag_drop_group,
+                                               notifyEnter: function (source, e, data) {
+                                                       outer.el.highlight();
+                                                       return Ext.dd.DropTarget.prototype.notifyEnter.call(this);
+                                               },
+                                               notifyDrop: function (source, e, data) {
+                                                       outer.setValue(data.selections[0].id);
+                                                       return true;
+                                               },
+                                       });
+                               }, this);
+                       }
+               }
+               Gilbert.lib.ui.forms.ModelChoiceField.superclass.initComponent.call(this);
+       },
+       
+       setValue: function (v) {
+               if (this.model && !this.store_loaded) {
+                       this.el.dom.value = this.loadingText;
+                       this.store.on('load', this.setValue.createDelegate(this, [v]), null, {single: true});
+                       return;
+               }
+               return Gilbert.lib.ui.forms.ModelChoiceField.superclass.setValue.call(this, v);
+       }
+       
+});
+Ext.reg('gilbertmodelchoicefield', Gilbert.lib.ui.forms.ModelChoiceField);
+
+
+Gilbert.lib.ui.forms.MultipleChoiceField = Ext.extend(Ext.ux.form.SuperBoxSelect, {});
+Ext.reg('gilbertmultiplechoicefield', Gilbert.lib.ui.forms.MultipleChoiceField);
+
+
+Gilbert.lib.ui.forms.ModelMultipleChoiceField = Ext.extend(Gilbert.lib.ui.forms.MultipleChoiceField, {});
+Ext.reg('gilbertmodelmultiplechoicefield', Gilbert.lib.ui.forms.ModelMultipleChoiceField);
+
+
+/*
+Gilbert.lib.ui.DateTimeField is derived from revision 813 of Ext.ux.form.DateTime by Ing. Jozef SakáloÅ¡ as posted at http://extjs.com/forum/showthread.php?t=22661. 
+It, and the original, is licensed under the GNU LGPL version 3.0 (http://www.gnu.org/licenses/lgpl.html).
+*/
+
+/**
+ * Creates new DateTimeField
+ * @constructor
+ * @param {Object} config A config object
+ */
+Gilbert.lib.ui.DateTimeField = Ext.extend(Ext.form.Field, {
+       /**
+        * @cfg {Function} dateValidator A custom validation function to be called during date field
+        * validation (defaults to null)
+        */
+       dateValidator: null
+       /**
+        * @cfg {String/Object} defaultAutoCreate DomHelper element spec
+        * Let superclass to create hidden field instead of textbox. Hidden will be submittend to server
+        */
+       ,
+       defaultAutoCreate: {
+               tag: 'input',
+               type: 'hidden'
+       }
+       /**
+        * @cfg {String} dtSeparator Date - Time separator. Used to split date and time (defaults to ' ' (space))
+        */
+       ,
+       dtSeparator: ' '
+       /**
+        * @cfg {String} hiddenFormat Format of datetime used to store value in hidden field
+        * and submitted to server (defaults to 'Y-m-d H:i:s' that is mysql format)
+        */
+       ,
+       hiddenFormat: 'Y-m-d H:i:s'
+       /**
+        * @cfg {Boolean} otherToNow Set other field to now() if not explicly filled in (defaults to true)
+        */
+       ,
+       otherToNow: true
+       /**
+        * @cfg {Boolean} emptyToNow Set field value to now on attempt to set empty value.
+        * If it is true then setValue() sets value of field to current date and time (defaults to false)
+        */
+       /**
+        * @cfg {String} timePosition Where the time field should be rendered. 'right' is suitable for forms
+        * and 'below' is suitable if the field is used as the grid editor (defaults to 'right')
+        */
+       ,
+       timePosition: 'right'
+       // valid values:'below', 'right'
+       /**
+        * @cfg {Function} timeValidator A custom validation function to be called during time field
+        * validation (defaults to null)
+        */
+       ,
+       timeValidator: null
+       /**
+        * @cfg {Number} timeWidth Width of time field in pixels (defaults to 100)
+        */
+       ,
+       timeWidth: 100
+       /**
+        * @cfg {String} dateFormat Format of DateField. Can be localized. (defaults to 'm/y/d')
+        */
+       ,
+       dateFormat: 'm/d/y'
+       /**
+        * @cfg {String} timeFormat Format of TimeField. Can be localized. (defaults to 'g:i A')
+        */
+       ,
+       timeFormat: 'g:i A'
+       /**
+        * @cfg {Object} dateConfig Config for DateField constructor.
+        */
+       /**
+        * @cfg {Object} timeConfig Config for TimeField constructor.
+        */
+
+       // {{{
+       /**
+        * @private
+        * creates DateField and TimeField and installs the necessary event handlers
+        */
+       ,
+       initComponent: function() {
+               // call parent initComponent
+               Gilbert.lib.ui.DateTimeField.superclass.initComponent.call(this);
+               
+               // create DateField
+               var dateConfig = Ext.apply({},
+               {
+                       id: this.id + '-date'
+                       ,
+                       format: this.dateFormat || Ext.form.DateField.prototype.format
+                       ,
+                       width: this.timeWidth
+                       ,
+                       selectOnFocus: this.selectOnFocus
+                       ,
+                       validator: this.dateValidator
+                       ,
+                       listeners: {
+                               blur: {
+                                       scope: this,
+                                       fn: this.onBlur
+                               }
+                               ,
+                               focus: {
+                                       scope: this,
+                                       fn: this.onFocus
+                               }
+                       }
+               },
+               this.dateConfig);
+               this.df = new Ext.form.DateField(dateConfig);
+               this.df.ownerCt = this;
+               delete(this.dateFormat);
+               
+               // create TimeField
+               var timeConfig = Ext.apply({},
+               {
+                       id: this.id + '-time'
+                       ,
+                       format: this.timeFormat || Ext.form.TimeField.prototype.format
+                       ,
+                       width: this.timeWidth
+                       ,
+                       selectOnFocus: this.selectOnFocus
+                       ,
+                       validator: this.timeValidator
+                       ,
+                       listeners: {
+                               blur: {
+                                       scope: this,
+                                       fn: this.onBlur
+                               }
+                               ,
+                               focus: {
+                                       scope: this,
+                                       fn: this.onFocus
+                               }
+                       }
+               },
+               this.timeConfig);
+               this.tf = new Ext.form.TimeField(timeConfig);
+               this.tf.ownerCt = this;
+               delete(this.timeFormat);
+               
+               // relay events
+               this.relayEvents(this.df, ['focus', 'specialkey', 'invalid', 'valid']);
+               this.relayEvents(this.tf, ['focus', 'specialkey', 'invalid', 'valid']);
+               
+               this.on('specialkey', this.onSpecialKey, this);
+               
+       }
+       // eo function initComponent
+       // }}}
+       // {{{
+       /**
+        * @private
+        * Renders underlying DateField and TimeField and provides a workaround for side error icon bug
+        */
+       ,
+       onRender: function(ct, position) {
+               // don't run more than once
+               if (this.isRendered) {
+                       return;
+               }
+               
+               // render underlying hidden field
+               Gilbert.lib.ui.DateTimeField.superclass.onRender.call(this, ct, position);
+               
+               // render DateField and TimeField
+               // create bounding table
+               var t;
+               if ('below' === this.timePosition || 'bellow' === this.timePosition) {
+                       t = Ext.DomHelper.append(ct, {
+                               tag: 'table',
+                               style: 'border-collapse:collapse',
+                               children: [
+                               {
+                                       tag: 'tr',
+                                       children: [{
+                                               tag: 'td',
+                                               style: 'padding-bottom:1px',
+                                               cls: 'ux-datetime-date'
+                                       }]
+                               }
+                               ,
+                               {
+                                       tag: 'tr',
+                                       children: [{
+                                               tag: 'td',
+                                               cls: 'ux-datetime-time'
+                                       }]
+                               }
+                               ]
+                       },
+                       true);
+               }
+               else {
+                       t = Ext.DomHelper.append(ct, {
+                               tag: 'table',
+                               style: 'border-collapse:collapse',
+                               children: [
+                               {
+                                       tag: 'tr',
+                                       children: [
+                                       {
+                                               tag: 'td',
+                                               style: 'padding-right:4px',
+                                               cls: 'ux-datetime-date'
+                                       },
+                                       {
+                                               tag: 'td',
+                                               cls: 'ux-datetime-time'
+                                       }
+                                       ]
+                               }
+                               ]
+                       },
+                       true);
+               }
+               
+               this.tableEl = t;
+               this.wrap = t.wrap({
+                       cls: 'x-form-field-wrap'
+               });
+               //                this.wrap = t.wrap();
+               this.wrap.on("mousedown", this.onMouseDown, this, {
+                       delay: 10
+               });
+               
+               // render DateField & TimeField
+               this.df.render(t.child('td.ux-datetime-date'));
+               this.tf.render(t.child('td.ux-datetime-time'));
+               
+               // workaround for IE trigger misalignment bug
+               // see http://extjs.com/forum/showthread.php?p=341075#post341075
+               //                if(Ext.isIE && Ext.isStrict) {
+               //                        t.select('input').applyStyles({top:0});
+               //                }
+               this.df.el.swallowEvent(['keydown', 'keypress']);
+               this.tf.el.swallowEvent(['keydown', 'keypress']);
+               
+               // create icon for side invalid errorIcon
+               if ('side' === this.msgTarget) {
+                       var elp = this.el.findParent('.x-form-element', 10, true);
+                       if (elp) {
+                               this.errorIcon = elp.createChild({
+                                       cls: 'x-form-invalid-icon'
+                               });
+                       }
+                       
+                       var o = {
+                               errorIcon: this.errorIcon
+                               ,
+                               msgTarget: 'side'
+                               ,
+                               alignErrorIcon: this.alignErrorIcon.createDelegate(this)
+                       };
+                       Ext.apply(this.df, o);
+                       Ext.apply(this.tf, o);
+                       //                        this.df.errorIcon = this.errorIcon;
+                       //                        this.tf.errorIcon = this.errorIcon;
+               }
+               
+               // setup name for submit
+               this.el.dom.name = this.hiddenName || this.name || this.id;
+               
+               // prevent helper fields from being submitted
+               this.df.el.dom.removeAttribute("name");
+               this.tf.el.dom.removeAttribute("name");
+               
+               // we're rendered flag
+               this.isRendered = true;
+               
+               // update hidden field
+               this.updateHidden();
+               
+       }
+       // eo function onRender
+       // }}}
+       // {{{
+       /**
+        * @private
+        */
+       ,
+       adjustSize: Ext.BoxComponent.prototype.adjustSize
+       // }}}
+       // {{{
+       /**
+        * @private
+        */
+       ,
+       alignErrorIcon: function() {
+               this.errorIcon.alignTo(this.tableEl, 'tl-tr', [2, 0]);
+       }
+       // }}}
+       // {{{
+       /**
+        * @private initializes internal dateValue
+        */
+       ,
+       initDateValue: function() {
+               this.dateValue = this.otherToNow ? new Date() : new Date(1970, 0, 1, 0, 0, 0);
+       }
+       // }}}
+       // {{{
+       /**
+        * Calls clearInvalid on the DateField and TimeField
+        */
+       ,
+       clearInvalid: function() {
+               this.df.clearInvalid();
+               this.tf.clearInvalid();
+       }
+       // eo function clearInvalid
+       // }}}
+       // {{{
+       /**
+        * Calls markInvalid on both DateField and TimeField
+        * @param {String} msg Invalid message to display
+        */
+       ,
+       markInvalid: function(msg) {
+               this.df.markInvalid(msg);
+               this.tf.markInvalid(msg);
+       }
+       // eo function markInvalid
+       // }}}
+       // {{{
+       /**
+        * @private
+        * called from Component::destroy. 
+        * Destroys all elements and removes all listeners we've created.
+        */
+       ,
+       beforeDestroy: function() {
+               if (this.isRendered) {
+                       //                        this.removeAllListeners();
+                       this.wrap.removeAllListeners();
+                       this.wrap.remove();
+                       this.tableEl.remove();
+                       this.df.destroy();
+                       this.tf.destroy();
+               }
+       }
+       // eo function beforeDestroy
+       // }}}
+       // {{{
+       /**
+        * Disable this component.
+        * @return {Ext.Component} this
+        */
+       ,
+       disable: function() {
+               if (this.isRendered) {
+                       this.df.disabled = this.disabled;
+                       this.df.onDisable();
+                       this.tf.onDisable();
+               }
+               this.disabled = true;
+               this.df.disabled = true;
+               this.tf.disabled = true;
+               this.fireEvent("disable", this);
+               return this;
+       }
+       // eo function disable
+       // }}}
+       // {{{
+       /**
+        * Enable this component.
+        * @return {Ext.Component} this
+        */
+       ,
+       enable: function() {
+               if (this.rendered) {
+                       this.df.onEnable();
+                       this.tf.onEnable();
+               }
+               this.disabled = false;
+               this.df.disabled = false;
+               this.tf.disabled = false;
+               this.fireEvent("enable", this);
+               return this;
+       }
+       // eo function enable
+       // }}}
+       // {{{
+       /**
+        * @private Focus date filed
+        */
+       ,
+       focus: function() {
+               this.df.focus();
+       }
+       // eo function focus
+       // }}}
+       // {{{
+       /**
+        * @private
+        */
+       ,
+       getPositionEl: function() {
+               return this.wrap;
+       }
+       // }}}
+       // {{{
+       /**
+        * @private
+        */
+       ,
+       getResizeEl: function() {
+               return this.wrap;
+       }
+       // }}}
+       // {{{
+       /**
+        * @return {Date/String} Returns value of this field
+        */
+       ,
+       getValue: function() {
+               // create new instance of date
+               return this.dateValue ? new Date(this.dateValue) : '';
+       }
+       // eo function getValue
+       // }}}
+       // {{{
+       /**
+        * @return {Boolean} true = valid, false = invalid
+        * @private Calls isValid methods of underlying DateField and TimeField and returns the result
+        */
+       ,
+       isValid: function() {
+               return this.df.isValid() && this.tf.isValid();
+       }
+       // eo function isValid
+       // }}}
+       // {{{
+       /**
+        * Returns true if this component is visible
+        * @return {boolean} 
+        */
+       ,
+       isVisible: function() {
+               return this.df.rendered && this.df.getActionEl().isVisible();
+       }
+       // eo function isVisible
+       // }}}
+       // {{{
+       /** 
+        * @private Handles blur event
+        */
+       ,
+       onBlur: function(f) {
+               // called by both DateField and TimeField blur events
+               // revert focus to previous field if clicked in between
+               if (this.wrapClick) {
+                       f.focus();
+                       this.wrapClick = false;
+               }
+               
+               // update underlying value
+               if (f === this.df) {
+                       this.updateDate();
+               }
+               else {
+                       this.updateTime();
+               }
+               this.updateHidden();
+               
+               this.validate();
+               
+               // fire events later
+               (function() {
+                       if (!this.df.hasFocus && !this.tf.hasFocus) {
+                               var v = this.getValue();
+                               if (String(v) !== String(this.startValue)) {
+                                       this.fireEvent("change", this, v, this.startValue);
+                               }
+                               this.hasFocus = false;
+                               this.fireEvent('blur', this);
+                       }
+               }).defer(100, this);
+               
+       }
+       // eo function onBlur
+       // }}}
+       // {{{
+       /**
+        * @private Handles focus event
+        */
+       ,
+       onFocus: function() {
+               if (!this.hasFocus) {
+                       this.hasFocus = true;
+                       this.startValue = this.getValue();
+                       this.fireEvent("focus", this);
+               }
+       }
+       // }}}
+       // {{{
+       /**
+        * @private Just to prevent blur event when clicked in the middle of fields
+        */
+       ,
+       onMouseDown: function(e) {
+               if (!this.disabled) {
+                       this.wrapClick = 'td' === e.target.nodeName.toLowerCase();
+               }
+       }
+       // }}}
+       // {{{
+       /**
+        * @private
+        * Handles Tab and Shift-Tab events
+        */
+       ,
+       onSpecialKey: function(t, e) {
+               var key = e.getKey();
+               if (key === e.TAB) {
+                       if (t === this.df && !e.shiftKey) {
+                               e.stopEvent();
+                               this.tf.focus();
+                       }
+                       if (t === this.tf && e.shiftKey) {
+                               e.stopEvent();
+                               this.df.focus();
+                       }
+                       this.updateValue();
+               }
+               // otherwise it misbehaves in editor grid
+               if (key === e.ENTER) {
+                       this.updateValue();
+               }
+               
+       }
+       // eo function onSpecialKey
+       // }}}
+       // {{{
+       /**
+        * Resets the current field value to the originally loaded value 
+        * and clears any validation messages. See Ext.form.BasicForm.trackResetOnLoad
+        */
+       ,
+       reset: function() {
+               this.df.setValue(this.originalValue);
+               this.tf.setValue(this.originalValue);
+       }
+       // eo function reset
+       // }}}
+       // {{{
+       /**
+        * @private Sets the value of DateField
+        */
+       ,
+       setDate: function(date) {
+               this.df.setValue(date);
+       }
+       // eo function setDate
+       // }}}
+       // {{{
+       /** 
+        * @private Sets the value of TimeField
+        */
+       ,
+       setTime: function(date) {
+               this.tf.setValue(date);
+       }
+       // eo function setTime
+       // }}}
+       // {{{
+       /**
+        * @private
+        * Sets correct sizes of underlying DateField and TimeField
+        * With workarounds for IE bugs
+        */
+       ,
+       setSize: function(w, h) {
+               if (!w) {
+                       return;
+               }
+               if ('below' === this.timePosition) {
+                       this.df.setSize(w, h);
+                       this.tf.setSize(w, h);
+                       if (Ext.isIE) {
+                               this.df.el.up('td').setWidth(w);
+                               this.tf.el.up('td').setWidth(w);
+                       }
+               }
+               else {
+                       this.df.setSize(w - this.timeWidth - 4, h);
+                       this.tf.setSize(this.timeWidth, h);
+                       
+                       if (Ext.isIE) {
+                               this.df.el.up('td').setWidth(w - this.timeWidth - 4);
+                               this.tf.el.up('td').setWidth(this.timeWidth);
+                       }
+               }
+       }
+       // eo function setSize
+       // }}}
+       // {{{
+       /**
+        * @param {Mixed} val Value to set
+        * Sets the value of this field
+        */
+       ,
+       setValue: function(val) {
+               if (!val && true === this.emptyToNow) {
+                       this.setValue(new Date());
+                       return;
+               }
+               else if (!val) {
+                       this.setDate('');
+                       this.setTime('');
+                       this.updateValue();
+                       return;
+               }
+               if ('number' === typeof val) {
+                       val = new Date(val);
+               }
+               else if ('string' === typeof val && this.hiddenFormat) {
+                       val = Date.parseDate(val, this.hiddenFormat);
+               }
+               val = val ? val: new Date(1970, 0, 1, 0, 0, 0);
+               var da;
+               if (val instanceof Date) {
+                       this.setDate(val);
+                       this.setTime(val);
+                       this.dateValue = new Date(Ext.isIE ? val.getTime() : val);
+               }
+               else {
+                       da = val.split(this.dtSeparator);
+                       this.setDate(da[0]);
+                       if (da[1]) {
+                               if (da[2]) {
+                                       // add am/pm part back to time
+                                       da[1] += da[2];
+                               }
+                               this.setTime(da[1]);
+                       }
+               }
+               this.updateValue();
+       }
+       // eo function setValue
+       // }}}
+       // {{{
+       /**
+        * Hide or show this component by boolean
+        * @return {Ext.Component} this
+        */
+       ,
+       setVisible: function(visible) {
+               if (visible) {
+                       this.df.show();
+                       this.tf.show();
+               } else {
+                       this.df.hide();
+                       this.tf.hide();
+               }
+               return this;
+       }
+       // eo function setVisible
+       // }}}
+       //{{{
+       ,
+       show: function() {
+               return this.setVisible(true);
+       }
+       // eo function show
+       //}}}
+       //{{{
+       ,
+       hide: function() {
+               return this.setVisible(false);
+       }
+       // eo function hide
+       //}}}
+       // {{{
+       /**
+        * @private Updates the date part
+        */
+       ,
+       updateDate: function() {
+               
+               var d = this.df.getValue();
+               if (d) {
+                       if (! (this.dateValue instanceof Date)) {
+                               this.initDateValue();
+                               if (!this.tf.getValue()) {
+                                       this.setTime(this.dateValue);
+                               }
+                       }
+                       this.dateValue.setMonth(0);
+                       // because of leap years
+                       this.dateValue.setFullYear(d.getFullYear());
+                       this.dateValue.setMonth(d.getMonth(), d.getDate());
+                       //                        this.dateValue.setDate(d.getDate());
+               }
+               else {
+                       this.dateValue = '';
+                       this.setTime('');
+               }
+       }
+       // eo function updateDate
+       // }}}
+       // {{{
+       /**
+        * @private
+        * Updates the time part
+        */
+       ,
+       updateTime: function() {
+               var t = this.tf.getValue();
+               if (t && !(t instanceof Date)) {
+                       t = Date.parseDate(t, this.tf.format);
+               }
+               if (t && !this.df.getValue()) {
+                       this.initDateValue();
+                       this.setDate(this.dateValue);
+               }
+               if (this.dateValue instanceof Date) {
+                       if (t) {
+                               this.dateValue.setHours(t.getHours());
+                               this.dateValue.setMinutes(t.getMinutes());
+                               this.dateValue.setSeconds(t.getSeconds());
+                       }
+                       else {
+                               this.dateValue.setHours(0);
+                               this.dateValue.setMinutes(0);
+                               this.dateValue.setSeconds(0);
+                       }
+               }
+       }
+       // eo function updateTime
+       // }}}
+       // {{{
+       /**
+        * @private Updates the underlying hidden field value
+        */
+       ,
+       updateHidden: function() {
+               if (this.isRendered) {
+                       var value = this.dateValue instanceof Date ? this.dateValue.format(this.hiddenFormat) : '';
+                       this.el.dom.value = value;
+               }
+       }
+       // }}}
+       // {{{
+       /**
+        * @private Updates all of Date, Time and Hidden
+        */
+       ,
+       updateValue: function() {
+               
+               this.updateDate();
+               this.updateTime();
+               this.updateHidden();
+               
+               return;
+       }
+       // eo function updateValue
+       // }}}
+       // {{{
+       /**
+        * @return {Boolean} true = valid, false = invalid
+        * calls validate methods of DateField and TimeField
+        */
+       ,
+       validate: function() {
+               return this.df.validate() && this.tf.validate();
+       }
+       // eo function validate
+       // }}}
+       // {{{
+       /**
+        * Returns renderer suitable to render this field
+        * @param {Object} Column model config
+        */
+       ,
+       renderer: function(field) {
+               var format = field.editor.dateFormat || Gilbert.lib.ui.DateTimeField.prototype.dateFormat;
+               format += ' ' + (field.editor.timeFormat || Gilbert.lib.ui.DateTimeField.prototype.timeFormat);
+               var renderer = function(val) {
+                       var retval = Ext.util.Format.date(val, format);
+                       return retval;
+               };
+               return renderer;
+       }
+       // eo function renderer
+       // }}}
+});
+// eo extend
+// register xtype
+Ext.reg('gilbertdatetimefield', Gilbert.lib.ui.DateTimeField);
diff --git a/contrib/gilbert/media/gilbert/lib/ui/ui.js b/contrib/gilbert/media/gilbert/lib/ui/ui.js
new file mode 100644 (file)
index 0000000..9abe372
--- /dev/null
@@ -0,0 +1,44 @@
+Ext.ns('Gilbert.lib.ui');
+
+
+Gilbert.lib.ui.DjangoForm = Ext.extend(Ext.FormPanel, {
+       initComponent: function () {
+               /*if (this.djangoFields) {
+                       this.initDjangoForm();
+               }*/
+               Gilbert.lib.ui.DjangoForm.superclass.initComponent.call(this);
+       },
+/*     initDjangoForm: function () {
+               this.items = this.items || [];
+               Ext.each(this.djangoFields, this.addDjangoField, this);
+       },
+       addDjangoField: function(field, index, all) {
+               this.items.push(Gilbert.lib.ui.DjangoFormHelper.get_field_converter(field.type)(field));
+       },*/
+});
+
+
+Gilbert.lib.ui.HTMLWindow = Ext.extend(Ext.Window, {
+       html_source: undefined,
+       onRender: function() {
+               if (this.html_source) {
+                       this.bodyCfg = {
+                               tag: 'iframe',
+                               cls: this.bodyCls,
+                       };
+                       Gilbert.lib.ui.HTMLWindow.superclass.onRender.apply(this, arguments);
+                       var iframe = this.body.dom;
+                       var doc = iframe.document;
+                       if (iframe.contentDocument) {
+                               doc = iframe.contentDocument;
+                       } else if (iframe.contentWindow) {
+                               doc = iframe.contentWindow.document;
+                       }
+                       doc.open();
+                       doc.writeln(this.html_source);
+                       doc.close();
+               } else {
+                       Gilbert.lib.ui.HTMLWindow.superclass.onRender.apply(this, arguments);
+               }
+       }
+});
\ No newline at end of file
diff --git a/contrib/gilbert/media/gilbert/lib/ui/windows.js b/contrib/gilbert/media/gilbert/lib/ui/windows.js
new file mode 100644 (file)
index 0000000..f60eb11
--- /dev/null
@@ -0,0 +1,41 @@
+Gilbert.lib.ui.DjangoForm = Ext.extend(Ext.FormPanel, {
+       initComponent: function () {
+               /*if (this.djangoFields) {
+                       this.initDjangoForm();
+               }*/
+               Gilbert.lib.ui.DjangoForm.superclass.initComponent.call(this);
+       },
+/*     initDjangoForm: function () {
+               this.items = this.items || [];
+               Ext.each(this.djangoFields, this.addDjangoField, this);
+       },
+       addDjangoField: function(field, index, all) {
+               this.items.push(Gilbert.lib.ui.DjangoFormHelper.get_field_converter(field.type)(field));
+       },*/
+});
+
+
+Gilbert.lib.ui.HTMLWindow = Ext.extend(Ext.Window, {
+       html_source: undefined,
+       onRender: function() {
+               if (this.html_source) {
+                       this.bodyCfg = {
+                               tag: 'iframe',
+                               cls: this.bodyCls,
+                       };
+                       Gilbert.lib.ui.HTMLWindow.superclass.onRender.apply(this, arguments);
+                       var iframe = this.body.dom;
+                       var doc = iframe.document;
+                       if (iframe.contentDocument) {
+                               doc = iframe.contentDocument;
+                       } else if (iframe.contentWindow) {
+                               doc = iframe.contentWindow.document;
+                       }
+                       doc.open();
+                       doc.writeln(this.html_source);
+                       doc.close();
+               } else {
+                       Gilbert.lib.ui.HTMLWindow.superclass.onRender.apply(this, arguments);
+               }
+       }
+});
\ No newline at end of file
diff --git a/contrib/gilbert/media/gilbert/plugins/auth.js b/contrib/gilbert/media/gilbert/plugins/auth.js
new file mode 100644 (file)
index 0000000..e31d6fd
--- /dev/null
@@ -0,0 +1,111 @@
+Ext.ns('Gilbert.lib.plugins.auth');
+
+
+Gilbert.lib.plugins.auth.PreferencesWindow = Ext.extend(Ext.Window, {
+       constructor: function (config, application) {
+               Gilbert.lib.plugins.auth.PreferencesWindow.superclass.constructor.call(this, Ext.applyIf(config||{},{
+                       width: 320,
+                       height: 200,
+                       title: 'Preferences',
+               }));
+       }
+});
+
+
+Gilbert.lib.plugins.auth.Plugin = Ext.extend(Gilbert.lib.plugins.Plugin, {
+       
+       init: function (application) {
+               Gilbert.lib.plugins.auth.Plugin.superclass.init.call(this, application);
+               
+               var preferences_window = new Gilbert.lib.plugins.auth.PreferencesWindow({}, application);
+               
+               Gilbert.api.plugins.auth.whoami(function (whoami) {
+                       application.mainmenu.add({
+                               xtype: 'tbfill',
+                       },{
+                               xtype: 'tbseparator',
+                       },{
+                               xtype: 'button',
+                               iconCls: 'icon-user-silhouette',
+                               text: '<span style="font-weight: bolder;">' + whoami + '</span>',
+                               menu: [{
+                                               text: 'Preferences...',
+                                               iconCls: 'icon-switch',
+                                               handler: function (button, event) {
+                                                       preferences_window.show();
+                                               },
+                                       },{
+                                               xtype: 'menuseparator',
+                                       },{
+                                       text: 'Change password',
+                                       iconCls: 'icon-key--pencil',
+                                       handler: function(button, event) {
+                                               Gilbert.api.plugins.auth.get_passwd_form(function(formspec) {
+                                                       var formspec = formspec;
+                                                       for (var item_index in formspec.items) {
+                                                               var item = formspec.items[item_index];
+                                                               Ext.apply(item, {
+                                                                       plugins: [ Ext.ux.FieldLabeler ],
+                                                               });
+                                                       }
+                                                       var change_password_window = application.create_window({
+                                                               layout: 'fit',
+                                                               resizable: true,
+                                                               title: 'Change password',
+                                                               iconCls: 'icon-key--pencil',
+                                                               width: 360,
+                                                               height: 100,
+                                                               items: change_password_form = new Ext.FormPanel(Ext.applyIf({
+                                                                       layout: {
+                                                                               type: 'vbox',
+                                                                               align: 'stretch',
+                                                                       },
+                                                                       baseCls: 'x-plain',
+                                                                       bodyStyle: 'padding: 5px;',
+                                                                       frame: true,
+                                                                       buttons: [{
+                                                                               text: 'Change password',
+                                                                               iconCls: 'icon-key--pencil',
+                                                                               handler: function(button, event) {
+                                                                                       change_password_form.getForm().submit({
+                                                                                               success: function(form, action) {
+                                                                                                       Ext.MessageBox.alert('Password changed', 'Your password has been changed.');
+                                                                                               },
+                                                                                       });
+                                                                               },
+                                                                       }],
+                                                                       api: {
+                                                                               submit: Gilbert.api.plugins.auth.save_passwd_form,
+                                                                       },
+                                                               }, formspec))
+                                                       });
+                                                       change_password_window.doLayout();
+                                                       change_password_window.show(button.el);
+                                               });
+                                               
+                                       },
+                               },{
+                                       text: 'Log out',
+                                       iconCls: 'icon-door-open-out',
+                                       handler: function(button, event) {
+                                               Gilbert.api.plugins.auth.logout(function(success) {
+                                                       if (success) {
+                                                               window.onbeforeunload = undefined;
+                                                               document.location.reload();
+                                                       } else {
+                                                               Ext.MessageBox.alert('Log out failed', 'You have <strong>not</strong> been logged out. This could mean that your connection with the server has been severed. Please try again.');
+                                                       }
+                                               })
+                                       }
+                               }],
+                       });
+                       application.do_layout();
+               });
+       },
+
+});
+
+
+Gilbert.on('ready', function (application) {
+       application.register_plugin('auth', new Gilbert.lib.plugins.auth.Plugin());
+});
\ No newline at end of file
diff --git a/contrib/gilbert/media/gilbert/plugins/models.js b/contrib/gilbert/media/gilbert/plugins/models.js
new file mode 100644 (file)
index 0000000..e7c1541
--- /dev/null
@@ -0,0 +1,477 @@
+Ext.ns('Gilbert.lib.plugins.models.ui');
+
+
+Ext.override(Gilbert.lib.models.Model, {
+       create_new_form: function (callback, config) {
+               var model = this;
+               var config = config;
+               model.api.get_form({}, function (formspec) {
+                       /*var formspec = formspec;
+                       for (var item_index in formspec.items) {
+                               var item = formspec.items[item_index];
+                               Ext.apply(item, {
+                                       anchor: '100%',
+                               });
+                       }*/
+                       var form_panel = new Gilbert.lib.ui.DjangoForm(Ext.applyIf(Ext.applyIf(config||{},{
+                               title: 'New '+model.verbose_name,
+                               header: false,
+                               iconCls: 'icon-plus',
+                               baseCls: 'x-plain',
+                               autoScroll: true,
+                               api: {
+                                       submit: model.api.save_form,
+                               },
+                       }), formspec));
+                       callback(form_panel);
+               });
+       },
+       create_edit_form: function (callback, pk, config) {
+               var model = this;
+               var config = config;
+               model.api.get_form({'pk': pk}, function (formspec) {
+                       /*var formspec = formspec;
+                       for (var item_index in formspec.items) {
+                               var item = formspec.items[item_index];
+                               Ext.apply(item, {
+                                       anchor: '100%',
+                               });
+                       }*/
+                       callback(new Gilbert.lib.ui.DjangoForm(Ext.applyIf(Ext.applyIf(config||{},{
+                               title: 'Editing '+model.verbose_name+' ('+pk+')',
+                               header: false,
+                               iconCls: 'icon-pencil',
+                               baseCls: 'x-plain',
+                               autoScroll: true,
+                               api: {
+                                       submit: model.api.save_form,
+                               },
+                               baseParams: {
+                                       pk: pk,
+                               },
+                       }), formspec)));
+               });
+       },
+});
+
+
+Gilbert.lib.plugins.models.ui.DestructionConsequencesWindow = Ext.extend(Ext.Window, {
+       constructor: function (consequences, confirm_handler, cancel_handler, config) {
+               var convert_consequences_array = function (consequences) {
+                       var last_parent = consequences[0];
+                       Ext.each(consequences, function (consequence, index) {
+                               if (index != 0) {
+                                       if (!Ext.isArray(consequence)) {
+                                               last_parent = consequence;
+                                       } else {
+                                               last_parent['children'] = convert_consequences_array(consequence);
+                                               delete consequences[index];
+                                       }
+                               }
+                       });
+                       new_consequences = [];
+                       Ext.each(consequences, function (consequence) {
+                               if (consequence) {
+                                       var new_consequence = {};
+                                       if (!consequence['children']) {
+                                               new_consequence['leaf'] = true;
+                                       } else {
+                                               new_consequence['leaf'] = false;
+                                               new_consequence['children'] = consequence['children'];
+                                       }
+                                       var app_label = consequence['app_label'];
+                                       var name = consequence['name'];
+                                       var model = Gilbert.get_model(app_label, name);
+                                       if (model) {
+                                               new_consequence['text'] = consequence['__unicode__'];
+                                               new_consequence['iconCls'] = model.iconCls;
+                                       } else {
+                                               new_consequence['text'] = '(' + consequence['name'] + ') ' + consequence['__unicode__'];
+                                               new_consequence['iconCls'] = 'icon-block';
+                                       }
+                                       new_consequence['disabled'] = true;
+                                       new_consequences.push(new_consequence);
+                               }
+                       });
+                       return new_consequences;
+               };
+               
+               var tree = this.tree = new Ext.tree.TreePanel({
+                       loader: new Ext.tree.TreeLoader(),
+                       enableDD: false,
+                       animate: false,
+                       trackMouseOver: false,
+                       autoScroll: true,
+                       root: {
+                               'disabled': true,
+                               'text': 'To be deleted',
+                               'iconCls': 'icon-minus',
+                               'leaf': false,
+                               'children': convert_consequences_array(consequences),
+                       },
+                       useArrows: true,
+                       rootVisible: false,
+                       region: 'center',
+               });
+               
+               Gilbert.lib.plugins.models.ui.DestructionConsequencesWindow.superclass.constructor.call(this, Ext.applyIf(config||{}, {
+                       layout: 'border',
+                       width: 300,
+                       height: 300,
+                       modal: true,
+                       title: 'Confirm deletion',
+                       iconCls: 'icon-minus',
+                       items: [
+                               {
+                                       region: 'north',
+                                       xtype: 'panel',
+                                       html: 'Are you sure you want to delete these items?',
+                                       bodyStyle: 'padding: 15px;',
+                               },
+                               tree,
+                       ],
+                       bbar: [
+                               {
+                                       xtype: 'button',
+                                       text: 'Cancel',
+                                       handler: cancel_handler,
+                               },
+                               '->',
+                               {
+                                       xtype: 'button',
+                                       text: 'Confirm',
+                                       handler: confirm_handler,
+                               },
+                       ],
+               }));
+       },
+});
+
+
+Gilbert.lib.plugins.models.ui.ModelPanel = Ext.extend(Ext.Panel, {
+       constructor: function (model, plugin, config) {
+               var model = this.model = model;
+               var plugin = this.plugin = plugin;
+               var application = this.application = plugin.application;
+               var outer = this;
+               
+               var store = this.store = model.create_store({
+                       autoLoad: true,
+                       autoDestroy: true,
+                       autoSave: false,
+                       baseParams: {
+                               start: 0,
+                               limit: 25,
+                       },
+               });
+               
+               var grid = this.grid = new Ext.grid.GridPanel({
+                       ddGroup: model.drag_drop_group,
+                       enableDragDrop: true,
+                       loadMask: true,
+                       store: store,
+                       columns: model.columns,
+                       columnLines: true,
+                       stripeRows: true,
+                       viewConfig: {
+                               forceFit: true,
+                       },
+                       selModel: new Ext.grid.RowSelectionModel({
+                               //singleSelect: true,
+                       }),
+                       bbar: new Ext.PagingToolbar({
+                               pageSize: 25,
+                               store: store,
+                               displayInfo: true,
+                               displayMsg: 'Displaying '+model.verbose_name_plural+' {0} - {1} of {2}',
+                               emptyMsg: 'No '+model.verbose_name_plural+' to display',
+                               items: (function () {
+                                       if (model.searchable) {
+                                               return [
+                                                       {
+                                                               xtype: 'tbseparator',
+                                                       },
+                                                       new Ext.ux.form.SearchField({
+                                                               store: store,
+                                                       }),
+                                               ];
+                                       } else {
+                                               return [];
+                                       }
+                               })(),
+                       }),
+               });
+               
+               var new_action = this.new_action = new Ext.Action({
+                       text: 'New '+model.verbose_name.capfirst(),
+                       iconCls: 'icon-plus',
+                       handler: function () {
+                               plugin.create_instance_window(model, undefined, function (win) {
+                                       win.on('saved', function () {
+                                               store.reload();
+                                       });
+                                       win.show();
+                               });
+                       },
+               });
+               
+               var edit_action = this.edit_action = new Ext.Action({
+                       text: 'Edit',
+                       iconCls: 'icon-pencil',
+                       handler: function () {
+                               Ext.each(grid.getSelectionModel().getSelections(), function (record, index) {
+                                       plugin.create_instance_window(model, record.id, function (win) {
+                                               win.on('saved', function () {
+                                                       store.reload();
+                                               });
+                                               win.show();
+                                       });
+                               });
+                       }
+               });
+               
+               var delete_action = this.delete_action = new Ext.Action({
+                       text: 'Delete',
+                       iconCls: 'icon-minus',
+                       handler: function () {
+                               var records = grid.getSelectionModel().getSelections();
+                               var pks = [];
+                               Ext.each(records, function (record, index) {
+                                       pks.push(record.id);
+                               });
+                               model.api.data_destroy_consequences(pks, function (consequences) {
+                                       var consequences_win = new Gilbert.lib.plugins.models.ui.DestructionConsequencesWindow(consequences, function () {
+                                               consequences_win.close();
+                                               store.remove(records);
+                                               store.save();
+                                               store.reload();
+                                       }, function () {
+                                               consequences_win.close();
+                                       });
+                                       consequences_win.show();
+                               });
+                       }
+               });
+               
+               grid.on('cellcontextmenu', function (grid, rowIndex, cellIndex, e) {
+                       e.stopEvent();
+                       grid.getSelectionModel().selectRow(rowIndex);
+                       var contextmenu = new Ext.menu.Menu({
+                               items: [
+                                       edit_action,
+                                       delete_action,
+                               ],
+                       });
+                       contextmenu.showAt(e.xy);
+               });
+               
+               Gilbert.lib.plugins.models.ui.ModelPanel.superclass.constructor.call(this, Ext.applyIf(config||{}, {
+                       layout: 'fit',
+                       tbar: new Ext.Toolbar({
+                               items: [
+                                       new_action,
+                                       { xtype: 'tbseparator' },
+                                       edit_action,
+                                       { xtype: 'tbseparator' },
+                                       delete_action,
+                                       '->',
+                                       {
+                                               text: 'Advanced',
+                                               iconCls: 'icon-gear',
+                                               disabled: true,
+                                               menu: [],
+                                       },
+                               ],
+                       }),
+                       items: [grid],
+               }));
+       },
+});
+
+
+Gilbert.lib.plugins.models.Plugin = Ext.extend(Gilbert.lib.plugins.Plugin, {
+       
+       init: function (application) {
+               Gilbert.lib.plugins.models.Plugin.superclass.init.call(this, application);
+               
+               var new_menu = this.new_menu = new Ext.menu.Menu();
+               var manage_menu = this.manage_menu = new Ext.menu.Menu();
+               
+               application.mainmenu.insert(2, {
+                       xtype: 'button',
+                       iconCls: 'icon-plus',
+                       text: 'New',
+                       menu: new_menu,
+               });
+               
+               application.mainmenu.insert(3, {
+                       xtype: 'button',
+                       iconCls: 'icon-databases',
+                       text: 'Manage',
+                       menu: manage_menu,
+               });
+               
+               application.do_layout();
+               
+               Ext.iterate(application.models, function (app_label, models) {
+                       Ext.iterate(models, function (name, model) {
+                               this.handle_new_model(model);
+                       }, this);
+               }, this);
+               
+               application.on('model_registered', function (model) {
+                       this.handle_new_model(model);
+               }, this);
+       },
+       
+       handle_new_model: function (model) {
+               var outer = this;
+               model.api.has_add_permission(function (has_add_permission) {
+                       if (has_add_permission) {
+                               outer.add_to_new_menu(model);
+                       }
+               });
+               model.api.has_read_permission(function (has_read_permission) {
+                       if (has_read_permission) {
+                               outer.add_to_manage_menu(model);
+                       }
+               });
+       },
+       
+       add_to_new_menu: function (model) {
+               var outer = this;
+               this.new_menu.add({
+                       text: model.verbose_name.capfirst(),
+                       iconCls: model.iconCls,
+                       model: model,
+                       handler: function (button, event) {
+                               outer.create_instance_window(this.model, undefined, function (win) {
+                                       win.show();
+                               });
+                       },
+               });
+       },
+       
+       add_to_manage_menu: function (model) {
+               var outer = this;
+               this.manage_menu.add({
+                       text: model.verbose_name_plural.capfirst(),
+                       iconCls: model.iconCls,
+                       model: model,
+                       handler: function (button, event) {
+                               var win = outer.create_model_management_window(this.model);
+                               win.show(button.el);
+                       },
+               });
+       },
+       
+       create_model_management_window: function (model, config, cls) {
+               var model = model;
+               var panel = new Gilbert.lib.plugins.models.ui.ModelPanel(model, this);
+               var win = this.application.create_window(Ext.applyIf(config||{},{
+                       layout: 'fit',
+                       title: model.verbose_name_plural.capfirst(),
+                       iconCls: model.iconCls,
+                       width: 640,
+                       height: 320,
+                       maximizable: true,
+                       items: [panel],
+               }), cls);
+               return win;
+       },
+       
+       create_instance_window: function (model, pk, callback, config, cls) {
+               var pk = pk;
+               var callback = callback;
+               var application = this.application;
+               var outer = this;
+               
+               var form_callback = function (form) {
+                       var oldform = form;
+                       var win = application.create_window({
+                               layout: 'fit',
+                               title: form.title,
+                               iconCls: form.iconCls,
+                               bodyStyle: 'padding: 5px; background: solid;',
+                               width: 640,
+                               height: 320,
+                               maximizable: true,
+                               items: [form],
+                               bbar: [
+                                       '->',
+                                       {
+                                               xtype: 'button',
+                                               text: 'Save and Close',
+                                               iconCls: 'icon-database-import',
+                                               handler: function (button) {
+                                                       var loading_mask = new Ext.LoadMask(win.body, {
+                                                               msg: 'Saving...',
+                                                               removeMask: true,
+                                                       });
+                                                       loading_mask.show();
+                                                       win.items.items[0].getForm().submit({
+                                                               success: function (form, action) {
+                                                                       loading_mask.hide();
+                                                                       win.fireEvent('saved');
+                                                                       win.close();
+                                                               },
+                                                               failure: function (form, action) {
+                                                                       loading_mask.hide();
+                                                               },
+                                                       });
+                                               }
+                                       },
+                                       {
+                                               xtype: 'button',
+                                               text: 'Save',
+                                               iconCls: 'icon-database-import',
+                                               handler: function (button) {
+                                                       var loading_mask = new Ext.LoadMask(win.body, {
+                                                               msg: 'Saving...',
+                                                               removeMask: true,
+                                                       });
+                                                       loading_mask.show();
+                                                       win.items.items[0].getForm().submit({
+                                                               success: function (form, action) {
+                                                                       win.fireEvent('saved');
+                                                                       var pk = action.result.pk;
+                                                                       model.create_edit_form(function (newform) {
+                                                                               win.remove(oldform);
+                                                                               win.add(newform);
+                                                                               loading_mask.hide();
+                                                                               win.setTitle(newform.title);
+                                                                               win.setIconClass(newform.iconCls);
+                                                                               win.doLayout();
+                                                                       }, pk);
+                                                               },
+                                                               failure: function (form, action) {
+                                                                       loading_mask.hide();
+                                                               },
+                                                       });
+                                               },
+                                       },
+                               ],
+                       });
+                       win.addEvents({
+                               'saved': true,
+                       });
+                       callback(win);
+               };
+
+               if (pk) {
+                       model.create_edit_form(form_callback, pk, {
+                               bodyStyle: 'padding: 10px;',
+                       });
+               } else {
+                       model.create_new_form(form_callback, {
+                               bodyStyle: 'padding: 10px;',
+                       });
+               }
+       },
+       
+});
+
+
+Gilbert.on('ready', function (application) {
+       application.register_plugin('auth', new Gilbert.lib.plugins.models.Plugin());
+});
diff --git a/contrib/gilbert/media/gilbert/superboxselect/SuperBoxSelect.js b/contrib/gilbert/media/gilbert/superboxselect/SuperBoxSelect.js
new file mode 100755 (executable)
index 0000000..4087666
--- /dev/null
@@ -0,0 +1,1699 @@
+Ext.namespace('Ext.ux.form');\r
+/**\r
+ * <p>SuperBoxSelect is an extension of the ComboBox component that displays selected items as labelled boxes within the form field. As seen on facebook, hotmail and other sites.</p>\r
+ * <p>The SuperBoxSelect component was inspired by the BoxSelect component found here: http://efattal.fr/en/extjs/extuxboxselect/</p>\r
+ * \r
+ * @author <a href="mailto:dan.humphrey@technomedia.co.uk">Dan Humphrey</a>\r
+ * @class Ext.ux.form.SuperBoxSelect\r
+ * @extends Ext.form.ComboBox\r
+ * @constructor\r
+ * @component\r
+ * @version 1.0\r
+ * @license TBA (To be announced)\r
+ * \r
+ */\r
+Ext.ux.form.SuperBoxSelect = function(config) {\r
+    Ext.ux.form.SuperBoxSelect.superclass.constructor.call(this,config);\r
+    this.addEvents(\r
+        /**\r
+         * Fires before an item is added to the component via user interaction. Return false from the callback function to prevent the item from being added.\r
+         * @event beforeadditem\r
+         * @memberOf Ext.ux.form.SuperBoxSelect\r
+         * @param {SuperBoxSelect} this\r
+         * @param {Mixed} value The value of the item to be added\r
+         */\r
+        'beforeadditem',\r
+\r
+        /**\r
+         * Fires after a new item is added to the component.\r
+         * @event additem\r
+         * @memberOf Ext.ux.form.SuperBoxSelect\r
+         * @param {SuperBoxSelect} this\r
+         * @param {Mixed} value The value of the item which was added\r
+         * @param {Record} record The store record which was added\r
+         */\r
+        'additem',\r
+\r
+        /**\r
+         * Fires when the allowAddNewData config is set to true, and a user attempts to add an item that is not in the data store.\r
+         * @event newitem\r
+         * @memberOf Ext.ux.form.SuperBoxSelect\r
+         * @param {SuperBoxSelect} this\r
+         * @param {Mixed} value The new item's value\r
+         */\r
+        'newitem',\r
+\r
+        /**\r
+         * Fires when an item's remove button is clicked. Return false from the callback function to prevent the item from being removed.\r
+         * @event beforeremoveitem\r
+         * @memberOf Ext.ux.form.SuperBoxSelect\r
+         * @param {SuperBoxSelect} this\r
+         * @param {Mixed} value The value of the item to be removed\r
+         */\r
+        'beforeremoveitem',\r
+\r
+        /**\r
+         * Fires after an item has been removed.\r
+         * @event removeitem\r
+         * @memberOf Ext.ux.form.SuperBoxSelect\r
+         * @param {SuperBoxSelect} this\r
+         * @param {Mixed} value The value of the item which was removed\r
+         * @param {Record} record The store record which was removed\r
+         */\r
+        'removeitem',\r
+        /**\r
+         * Fires after the component values have been cleared.\r
+         * @event clear\r
+         * @memberOf Ext.ux.form.SuperBoxSelect\r
+         * @param {SuperBoxSelect} this\r
+         */\r
+        'clear'\r
+    );\r
+    \r
+};\r
+/**\r
+ * @private hide from doc gen\r
+ */\r
+Ext.ux.form.SuperBoxSelect = Ext.extend(Ext.ux.form.SuperBoxSelect,Ext.form.ComboBox,{\r
+    /**\r
+     * @cfg {Boolean} allowAddNewData When set to true, allows items to be added (via the setValueEx and addItem methods) that do not already exist in the data store. Defaults to false.\r
+     */\r
+    allowAddNewData: false,\r
+\r
+    /**\r
+     * @cfg {Boolean} backspaceDeletesLastItem When set to false, the BACKSPACE key will focus the last selected item. When set to true, the last item will be immediately deleted. Defaults to true.\r
+     */\r
+    backspaceDeletesLastItem: true,\r
+\r
+    /**\r
+     * @cfg {String} classField The underlying data field that will be used to supply an additional class to each item.\r
+     */\r
+    classField: null,\r
+\r
+    /**\r
+     * @cfg {String} clearBtnCls An additional class to add to the in-field clear button.\r
+     */\r
+    clearBtnCls: '',\r
+\r
+    /**\r
+     * @cfg {String/XTemplate} displayFieldTpl A template for rendering the displayField in each selected item. Defaults to null.\r
+     */\r
+    displayFieldTpl: null,\r
+\r
+    /**\r
+     * @cfg {String} extraItemCls An additional css class to apply to each item.\r
+     */\r
+    extraItemCls: '',\r
+\r
+    /**\r
+     * @cfg {String/Object/Function} extraItemStyle Additional css style(s) to apply to each item. Should be a valid argument to Ext.Element.applyStyles.\r
+     */\r
+    extraItemStyle: '',\r
+\r
+    /**\r
+     * @cfg {String} expandBtnCls An additional class to add to the in-field expand button.\r
+     */\r
+    expandBtnCls: '',\r
+\r
+    /**\r
+     * @cfg {Boolean} fixFocusOnTabSelect When set to true, the component will not lose focus when a list item is selected with the TAB key. Defaults to true.\r
+     */\r
+    fixFocusOnTabSelect: true,\r
+    \r
+     /**\r
+     * @cfg {Boolean} forceFormValue When set to true, the component will always return a value to the parent form getValues method, and when the parent form is submitted manually. Defaults to false, meaning the component will only be included in the parent form submission (or getValues) if at least 1 item has been selected.  \r
+     */\r
+    forceFormValue: true,\r
+    /**\r
+     * @cfg {Number} itemDelimiterKey The key code which terminates keying in of individual items, and adds the current\r
+     * item to the list. Defaults to the ENTER key.\r
+     */\r
+    itemDelimiterKey: Ext.EventObject.ENTER,    \r
+    /**\r
+     * @cfg {Boolean} navigateItemsWithTab When set to true the tab key will navigate between selected items. Defaults to true.\r
+     */\r
+    navigateItemsWithTab: true,\r
+\r
+    /**\r
+     * @cfg {Boolean} pinList When set to true the select list will be pinned to allow for multiple selections. Defaults to true.\r
+     */\r
+    pinList: true,\r
+\r
+    /**\r
+     * @cfg {Boolean} preventDuplicates When set to true unique item values will be enforced. Defaults to true.\r
+     */\r
+    preventDuplicates: true,\r
+    \r
+    /**\r
+     * @cfg {String} queryValuesDelimiter Used to delimit multiple values queried from the server when mode is remote.\r
+     */\r
+    queryValuesDelimiter: '|',\r
+    \r
+    /**\r
+     * @cfg {String} queryValuesIndicator A request variable that is sent to the server (as true) to indicate that we are querying values rather than display data (as used in autocomplete) when mode is remote.\r
+     */\r
+    queryValuesIndicator: 'valuesqry',\r
+\r
+    /**\r
+     * @cfg {Boolean} removeValuesFromStore When set to true, selected records will be removed from the store. Defaults to true.\r
+     */\r
+    removeValuesFromStore: true,\r
+\r
+    /**\r
+     * @cfg {String} renderFieldBtns When set to true, will render in-field buttons for clearing the component, and displaying the list for selection. Defaults to true.\r
+     */\r
+    renderFieldBtns: true,\r
+\r
+    /**\r
+     * @cfg {Boolean} stackItems When set to true, the items will be stacked 1 per line. Defaults to false which displays the items inline.\r
+     */\r
+    stackItems: false,\r
+\r
+    /**\r
+     * @cfg {String} styleField The underlying data field that will be used to supply additional css styles to each item.\r
+     */\r
+    styleField : null,\r
+    \r
+     /**\r
+     * @cfg {Boolean} supressClearValueRemoveEvents When true, the removeitem event will not be fired for each item when the clearValue method is called, or when the clear button is used. Defaults to false.\r
+     */\r
+    supressClearValueRemoveEvents : false,\r
+    \r
+    /**\r
+     * @cfg {String/Boolean} validationEvent The event that should initiate field validation. Set to false to disable automatic validation (defaults to 'blur').\r
+     */\r
+       validationEvent : 'blur',\r
+       \r
+    /**\r
+     * @cfg {String} valueDelimiter The delimiter to use when joining and splitting value arrays and strings.\r
+     */\r
+    valueDelimiter: ',',\r
+    initComponent:function() {\r
+       Ext.apply(this, {\r
+            items           : new Ext.util.MixedCollection(false),\r
+            usedRecords     : new Ext.util.MixedCollection(false),\r
+            addedRecords       : [],\r
+            remoteLookup       : [],\r
+            hideTrigger     : true,\r
+            grow            : false,\r
+            resizable       : false,\r
+            multiSelectMode : false,\r
+            preRenderValue  : null\r
+        });\r
+        \r
+        if(this.transform){\r
+            this.doTransform();\r
+        }\r
+        if(this.forceFormValue){\r
+               this.items.on({\r
+                  add: this.manageNameAttribute,\r
+                  remove: this.manageNameAttribute,\r
+                  clear: this.manageNameAttribute,\r
+                  scope: this\r
+               });\r
+        }\r
+        \r
+        Ext.ux.form.SuperBoxSelect.superclass.initComponent.call(this);\r
+        if(this.mode === 'remote' && this.store){\r
+               this.store.on('load', this.onStoreLoad, this);\r
+        }\r
+    },\r
+    onRender:function(ct, position) {\r
+       var h = this.hiddenName;\r
+       this.hiddenName = null;\r
+        Ext.ux.form.SuperBoxSelect.superclass.onRender.call(this, ct, position);\r
+        this.hiddenName = h;\r
+        this.manageNameAttribute();\r
+       \r
+        var extraClass = (this.stackItems === true) ? 'x-superboxselect-stacked' : '';\r
+        if(this.renderFieldBtns){\r
+            extraClass += ' x-superboxselect-display-btns';\r
+        }\r
+        this.el.removeClass('x-form-text').addClass('x-superboxselect-input-field');\r
+        \r
+        this.wrapEl = this.el.wrap({\r
+            tag : 'ul'\r
+        });\r
+        \r
+        this.outerWrapEl = this.wrapEl.wrap({\r
+            tag : 'div',\r
+            cls: 'x-form-text x-superboxselect ' + extraClass\r
+        });\r
+       \r
+        this.inputEl = this.el.wrap({\r
+            tag : 'li',\r
+            cls : 'x-superboxselect-input'\r
+        });\r
+        \r
+        if(this.renderFieldBtns){\r
+            this.setupFieldButtons().manageClearBtn();\r
+        }\r
+        \r
+        this.setupFormInterception();\r
+    },\r
+    onStoreLoad : function(store, records, options){\r
+       //accomodating for bug in Ext 3.0.0 where options.params are empty\r
+       var q = options.params[this.queryParam] || store.baseParams[this.queryParam] || "",\r
+               isValuesQuery = options.params[this.queryValuesIndicator] || store.baseParams[this.queryValuesIndicator];\r
+       \r
+       if(this.removeValuesFromStore){\r
+               this.store.each(function(record) {\r
+                               if(this.usedRecords.containsKey(record.get(this.valueField))){\r
+                                       this.store.remove(record);\r
+                               }\r
+                       }, this);\r
+       }\r
+       //queried values\r
+       if(isValuesQuery){\r
+               var params = q.split(this.queryValuesDelimiter);\r
+               Ext.each(params,function(p){\r
+                       this.remoteLookup.remove(p);\r
+                       var rec = this.findRecord(this.valueField,p);\r
+                       if(rec){\r
+                               this.addRecord(rec);\r
+                       }\r
+               },this);\r
+               \r
+               if(this.setOriginal){\r
+                       this.setOriginal = false;\r
+                       this.originalValue = this.getValue();\r
+               }\r
+       }\r
+\r
+       //queried display (autocomplete) & addItem\r
+       if(q !== '' && this.allowAddNewData){\r
+               Ext.each(this.remoteLookup,function(r){\r
+                       if(typeof r == "object" && r[this.displayField] == q){\r
+                               this.remoteLookup.remove(r);\r
+                                       if(records.length && records[0].get(this.displayField) === q) {\r
+                                               this.addRecord(records[0]);\r
+                                               return;\r
+                                       }\r
+                                       var rec = this.createRecord(r);\r
+                                       this.store.add(rec);\r
+                               this.addRecord(rec);\r
+                               this.addedRecords.push(rec); //keep track of records added to store\r
+                               (function(){\r
+                                       if(this.isExpanded()){\r
+                                               this.collapse();\r
+                                       }\r
+                               }).defer(10,this);\r
+                               return;\r
+                       }\r
+               },this);\r
+       }\r
+       \r
+       var toAdd = [];\r
+       if(q === ''){\r
+               Ext.each(this.addedRecords,function(rec){\r
+                       if(this.preventDuplicates && this.usedRecords.containsKey(rec.get(this.valueField))){\r
+                                       return;                         \r
+                       }\r
+                       toAdd.push(rec);\r
+                       \r
+               },this);\r
+               \r
+       }else{\r
+               var re = new RegExp(Ext.escapeRe(q) + '.*','i');\r
+               Ext.each(this.addedRecords,function(rec){\r
+                       if(this.preventDuplicates && this.usedRecords.containsKey(rec.get(this.valueField))){\r
+                                       return;                         \r
+                       }\r
+                       if(re.test(rec.get(this.displayField))){\r
+                               toAdd.push(rec);\r
+                       }\r
+               },this);\r
+           }\r
+       this.store.add(toAdd);\r
+       this.store.sort(this.displayField, 'ASC');\r
+       \r
+               if(this.store.getCount() === 0 && this.isExpanded()){\r
+                       this.collapse();\r
+               }\r
+               \r
+       },\r
+    doTransform : function() {\r
+       var s = Ext.getDom(this.transform), transformValues = [];\r
+            if(!this.store){\r
+                this.mode = 'local';\r
+                var d = [], opts = s.options;\r
+                for(var i = 0, len = opts.length;i < len; i++){\r
+                    var o = opts[i], oe = Ext.get(o),\r
+                        value = oe.getAttributeNS(null,'value') || '',\r
+                        cls = oe.getAttributeNS(null,'className') || '',\r
+                        style = oe.getAttributeNS(null,'style') || '';\r
+                    if(o.selected) {\r
+                        transformValues.push(value);\r
+                    }\r
+                    d.push([value, o.text, cls, typeof(style) === "string" ? style : style.cssText]);\r
+                }\r
+                this.store = new Ext.data.SimpleStore({\r
+                    'id': 0,\r
+                    fields: ['value', 'text', 'cls', 'style'],\r
+                    data : d\r
+                });\r
+                Ext.apply(this,{\r
+                    valueField: 'value',\r
+                    displayField: 'text',\r
+                    classField: 'cls',\r
+                    styleField: 'style'\r
+                });\r
+            }\r
+           \r
+            if(transformValues.length){\r
+                this.value = transformValues.join(',');\r
+            }\r
+    },\r
+    setupFieldButtons : function(){\r
+        this.buttonWrap = this.outerWrapEl.createChild({\r
+            cls: 'x-superboxselect-btns'\r
+        });\r
+        \r
+        this.buttonClear = this.buttonWrap.createChild({\r
+            tag:'div',\r
+            cls: 'x-superboxselect-btn-clear ' + this.clearBtnCls\r
+        });\r
+        \r
+        this.buttonExpand = this.buttonWrap.createChild({\r
+            tag:'div',\r
+            cls: 'x-superboxselect-btn-expand ' + this.expandBtnCls\r
+        });\r
+        \r
+        this.initButtonEvents();\r
+        \r
+        return this;\r
+    },\r
+    initButtonEvents : function() {\r
+        this.buttonClear.addClassOnOver('x-superboxselect-btn-over').on('click', function(e) {\r
+            e.stopEvent();\r
+            if (this.disabled) {\r
+                return;\r
+            }\r
+            this.clearValue();\r
+            this.el.focus();\r
+        }, this);\r
+\r
+        this.buttonExpand.addClassOnOver('x-superboxselect-btn-over').on('click', function(e) {\r
+            e.stopEvent();\r
+            if (this.disabled) {\r
+                return;\r
+            }\r
+            if (this.isExpanded()) {\r
+                this.multiSelectMode = false;\r
+            } else if (this.pinList) {\r
+                this.multiSelectMode = true;\r
+            }\r
+            this.onTriggerClick();\r
+        }, this);\r
+    },\r
+    removeButtonEvents : function() {\r
+        this.buttonClear.removeAllListeners();\r
+        this.buttonExpand.removeAllListeners();\r
+        return this;\r
+    },\r
+    clearCurrentFocus : function(){\r
+        if(this.currentFocus){\r
+            this.currentFocus.onLnkBlur();\r
+            this.currentFocus = null;\r
+        }  \r
+        return this;        \r
+    },\r
+    initEvents : function() {\r
+        var el = this.el;\r
+\r
+        el.on({\r
+            click   : this.onClick,\r
+            focus   : this.clearCurrentFocus,\r
+            blur    : this.onBlur,\r
+\r
+            keydown : this.onKeyDownHandler,\r
+            keyup   : this.onKeyUpBuffered,\r
+\r
+            scope   : this\r
+        });\r
+\r
+        this.on({\r
+            collapse: this.onCollapse,\r
+            expand: this.clearCurrentFocus,\r
+            scope: this\r
+        });\r
+\r
+        this.wrapEl.on('click', this.onWrapClick, this);\r
+        this.outerWrapEl.on('click', this.onWrapClick, this);\r
+        \r
+        this.inputEl.focus = function() {\r
+            el.focus();\r
+        };\r
+\r
+        Ext.ux.form.SuperBoxSelect.superclass.initEvents.call(this);\r
+\r
+        Ext.apply(this.keyNav, {\r
+            tab: function(e) {\r
+                if (this.fixFocusOnTabSelect && this.isExpanded()) {\r
+                    e.stopEvent();\r
+                    el.blur();\r
+                    this.onViewClick(false);\r
+                    this.focus(false, 10);\r
+                    return true;\r
+                }\r
+\r
+                this.onViewClick(false);\r
+                if (el.dom.value !== '') {\r
+                    this.setRawValue('');\r
+                }\r
+\r
+                return true;\r
+            },\r
+\r
+            down: function(e) {\r
+                if (!this.isExpanded() && !this.currentFocus) {\r
+                    this.onTriggerClick();\r
+                } else {\r
+                    this.inKeyMode = true;\r
+                    this.selectNext();\r
+                }\r
+            },\r
+\r
+            enter: function(){}\r
+        });\r
+    },\r
+\r
+    onClick: function() {\r
+        this.clearCurrentFocus();\r
+        this.collapse();\r
+        this.autoSize();\r
+    },\r
+\r
+    beforeBlur: Ext.form.ComboBox.superclass.beforeBlur,\r
+\r
+    onFocus: function() {\r
+        this.outerWrapEl.addClass(this.focusClass);\r
+\r
+        Ext.ux.form.SuperBoxSelect.superclass.onFocus.call(this);\r
+    },\r
+\r
+    onBlur: function() {\r
+        this.outerWrapEl.removeClass(this.focusClass);\r
+\r
+        this.clearCurrentFocus();\r
+\r
+        if (this.el.dom.value !== '') {\r
+            this.applyEmptyText();\r
+            this.autoSize();\r
+        }\r
+\r
+        Ext.ux.form.SuperBoxSelect.superclass.onBlur.call(this);\r
+    },\r
+\r
+    onCollapse: function() {\r
+       this.view.clearSelections();\r
+        this.multiSelectMode = false;\r
+    },\r
+\r
+    onWrapClick: function(e) {\r
+        e.stopEvent();\r
+        this.collapse();\r
+        this.el.focus();\r
+        this.clearCurrentFocus();\r
+    },\r
+    markInvalid : function(msg) {\r
+        var elp, t;\r
+\r
+        if (!this.rendered || this.preventMark ) {\r
+            return;\r
+        }\r
+        this.outerWrapEl.addClass(this.invalidClass);\r
+        msg = msg || this.invalidText;\r
+\r
+        switch (this.msgTarget) {\r
+            case 'qtip':\r
+                Ext.apply(this.el.dom, {\r
+                    qtip    : msg,\r
+                    qclass  : 'x-form-invalid-tip'\r
+                });\r
+                Ext.apply(this.wrapEl.dom, {\r
+                    qtip    : msg,\r
+                    qclass  : 'x-form-invalid-tip'\r
+                });\r
+                if (Ext.QuickTips) { // fix for floating editors interacting with DND\r
+                    Ext.QuickTips.enable();\r
+                }\r
+                break;\r
+            case 'title':\r
+                this.el.dom.title = msg;\r
+                this.wrapEl.dom.title = msg;\r
+                this.outerWrapEl.dom.title = msg;\r
+                break;\r
+            case 'under':\r
+                if (!this.errorEl) {\r
+                    elp = this.getErrorCt();\r
+                    if (!elp) { // field has no container el\r
+                        this.el.dom.title = msg;\r
+                        break;\r
+                    }\r
+                    this.errorEl = elp.createChild({cls:'x-form-invalid-msg'});\r
+                    this.errorEl.setWidth(elp.getWidth(true) - 20);\r
+                }\r
+                this.errorEl.update(msg);\r
+                Ext.form.Field.msgFx[this.msgFx].show(this.errorEl, this);\r
+                break;\r
+            case 'side':\r
+                if (!this.errorIcon) {\r
+                    elp = this.getErrorCt();\r
+                    if (!elp) { // field has no container el\r
+                        this.el.dom.title = msg;\r
+                        break;\r
+                    }\r
+                    this.errorIcon = elp.createChild({cls:'x-form-invalid-icon'});\r
+                }\r
+                this.alignErrorIcon();\r
+                Ext.apply(this.errorIcon.dom, {\r
+                    qtip    : msg,\r
+                    qclass  : 'x-form-invalid-tip'\r
+                });\r
+                this.errorIcon.show();\r
+                this.on('resize', this.alignErrorIcon, this);\r
+                break;\r
+            default:\r
+                t = Ext.getDom(this.msgTarget);\r
+                t.innerHTML = msg;\r
+                t.style.display = this.msgDisplay;\r
+                break;\r
+        }\r
+        this.fireEvent('invalid', this, msg);\r
+    },\r
+    clearInvalid : function(){\r
+        if(!this.rendered || this.preventMark){ // not rendered\r
+            return;\r
+        }\r
+        this.outerWrapEl.removeClass(this.invalidClass);\r
+        switch(this.msgTarget){\r
+            case 'qtip':\r
+                this.el.dom.qtip = '';\r
+                this.wrapEl.dom.qtip ='';\r
+                break;\r
+            case 'title':\r
+                this.el.dom.title = '';\r
+                this.wrapEl.dom.title = '';\r
+                this.outerWrapEl.dom.title = '';\r
+                break;\r
+            case 'under':\r
+                if(this.errorEl){\r
+                    Ext.form.Field.msgFx[this.msgFx].hide(this.errorEl, this);\r
+                }\r
+                break;\r
+            case 'side':\r
+                if(this.errorIcon){\r
+                    this.errorIcon.dom.qtip = '';\r
+                    this.errorIcon.hide();\r
+                    this.un('resize', this.alignErrorIcon, this);\r
+                }\r
+                break;\r
+            default:\r
+                var t = Ext.getDom(this.msgTarget);\r
+                t.innerHTML = '';\r
+                t.style.display = 'none';\r
+                break;\r
+        }\r
+        this.fireEvent('valid', this);\r
+    },\r
+    alignErrorIcon : function(){\r
+        if(this.wrap){\r
+            this.errorIcon.alignTo(this.wrap, 'tl-tr', [Ext.isIE ? 5 : 2, 3]);\r
+        }\r
+    },\r
+    expand : function(){\r
+        if (this.isExpanded() || !this.hasFocus) {\r
+            return;\r
+        }\r
+        this.list.alignTo(this.outerWrapEl, this.listAlign).show();\r
+        this.innerList.setOverflow('auto'); // necessary for FF 2.0/Mac\r
+        Ext.getDoc().on({\r
+            mousewheel: this.collapseIf,\r
+            mousedown: this.collapseIf,\r
+            scope: this\r
+        });\r
+        this.fireEvent('expand', this);\r
+    },\r
+    restrictHeight : function(){\r
+        var inner = this.innerList.dom,\r
+            st = inner.scrollTop, \r
+            list = this.list;\r
+        \r
+        inner.style.height = '';\r
+        \r
+        var pad = list.getFrameWidth('tb')+(this.resizable?this.handleHeight:0)+this.assetHeight,\r
+            h = Math.max(inner.clientHeight, inner.offsetHeight, inner.scrollHeight),\r
+            ha = this.getPosition()[1]-Ext.getBody().getScroll().top,\r
+            hb = Ext.lib.Dom.getViewHeight()-ha-this.getSize().height,\r
+            space = Math.max(ha, hb, this.minHeight || 0)-list.shadowOffset-pad-5;\r
+        \r
+        h = Math.min(h, space, this.maxHeight);\r
+        this.innerList.setHeight(h);\r
+\r
+        list.beginUpdate();\r
+        list.setHeight(h+pad);\r
+        list.alignTo(this.outerWrapEl, this.listAlign);\r
+        list.endUpdate();\r
+        \r
+        if(this.multiSelectMode){\r
+            inner.scrollTop = st;\r
+        }\r
+    },\r
+    \r
+    validateValue: function(val){\r
+        if(this.items.getCount() === 0){\r
+             if(this.allowBlank){\r
+                 this.clearInvalid();\r
+                 return true;\r
+             }else{\r
+                 this.markInvalid(this.blankText);\r
+                 return false;\r
+             }\r
+        }\r
+        \r
+        this.clearInvalid();\r
+        return true;\r
+    },\r
+\r
+    manageNameAttribute :  function(){\r
+       if(this.items.getCount() === 0 && this.forceFormValue){\r
+          this.el.dom.setAttribute('name', this.hiddenName || this.name);\r
+       }else{\r
+               this.el.dom.removeAttribute('name');\r
+       }\r
+    },\r
+    setupFormInterception : function(){\r
+        var form;\r
+        this.findParentBy(function(p){ \r
+            if(p.getForm){\r
+                form = p.getForm();\r
+            }\r
+        });\r
+        if(form){\r
+               \r
+               var formGet = form.getValues;\r
+            form.getValues = function(asString){\r
+                this.el.dom.disabled = true;\r
+                var oldVal = this.el.dom.value;\r
+                this.setRawValue('');\r
+                var vals = formGet.call(form);\r
+                this.el.dom.disabled = false;\r
+                this.setRawValue(oldVal);\r
+                if(this.forceFormValue && this.items.getCount() === 0){\r
+                       vals[this.name] = '';\r
+                }\r
+                return asString ? Ext.urlEncode(vals) : vals ;\r
+            }.createDelegate(this);\r
+        }\r
+    },\r
+    onResize : function(w, h, rw, rh) {\r
+        var reduce = Ext.isIE6 ? 4 : Ext.isIE7 ? 1 : Ext.isIE8 ? 1 : 0;\r
+        if(this.wrapEl){\r
+            this._width = w;\r
+            this.outerWrapEl.setWidth(w - reduce);\r
+            if (this.renderFieldBtns) {\r
+                reduce += (this.buttonWrap.getWidth() + 20);\r
+                this.wrapEl.setWidth(w - reduce);\r
+        }\r
+        }\r
+        Ext.ux.form.SuperBoxSelect.superclass.onResize.call(this, w, h, rw, rh);\r
+        this.autoSize();\r
+    },\r
+    onEnable: function(){\r
+        Ext.ux.form.SuperBoxSelect.superclass.onEnable.call(this);\r
+        this.items.each(function(item){\r
+            item.enable();\r
+        });\r
+        if (this.renderFieldBtns) {\r
+            this.initButtonEvents();\r
+        }\r
+    },\r
+    onDisable: function(){\r
+        Ext.ux.form.SuperBoxSelect.superclass.onDisable.call(this);\r
+        this.items.each(function(item){\r
+            item.disable();\r
+        });\r
+        if(this.renderFieldBtns){\r
+            this.removeButtonEvents();\r
+        }\r
+    },\r
+    /**\r
+     * Clears all values from the component.\r
+     * @methodOf Ext.ux.form.SuperBoxSelect\r
+     * @name clearValue\r
+     * @param {Boolean} supressRemoveEvent [Optional] When true, the 'removeitem' event will not fire for each item that is removed.    \r
+     */\r
+    clearValue : function(supressRemoveEvent){\r
+        Ext.ux.form.SuperBoxSelect.superclass.clearValue.call(this);\r
+        this.preventMultipleRemoveEvents = supressRemoveEvent || this.supressClearValueRemoveEvents || false;\r
+       this.removeAllItems();\r
+       this.preventMultipleRemoveEvents = false;\r
+        this.fireEvent('clear',this);\r
+        return this;\r
+    },\r
+    onKeyUp : function(e) {\r
+        if (this.editable !== false && (!e.isSpecialKey() || e.getKey() === e.BACKSPACE) && e.getKey() !== this.itemDelimiterKey && (!e.hasModifier() || e.shiftKey)) {\r
+            this.lastKey = e.getKey();\r
+            this.dqTask.delay(this.queryDelay);\r
+        }        \r
+    },\r
+    onKeyDownHandler : function(e,t) {\r
+               \r
+        var toDestroy,nextFocus,idx;\r
+        if ((e.getKey() === e.DELETE || e.getKey() === e.SPACE) && this.currentFocus){\r
+            e.stopEvent();\r
+            toDestroy = this.currentFocus;\r
+            this.on('expand',function(){this.collapse();},this,{single: true});\r
+            idx = this.items.indexOfKey(this.currentFocus.key);\r
+            \r
+            this.clearCurrentFocus();\r
+            \r
+            if(idx < (this.items.getCount() -1)){\r
+                nextFocus = this.items.itemAt(idx+1);\r
+            }\r
+            \r
+            toDestroy.preDestroy(true);\r
+            if(nextFocus){\r
+                (function(){\r
+                    nextFocus.onLnkFocus();\r
+                    this.currentFocus = nextFocus;\r
+                }).defer(200,this);\r
+            }\r
+        \r
+            return true;\r
+        }\r
+        \r
+        var val = this.el.dom.value, it, ctrl = e.ctrlKey;\r
+        if(e.getKey() === this.itemDelimiterKey){\r
+            e.stopEvent();\r
+            if (val !== "") {\r
+                if (ctrl || !this.isExpanded())  {  //ctrl+enter for new items\r
+                       this.view.clearSelections();\r
+                    this.collapse();\r
+                    this.setRawValue('');\r
+                    this.fireEvent('newitem', this, val);\r
+                }\r
+                else {\r
+                       this.onViewClick();\r
+                    //removed from 3.0.1\r
+                    if(this.unsetDelayCheck){\r
+                        this.delayedCheck = true;\r
+                        this.unsetDelayCheck.defer(10, this);\r
+                    }\r
+                }\r
+            }else{\r
+                if(!this.isExpanded()){\r
+                    return;\r
+                }\r
+                this.onViewClick();\r
+                //removed from 3.0.1\r
+                if(this.unsetDelayCheck){\r
+                    this.delayedCheck = true;\r
+                    this.unsetDelayCheck.defer(10, this);\r
+                }\r
+            }\r
+            return true;\r
+        }\r
+        \r
+        if(val !== '') {\r
+            this.autoSize();\r
+            return;\r
+        }\r
+        \r
+        //select first item\r
+        if(e.getKey() === e.HOME){\r
+            e.stopEvent();\r
+            if(this.items.getCount() > 0){\r
+                this.collapse();\r
+                it = this.items.get(0);\r
+                it.el.focus();\r
+                \r
+            }\r
+            return true;\r
+        }\r
+        //backspace remove\r
+        if(e.getKey() === e.BACKSPACE){\r
+            e.stopEvent();\r
+            if(this.currentFocus) {\r
+                toDestroy = this.currentFocus;\r
+                this.on('expand',function(){\r
+                    this.collapse();\r
+                },this,{single: true});\r
+                \r
+                idx = this.items.indexOfKey(toDestroy.key);\r
+                \r
+                this.clearCurrentFocus();\r
+                if(idx < (this.items.getCount() -1)){\r
+                    nextFocus = this.items.itemAt(idx+1);\r
+                }\r
+                \r
+                toDestroy.preDestroy(true);\r
+                \r
+                if(nextFocus){\r
+                    (function(){\r
+                        nextFocus.onLnkFocus();\r
+                        this.currentFocus = nextFocus;\r
+                    }).defer(200,this);\r
+                }\r
+                \r
+                return;\r
+            }else{\r
+                it = this.items.get(this.items.getCount() -1);\r
+                if(it){\r
+                    if(this.backspaceDeletesLastItem){\r
+                        this.on('expand',function(){this.collapse();},this,{single: true});\r
+                        it.preDestroy(true);\r
+                    }else{\r
+                        if(this.navigateItemsWithTab){\r
+                            it.onElClick();\r
+                        }else{\r
+                            this.on('expand',function(){\r
+                                this.collapse();\r
+                                this.currentFocus = it;\r
+                                this.currentFocus.onLnkFocus.defer(20,this.currentFocus);\r
+                            },this,{single: true});\r
+                        }\r
+                    }\r
+                }\r
+                return true;\r
+            }\r
+        }\r
+        \r
+        if(!e.isNavKeyPress()){\r
+            this.multiSelectMode = false;\r
+            this.clearCurrentFocus();\r
+            return;\r
+        }\r
+        //arrow nav\r
+        if(e.getKey() === e.LEFT || (e.getKey() === e.UP && !this.isExpanded())){\r
+            e.stopEvent();\r
+            this.collapse();\r
+            //get last item\r
+            it = this.items.get(this.items.getCount()-1);\r
+            if(this.navigateItemsWithTab){ \r
+                //focus last el\r
+                if(it){\r
+                    it.focus(); \r
+                }\r
+            }else{\r
+                //focus prev item\r
+                if(this.currentFocus){\r
+                    idx = this.items.indexOfKey(this.currentFocus.key);\r
+                    this.clearCurrentFocus();\r
+                    \r
+                    if(idx !== 0){\r
+                        this.currentFocus = this.items.itemAt(idx-1);\r
+                        this.currentFocus.onLnkFocus();\r
+                    }\r
+                }else{\r
+                    this.currentFocus = it;\r
+                    if(it){\r
+                        it.onLnkFocus();\r
+                    }\r
+                }\r
+            }\r
+            return true;\r
+        }\r
+        if(e.getKey() === e.DOWN){\r
+            if(this.currentFocus){\r
+                this.collapse();\r
+                e.stopEvent();\r
+                idx = this.items.indexOfKey(this.currentFocus.key);\r
+                if(idx == (this.items.getCount() -1)){\r
+                    this.clearCurrentFocus.defer(10,this);\r
+                }else{\r
+                    this.clearCurrentFocus();\r
+                    this.currentFocus = this.items.itemAt(idx+1);\r
+                    if(this.currentFocus){\r
+                        this.currentFocus.onLnkFocus();\r
+                    }\r
+                }\r
+                return true;\r
+            }\r
+        }\r
+        if(e.getKey() === e.RIGHT){\r
+            this.collapse();\r
+            it = this.items.itemAt(0);\r
+            if(this.navigateItemsWithTab){ \r
+                //focus first el\r
+                if(it){\r
+                    it.focus(); \r
+                }\r
+            }else{\r
+                if(this.currentFocus){\r
+                    idx = this.items.indexOfKey(this.currentFocus.key);\r
+                    this.clearCurrentFocus();\r
+                    if(idx < (this.items.getCount() -1)){\r
+                        this.currentFocus = this.items.itemAt(idx+1);\r
+                        if(this.currentFocus){\r
+                            this.currentFocus.onLnkFocus();\r
+                        }\r
+                    }\r
+                }else{\r
+                    this.currentFocus = it;\r
+                    if(it){\r
+                        it.onLnkFocus();\r
+                    }\r
+                }\r
+            }\r
+        }\r
+    },\r
+    onKeyUpBuffered : function(e){\r
+        if(!e.isNavKeyPress()){\r
+            this.autoSize();\r
+        }\r
+    },\r
+    reset :  function(){\r
+       this.killItems();\r
+        Ext.ux.form.SuperBoxSelect.superclass.reset.call(this);\r
+        this.addedRecords = [];\r
+        this.autoSize().setRawValue('');\r
+    },\r
+    applyEmptyText : function(){\r
+               this.setRawValue('');\r
+        if(this.items.getCount() > 0){\r
+            this.el.removeClass(this.emptyClass);\r
+            this.setRawValue('');\r
+            return this;\r
+        }\r
+        if(this.rendered && this.emptyText && this.getRawValue().length < 1){\r
+            this.setRawValue(this.emptyText);\r
+            this.el.addClass(this.emptyClass);\r
+        }\r
+        return this;\r
+    },\r
+    /**\r
+     * @private\r
+     * \r
+     * Use clearValue instead\r
+     */\r
+    removeAllItems: function(){\r
+       this.items.each(function(item){\r
+            item.preDestroy(true);\r
+        },this);\r
+        this.manageClearBtn();\r
+        return this;\r
+    },\r
+    killItems : function(){\r
+       this.items.each(function(item){\r
+            item.kill();\r
+        },this);\r
+        this.resetStore();\r
+        this.items.clear();\r
+        this.manageClearBtn();\r
+        return this;\r
+    },\r
+    resetStore: function(){\r
+        this.store.clearFilter();\r
+        if(!this.removeValuesFromStore){\r
+            return this;\r
+        }\r
+        this.usedRecords.each(function(rec){\r
+            this.store.add(rec);\r
+        },this);\r
+        this.usedRecords.clear();\r
+        this.sortStore();\r
+        return this;\r
+    },\r
+    sortStore: function(){\r
+        var ss = this.store.getSortState();\r
+        if(ss && ss.field){\r
+            this.store.sort(ss.field, ss.direction);\r
+        }\r
+        return this;\r
+    },\r
+    getCaption: function(dataObject){\r
+        if(typeof this.displayFieldTpl === 'string') {\r
+            this.displayFieldTpl = new Ext.XTemplate(this.displayFieldTpl);\r
+        }\r
+        var caption, recordData = dataObject instanceof Ext.data.Record ? dataObject.data : dataObject;\r
+      \r
+        if(this.displayFieldTpl) {\r
+            caption = this.displayFieldTpl.apply(recordData);\r
+        } else if(this.displayField) {\r
+            caption = recordData[this.displayField];\r
+        }\r
+        \r
+        return caption;\r
+    },\r
+    addRecord : function(record) {\r
+        var display = record.data[this.displayField],\r
+            caption = this.getCaption(record),\r
+            val = record.data[this.valueField],\r
+            cls = this.classField ? record.data[this.classField] : '',\r
+            style = this.styleField ? record.data[this.styleField] : '';\r
+\r
+        if (this.removeValuesFromStore) {\r
+            this.usedRecords.add(val, record);\r
+            this.store.remove(record);\r
+        }\r
+        \r
+        this.addItemBox(val, display, caption, cls, style);\r
+        this.fireEvent('additem', this, val, record);\r
+    },\r
+    createRecord : function(recordData){\r
+        if(!this.recordConstructor){\r
+            var recordFields = [\r
+                {name: this.valueField},\r
+                {name: this.displayField}\r
+            ];\r
+            if(this.classField){\r
+                recordFields.push({name: this.classField});\r
+            }\r
+            if(this.styleField){\r
+                recordFields.push({name: this.styleField});\r
+            }\r
+            this.recordConstructor = Ext.data.Record.create(recordFields);\r
+        }\r
+        return new this.recordConstructor(recordData);\r
+    },\r
+    /**\r
+     * Adds an array of items to the SuperBoxSelect component if the {@link #Ext.ux.form.SuperBoxSelect-allowAddNewData} config is set to true.\r
+     * @methodOf Ext.ux.form.SuperBoxSelect\r
+     * @name addItem\r
+     * @param {Array} newItemObjects An Array of object literals containing the property names and values for an item. The property names must match those specified in {@link #Ext.ux.form.SuperBoxSelect-displayField}, {@link #Ext.ux.form.SuperBoxSelect-valueField} and {@link #Ext.ux.form.SuperBoxSelect-classField} \r
+     */\r
+    addItems : function(newItemObjects){\r
+       if (Ext.isArray(newItemObjects)) {\r
+                       Ext.each(newItemObjects, function(item) {\r
+                               this.addItem(item);\r
+                       }, this);\r
+               } else {\r
+                       this.addItem(newItemObjects);\r
+               }\r
+    },\r
+    /**\r
+     * Adds a new non-existing item to the SuperBoxSelect component if the {@link #Ext.ux.form.SuperBoxSelect-allowAddNewData} config is set to true.\r
+     * This method should be used in place of addItem from within the newitem event handler.\r
+     * @methodOf Ext.ux.form.SuperBoxSelect\r
+     * @name addNewItem\r
+     * @param {Object} newItemObject An object literal containing the property names and values for an item. The property names must match those specified in {@link #Ext.ux.form.SuperBoxSelect-displayField}, {@link #Ext.ux.form.SuperBoxSelect-valueField} and {@link #Ext.ux.form.SuperBoxSelect-classField} \r
+     */\r
+    addNewItem : function(newItemObject){\r
+       this.addItem(newItemObject,true);\r
+    },\r
+    /**\r
+     * Adds an item to the SuperBoxSelect component if the {@link #Ext.ux.form.SuperBoxSelect-allowAddNewData} config is set to true.\r
+     * @methodOf Ext.ux.form.SuperBoxSelect\r
+     * @name addItem\r
+     * @param {Object} newItemObject An object literal containing the property names and values for an item. The property names must match those specified in {@link #Ext.ux.form.SuperBoxSelect-displayField}, {@link #Ext.ux.form.SuperBoxSelect-valueField} and {@link #Ext.ux.form.SuperBoxSelect-classField} \r
+     */\r
+    addItem : function(newItemObject, /*hidden param*/ forcedAdd){\r
+        \r
+        var val = newItemObject[this.valueField];\r
+\r
+        if(this.disabled) {\r
+            return false;\r
+        }\r
+        if(this.preventDuplicates && this.hasValue(val)){\r
+            return;\r
+        }\r
+        \r
+        //use existing record if found\r
+        var record = this.findRecord(this.valueField, val);\r
+        if (record) {\r
+            this.addRecord(record);\r
+            return;\r
+        } else if (!this.allowAddNewData) { // else it's a new item\r
+            return;\r
+        }\r
+        \r
+        if(this.mode === 'remote'){\r
+               this.remoteLookup.push(newItemObject); \r
+               this.doQuery(val,false,false,forcedAdd);\r
+               return;\r
+        }\r
+        \r
+        var rec = this.createRecord(newItemObject);\r
+        this.store.add(rec);\r
+        this.addRecord(rec);\r
+        \r
+        return true;\r
+    },\r
+    addItemBox : function(itemVal,itemDisplay,itemCaption, itemClass, itemStyle) {\r
+        var hConfig, parseStyle = function(s){\r
+            var ret = '';\r
+            if(typeof s == 'function'){\r
+                ret = s.call();\r
+            }else if(typeof s == 'object'){\r
+                for(var p in s){\r
+                    ret+= p +':'+s[p]+';';\r
+                }\r
+            }else if(typeof s == 'string'){\r
+                ret = s + ';';\r
+            }\r
+            return ret;\r
+        }, itemKey = Ext.id(null,'sbx-item'), box = new Ext.ux.form.SuperBoxSelectItem({\r
+            owner: this,\r
+            disabled: this.disabled,\r
+            renderTo: this.wrapEl,\r
+            cls: this.extraItemCls + ' ' + itemClass,\r
+            style: parseStyle(this.extraItemStyle) + ' ' + itemStyle,\r
+            caption: itemCaption,\r
+            display: itemDisplay,\r
+            value:  itemVal,\r
+            key: itemKey,\r
+            listeners: {\r
+                'remove': function(item){\r
+                    if(this.fireEvent('beforeremoveitem',this,item.value) === false){\r
+                        return;\r
+                    }\r
+                    this.items.removeKey(item.key);\r
+                    if(this.removeValuesFromStore){\r
+                        if(this.usedRecords.containsKey(item.value)){\r
+                            this.store.add(this.usedRecords.get(item.value));\r
+                            this.usedRecords.removeKey(item.value);\r
+                            this.sortStore();\r
+                            if(this.view){\r
+                                this.view.render();\r
+                            }\r
+                        }\r
+                    }\r
+                    if(!this.preventMultipleRemoveEvents){\r
+                       this.fireEvent.defer(250,this,['removeitem',this,item.value, this.findInStore(item.value)]);\r
+                    }\r
+                },\r
+                destroy: function(){\r
+                    this.collapse();\r
+                    this.autoSize().manageClearBtn().validateValue();\r
+                },\r
+                scope: this\r
+            }\r
+        });\r
+        box.render();\r
+        \r
+        hConfig = {\r
+            tag :'input', \r
+            type :'hidden', \r
+            value : itemVal,\r
+            name : (this.hiddenName || this.name)\r
+        };\r
+        \r
+        if(this.disabled){\r
+               Ext.apply(hConfig,{\r
+                  disabled : 'disabled'\r
+               })\r
+        }\r
+        box.hidden = this.el.insertSibling(hConfig,'before');\r
+\r
+        this.items.add(itemKey,box);\r
+        this.applyEmptyText().autoSize().manageClearBtn().validateValue();\r
+    },\r
+    manageClearBtn : function() {\r
+        if (!this.renderFieldBtns || !this.rendered) {\r
+            return this;\r
+        }\r
+        var cls = 'x-superboxselect-btn-hide';\r
+        if (this.items.getCount() === 0) {\r
+            this.buttonClear.addClass(cls);\r
+        } else {\r
+            this.buttonClear.removeClass(cls);\r
+        }\r
+        return this;\r
+    },\r
+    findInStore : function(val){\r
+        var index = this.store.find(this.valueField, val);\r
+        if(index > -1){\r
+            return this.store.getAt(index);\r
+        }\r
+        return false;\r
+    },\r
+    /**\r
+     * Returns a String value containing a concatenated list of item values. The list is concatenated with the {@link #Ext.ux.form.SuperBoxSelect-valueDelimiter}.\r
+     * @methodOf Ext.ux.form.SuperBoxSelect\r
+     * @name getValue\r
+     * @return {String} a String value containing a concatenated list of item values. \r
+     */\r
+    getValue : function() {\r
+        var ret = [];\r
+        this.items.each(function(item){\r
+            ret.push(item.value);\r
+        });\r
+        return ret.join(this.valueDelimiter);\r
+    },\r
+    /**\r
+     * Returns an Array of item objects containing the {@link #Ext.ux.form.SuperBoxSelect-displayField}, {@link #Ext.ux.form.SuperBoxSelect-valueField}, {@link #Ext.ux.form.SuperBoxSelect-classField} and {@link #Ext.ux.form.SuperBoxSelect-styleField} properties.\r
+     * @methodOf Ext.ux.form.SuperBoxSelect\r
+     * @name getValueEx\r
+     * @return {Array} an array of item objects. \r
+     */\r
+    getValueEx : function() {\r
+        var ret = [];\r
+        this.items.each(function(item){\r
+            var newItem = {};\r
+            newItem[this.valueField] = item.value;\r
+            newItem[this.displayField] = item.display;\r
+            if(this.classField){\r
+                newItem[this.classField] = item.cls || '';\r
+            }\r
+            if(this.styleField){\r
+                newItem[this.styleField] = item.style || '';\r
+            }\r
+            ret.push(newItem);\r
+        },this);\r
+        return ret;\r
+    },\r
+    // private\r
+    initValue : function(){\r
\r
+        Ext.ux.form.SuperBoxSelect.superclass.initValue.call(this);\r
+        if(this.mode === 'remote') {\r
+               this.setOriginal = true;\r
+        }\r
+    },\r
+    /**\r
+     * Sets the value of the SuperBoxSelect component.\r
+     * @methodOf Ext.ux.form.SuperBoxSelect\r
+     * @name setValue\r
+     * @param {String|Array} value An array of item values, or a String value containing a delimited list of item values. (The list should be delimited with the {@link #Ext.ux.form.SuperBoxSelect-valueDelimiter) \r
+     */\r
+    setValue : function(value){\r
+        if(!this.rendered){\r
+            this.value = value;\r
+            return;\r
+        }\r
+            \r
+        this.removeAllItems().resetStore();\r
+        this.remoteLookup = [];\r
+        \r
+        if(Ext.isEmpty(value)){\r
+               return;\r
+        }\r
+        \r
+        var values = value;\r
+        if(!Ext.isArray(value)){\r
+            value = '' + value;\r
+            values = value.split(this.valueDelimiter); \r
+        }\r
+        \r
+        Ext.each(values,function(val){\r
+            var record = this.findRecord(this.valueField, val);\r
+            if(record){\r
+                this.addRecord(record);\r
+            }else if(this.mode === 'remote'){\r
+                               this.remoteLookup.push(val);                    \r
+            }\r
+        },this);\r
+        \r
+        if(this.mode === 'remote'){\r
+               var q = this.remoteLookup.join(this.queryValuesDelimiter); \r
+               this.doQuery(q,false, true); //3rd param to specify a values query\r
+        }\r
+        \r
+    },\r
+    /**\r
+     * Sets the value of the SuperBoxSelect component, adding new items that don't exist in the data store if the {@link #Ext.ux.form.SuperBoxSelect-allowAddNewData} config is set to true.\r
+     * @methodOf Ext.ux.form.SuperBoxSelect\r
+     * @name setValue\r
+     * @param {Array} data An Array of item objects containing the {@link #Ext.ux.form.SuperBoxSelect-displayField}, {@link #Ext.ux.form.SuperBoxSelect-valueField} and {@link #Ext.ux.form.SuperBoxSelect-classField} properties.  \r
+     */\r
+    setValueEx : function(data){\r
+        this.removeAllItems().resetStore();\r
+        \r
+        if(!Ext.isArray(data)){\r
+            data = [data];\r
+        }\r
+        this.remoteLookup = [];\r
+        \r
+        if(this.allowAddNewData && this.mode === 'remote'){ // no need to query\r
+            Ext.each(data, function(d){\r
+               var r = this.findRecord(this.valueField, d[this.valueField]) || this.createRecord(d);\r
+                this.addRecord(r);\r
+            },this);\r
+            return;\r
+        }\r
+        \r
+        Ext.each(data,function(item){\r
+            this.addItem(item);\r
+        },this);\r
+    },\r
+    /**\r
+     * Returns true if the SuperBoxSelect component has a selected item with a value matching the 'val' parameter.\r
+     * @methodOf Ext.ux.form.SuperBoxSelect\r
+     * @name hasValue\r
+     * @param {Mixed} val The value to test.\r
+     * @return {Boolean} true if the component has the selected value, false otherwise.\r
+     */\r
+    hasValue: function(val){\r
+        var has = false;\r
+        this.items.each(function(item){\r
+            if(item.value == val){\r
+                has = true;\r
+                return false;\r
+            }\r
+        },this);\r
+        return has;\r
+    },\r
+    onSelect : function(record, index) {\r
+       if (this.fireEvent('beforeselect', this, record, index) !== false){\r
+            var val = record.data[this.valueField];\r
+            \r
+            if(this.preventDuplicates && this.hasValue(val)){\r
+                return;\r
+            }\r
+            \r
+            this.setRawValue('');\r
+            this.lastSelectionText = '';\r
+            \r
+            if(this.fireEvent('beforeadditem',this,val) !== false){\r
+                this.addRecord(record);\r
+            }\r
+            if(this.store.getCount() === 0 || !this.multiSelectMode){\r
+                this.collapse();\r
+            }else{\r
+                this.restrictHeight();\r
+            }\r
+       }\r
+    },\r
+    onDestroy : function() {\r
+        this.items.purgeListeners();\r
+        this.killItems();\r
+        if (this.renderFieldBtns) {\r
+            Ext.destroy(\r
+                this.buttonClear,\r
+                this.buttonExpand,\r
+                this.buttonWrap\r
+            );\r
+        }\r
+\r
+        Ext.destroy(\r
+            this.inputEl,\r
+            this.wrapEl,\r
+            this.outerWrapEl\r
+        );\r
+\r
+        Ext.ux.form.SuperBoxSelect.superclass.onDestroy.call(this);\r
+    },\r
+    autoSize : function(){\r
+        if(!this.rendered){\r
+            return this;\r
+        }\r
+        if(!this.metrics){\r
+            this.metrics = Ext.util.TextMetrics.createInstance(this.el);\r
+        }\r
+        var el = this.el,\r
+            v = el.dom.value,\r
+            d = document.createElement('div');\r
+\r
+        if(v === "" && this.emptyText && this.items.getCount() < 1){\r
+            v = this.emptyText;\r
+        }\r
+        d.appendChild(document.createTextNode(v));\r
+        v = d.innerHTML;\r
+        d = null;\r
+        v += "&#160;";\r
+        var w = Math.max(this.metrics.getWidth(v) +  24, 24);\r
+        if(typeof this._width != 'undefined'){\r
+            w = Math.min(this._width, w);\r
+        }\r
+        this.el.setWidth(w);\r
+        \r
+        if(Ext.isIE){\r
+            this.el.dom.style.top='0';\r
+        }\r
+        return this;\r
+    },\r
+    doQuery : function(q, forceAll,valuesQuery, forcedAdd){\r
+        q = Ext.isEmpty(q) ? '' : q;\r
+        var qe = {\r
+            query: q,\r
+            forceAll: forceAll,\r
+            combo: this,\r
+            cancel:false\r
+        };\r
+        if(this.fireEvent('beforequery', qe)===false || qe.cancel){\r
+            return false;\r
+        }\r
+        q = qe.query;\r
+        forceAll = qe.forceAll;\r
+        if(forceAll === true || (q.length >= this.minChars) || valuesQuery && !Ext.isEmpty(q)){\r
+            if(this.lastQuery !== q || forcedAdd){\r
+               this.lastQuery = q;\r
+                if(this.mode == 'local'){\r
+                    this.selectedIndex = -1;\r
+                    if(forceAll){\r
+                        this.store.clearFilter();\r
+                    }else{\r
+                        this.store.filter(this.displayField, q);\r
+                    }\r
+                    this.onLoad();\r
+                }else{\r
+                       \r
+                    this.store.baseParams[this.queryParam] = q;\r
+                    this.store.baseParams[this.queryValuesIndicator] = valuesQuery;\r
+                    this.store.load({\r
+                        params: this.getParams(q)\r
+                    });\r
+                    if(!forcedAdd){\r
+                        this.expand();\r
+                    }\r
+                }\r
+            }else{\r
+                this.selectedIndex = -1;\r
+                this.onLoad();\r
+            }\r
+        }\r
+    }\r
+});\r
+Ext.reg('superboxselect', Ext.ux.form.SuperBoxSelect);\r
+/*\r
+ * @private\r
+ */\r
+Ext.ux.form.SuperBoxSelectItem = function(config){\r
+    Ext.apply(this,config);\r
+    Ext.ux.form.SuperBoxSelectItem.superclass.constructor.call(this); \r
+};\r
+/*\r
+ * @private\r
+ */\r
+Ext.ux.form.SuperBoxSelectItem = Ext.extend(Ext.ux.form.SuperBoxSelectItem,Ext.Component, {\r
+    initComponent : function(){\r
+        Ext.ux.form.SuperBoxSelectItem.superclass.initComponent.call(this); \r
+    },\r
+    onElClick : function(e){\r
+        var o = this.owner;\r
+        o.clearCurrentFocus().collapse();\r
+        if(o.navigateItemsWithTab){\r
+            this.focus();\r
+        }else{\r
+            o.el.dom.focus();\r
+            var that = this;\r
+            (function(){\r
+                this.onLnkFocus();\r
+                o.currentFocus = this;\r
+            }).defer(10,this);\r
+        }\r
+    },\r
+    \r
+    onLnkClick : function(e){\r
+        if(e) {\r
+            e.stopEvent();\r
+        }\r
+        this.preDestroy();\r
+        if(!this.owner.navigateItemsWithTab){\r
+            this.owner.el.focus();\r
+        }\r
+    },\r
+    onLnkFocus : function(){\r
+        this.el.addClass("x-superboxselect-item-focus");\r
+        this.owner.outerWrapEl.addClass("x-form-focus");\r
+    },\r
+    \r
+    onLnkBlur : function(){\r
+        this.el.removeClass("x-superboxselect-item-focus");\r
+        this.owner.outerWrapEl.removeClass("x-form-focus");\r
+    },\r
+    \r
+    enableElListeners : function() {\r
+        this.el.on('click', this.onElClick, this, {stopEvent:true});\r
+       \r
+        this.el.addClassOnOver('x-superboxselect-item x-superboxselect-item-hover');\r
+    },\r
+\r
+    enableLnkListeners : function() {\r
+        this.lnk.on({\r
+            click: this.onLnkClick,\r
+            focus: this.onLnkFocus,\r
+            blur:  this.onLnkBlur,\r
+            scope: this\r
+        });\r
+    },\r
+    \r
+    enableAllListeners : function() {\r
+        this.enableElListeners();\r
+        this.enableLnkListeners();\r
+    },\r
+    disableAllListeners : function() {\r
+        this.el.removeAllListeners();\r
+        this.lnk.un('click', this.onLnkClick, this);\r
+        this.lnk.un('focus', this.onLnkFocus, this);\r
+        this.lnk.un('blur', this.onLnkBlur, this);\r
+    },\r
+    onRender : function(ct, position){\r
+        \r
+        Ext.ux.form.SuperBoxSelectItem.superclass.onRender.call(this, ct, position);\r
+        \r
+        var el = this.el;\r
+        if(el){\r
+            el.remove();\r
+        }\r
+        \r
+        this.el = el = ct.createChild({ tag: 'li' }, ct.last());\r
+        el.addClass('x-superboxselect-item');\r
+        \r
+        var btnEl = this.owner.navigateItemsWithTab ? ( Ext.isSafari ? 'button' : 'a') : 'span';\r
+        var itemKey = this.key;\r
+        \r
+        Ext.apply(el, {\r
+            focus: function(){\r
+                var c = this.down(btnEl +'.x-superboxselect-item-close');\r
+                if(c){\r
+                       c.focus();\r
+                }\r
+            },\r
+            preDestroy: function(){\r
+                this.preDestroy();\r
+            }.createDelegate(this)\r
+        });\r
+        \r
+        this.enableElListeners();\r
+\r
+        el.update(this.caption);\r
+\r
+        var cfg = {\r
+            tag: btnEl,\r
+            'class': 'x-superboxselect-item-close',\r
+            tabIndex : this.owner.navigateItemsWithTab ? '0' : '-1'\r
+        };\r
+        if(btnEl === 'a'){\r
+            cfg.href = '#';\r
+        }\r
+        this.lnk = el.createChild(cfg);\r
+        \r
+        \r
+        if(!this.disabled) {\r
+            this.enableLnkListeners();\r
+        }else {\r
+            this.disableAllListeners();\r
+        }\r
+        \r
+        this.on({\r
+            disable: this.disableAllListeners,\r
+            enable: this.enableAllListeners,\r
+            scope: this\r
+        });\r
+\r
+        this.setupKeyMap();\r
+    },\r
+    setupKeyMap : function(){\r
+        this.keyMap = new Ext.KeyMap(this.lnk, [\r
+            {\r
+                key: [\r
+                    Ext.EventObject.BACKSPACE, \r
+                    Ext.EventObject.DELETE, \r
+                    Ext.EventObject.SPACE\r
+                ],\r
+                fn: this.preDestroy,\r
+                scope: this\r
+            }, {\r
+                key: [\r
+                    Ext.EventObject.RIGHT,\r
+                    Ext.EventObject.DOWN\r
+                ],\r
+                fn: function(){\r
+                    this.moveFocus('right');\r
+                },\r
+                scope: this\r
+            },\r
+            {\r
+                key: [Ext.EventObject.LEFT,Ext.EventObject.UP],\r
+                fn: function(){\r
+                    this.moveFocus('left');\r
+                },\r
+                scope: this\r
+            },\r
+            {\r
+                key: [Ext.EventObject.HOME],\r
+                fn: function(){\r
+                    var l = this.owner.items.get(0).el.focus();\r
+                    if(l){\r
+                        l.el.focus();\r
+                    }\r
+                },\r
+                scope: this\r
+            },\r
+            {\r
+                key: [Ext.EventObject.END],\r
+                fn: function(){\r
+                    this.owner.el.focus();\r
+                },\r
+                scope: this\r
+            },\r
+            {\r
+                key: Ext.EventObject.ENTER,\r
+                fn: function(){\r
+                }\r
+            }\r
+        ]);\r
+        this.keyMap.stopEvent = true;\r
+    },\r
+    moveFocus : function(dir) {\r
+        var el = this.el[dir == 'left' ? 'prev' : 'next']() || this.owner.el;\r
+       \r
+        el.focus.defer(100,el);\r
+    },\r
+\r
+    preDestroy : function(supressEffect) {\r
+       if(this.fireEvent('remove', this) === false){\r
+               return;\r
+           }   \r
+       var actionDestroy = function(){\r
+            if(this.owner.navigateItemsWithTab){\r
+                this.moveFocus('right');\r
+            }\r
+            this.hidden.remove();\r
+            this.hidden = null;\r
+            this.destroy();\r
+        };\r
+        \r
+        if(supressEffect){\r
+            actionDestroy.call(this);\r
+        } else {\r
+            this.el.hide({\r
+                duration: 0.2,\r
+                callback: actionDestroy,\r
+                scope: this\r
+            });\r
+        }\r
+        return this;\r
+    },\r
+    kill : function(){\r
+       this.hidden.remove();\r
+        this.hidden = null;\r
+        this.purgeListeners();\r
+        this.destroy();\r
+    },\r
+    onDisable : function() {\r
+       if(this.hidden){\r
+           this.hidden.dom.setAttribute('disabled', 'disabled');\r
+       }\r
+       this.keyMap.disable();\r
+       Ext.ux.form.SuperBoxSelectItem.superclass.onDisable.call(this);\r
+    },\r
+    onEnable : function() {\r
+       if(this.hidden){\r
+           this.hidden.dom.removeAttribute('disabled');\r
+       }\r
+       this.keyMap.enable();\r
+       Ext.ux.form.SuperBoxSelectItem.superclass.onEnable.call(this);\r
+    },\r
+    onDestroy : function() {\r
+        Ext.destroy(\r
+            this.lnk,\r
+            this.el\r
+        );\r
+        \r
+        Ext.ux.form.SuperBoxSelectItem.superclass.onDestroy.call(this);\r
+    }\r
+});
\ No newline at end of file
diff --git a/contrib/gilbert/media/gilbert/superboxselect/clear.png b/contrib/gilbert/media/gilbert/superboxselect/clear.png
new file mode 100755 (executable)
index 0000000..0052e5f
Binary files /dev/null and b/contrib/gilbert/media/gilbert/superboxselect/clear.png differ
diff --git a/contrib/gilbert/media/gilbert/superboxselect/close.png b/contrib/gilbert/media/gilbert/superboxselect/close.png
new file mode 100755 (executable)
index 0000000..0245558
Binary files /dev/null and b/contrib/gilbert/media/gilbert/superboxselect/close.png differ
diff --git a/contrib/gilbert/media/gilbert/superboxselect/expand.png b/contrib/gilbert/media/gilbert/superboxselect/expand.png
new file mode 100755 (executable)
index 0000000..52d7fe6
Binary files /dev/null and b/contrib/gilbert/media/gilbert/superboxselect/expand.png differ
diff --git a/contrib/gilbert/media/gilbert/superboxselect/superboxselect-gray-extend.css b/contrib/gilbert/media/gilbert/superboxselect/superboxselect-gray-extend.css
new file mode 100755 (executable)
index 0000000..c376da6
--- /dev/null
@@ -0,0 +1,24 @@
+@charset "utf-8";\r
+.x-superboxselect {position:relative; height: auto !important; margin: 0px; overflow: hidden; padding:2px; display:block; outline: none !important;}\r
+.x-superboxselect ul {overflow: hidden; cursor: text;}\r
+.x-superboxselect-display-btns {padding-right: 33px !important;}\r
+.x-superboxselect-btns {position: absolute; right: 1px; top: 0; overflow:hidden; padding:2px;}\r
+.x-superboxselect-btns div {float: left; width: 16px; height: 16px; margin-top: 4px;}\r
+.x-superboxselect-btn-clear {background: url(clear.png) no-repeat scroll left 0px;}\r
+.x-superboxselect-btn-expand {background: url(expand.png) no-repeat scroll left 0px;}\r
+.x-superboxselect-btn-over {background-position: left -16px}\r
+.x-superboxselect-btn-hide {display:none;}\r
+.x-superboxselect li {float: left; margin: 1px 1px 2px 1px; padding: 0;line-height: 18px;}\r
+.x-superboxselect-stacked li {float: none !important;}\r
+.x-superboxselect-input input { border: none; outline: none; margin-top: 4px; margin-bottom: 4px;}\r
+body.ext-ie .x-superboxselect-input input {background: none; border: none; margin-top: 3px;}\r
+.x-superboxselect-item {position: relative; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px; o-border-radius: 6px; khtml-border-radius: 6px; border: 1px solid #d7d7d7; background-color: #e7e7e7; padding: 1px 15px 1px 5px !important; }\r
+body.ext-ie7 .x-superboxselect-item {margin: 2px 1px 2px 1px; line-height: 1.2em; padding: 2px 17px 4px 5px !important;}\r
+body.ext-ie6 .x-superboxselect-item {margin: 2px 1px 2px 1px; line-height: 1.2em; padding: 2px 19px 4px 5px !important;}\r
+.x-superboxselect-item-hover {background: #cdcdcd; border: 1px solid #949494;}\r
+.x-superboxselect-item-focus {border-color: #8b8b8b; background: #8b8b8b; color: #fff;}\r
+.x-superboxselect-item-close {background: url(close.png) no-repeat scroll left 0px; border: none; cursor: default; font-size: 1px; height: 16px;padding:0; position: absolute; right: 0px; top: 2px; width: 13px;display:block;cursor:pointer;}\r
+\r
+.x-superboxselect-item-close:hover, .x-superboxselect-item-close:active  { background-position: left -12px;}\r
+.x-superboxselect-item-focus .x-superboxselect-item-close{ background-position: left -24px}\r
+.x-item-disabled .x-superboxselect-item-close{ background-position: left -36px}
\ No newline at end of file
diff --git a/contrib/gilbert/media/gilbert/superboxselect/superboxselect.css b/contrib/gilbert/media/gilbert/superboxselect/superboxselect.css
new file mode 100755 (executable)
index 0000000..d1cb001
--- /dev/null
@@ -0,0 +1,25 @@
+@charset "utf-8";\r
+.x-superboxselect {position:relative; height: auto !important; margin: 0px; overflow: hidden; padding:2px; display:block; outline: none !important;}\r
+.x-superboxselect input[disabled] {background-color: transparent;};\r
+.x-superboxselect ul {overflow: hidden; cursor: text;}\r
+.x-superboxselect-display-btns {padding-right: 33px !important;}\r
+.x-superboxselect-btns {position: absolute; right: 1px; top: 0; overflow:hidden; padding:2px;}\r
+.x-superboxselect-btns div {float: left; width: 16px; height: 16px; margin-top: 4px;}\r
+.x-superboxselect-btn-clear {background: url(clear.png) no-repeat scroll left 0px;}\r
+.x-superboxselect-btn-expand {background: url(expand.png) no-repeat scroll left 0px;}\r
+.x-superboxselect-btn-over {background-position: left -16px}\r
+.x-superboxselect-btn-hide {display:none;}\r
+.x-superboxselect li {float: left; margin: 1px 1px 2px 1px; padding: 0;line-height: 18px;}\r
+.x-superboxselect-stacked li {float: none !important;}\r
+.x-superboxselect-input input { border: none; outline: none; margin-top: 4px; margin-bottom: 4px;}\r
+body.ext-ie .x-superboxselect-input input {background: none; border: none; margin-top: 3px;}\r
+.x-superboxselect-item {position: relative; -moz-border-radius: 6px; -webkit-border-radius: 6px; border-radius: 6px; o-border-radius: 6px; khtml-border-radius: 6px; border: 1px solid #CAD8F3; background-color: #DEE7F8; padding: 1px 15px 1px 5px !important; }\r
+body.ext-ie7 .x-superboxselect-item {margin: 2px 1px 2px 1px; line-height: 1.2em; padding: 2px 17px 4px 5px !important;}\r
+body.ext-ie6 .x-superboxselect-item {margin: 2px 1px 2px 1px; line-height: 1.2em; padding: 2px 19px 4px 5px !important;}\r
+.x-superboxselect-item-hover {background: #BBCEF1; border: 1px solid #6D95E0;}\r
+.x-superboxselect-item-focus {border-color: #598BEC; background: #598BEC; color: #fff;}\r
+.x-superboxselect-item-close {background: url(close.png) no-repeat scroll left 0px; border: none; cursor: default; font-size: 1px; height: 16px;padding:0; position: absolute; right: 0px; top: 2px; width: 13px;display:block;cursor:pointer;}\r
+\r
+.x-superboxselect-item-close:hover, .x-superboxselect-item-close:active  { background-position: left -12px;}\r
+.x-superboxselect-item-focus .x-superboxselect-item-close{ background-position: left -24px}\r
+.x-item-disabled .x-superboxselect-item-close{ background-position: left -36px}
\ No newline at end of file
diff --git a/contrib/gilbert/models.py b/contrib/gilbert/models.py
new file mode 100644 (file)
index 0000000..08ed4f5
--- /dev/null
@@ -0,0 +1,8 @@
+from django.db import models
+from django.contrib.auth.models import User
+from philo.models.fields import JSONField
+
+
+class UserPreferences(models.Model):
+       user = models.OneToOneField(User, related_name='gilbert_userpreferences')
+       preferences = JSONField(default=dict())
\ No newline at end of file
diff --git a/contrib/gilbert/plugins.py b/contrib/gilbert/plugins.py
deleted file mode 100644 (file)
index 80ef1ab..0000000
+++ /dev/null
@@ -1,167 +0,0 @@
-from inspect import isclass, getargspec
-from functools import wraps
-from django.utils.encoding import force_unicode
-from django.forms.widgets import Widget, Input, HiddenInput, FileInput, DateInput, TimeInput, Textarea, CheckboxInput, Select, SelectMultiple
-from django.forms.fields import FileField
-from django.forms.forms import BaseForm
-
-
-def _render_ext(self, name, value):
-       ext_spec = {'name': name}
-       if value is not None:
-               ext_spec['value'] = value
-       if isinstance(self, Input):
-               if isinstance(self, HiddenInput):
-                       ext_spec['xtype'] = 'hidden'
-               elif isinstance(self, FileInput):
-                       ext_spec['xtype'] = 'fileuploadfield'
-               elif isinstance(self, DateInput):
-                       ext_spec['xtype'] = 'datefield'
-               elif isinstance(self, TimeInput):
-                       ext_spec['xtype'] = 'timefield'
-               else:
-                       ext_spec['xtype'] = 'textfield'
-                       ext_spec['inputType'] = self.input_type
-       elif isinstance(self, Textarea):
-               ext_spec['xtype'] = 'textarea'
-       elif isinstance(self, CheckboxInput):
-               ext_spec['xtype'] = 'checkbox'
-       elif isinstance(self, Select):
-               ext_spec['xtype'] = 'combo'
-               ext_spec['store'] = self.choices
-               ext_spec['typeAhead'] = True
-               if isinstance(self, SelectMultiple):
-                       pass
-       if ext_spec:
-               return ext_spec
-       return None
-
-
-Widget.render_ext = _render_ext
-
-
-def _as_ext(self):
-       ext_spec = {}
-       
-       fields = []
-       for bf in self:
-               if bf.label:
-                       label = force_unicode(bf.label)
-               else:
-                       label = ''
-               
-               if bf.field.show_hidden_initial:
-                       only_initial = True
-               else:
-                       only_initial = False
-               
-               widget = bf.field.widget
-               
-               if not self.is_bound:
-                       data = self.initial.get(bf.name, bf.field.initial)
-                       if callable(data):
-                               data = data()
-               else:
-                       if isinstance(bf.field, FileField) and bf.data is None:
-                               data = self.initial.get(bf.name, bf.field.initial)
-                       else:
-                               data = bf.data
-               if not only_initial:
-                       name = bf.html_name
-               else:
-                       name = bf.html_initial_name
-               
-               rendered = widget.render_ext(name, data)
-               if rendered is not None:
-                       rendered['fieldLabel'] = label
-                       fields.append(rendered)
-       ext_spec['items'] = fields
-       ext_spec['labelSeparator'] = self.label_suffix
-       return ext_spec
-
-
-BaseForm.as_ext = _as_ext
-
-
-def is_gilbert_method(function):
-       return getattr(function, 'gilbert_method', False)
-
-
-def gilbert_method(function=None, name=None, argc=None, form_handler=False, restricted=True):
-       def setter(function):
-               setattr(function, 'gilbert_method', True)
-               setattr(function, 'name', name or function.__name__)
-               setattr(function, 'form_handler', form_handler)
-               setattr(function, 'restricted', restricted)
-               new_argc = argc
-               if new_argc is None:
-                       args = getargspec(function)[0]
-                       new_argc = len(args)
-                       if new_argc > 0:
-                               if args[0] == 'self':
-                                       args = args[1:]
-                                       new_argc = new_argc - 1
-                               if new_argc > 0:
-                                       if args[0] == 'request':
-                                               args = args[1:]
-                                               new_argc = new_argc - 1
-               setattr(function, 'argc', new_argc)
-               return function
-       if function is not None:
-               return setter(function)
-       return setter
-
-
-class GilbertPluginBase(type):
-       def __new__(cls, name, bases, attrs):
-               if 'methods' not in attrs:
-                       methods = []
-                       for attr in attrs.values():
-                               if is_gilbert_method(attr):
-                                       methods.append(attr.name)
-                       attrs['methods'] = methods
-               return super(GilbertPluginBase, cls).__new__(cls, name, bases, attrs)
-
-
-class GilbertPlugin(object):
-       __metaclass__ = GilbertPluginBase
-       
-       def __init__(self, site):
-               self.site = site
-       
-       def get_method(self, method_name):
-               method = getattr(self, method_name, None)
-               if not is_gilbert_method(method):
-                       return None
-               return method
-       
-       @property
-       def urls(self):
-               return []
-       
-       @property
-       def js(self):
-               return []
-       
-       @property
-       def css(self):
-               return []
-       
-       @property
-       def fugue_icons(self):
-               return []
-
-
-class GilbertModelAdmin(GilbertPlugin):
-       def __init__(self, site, model):
-               self.model = model
-               self.name = model._meta.object_name
-               super(GilbertModelAdmin, self).__init__(site)
-       
-       @gilbert_method
-       def all(self):
-               return list(self.model._default_manager.all().values())
-       
-       @gilbert_method
-       def get(self, constraint):
-               return self.model._default_manager.all().values().get(**constraint)
\ No newline at end of file
diff --git a/contrib/gilbert/plugins/__init__.py b/contrib/gilbert/plugins/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/contrib/gilbert/plugins/auth.py b/contrib/gilbert/plugins/auth.py
new file mode 100644 (file)
index 0000000..e964559
--- /dev/null
@@ -0,0 +1,72 @@
+from django.contrib.auth import logout
+from django.contrib.auth.forms import PasswordChangeForm
+import staticmedia
+from .base import Plugin
+from ..extdirect import ext_action, ext_method
+from ..models import UserPreferences
+
+
+@ext_action(name='auth')
+class Auth(Plugin):
+       @property
+       def index_js_urls(self):
+               return super(Auth, self).index_js_urls + [
+                       staticmedia.url('gilbert/plugins/auth.js'),
+               ]
+       
+       @property
+       def icon_names(self):
+               return super(Auth, self).icon_names + [
+                       'user-silhouette',
+                       'switch',
+                       'key--pencil',
+                       'door-open-out',
+               ]
+       
+       @ext_method
+       def whoami(self, request):
+               user = request.user
+               return user.get_full_name() or user.username
+       
+       @ext_method
+       def logout(self, request):
+               logout(request)
+               return True
+       
+       @ext_method
+       def get_passwd_form(self, request):
+               return PasswordChangeForm(request.user).as_extdirect()
+       
+       @ext_method(form_handler=True)
+       def save_passwd_form(self, request):
+               form = PasswordChangeForm(request.user, data=request.POST)
+               try:
+                       form.save()
+                       return True, None
+               except:
+                       return False, form.errors
+       
+       @ext_method
+       def get_preferences(self, request):
+               user_preferences, created = UserPreferences.objects.get_or_create(user=request.user)
+               
+               return user_preferences.preferences
+       
+       @ext_method
+       def set_preferences(self, request, preferences):
+               user_preferences, created = UserPreferences.objects.get_or_create(user=request.user)
+               user_preferences.preferences = preferences
+               
+               user_preferences.save()
+               return True
+       
+       @ext_method
+       def get_preference(self, request, key):
+               preferences = self.get_preferences(request)
+               return preferences.get(key, None)
+       
+       @ext_method
+       def set_preference(self, request, key, value):
+               preferences = self.get_preferences(request)
+               preferences[key] = value
+               return self.set_preferences(request, preferences)
\ No newline at end of file
diff --git a/contrib/gilbert/plugins/base.py b/contrib/gilbert/plugins/base.py
new file mode 100644 (file)
index 0000000..2be851b
--- /dev/null
@@ -0,0 +1,23 @@
+from ..extdirect import ext_action
+
+
+@ext_action
+class Plugin(object):
+       def __init__(self, site):
+               self.site = site
+       
+       @property
+       def index_css_urls(self):
+               return []
+       
+       @property
+       def index_js_urls(self):
+               return []
+       
+       @property
+       def index_extrahead(self):
+               return ''
+       
+       @property
+       def icon_names(self):
+               return []
\ No newline at end of file
diff --git a/contrib/gilbert/plugins/models.py b/contrib/gilbert/plugins/models.py
new file mode 100644 (file)
index 0000000..b765482
--- /dev/null
@@ -0,0 +1,379 @@
+import operator
+from django.forms.models import ModelForm, modelform_factory
+from django.db.models import Q
+from django.db.models.fields.related import ManyToOneRel
+from django.db.models.fields.files import FieldFile, ImageFieldFile, FileField
+from django.contrib.admin.util import lookup_field, label_for_field, display_for_field, NestedObjects
+from django.utils.encoding import smart_unicode
+import staticmedia
+from .base import Plugin
+from ..extdirect import ext_action, ext_method
+from django.core.exceptions import PermissionDenied
+from django.utils import simplejson as json
+
+
+@ext_action(name='models')
+class Models(Plugin):
+       """
+       Plugin providing model-related UI and functionality on the client
+       side.
+       
+       """
+       
+       @property
+       def index_js_urls(self):
+               return super(Models, self).index_js_urls + [
+                       staticmedia.url('gilbert/extjs/examples/ux/SearchField.js'),
+                       staticmedia.url('gilbert/plugins/models.js'),
+               ]
+       
+       @property
+       def icon_names(self):
+               return super(Models, self).icon_names + [
+                       'databases',
+                       'database',
+                       'plus',
+                       'minus',
+                       'gear',
+                       'pencil',
+                       'database-import',
+                       'block',
+               ]
+
+
+@ext_action
+class ModelAdmin(Plugin):
+       """
+       Default ModelAdmin class used by Sites to expose a model-centric API
+       on the client side.
+       
+       """
+       
+       form = ModelForm
+       icon_name = 'block'
+       search_fields = ()
+       data_columns = ('__unicode__',)
+       data_editable_columns = ()
+       
+       def __init__(self, site, model):
+               super(ModelAdmin, self).__init__(site)
+               self.model = model
+               self.model_meta = model._meta
+       
+       @classmethod
+       def data_serialize_model_instance(cls, obj):
+               return {
+                       'app_label': obj._meta.app_label,
+                       'name': obj._meta.object_name,
+                       'pk': obj.pk,
+                       '__unicode__': unicode(obj),
+               }
+       
+       @classmethod
+       def data_serialize_field_value(cls, field, value):
+               if field is None:
+                       #return smart_unicode(value)
+                       return value
+               if isinstance(field.rel, ManyToOneRel):
+                       if value is not None:
+                               return cls.data_serialize_model_instance(value)
+               elif isinstance(value, FieldFile):
+                       new_value = {
+                               'path': value.path,
+                               'url': value.url,
+                               'size': value.size,
+                       }
+                       if isinstance(value, ImageFieldFile):
+                               new_value.update({
+                                       'width': value.width,
+                                       'height': value.height,
+                               })
+               else:
+                       return value
+       
+       @property
+       def sortable_fields(self):
+               return [field.name for field in self.model_meta.fields]
+       
+       @property
+       def data_fields(self):
+               fields = ['pk', '__unicode__']
+               fields.extend(self.data_columns)
+               fields.extend(field.name for field in self.model_meta.fields)
+               return tuple(set(fields))
+       
+       @property
+       def data_fields_spec(self):
+               spec = []
+               for field_name in self.data_fields:
+                       field_spec = {
+                               'name': field_name,
+                       }
+                       if field_name in [field.name for field in self.model_meta.fields if isinstance(field.rel, ManyToOneRel)]:
+                               field_spec['type'] = 'gilbertmodelforeignkey'
+                       elif field_name in [field.name for field in self.model_meta.fields if isinstance(field, FileField)]:
+                               field_spec['type'] = 'gilbertmodelfilefield'
+                       spec.append(field_spec)
+               return spec
+       
+       @property
+       def data_columns_spec(self):
+               spec = []
+               for field_name in self.data_columns:
+                       column = {
+                               'dataIndex': field_name,
+                               'sortable': False,
+                               'editable': False,
+                       }
+                       header, attr = label_for_field(field_name, self.model, model_admin=self, return_attr=True)
+                       column['header'] = header
+                       if (field_name in self.sortable_fields) or (getattr(attr, 'admin_order_field', None) in self.sortable_fields):
+                               column['sortable'] = True
+                       if field_name in self.data_editable_columns:
+                               column['editable'] = True
+                       spec.append(column)
+               return spec
+       
+       @property
+       def data_columns_spec_json(self):
+               return json.dumps(self.data_columns_spec)
+       
+       @property
+       def icon_names(self):
+               return super(ModelAdmin, self).icon_names + [
+                       self.icon_name
+               ]
+       
+       @ext_method
+       def has_permission(self, request):
+               return self.has_read_permission(request) or self.has_add_permission(request)
+       
+       @ext_method
+       def has_read_permission(self, request):
+               return self.has_change_permission(request)
+       
+       @ext_method
+       def has_add_permission(self, request):
+               return request.user.has_perm(self.model_meta.app_label + '.' + self.model_meta.get_add_permission())
+       
+       @ext_method
+       def has_change_permission(self, request):
+               return request.user.has_perm(self.model_meta.app_label + '.' + self.model_meta.get_change_permission())
+       
+       @ext_method
+       def has_delete_permission(self, request):
+               return request.user.has_perm(self.model_meta.app_label + '.' + self.model_meta.get_delete_permission())
+       
+       @ext_method
+       def all(self, request):
+               if not self.has_read_permission(request):
+                       raise PermissionDenied
+               return self.model._default_manager.all()
+       
+       @ext_method
+       def filter(self, request, **kwargs):
+               if not self.has_read_permission(request):
+                       raise PermissionDenied
+               return self.model._default_manager.all().filter(**kwargs)
+       
+       @ext_method
+       def get(self, request, **kwargs):
+               if not self.has_read_permission(request):
+                       raise PermissionDenied
+               return self.model._default_manager.all().values().get(**kwargs)
+       
+       @property
+       def form_class(self):
+               return modelform_factory(self.model, form=self.form)
+       
+       @ext_method
+       def get_form(self, request, **kwargs):
+               if len(kwargs) > 0:
+                       instance = self.model._default_manager.all().get(**kwargs)
+               else:
+                       if not self.has_add_permission(request):
+                               raise PermissionDenied
+                       instance = None
+               
+               if (instance and not self.has_change_permission(request)) or not self.has_add_permission(request):
+                       raise PermissionDenied
+               
+               return self.form_class(instance=instance).as_extdirect()
+       
+       @ext_method(form_handler=True)
+       def save_form(self, request):
+               if 'pk' in request.POST:
+                       try:
+                               instance = self.model._default_manager.all().get(pk=request.POST['pk'])
+                       except ObjectDoesNotExist:
+                               instance = None
+               else:
+                       instance = None
+               
+               if (instance and not self.has_change_permission(request)) or not self.has_add_permission(request):
+                       raise PermissionDenied
+               
+               form = self.form_class(request.POST, request.FILES, instance=instance)
+               
+               try:
+                       saved = form.save()
+                       return True, None, saved.pk
+               except ValueError:
+                       return False, form.errors
+       
+       def data_serialize_object(self, obj):
+               row = {}
+               for field_name in self.data_fields:
+                       result = None
+                       try:
+                               field, attr, value = lookup_field(field_name, obj, self)
+                       except (AttributeError, ObjectDoesNotExist):
+                               pass
+                       else:
+                               result = self.data_serialize_field_value(field, value)
+                       row[field_name] = result
+               return row
+       
+       @property
+       def data_metadata(self):
+               return {
+                       'idProperty': 'pk',
+                       'root': 'root',
+                       'totalProperty': 'total',
+                       'successProperty': 'success',
+                       'fields': self.data_fields_spec,
+               }
+       
+       def data_serialize_queryset(self, queryset, params=None):
+               serialized = {
+                       'metaData': self.data_metadata,
+                       'root': [],
+                       'total': queryset.count(),
+                       'success': True,
+               }
+               
+               if params is not None:
+                       if 'sort' in params:
+                               order_by = params['sort']
+                               if order_by in self.data_fields:
+                                       if order_by not in self.sortable_fields:
+                                               try:
+                                                       if hasattr(self, order_by):
+                                                               attr = getattr(self, order_by)
+                                                       else:
+                                                               attr = getattr(self.model, order_by)
+                                                       order_by = attr.admin_order_field
+                                               except AttributeError:
+                                                       order_by = None
+                                       if order_by is not None:
+                                               if params.get('dir', 'ASC') == 'DESC':
+                                                       order_by = '-' + order_by
+                                               serialized['metaData']['sortInfo'] = {
+                                                       'field': params['sort'],
+                                                       'direction': params.get('dir', 'ASC'),
+                                               }
+                                               queryset = queryset.order_by(order_by)
+                       if 'start' in params:
+                               start = params['start']
+                               serialized['metaData']['start'] = start
+                               if 'limit' in params:
+                                       limit = params['limit']
+                                       serialized['metaData']['limit'] = limit
+                                       queryset = queryset[start:(start+limit)]
+                               else:
+                                       queryset = queryset[start:]
+               
+               for obj in queryset:
+                       serialized['root'].append(self.data_serialize_object(obj))
+               
+               return serialized
+       
+       @ext_method
+       def data_read(self, request, **params):
+               if not self.has_read_permission(request):
+                       raise PermissionDenied
+               
+               queryset = self.model._default_manager.all()
+               query = params.pop('query', None)
+               filters = params.pop('filters', None)
+               
+               if filters:
+                       if isinstance(filters, Q):
+                               queryset = queryset.filter(filters)
+                       elif isinstance(filters, dict):
+                               queryset = queryset.filter(**filters)
+                       else:
+                               raise TypeError('Invalid filters parameter')
+               
+               def construct_search(field_name):
+                       if field_name.startswith('^'):
+                               return "%s__istartswith" % field_name[1:]
+                       elif field_name.startswith('='):
+                               return "%s__iexact" % field_name[1:]
+                       elif field_name.startswith('@'):
+                               return "%s__search" % field_name[1:]
+                       else:
+                               return "%s__icontains" % field_name
+               
+               if self.search_fields and query:
+                       for word in query.split():
+                               or_queries = [Q(**{construct_search(str(field_name)): word}) for field_name in self.search_fields]
+                               queryset = queryset.filter(reduce(operator.or_, or_queries))
+                       for field_name in self.search_fields:
+                               if '__' in field_name:
+                                       queryset = queryset.distinct()
+                                       break
+               
+               return self.data_serialize_queryset(queryset, params)
+       
+       @ext_method
+       def data_create(self, request, **kwargs):
+               if not self.has_add_permission(request):
+                       raise PermissionDenied
+               
+               return kwargs
+       
+       @ext_method
+       def data_update(self, request, **kwargs):
+               if not self.has_change_permission(request):
+                       raise PermissionDenied
+               
+               return kwargs
+       
+       @ext_method
+       def data_destroy(self, request, **params):
+               if not self.has_delete_permission(request):
+                       raise PermissionDenied
+               
+               pks = params['root']
+               
+               if type(pks) is not list:
+                       pks = [pks]
+               
+               for pk in pks:
+                       if type(pk) is dict:
+                               pk = pk['pk']
+                       obj = self.model._default_manager.all().get(pk=pk)
+                       obj.delete()
+               
+               return {
+                       'metaData': self.data_metadata,
+                       'success': True,
+                       'root': list(),
+               }
+       
+       @ext_method
+       def data_destroy_consequences(self, request, pks):
+               if not self.has_delete_permission(request):
+                       raise PermissionDenied
+               
+               if type(pks) is not list:
+                       pks = [pks]
+               objs = [self.model._default_manager.all().get(pk=pk) for pk in pks]
+               
+               collector = NestedObjects()
+               
+               for obj in objs:
+                       obj._collect_sub_objects(collector)
+               
+               return collector.nested(self.data_serialize_model_instance)
\ No newline at end of file
index 8f666f9..e181fdf 100644 (file)
-from django.contrib.admin.sites import AdminSite
-from django.contrib.auth import authenticate, login, logout
 from django.conf.urls.defaults import url, patterns, include
 from django.core.urlresolvers import reverse
 from django.shortcuts import render_to_response
 from django.conf import settings
 from django.utils import simplejson as json
 from django.utils.datastructures import SortedDict
-from django.http import HttpResponse
+
 from django.db.models.base import ModelBase
 from philo.utils import fattr
-from philo.contrib.gilbert.plugins import GilbertModelAdmin, GilbertPlugin, is_gilbert_method, gilbert_method
-from philo.contrib.gilbert.exceptions import AlreadyRegistered, NotRegistered
+from .exceptions import AlreadyRegistered, NotRegistered
 from django.forms.models import model_to_dict
 import sys
-from traceback import format_tb
 from inspect import getargspec
 from django.views.decorators.cache import never_cache
-from philo.contrib.gilbert import __version__ as gilbert_version
+from . import __version__ as gilbert_version
 import staticmedia
 import os
+import datetime
+from .extdirect import ExtAction, ExtRouter
+
+from functools import partial
+from django.http import HttpResponse, HttpResponseRedirect
+from django.template import RequestContext
+from django.core.context_processors import csrf
+from django.utils.functional import update_wrapper
+from django.contrib.auth import authenticate, login
+from .plugins.models import Models, ModelAdmin
+from .plugins.auth import Auth
+
 
 __all__ = ('GilbertSite', 'site')
 
 
-class GilbertAuthPlugin(GilbertPlugin):
-       name = 'auth'
+class CoreRouter(ExtRouter):
+       def __init__(self, site):
+               self.site = site
+               self._actions = {}
        
        @property
-       def js(self):
-               return [staticmedia.url('gilbert/Gilbert.api.auth.js')]
+       def namespace(self):
+               return 'Gilbert.api.plugins'
        
        @property
-       def fugue_icons(self):
-               return ['user-silhouette', 'key--pencil', 'door-open-out', 'door-open-in']
-       
-       @gilbert_method(restricted=False)
-       def login(self, request, username, password):
-               user = authenticate(username=username, password=password)
-               if user is not None and user.is_active:
-                       login(request, user)
-                       return True
-               else:
-                       return False
-       
-       @gilbert_method
-       def logout(self, request):
-               logout(request)
-               return True
-       
-       @gilbert_method
-       def get_passwd_form(self, request):
-               from django.contrib.auth.forms import PasswordChangeForm
-               return PasswordChangeForm(request.user).as_ext()
-       
-       @gilbert_method(form_handler=True)
-       def submit_passwd_form(self, request):
-               from django.contrib.auth.forms import PasswordChangeForm
-               form = PasswordChangeForm(request.user, data=request.POST)
-               if form.is_valid():
-                       form.save()
-                       return {'success': True}
-               else:
-                       return {'success': False, 'errors': form.errors}
+       def url(self):
+               return reverse('%s:router' % self.site.namespace, current_app=self.site.app_name)
+       
+       @property
+       def type(self):
+               return 'remoting'
        
-       @gilbert_method
-       def whoami(self, request):
-               user = request.user
-               return user.get_full_name() or user.username
+       @property
+       def actions(self):
+               return self._actions
+       
+       @property
+       def plugins(self):
+               return list(action.obj for action in self._actions.itervalues())
+       
+       def register_plugin(self, plugin):
+               action = ExtAction(plugin)
+               self._actions[action.name] = action
+
+
+class ModelRouter(ExtRouter):
+       def __init__(self, site, app_label):
+               self.site = site
+               self.app_label = app_label
+               self._actions = {}
+       
+       @property
+       def namespace(self):
+               return 'Gilbert.api.models.%s' % self.app_label
+       
+       @property
+       def url(self):
+               return reverse('%s:model_router' % self.site.namespace, current_app=self.site.app_name, kwargs={'app_label': self.app_label})
+       
+       @property
+       def type(self):
+               return 'remoting'
+       
+       @property
+       def actions(self):
+               return self._actions
+       
+       @property
+       def models(self):
+               return dict((name, action.obj) for name, action in self._actions.iteritems())
+       
+       def register_admin(self, name, admin):
+               action = ExtAction(admin)
+               action.name = name
+               self._actions[action.name] = action
 
 
 class GilbertSite(object):
        version = gilbert_version
        
-       def __init__(self, namespace='gilbert', app_name='gilbert', title='Gilbert'):
+       def __init__(self, namespace='gilbert', app_name='gilbert', title=None):
                self.namespace = namespace
                self.app_name = app_name
-               self.title = title
-               self.model_registry = SortedDict()
-               self.plugin_registry = SortedDict()
-               self.register_plugin(GilbertAuthPlugin)
+               if title is None:
+                       self.title = getattr(settings, 'GILBERT_TITLE', 'Gilbert')
+               else:
+                       self.title = title
+               
+               self.core_router = CoreRouter(self)
+               self.model_routers = SortedDict()
+               
+               self.register_plugin(Models)
+               self.register_plugin(Auth)
        
        def register_plugin(self, plugin):
-               if plugin.name in self.plugin_registry:
-                       raise AlreadyRegistered('A plugin named \'%s\' is already registered' % plugin.name)
-               self.plugin_registry[plugin.name] = plugin(self)
+               self.core_router.register_plugin(plugin(self))
        
-       def register_model(self, model_or_iterable, admin_class=GilbertModelAdmin, **admin_attrs):
+       def register_model(self, model_or_iterable, admin_class=ModelAdmin, **admin_attrs):
                if isinstance(model_or_iterable, ModelBase):
                        model_or_iterable = [model_or_iterable]
                for model in model_or_iterable:
-                       if model._meta.app_label not in self.model_registry:
-                               self.model_registry[model._meta.app_label] = SortedDict()
-                       if model._meta.object_name in self.model_registry[model._meta.app_label]:
-                               raise AlreadyRegistered('The model %s.%s is already registered' % (model._meta.app_label, model.__name__))
+                       app_label = model._meta.app_label
+                       name = model._meta.object_name
+                       
+                       if app_label not in self.model_routers:
+                               self.model_routers[app_label] = ModelRouter(self, app_label)
+                       router = self.model_routers[app_label]
+                       
                        if admin_attrs:
                                admin_attrs['__module__'] = __name__
                                admin_class = type('%sAdmin' % model.__name__, (admin_class,), admin_attrs)
-                       self.model_registry[model._meta.app_label][model._meta.object_name] = admin_class(self, model)
+                       
+                       router.register_admin(name, admin_class(self, model))
        
        def has_permission(self, request):
                return request.user.is_active and request.user.is_staff
        
+       def protected_view(self, view, login_page=True, cacheable=False):
+               def inner(request, *args, **kwargs):
+                       if not self.has_permission(request):
+                               if login_page:
+                                       return self.login(request)
+                               else:
+                                       return HttpResponse(status=403)
+                       return view(request, *args, **kwargs)
+               if not cacheable:
+                       inner = never_cache(inner)
+               return update_wrapper(inner, view)
+       
        @property
        def urls(self):
                urlpatterns = patterns('',
-                       url(r'^$', self.index, name='index'),
-                       url(r'^css$', self.css, name='css'),
-                       url(r'^api$', self.api, name='api'),
-                       url(r'^router/?$', self.router, name='router'),
-                       url(r'^router/models/(?P<app_label>\w+)/?$', self.router, name='models'),
-                       url(r'^login$', self.router, name='login'),
+                       url(r'^$', self.protected_view(self.index), name='index'),
+                       url(r'^api.js$', self.protected_view(self.api, login_page=False), name='api'),
+                       url(r'^icons.css$', self.protected_view(self.icons, login_page=False), name='icons'),
+                       url(r'^router$', self.protected_view(self.router, login_page=False), name='router'),
+                       url(r'^router/models/(?P<app_label>\w+)$', self.protected_view(self.router, login_page=False), name='model_router'),
                )
-               
                return (urlpatterns, self.app_name, self.namespace)
        
-       def request_context(self, request, extra_context=None):
-               from django.template import RequestContext
-               context = RequestContext(request, current_app=self.namespace)
-               context.update(extra_context or {})
-               context.update({'gilbert': self, 'user': request.user, 'logged_in': self.has_permission(request)})
-               return context
+       def login(self, request):
+               context = {
+                       'gilbert': self,
+                       'form_url': request.get_full_path(),
+               }
+               context.update(csrf(request))
+               
+               if request.POST:
+                       if request.session.test_cookie_worked():
+                               request.session.delete_test_cookie()
+                               username = request.POST.get('username', None)
+                               password = request.POST.get('password', None)
+                               user = authenticate(username=username, password=password)
+                               if user is not None:
+                                       if user.is_active and user.is_staff:
+                                               login(request, user)
+                                               return HttpResponseRedirect(request.get_full_path())
+                                       else:
+                                               context.update({
+                                                       'error_message_short': 'Not staff',
+                                                       'error_message': 'You do not have access to this page.',
+                                               })
+                               else:
+                                       context.update({
+                                               'error_message_short': 'Invalid credentials',
+                                               'error_message': 'Unable to authenticate using the provided credentials. Please try again.',
+                                       })
+                       else:
+                               context.update({
+                                       'error_message_short': 'Cookies disabled',
+                                       'error_message': 'Please enable cookies, reload this page, and try logging in again.',
+                               })
+               
+               request.session.set_test_cookie()
+               return render_to_response('gilbert/login.html', context, context_instance=RequestContext(request))
        
-       @never_cache
-       def index(self, request, extra_context=None):
-               return render_to_response('gilbert/index.html', context_instance=self.request_context(request, extra_context))
+       def index(self, request):
+               return render_to_response('gilbert/index.html', {
+                       'gilbert': self,
+                       'plugins': self.core_router.plugins # needed as the template language will not traverse callables
+               }, context_instance=RequestContext(request))
        
-       def css(self, request, extra_context=None):
-               icon_names = []
-               for plugin in self.plugin_registry.values():
-                       icon_names.extend(plugin.fugue_icons)
+       def api(self, request):
+               providers = []
+               model_registry = {}
+               
+               for app_label, router in self.model_routers.items():
+                       if request.user.has_module_perms(app_label):
+                               providers.append(router.spec)
+                               model_registry[app_label] = dict((model_name, admin) for model_name, admin in router.models.items() if admin.has_permission(request))
                
-               icons = dict([(icon_name, staticmedia.url('gilbert/fugue-icons/icons/%s.png' % icon_name)) for icon_name in set(icon_names)])
+               providers.append(self.core_router.spec)
                
-               context = extra_context or {}
-               context.update({'icons': icons})
+               context = {
+                       'gilbert': self,
+                       'providers': [json.dumps(provider, separators=(',', ':')) for provider in providers],
+                       'model_registry': model_registry,
+               }
+               context.update(csrf(request))
                
-               return render_to_response('gilbert/styles.css', context_instance=self.request_context(request, context), mimetype='text/css')
+               return render_to_response('gilbert/api.js', context, mimetype='text/javascript')
        
-       @never_cache
-       def api(self, request, extra_context=None):
-               providers = []
-               for app_label, models in self.model_registry.items():
-                       app_provider = {
-                               'namespace': 'Gilbert.api.models.%s' % app_label,
-                               'url': reverse('%s:models' % self.namespace, current_app=self.app_name, kwargs={'app_label': app_label}),
-                               'type': 'remoting',
-                       }
-                       model_actions = {}
-                       for model_name, admin in models.items():
-                               model_methods = []
-                               for method in [admin.get_method(method_name) for method_name in admin.methods]:
-                                       if method.restricted and not self.has_permission(request):
-                                               continue
-                                       model_methods.append({
-                                               'name': method.name,
-                                               'len': method.argc,
-                                               'formHandler': method.form_handler,
-                                       })
-                               if model_methods:
-                                       model_actions[model_name] = model_methods
-                       if model_actions:
-                               app_provider['actions'] = model_actions
-                               providers.append(app_provider)
+       def icons(self, request):
+               icon_names = []
                
-               plugin_provider = {
-                       'namespace': 'Gilbert.api',
-                       'url': reverse('%s:router' % self.namespace, current_app=self.app_name),
-                       'type': 'remoting',
-               }
-               plugin_actions = {}
-               for plugin_name, plugin in self.plugin_registry.items():
-                       plugin_methods = []
-                       for method in [plugin.get_method(method_name) for method_name in plugin.methods]:
-                               if method.restricted and not self.has_permission(request):
-                                       continue
-                               plugin_methods.append({
-                                       'name': method.name,
-                                       'len': method.argc,
-                                       'formHandler': method.form_handler,
-                               })
-                       if plugin_methods:
-                               plugin_actions[plugin_name] = plugin_methods
-               if plugin_actions:
-                       plugin_provider['actions'] = plugin_actions
-                       providers.append(plugin_provider)
+               for plugin in self.core_router.plugins:
+                       icon_names.extend(plugin.icon_names)
+               
+               for router in self.model_routers.values():
+                       for admin in router.models.values():
+                               icon_names.extend(admin.icon_names)
                
-               return HttpResponse(''.join(['Ext.Direct.addProvider('+json.dumps(provider, separators=(',', ':'))+');' for provider in providers]), mimetype='text/javascript')
+               return render_to_response('gilbert/icons.css', {
+                       'icon_names': set(icon_names),
+               }, mimetype='text/css')
        
        def router(self, request, app_label=None, extra_context=None):
-               submitted_form = False
-               if request.META['CONTENT_TYPE'].startswith('application/x-www-form-urlencoded'):
-                       submitted_form = True
-               
-               if submitted_form:
-                       ext_request = {
-                               'action': request.POST.get('extAction'),
-                               'method': request.POST.get('extMethod'),
-                               'type': request.POST.get('extType'),
-                               'tid': request.POST.get('extTID'),
-                               'upload': request.POST.get('extUpload', False),
-                               'data': None,
-                       }
-                       response = self.handle_ext_request(request, ext_request, app_label)
+               if app_label is None:
+                       return self.core_router.render_to_response(request)
                else:
-                       ext_requests = json.loads(request.raw_post_data)
-                       if type(ext_requests) is dict:
-                               ext_requests['upload'] = False
-                               response = self.handle_ext_request(request, ext_requests, app_label)
-                       else:
-                               responses = []
-                               for ext_request in ext_requests:
-                                       ext_request['upload'] = False
-                                       responses.append(self.handle_ext_request(request, ext_request, app_label))
-                               response = responses
-               
-               if submitted_form:
-                       if ext_request['upload'] is True:
-                               return HttpResponse(('<html><body><textarea>%s</textarea></body></html>' % json.dumps(response)))
-               return HttpResponse(json.dumps(response), content_type=('application/json; charset=%s' % settings.DEFAULT_CHARSET))
-       
-       def handle_ext_request(self, request, ext_request, app_label=None):
-               try:
-                       plugin = None
-                       
-                       if app_label is not None:
-                               try:
-                                       plugin = self.model_registry[app_label][ext_request['action']]
-                               except KeyError:
-                                       raise NotImplementedError('A model named \'%s\' has not been registered' % ext_request['action'])
-                       else:
-                               try:
-                                       plugin = self.plugin_registry[ext_request['action']]
-                               except KeyError:
-                                       raise NotImplementedError('Gilbert does not provide a class named \'%s\'' % ext_request['action'])
-                       
-                       method = plugin.get_method(ext_request['method'])
-                       
-                       if method is None or (method.restricted and not self.has_permission(request)):
-                               raise NotImplementedError('The method named \'%s\' is not available' % method.name)
-                       
-                       return {'type': 'rpc', 'tid': ext_request['tid'], 'action': ext_request['action'], 'method': ext_request['method'], 'result': method(request, *(ext_request['data'] or []))}
-               except:
-                       exc_type, exc_value, exc_traceback = sys.exc_info()
-                       return {'type': 'exception', 'tid': ext_request['tid'], 'message': ('%s: %s' % (exc_type, exc_value)), 'where': format_tb(exc_traceback)[0]}
+                       return self.model_routers[app_label].render_to_response(request)
 
 
 site = GilbertSite()
\ No newline at end of file
diff --git a/contrib/gilbert/templates/gilbert/api.js b/contrib/gilbert/templates/gilbert/api.js
new file mode 100644 (file)
index 0000000..4281d66
--- /dev/null
@@ -0,0 +1,74 @@
+Ext.Ajax.on('beforerequest', function (connection, options) {
+       if (!(/^http:.*/.test(options.url) || /^https:.*/.test(options.url))) {
+               options.headers = Ext.apply(options.headers||{}, {
+                       'X-CSRFToken': '{{ csrf_token }}',
+               });
+       }
+});
+
+
+Ext.ns('Gilbert.api');
+{% for provider in providers %}Ext.Direct.addProvider({{ provider|safe }});{% endfor %}
+
+
+Gilbert.on('ready', function (application) {{% for app_label, models in model_registry.items %}{% for name, admin in models.items %}
+       application.register_model('{{ app_label }}', '{{ name }}', new Gilbert.lib.models.Model({
+               app_label: '{{ app_label }}',
+               name: '{{ name }}',
+               verbose_name: '{{ admin.model_meta.verbose_name }}',
+               verbose_name_plural: '{{ admin.model_meta.verbose_name_plural }}',
+               searchable: {% if admin.search_fields %}true{% else %}false{% endif %},
+               columns: {{ admin.data_columns_spec_json|safe }},
+               iconCls: 'icon-{{ admin.icon_name }}',
+               api: Gilbert.api.models.{{ app_label }}.{{ name }},
+       }));
+{% endfor %}{% endfor %}});
+
+
+Gilbert.on('ready', function (application) {
+       application.register_plugin('_about_window', {
+               init: function(application) {
+                       var application = this.application = application;
+                       
+                       var plugin = this;
+                       
+                       application.mainmenu.remove(application.mainmenu.items.items[0]);
+                       
+                       application.mainmenu.insert(0, {
+                               xtype: 'button',
+                               text: '<span style="font-weight: bolder; text-transform: uppercase;">{{ gilbert.title|safe }}</span>',
+                               handler: function(button, event) {
+                                       plugin.showAbout(button);
+                               },
+                       });
+               },
+               showAbout: function(sender) {
+                       var application = this.application;
+                       
+                       if (!this.about_window) {
+                               var about_window = this.about_window = application.create_window({
+                                       height: 176,
+                                       width: 284,
+                                       header: false,
+                                       html: '<h1>{{ gilbert.title|safe }}</h1><h2>Version {{ gilbert.version|safe }}</h2><div id="credits">{{ gilbert.credits|safe }}</div>',
+                                       bodyStyle: 'background: none; font-size: larger; line-height: 1.4em; text-align: center;',
+                                       modal: true,
+                                       closeAction: 'hide',
+                                       closable: false,
+                                       resizable: false,
+                                       draggable: false,
+                                       minimizable: false,
+                                       fbar: [{
+                                               text: 'OK',
+                                               handler: function(button, event) {
+                                                       about_window.hide();
+                                               }
+                                       }],
+                                       defaultButton: 0,
+                               });
+                       }
+                       this.about_window.show();
+                       this.about_window.focus();
+               },
+       });
+});
\ No newline at end of file
diff --git a/contrib/gilbert/templates/gilbert/base.html b/contrib/gilbert/templates/gilbert/base.html
new file mode 100644 (file)
index 0000000..c2635c9
--- /dev/null
@@ -0,0 +1,32 @@
+{% load staticmedia %}<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>{% block head %}
+       <title>{% block title %}{{ gilbert.title }}{% endblock %}</title>
+       
+       {% block css %}
+       <link rel="stylesheet" type="text/css" href="{% mediaurl 'gilbert/extjs/resources/css/ext-all-notheme.css' %}" />
+       <link rel="stylesheet" type="text/css" class="gilbert.theme" id="gilbert.theme.murano" title="murano" href="{% mediaurl 'gilbert/murano/murano.css' %}"{% if request.GET.theme and request.GET.theme != 'murano' %} disabled="disabled"{% endif %} />
+       <link rel="stylesheet" type="text/css" class="gilbert.theme" id="gilbert.theme.access" title="access" href="{% mediaurl 'gilbert/extjs/resources/css' %}/xtheme-access.css"{% if request.GET.theme != 'access' %} disabled="disabled"{% endif %} />
+       <link rel="stylesheet" type="text/css" class="gilbert.theme" id="gilbert.theme.blue" title="blue" href="{% mediaurl 'gilbert/extjs/resources/css' %}/xtheme-blue.css"{% if request.GET.theme != 'blue' %} disabled="disabled"{% endif %} />
+       <link rel="stylesheet" type="text/css" class="gilbert.theme" id="gilbert.theme.gray" title="gray" href="{% mediaurl 'gilbert/extjs/resources/css' %}/xtheme-gray.css"{% if request.GET.theme != 'gray' %} disabled="disabled"{% endif %} />
+       {% endblock %}
+       
+       {% block js %}
+       <script type="text/javascript" src="{% mediaurl 'gilbert/extjs/adapter/ext/ext-base.js' %}"></script>
+       {% if request.GET.debug %}
+       <script type="text/javascript" src="{% mediaurl 'gilbert/extjs/ext-all-debug.js' %}"></script>
+       {% else %}
+       <script type="text/javascript" src="{% mediaurl 'gilbert/extjs/ext-all.js' %}"></script>
+       {% endif %}
+       <script type="text/javascript">
+               Ext.BLANK_IMAGE_URL = '{% mediaurl "gilbert/extjs/resources/images/default/s.gif" %}';
+       </script>
+       {% endblock %}
+       
+       {% block extrahead %}
+       {% endblock %}
+       
+{% endblock %}</head>
+<body style="background-image: url({% mediaurl 'gilbert/wallpaper.jpg' %});">{% block body %}{% endblock %}</body>
+</html>
diff --git a/contrib/gilbert/templates/gilbert/icons.css b/contrib/gilbert/templates/gilbert/icons.css
new file mode 100644 (file)
index 0000000..cecb603
--- /dev/null
@@ -0,0 +1,6 @@
+{% load staticmedia %}
+{% for icon_name in icon_names %}
+.icon-{{ icon_name }} {
+       background: url({% mediaurl 'gilbert/fugue-icons/icons-shadowless' %}/{{ icon_name }}.png) 0 no-repeat !important;
+}
+{% endfor %}
\ No newline at end of file
index 91d293f..6d48a78 100644 (file)
@@ -1,86 +1,46 @@
-{% load staticmedia %}<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
-<head>{% block head %}
-       <title>{% block title %}{{ gilbert.title }}{% endblock %}</title>
+{% extends 'gilbert/base.html' %}
+{% load staticmedia %}
+
+{% block css %}{{ block.super }}
+       <link rel="stylesheet" type="text/css" href="{% mediaurl 'gilbert/extjs/examples/ux/fileuploadfield/css/fileuploadfield.css' %}" />
+       <link rel="stylesheet" type="text/css" href="{% mediaurl 'gilbert/superboxselect/superboxselect.css' %}" />
+       <link rel="stylesheet" type="text/css" href="{% url gilbert:icons %}" />
        
-       {% block css %}
-       <link rel="stylesheet" type="text/css" href="{% mediaurl 'gilbert/extjs/resources/css/ext-all-notheme.css' %}" />
-       <link rel="stylesheet" type="text/css" href="{% mediaurl 'gilbert/murano/murano.css' %}" />
-       <link rel="stylesheet" type="text/css" href="{% url gilbert:css %}" />
-       {% for plugin in gilbert.plugin_registry.values %}{% for css in plugin.css %}
-       <link rel="stylesheet" type="text/css" href="{{ css }}" />
+       {% for router in gilbert.model_routers.values %}{% for admin in router.models.values %}{% for css_url in admin.index_css_urls %}
+       <link rel="stylesheet" type="text/css" href="{{ css_url }}" />
+       {% endfor %}{% endfor %}{% endfor %}
+       
+       {% for plugin in plugins %}{% for css_url in plugin.index_css_urls %}
+       <link rel="stylesheet" type="text/css" href="{{ css_url }}" />
        {% endfor %}{% endfor %}
-       {% endblock %}
+{% endblock %}
+       
+{% block js %}{{ block.super }}
+       <script type="text/javascript" src="{% mediaurl 'gilbert/extjs/examples/ux/FieldLabeler.js' %}"></script>
+       <script type="text/javascript" src="{% mediaurl 'gilbert/extjs/examples/ux/fileuploadfield/FileUploadField.js' %}"></script>
+       <script type="text/javascript" src="{% mediaurl 'gilbert/superboxselect/SuperBoxSelect.js' %}"></script>
+       <script type="text/javascript" src="{% mediaurl 'gilbert/extjs/examples/ux/Reorderer.js' %}"></script>
+       <script type="text/javascript" src="{% mediaurl 'gilbert/extjs/examples/ux/ToolbarReorderer.js' %}"></script>
+       
+       <script type="text/javascript" src="{% mediaurl 'gilbert/lib/app.js' %}"></script>
+       <script type="text/javascript" src="{% mediaurl 'gilbert/lib/models.js' %}"></script>
+       <script type="text/javascript" src="{% mediaurl 'gilbert/lib/plugins.js' %}"></script>
+       <script type="text/javascript" src="{% mediaurl 'gilbert/lib/ui/forms.js' %}"></script>
+       <script type="text/javascript" src="{% mediaurl 'gilbert/lib/ui/windows.js' %}"></script>
+       <script type="text/javascript" src="{% mediaurl 'gilbert/gilbert.js' %}"></script>
        
-       {% block js %}
-       <script type="text/javascript" src="{% mediaurl 'gilbert/extjs/adapter/ext/ext-base.js' %}"></script>
-       <script type="text/javascript" src="{% mediaurl 'gilbert/extjs/ext-all.js' %}"></script>
-       <script type="text/javascript">
-               Ext.BLANK_IMAGE_URL = '{% mediaurl "gilbert/extjs/resources/images/default/s.gif" %}';
-       </script>
-       <script type="text/javascript" src="{% mediaurl 'gilbert/Gilbert.lib.js' %}"></script>
        <script type="text/javascript" src="{% url gilbert:api %}"></script>
-       <script type="text/javascript">
-               GILBERT_LOGGED_IN = {% if logged_in %}true{% else %}false{% endif %};
-               GILBERT_PLUGINS = [];
-       </script>
-       {% for plugin in gilbert.plugin_registry.values %}{% for js in plugin.js %}
-       <script type="text/javascript" src="{{ js }}"></script>
+       
+       {% for router in gilbert.model_routers.values %}{% for admin in router.models.values %}{% for js_url in admin.index_js_urls %}
+       <script type="text/javascript" src="{{ js_url }}"></script>
+       {% endfor %}{% endfor %}{% endfor %}
+       
+       {% for plugin in plugins %}{% for js_url in plugin.index_js_urls %}
+       <script type="text/javascript" src="{{ js_url }}"></script>
        {% endfor %}{% endfor %}
-       <script type="text/javascript">
-               Ext.onReady(function() {
-                       GILBERT_APPLICATION = new Gilbert.lib.Application({
-                               user: '{% filter force_escape %}{% firstof user.get_full_name user.username %}{% endfilter %}',
-                               plugins: [new (function() {
-                                       return {
-                                               init: function(application) {
-                                                       var plugin = this;
-                                                       application.mainmenu.insert(0, {
-                                                               xtype: 'button',
-                                                               text: '<span style="font-weight: bolder; text-transform: uppercase;">Gilbert</span>',
-                                                               handler: function(button, event) {
-                                                                       plugin.showAbout(button);
-                                                               },
-                                                       });
-                                                       application.mainmenu.insert(1, {
-                                                               xtype: 'tbseparator',
-                                                       });
-                                               },
-                                               showAbout: function(sender) {
-                                                       if (!this.about_window) {
-                                                               var about_window = this.about_window = new Ext.Window({
-                                                                       height: 176,
-                                                                       width: 284,
-                                                                       header: false,
-                                                                       html: '<h1>{{ gilbert.title }}</h1><h2>Version {{ gilbert.version }}</h2>',
-                                                                       bodyStyle: 'background: none; font-size: larger; line-height: 1.4em; text-align: center;',
-                                                                       modal: true,
-                                                                       closeAction: 'hide',
-                                                                       renderTo: Ext.getBody(),
-                                                                       closable: false,
-                                                                       resizable: false,
-                                                                       draggable: false,
-                                                                       fbar: [{
-                                                                               text: 'OK',
-                                                                               handler: function(button, event) {
-                                                                                       about_window.hide();
-                                                                               }
-                                                                       }],
-                                                                       defaultButton: 0,
-                                                               });
-                                                       }
-                                                       this.about_window.render(Ext.getBody());
-                                                       this.about_window.center();
-                                                       this.about_window.show();
-                                               },
-                                       }
-                               })()].concat(GILBERT_PLUGINS),
-                       });
-               });
-       </script>
-       {% endblock %}
+{% endblock %}
        
-{% endblock %}</head>
-<body style="background-image: url({% mediaurl 'gilbert/wallpaper.jpg' %});">{% block body %}{% endblock %}</body>
-</html>
+{% block extrahead %}
+       {% for router in gilbert.model_routers.values %}{% for admin in router.models.values %}{{ admin.index_extrahead }}{% endfor %}{% endfor %}
+       {% for plugin in plugins %}{{ plugin.index_extrahead }}{% endfor %}
+{% endblock %}
\ No newline at end of file
diff --git a/contrib/gilbert/templates/gilbert/login.html b/contrib/gilbert/templates/gilbert/login.html
new file mode 100644 (file)
index 0000000..13c79c3
--- /dev/null
@@ -0,0 +1,102 @@
+{% extends 'gilbert/base.html' %}
+{% load staticmedia %}
+
+{% block css %}{{ block.super }}
+       <link rel="stylesheet" type="text/css" href="{% mediaurl 'gilbert/extjs/examples/ux/statusbar/css/statusbar.css' %}" />
+       <style type="text/css">
+               .icon-door-open-in {
+                       background: url({% mediaurl 'gilbert/fugue-icons/icons-shadowless' %}/door-open-in.png) no-repeat !important;
+               }
+               .x-statusbar .x-status-error {
+                       cursor: help;
+                       background-image: url({% mediaurl 'gilbert/fugue-icons/icons-shadowless' %}/exclamation-red.png) !important;
+               }
+       </style>
+{% endblock %}
+
+{% block js %}{{ block.super }}
+       <script type="text/javascript" src="{% mediaurl 'gilbert/extjs/examples/ux/statusbar/StatusBar.js' %}"></script>
+       <script type="text/javascript" charset="utf-8">
+               Ext.onReady(function () {
+                       
+                       var login_form = new Ext.FormPanel({
+                               border: false,{% if request.GET.theme %}
+                               bodyStyle: 'padding: 10px',{% else %}
+                               bodyStyle: 'padding: 0px 10px 0px;',{% endif %}
+                               items: [
+                                       {
+                                               xtype: 'hidden',
+                                               name: 'csrfmiddlewaretoken',
+                                               value: '{{ csrf_token }}',
+                                       },
+                                       {
+                                               xtype: 'textfield',
+                                               anchor: '100%',
+                                               fieldLabel: 'Username',
+                                               name: 'username',
+                                       },
+                                       {
+                                               xtype: 'textfield',
+                                               inputType: 'password',
+                                               anchor: '100%',
+                                               fieldLabel: 'Password',
+                                               name: 'password',
+                                       }
+                               ],
+                               url: '{{ form_url }}',
+                               standardSubmit: true,
+                               keys: [
+                                       {
+                                               key: [Ext.EventObject.ENTER],
+                                               handler: function () {
+                                                       var form = login_form.getForm();
+                                                       form.submit();
+                                               },
+                                       },
+                               ],
+                       });
+                       
+                       var login_status_bar = window.status_bar = new Ext.ux.StatusBar({
+                               items: [
+                                       {
+                                               xtype: 'button',
+                                               text: 'Log in',
+                                               iconCls: 'icon-door-open-in',
+                                               handler: function () {
+                                                       var form = login_form.getForm();
+                                                       form.submit();
+                                               },
+                                       },
+                               ],
+                       });
+                       var login_window = new Ext.Window({
+                               header: false,
+                               closable: false,
+                               resizable: false,
+                               draggable: false,
+                               frame: true,
+                               autoHeight: true,
+                               width: 320,
+                               items: login_form,
+                               bbar: login_status_bar,
+                       });
+                       
+                       login_window.show();
+                       {% if error_message %}
+                       login_status_bar.setStatus({
+                               iconCls: 'x-status-error',
+                               text: '{{ error_message_short }}',
+                       });
+                       
+                       new Ext.ToolTip({
+                               target: login_status_bar.statusEl.el,
+                               anchor: 'top',
+                               title: '{{ error_message_short }}',
+                               html: '{{ error_message }}',
+                       });
+                       
+                       Ext.QuickTips.init();
+                       {% endif %}
+               });
+       </script>
+{% endblock %}
\ No newline at end of file
diff --git a/contrib/gilbert/templates/gilbert/styles.css b/contrib/gilbert/templates/gilbert/styles.css
deleted file mode 100644 (file)
index bf74df0..0000000
+++ /dev/null
@@ -1 +0,0 @@
-{% for icon, url in icons.items %}.{{icon}}{background:url({{ url }}) left center no-repeat !important;}{% endfor %}
\ No newline at end of file
diff --git a/contrib/penfield/gilbert.py b/contrib/penfield/gilbert.py
new file mode 100644 (file)
index 0000000..fb284a5
--- /dev/null
@@ -0,0 +1,11 @@
+from .models import Blog, BlogEntry, BlogView, NewsletterArticle, NewsletterIssue, Newsletter, NewsletterView
+from philo.contrib.gilbert import site
+
+
+site.register_model(Blog, icon_name='blog')
+site.register_model(BlogEntry, search_fields=('title', 'content',), icon_name='document-snippet')
+site.register_model(BlogView, icon_name='application-blog')
+site.register_model(NewsletterArticle, icon_name='document-snippet')
+site.register_model(NewsletterIssue, icon_name='newspaper')
+site.register_model(Newsletter, icon_name='newspapers')
+site.register_model(NewsletterView, icon_name='application')
\ No newline at end of file
index b6259a3..327a0eb 100644 (file)
@@ -43,7 +43,9 @@ class EntityFormBase(ModelForm):
 _old_metaclass_new = ModelFormMetaclass.__new__
 
 def _new_metaclass_new(cls, name, bases, attrs):
-       formfield_callback = attrs.get('formfield_callback', lambda f, **kwargs: f.formfield(**kwargs))
+       formfield_callback = attrs.get('formfield_callback', None)
+       if formfield_callback is None:
+               formfield_callback = lambda f, **kwargs: f.formfield(**kwargs)
        new_class = _old_metaclass_new(cls, name, bases, attrs)
        opts = new_class._meta
        if issubclass(new_class, EntityFormBase) and opts.model:
diff --git a/gilbert.py b/gilbert.py
new file mode 100644 (file)
index 0000000..f59a0a2
--- /dev/null
@@ -0,0 +1,11 @@
+from philo.models import Tag, Collection, Node, Redirect, File, Template, Page
+from philo.contrib.gilbert import site
+
+
+site.register_model(Tag, icon_name='tag-label', data_columns=('name', 'slug'), data_editable_columns=('name', 'slug'))
+site.register_model(Collection, icon_name='box')
+site.register_model(Node, icon_name='node')
+site.register_model(Redirect, icon_name='arrow-switch')
+site.register_model(File, icon_name='document-binary')
+site.register_model(Page, icon_name='document-globe')
+site.register_model(Template, icon_name='document-template')
\ No newline at end of file
diff --git a/hacks.py b/hacks.py
new file mode 100644 (file)
index 0000000..038f6f5
--- /dev/null
+++ b/hacks.py
@@ -0,0 +1,69 @@
+class Category(type):
+       """
+       Adds attributes to an existing class.
+       
+       """
+       
+       replace_attrs = False
+       dunder_attrs = False
+       never_attrs = ('__module__', '__metaclass__')
+       
+       def __new__(cls, name, bases, attrs):
+               if len(bases) != 1:
+                       raise AttributeError('%s: "%s" cannot add methods to more than one class.' % (cls.__name__, name))
+               
+               base = bases[0]
+               
+               for attr, value in attrs.iteritems():
+                       if attr in cls.never_attrs:
+                               continue
+                       if not cls.dunder_attrs and attr.startswith('__'):
+                               continue
+                       if not cls.replace_attrs and hasattr(base, attr):
+                               continue
+                       setattr(base, attr, value)
+               
+               return base
+
+
+class MonkeyPatch(type):
+       """
+       Similar to Category, except it will replace attributes.
+       
+       """
+       
+       replace_attrs = True
+       dunder_attrs = Category.dunder_attrs
+       never_attrs = Category.never_attrs
+       
+       unpatches = {}
+       
+       @classmethod
+       def unpatched(cls, klass, name):
+               try:
+                       return cls.unpatches[klass][name]
+               except:
+                       return getattr(klass, name)
+       
+       def __new__(cls, name, bases, attrs):
+               if len(bases) != 1:
+                       raise AttributeError('%s: "%s" cannot patch more than one class.' % (cls.__name__, name))
+               
+               base = bases[0]
+               
+               for attr, value in attrs.iteritems():
+                       if attr in cls.never_attrs:
+                               continue
+                       if not cls.dunder_attrs and attr.startswith('__'):
+                               continue
+                       if hasattr(base, attr):
+                               if not cls.replace_attrs:
+                                       continue
+                               else:
+                                       if base not in cls.unpatches:
+                                               cls.unpatches[base] = {}
+                                       cls.unpatches[base][attr] = getattr(base, attr)
+                       
+                       setattr(base, attr, value)
+               
+               return base
\ No newline at end of file