From: Joseph Spiros Date: Sat, 21 Aug 2010 08:22:31 +0000 (-0400) Subject: Initial commit of Gilbert, an advanced Django administration interface. X-Git-Url: http://git.ithinksw.org/philo.git/commitdiff_plain/c5ec4492ee19b3c624c654d29f04938e748510cd?ds=sidebyside Initial commit of Gilbert, an advanced Django administration interface. --- diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..dfdaa1d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "contrib/gilbert/media/gilbert/extjs"] + path = contrib/gilbert/media/gilbert/extjs + url = git://github.com/probonogeek/extjs.git diff --git a/contrib/gilbert/__init__.py b/contrib/gilbert/__init__.py new file mode 100644 index 0000000..d009571 --- /dev/null +++ b/contrib/gilbert/__init__.py @@ -0,0 +1,21 @@ +from philo.contrib.gilbert.sites import GilbertSite, site + + +def autodiscover(): + import copy + from django.conf import settings + from django.utils.importlib import import_module + from django.utils.module_loading import module_has_submodule + + 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) + import_module('%s.gilbert' % app) + except: + site.model_registry = before_import_model_registry + site.plugin_registry = before_import_plugin_registry + + if module_has_submodule(mod, 'gilbert'): + raise \ No newline at end of file diff --git a/contrib/gilbert/exceptions.py b/contrib/gilbert/exceptions.py new file mode 100644 index 0000000..e96ba25 --- /dev/null +++ b/contrib/gilbert/exceptions.py @@ -0,0 +1,6 @@ +class AlreadyRegistered(Exception): + pass + + +class NotRegistered(Exception): + pass \ No newline at end of file diff --git a/contrib/gilbert/media/gilbert/extjs b/contrib/gilbert/media/gilbert/extjs new file mode 160000 index 0000000..530ef4b --- /dev/null +++ b/contrib/gilbert/media/gilbert/extjs @@ -0,0 +1 @@ +Subproject commit 530ef4b6c5b943cfa68b779d11cf7de29aa878bf diff --git a/contrib/gilbert/media/gilbert/wallpaper.README b/contrib/gilbert/media/gilbert/wallpaper.README new file mode 100644 index 0000000..a14fe2e --- /dev/null +++ b/contrib/gilbert/media/gilbert/wallpaper.README @@ -0,0 +1 @@ +The source of the default wallpaper.jpg is http://webtreats.mysitemyway.com/tileable-classic-nebula-space-patterns/ \ No newline at end of file diff --git a/contrib/gilbert/media/gilbert/wallpaper.jpg b/contrib/gilbert/media/gilbert/wallpaper.jpg new file mode 100644 index 0000000..6188914 Binary files /dev/null and b/contrib/gilbert/media/gilbert/wallpaper.jpg differ diff --git a/contrib/gilbert/options.py b/contrib/gilbert/options.py new file mode 100644 index 0000000..fbdd4e3 --- /dev/null +++ b/contrib/gilbert/options.py @@ -0,0 +1,57 @@ +from philo.contrib.gilbert.utils import gilbert_method, is_gilbert_method, is_gilbert_class + + +class GilbertClassBase(type): + def __new__(cls, name, bases, attrs): + if 'gilbert_class' not in attrs: + attrs['gilbert_class'] = True + if 'gilbert_class_name' not in attrs: + attrs['gilbert_class_name'] = name + if 'gilbert_class_methods' not in attrs: + gilbert_class_methods = {} + for attr in attrs.values(): + if is_gilbert_method(attr): + gilbert_class_methods[attr.gilbert_method_name] = attr + attrs['gilbert_class_methods'] = gilbert_class_methods + return super(GilbertClassBase, cls).__new__(cls, name, bases, attrs) + + +class GilbertClass(object): + __metaclass__ = GilbertClassBase + + +class GilbertPluginBase(type): + def __new__(cls, name, bases, attrs): + if 'gilbert_plugin' not in attrs: + attrs['gilbert_plugin'] = True + if 'gilbert_plugin_name' not in attrs: + attrs['gilbert_plugin_name'] = name + if 'gilbert_plugin_classes' not in attrs: + gilbert_plugin_classes = {} + for attr_name, attr in attrs.items(): + if is_gilbert_class(attr): + gilbert_plugin_classes[attr_name] = attr + attrs['gilbert_plugin_classes'] = gilbert_plugin_classes + return super(GilbertPluginBase, cls).__new__(cls, name, bases, attrs) + + +class GilbertPlugin(object): + __metaclass__ = GilbertPluginBase + + def __init__(self, site): + self.site = site + + +class GilbertModelAdmin(GilbertClass): + def __init__(self, site, model): + self.site = site + self.model = model + self.gilbert_class_name = model._meta.object_name + + @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/sites.py b/contrib/gilbert/sites.py new file mode 100644 index 0000000..76c1d9a --- /dev/null +++ b/contrib/gilbert/sites.py @@ -0,0 +1,171 @@ +from django.contrib.admin.sites import AdminSite +from django.contrib.auth import authenticate, login, logout +from django.conf.urls.defaults import url, patterns +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.options import GilbertModelAdmin, GilbertPlugin, GilbertClass +from philo.contrib.gilbert.exceptions import AlreadyRegistered, NotRegistered +from django.forms.models import model_to_dict +import sys +from traceback import format_tb +from inspect import getargspec +from philo.contrib.gilbert.utils import is_gilbert_plugin, is_gilbert_class, is_gilbert_method, gilbert_method, call_gilbert_method + + +__all__ = ('GilbertSite', 'site') + + +class GilbertSitePlugin(GilbertPlugin): + class auth(GilbertClass): + @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(restricted=False) + def logout(self, request): + logout(request) + return True + + @gilbert_method + def passwd(self, request, current_password, new_password, new_password_confirm): + user = request.user + if user.check_password(current_password) and (new_password == new_password_confirm): + user.set_password(new_password) + user.save() + return True + return False + + +class GilbertSite(object): + def __init__(self, namespace='gilbert', app_name='gilbert', title='Gilbert'): + self.namespace = namespace + self.app_name = app_name + self.title = title + self.core_api = GilbertSitePlugin(self) + self.model_registry = SortedDict() + self.plugin_registry = SortedDict() + + def register_plugin(self, plugin): + if is_gilbert_plugin(plugin): + if plugin.gilbert_plugin_name in self.plugin_registry: + raise AlreadyRegistered('A plugin named \'%s\' is already registered' % plugin.gilbert_plugin_name) + self.plugin_registry[plugin.gilbert_plugin_name] = plugin(self) + else: + raise ValueError('register_plugin must be provided a valid plugin class or instance') + + def register_model(self, model_or_iterable, admin_class=GilbertModelAdmin, **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 is already registered' % model.__name__) + 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) + + def has_permission(self, request): + return request.user.is_active and request.user.is_staff + + @property + def urls(self): + return (patterns('', + url(r'^$', self.index, name='index'), + url(r'^css.css$', self.css, name='css'), + url(r'^api.js$', self.api, name='api'), + url(r'^router/?$', self.router, name='router'), + url(r'^models/(?P\w+)/?$', self.router, name='models'), + url(r'^plugins/(?P\w+)/?$', self.router, name='plugins'), + url(r'^login$', self.router, name='login'), + ), 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 index(self, request, extra_context=None): + return render_to_response('gilbert/index.html', context_instance=self.request_context(request, extra_context)) + + def css(self, request, extra_context=None): + return render_to_response('gilbert/css.css', context_instance=self.request_context(request, extra_context), mimetype='text/css') + + def api(self, request, extra_context=None): + return render_to_response('gilbert/api.js', context_instance=self.request_context(request, extra_context), mimetype='text/javascript') + + def router(self, request, app_label=None, plugin_name=None, extra_context=None): + submitted_form = False + if request.META['CONTENT_TYPE'].startswith('application/x-www-form-urlencoded'): + submitted_form = True + + if submitted_form: + post_dict = dict(request.POST) + ext_request = { + 'action': post_dict.pop('extAction'), + 'method': post_dict.pop('extMethod'), + 'type': post_dict.pop('extType'), + 'tid': post_dict.pop('extTID'), + 'data': None, + 'kwdata': post_dict, + } + if 'extUpload' in request.POST: + ext_request['upload'] = request.POST['extUpload'] + else: + ext_request = json.loads(request.raw_post_data) + ext_request['kwdata'] = None + + try: + gilbert_class = None + + if app_label is not None: + try: + gilbert_class = self.model_registry[app_label][ext_request['action']] + except KeyError: + raise NotImplementedError('A model named \'%s\' has not been registered' % ext_request['action']) + elif plugin_name is not None: + try: + gilbert_plugin = self.plugin_registry[plugin_name] + except KeyError: + raise NotImplementedError('A plugin named \'%s\' has not been registered' % plugin_name) + try: + gilbert_class = gilbert_plugin.gilbert_plugin_classes[ext_request['action']] + except KeyError: + raise NotImplementedError('The plugin named \'%s\' does not provide a class named \'%s\'' % (plugin_name, ext_request['action'])) + else: + try: + gilbert_class = self.core_api.gilbert_plugin_classes[ext_request['action']] + except KeyError: + raise NotImplementedError('Gilbert does not provide a class named \'%s\'' % ext_request['action']) + + try: + method = gilbert_class.gilbert_class_methods[ext_request['method']] + except KeyError: + raise NotImplementedError('The class named \'%s\' does not implement a method named \'%\'' % (gilbert_class.gilbert_class_name, ext_request['method'])) + if method.gilbert_method_restricted and not self.has_permission(request): + raise NotImplementedError('The method named \'%s\' is not available' % method.gilbert_method_name) + response = {'type': 'rpc', 'tid': ext_request['tid'], 'action': ext_request['action'], 'method': ext_request['method'], 'result': call_gilbert_method(method, gilbert_class, request, *(ext_request['data'] or []), **(ext_request['kwdata'] or {}))} + except: + exc_type, exc_value, exc_traceback = sys.exc_info() + response = {'type': 'exception', 'tid': ext_request['tid'], 'message': ('%s: %s' % (exc_type, exc_value)), 'where': format_tb(exc_traceback)[0]} + + if submitted_form: + return HttpResponse(('' % json.dumps(response))) + return HttpResponse(json.dumps(response), content_type=('application/json; charset=%s' % settings.DEFAULT_CHARSET)) + + +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 index 0000000..71aaab8 --- /dev/null +++ b/contrib/gilbert/templates/gilbert/api.js @@ -0,0 +1,265 @@ +{% load staticmedia %} + +Ext.Direct.addProvider({ + 'namespace': 'Gilbert.api', + 'url': '{% url gilbert:router %}', + 'type': 'remoting', + 'actions': {{% for gilbert_class in gilbert.core_api.gilbert_plugin_classes.values %} + '{{ gilbert_class.gilbert_class_name }}': [{% for method in gilbert_class.gilbert_class_methods.values %}{ + 'name': '{{ method.gilbert_method_name }}', + 'len': {{ method.gilbert_method_argc }} + },{% endfor %}],{% endfor %} + } +}); + +{% if not logged_in %} + +Ext.onReady(function() { + var 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: 'Login', + handler: function(sender) { + // document.location.reload(); + 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('Login failed', 'Unable to authenticate.', function() { + login_form.getForm().reset(); + }); + } + }); + } + } + ], + }); + var login_window = new Ext.Window({ + title: 'Login', + closable: false, + width: 266, + height: 130, + layout: 'fit', + items: login_form, + }); + login_window.show(); +}); + + +{% else %} + +Ext.ns('Gilbert', 'Gilbert.ui', 'Gilbert.models', 'Gilbert.plugins'); + +{% for app_label, models in gilbert.model_registry.items %}Ext.Direct.addProvider({ + 'namespace': 'Gilbert.models.{{ app_label }}', + 'url': '{% url gilbert:models app_label %}', + 'type': 'remoting', + 'actions': {{% for model_name, admin in models.items %} + '{{ model_name }}': [{% for method in admin.gilbert_class_methods.values %}{ + 'name': '{{ method.gilbert_method_name }}', + 'len': {{ method.gilbert_method_argc }} + },{% endfor %}],{% endfor %} + } +});{% endfor %} +{% for plugin in gilbert.plugin_registry.values %}Ext.Direct.addProvider({ + 'namespace': 'Gilbert.plugins.{{ plugin.gilbert_plugin_name }}', + 'url': '{% url gilbert:plugins plugin.gilbert_plugin_name %}', + 'type': 'remoting', + 'actions': {{% for gilbert_class in plugin.gilbert_plugin_classes %} + '{{ gilbert_class.gilbert_class_name }}': [{% for method in gilbert_class.gilbert_class_methods.values %}{} + 'name': '{{ method.gilbert_method_name }}', + 'len': {{ method.gilbert_method_argc }} + },{% endfor %}],{% endfor %} + } +});{% endfor %} + +Gilbert.ui.Application = function(cfg) { + Ext.apply(this, cfg, { + title: '{{ gilbert.title }}', + }); + this.addEvents({ + 'ready': true, + 'beforeunload': true, + }); + Ext.onReady(this.initApplication, this); +}; + +Ext.extend(Gilbert.ui.Application, Ext.util.Observable, { + initApplication: function() { + + Ext.QuickTips.init(); + + this.desktop = new Ext.Panel({ + region: 'center', + border: false, + padding: '5', + bodyStyle: 'background: none;', + }); + var desktop = this.desktop; + + this.toolbar = new Ext.Toolbar({ + region: 'north', + autoHeight: true, + items: [ + { + xtype: 'tbtext', + text: this.title, + style: 'font-weight: bolder; font-size: larger; text-transform: uppercase;', + }, + { + xtype: 'tbseparator', + } + ] + }); + var toolbar = this.toolbar; + + this.viewport = new Ext.Viewport({ + renderTo: Ext.getBody(), + layout: 'border', + items: [ + toolbar, + desktop, + ], + }); + var viewport = this.viewport; + + var windows = new Ext.WindowGroup(); + + this.createWindow = function(config, cls) { + var win = new(cls || Ext.Window)(Ext.applyIf(config || {}, + { + renderTo: desktop.el, + manager: windows, + constrainHeader: true, + maximizable: true, + })); + win.render(desktop.el); + return win; + }; + var createWindow = this.createWindow; + + if (this.plugins) { + for (var pluginNum = 0; pluginNum < this.plugins.length; pluginNum++) { + this.plugins[pluginNum].initWithApp(this); + }; + }; + + if (this.user) { + var user = this.user; + toolbar.add({ xtype: 'tbfill' }); + toolbar.add({ xtype: 'tbseparator' }); + toolbar.add({ + xtype: 'button', + text: '' + user + '', + style: 'font-weight: bolder !important; font-size: smaller !important; text-transform: uppercase !important;', + menu: [ + { + text: 'Change password', + handler: function(button, event) { + var edit_window = createWindow({ + layout: 'fit', + title: 'Change password', + width: 266, + height: 170, + layout: 'fit', + items: _change_password_form = new Ext.FormPanel({ + frame: true, + bodyStyle: 'padding: 5px 5px 0', + items: [ + { + fieldLabel: 'Current password', + name: 'current_password', + xtype: 'textfield', + inputType: 'password', + }, + { + fieldLabel: 'New password', + name: 'new_password', + xtype: 'textfield', + inputType: 'password', + }, + { + fieldLabel: 'New password (confirm)', + name: 'new_password_confirm', + xtype: 'textfield', + inputType: 'password', + } + ], + buttons: [ + { + text: 'Change password', + handler: function(sender) { + // document.location.reload(); + var the_form = _change_password_form.getForm().el.dom; + var current_password = the_form[0].value; + var new_password = the_form[1].value; + var new_password_confirm = the_form[2].value; + Gilbert.api.auth.passwd(current_password, new_password, new_password_confirm, function(result) { + if (result) { + Ext.MessageBox.alert('Password changed', 'Your password has been changed.'); + } else { + Ext.MessageBox.alert('Password unchanged', 'Unable to change your password.', function() { + _change_password_form.getForm().reset(); + }); + } + }); + } + } + ], + }), + }); + edit_window.show(this); + }, + }, + { + text: 'Log out', + handler: function(button, event) { + Gilbert.api.auth.logout(function(result) { + if (result) { + Ext.MessageBox.alert('Logout successful', 'You have been logged out.', function() { + document.location.reload(); + }); + } else { + Ext.MessageBox.alert('Logout failed', 'A bit odd, you might say.'); + } + }); + }, + }, + ], + }); + }; + + toolbar.doLayout(); + viewport.doLayout(); + }, +}); + +Ext.BLANK_IMAGE_URL = '{% mediaurl "gilbert/extjs/resources/images/default/s.gif" %}'; + +Ext.onReady(function(){ + Gilbert.Application = new Gilbert.ui.Application({ + user: '{% filter force_escape %}{% firstof user.get_full_name user.username %}{% endfilter %}', + plugins: [{% for plugin in gilbert.plugin_registry.values %}{% if plugin.gilbert_plugin_javascript %} + {{ plugin.gilbert_plugin_javascript|safe }}, + {% endif %}{% endfor %}], + }); +}); +{% endif %} \ 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 index 0000000..61a1c65 --- /dev/null +++ b/contrib/gilbert/templates/gilbert/base.html @@ -0,0 +1,21 @@ +{% load staticmedia %} + + +{% block head %} + {% block title %}{{ gilbert.title }}{% endblock %} + + {% block css %} + + + + {% endblock %} + + {% block js %} + + + + {% endblock %} + +{% endblock %} +{% block body %}{% endblock %} + diff --git a/contrib/gilbert/templates/gilbert/css.css b/contrib/gilbert/templates/gilbert/css.css new file mode 100644 index 0000000..e69de29 diff --git a/contrib/gilbert/templates/gilbert/index.html b/contrib/gilbert/templates/gilbert/index.html new file mode 100644 index 0000000..a4d9290 --- /dev/null +++ b/contrib/gilbert/templates/gilbert/index.html @@ -0,0 +1 @@ +{% extends 'gilbert/base.html' %} \ No newline at end of file diff --git a/contrib/gilbert/utils.py b/contrib/gilbert/utils.py new file mode 100644 index 0000000..0e7b071 --- /dev/null +++ b/contrib/gilbert/utils.py @@ -0,0 +1,52 @@ +from inspect import isclass, getargspec + + +def is_gilbert_plugin(class_or_instance): + from philo.contrib.gilbert.options import GilbertPluginBase, GilbertPlugin + return (isclass(class_or_instance) and issubclass(class_or_instance, GilbertPlugin)) or isinstance(class_or_instance, GilbertPlugin) or (getattr(class_or_instance, '__metaclass__', None) is GilbertPluginBase) or (getattr(class_or_instance, 'gilbert_plugin', False) and (getattr(class_or_instance, 'gilbert_plugin_name', None) is not None) and (getattr(class_or_instance, 'gilbert_plugin_classes', None) is not None)) + + +def is_gilbert_class(class_or_instance): + from philo.contrib.gilbert.options import GilbertClassBase, GilbertClass + return (isclass(class_or_instance) and issubclass(class_or_instance, GilbertClass)) or isinstance(class_or_instance, GilbertClass) or (getattr(class_or_instance, '__metaclass__', None) is GilbertClassBase) or (getattr(class_or_instance, 'gilbert_class', False) and (getattr(class_or_instance, 'gilbert_class_name', None) is not None) and (getattr(class_or_instance, 'gilbert_class_methods', None) is not None)) + + +def is_gilbert_method(function): + return getattr(function, 'gilbert_method', False) + + +def gilbert_method(function=None, name=None, argc=None, restricted=True): + def wrapper(function): + setattr(function, 'gilbert_method', True) + setattr(function, 'gilbert_method_name', name or function.__name__) + setattr(function, 'gilbert_method_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, 'gilbert_method_argc', new_argc) + return function + if function is not None: + return wrapper(function) + return wrapper + + +def call_gilbert_method(method, cls, request, *args, **kwargs): + arg_list = getargspec(method)[0] + if len(arg_list) > 0: + if arg_list[0] == 'self': + if len(arg_list) > 1 and arg_list[1] == 'request': + return method(cls, request, *args, **kwargs) + return method(cls, *args, **kwargs) + elif arg_list[0] == 'request': + return method(request, *args, **kwargs) + else: + return method(*args, **kwargs) \ No newline at end of file