Jelajahi Sumber

HUE-2962 [desktop] Add initial DefaultConfiguration model, api, tests

Related JIRAs
HUE-2962 [core] Group/default specific default settings for SQL
HUE-2325 [core] Default job queues by group of user
Jenny Kim 9 tahun lalu
induk
melakukan
a195f8e

+ 0 - 0
desktop/core/src/desktop/configuration/__init__.py


+ 194 - 0
desktop/core/src/desktop/configuration/api.py

@@ -0,0 +1,194 @@
+#!/usr/bin/env python
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+
+from django.contrib.auth.models import Group, User
+from django.utils.translation import ugettext as _
+from django.views.decorators.http import require_POST
+
+from desktop.lib.django_util import JsonResponse
+from desktop.lib.exceptions_renderable import PopupException
+from desktop.lib.i18n import force_unicode
+from desktop.models import DefaultConfiguration
+
+
+LOG = logging.getLogger(__name__)
+
+
+def api_error_handler(func):
+  def decorator(*args, **kwargs):
+    response = {}
+
+    try:
+      return func(*args, **kwargs)
+    except Exception, e:
+      LOG.exception('Error running %s' % func)
+      response['status'] = -1
+      response['message'] = force_unicode(str(e))
+    finally:
+      if response:
+        return JsonResponse(response)
+
+  return decorator
+
+
+@api_error_handler
+def get_configurable_apps(request):
+  # TODO: dynamically register apps that are configurable and return here
+  app_configs = []
+  try:
+    from notebook.connectors.hiveserver2 import HS2Api
+    app_configs.append(HS2Api.HIVE_PROPERTIES)
+    app_configs.append(HS2Api.IMPALA_PROPERTIES)
+  except ImportError, e:
+    LOG.error('Failed to import notebook libs')
+
+  return JsonResponse({
+    'status': 0,
+    'apps': app_configs
+  })
+
+
+@api_error_handler
+def search_default_configurations(request):
+  app = request.GET.get('app')
+  is_default = request.GET.get('is_default', None)
+  group_id = request.GET.get('group_id')
+  user_id = request.GET.get('user_id')
+
+  configs = DefaultConfiguration.objects.all()
+
+  if app:
+    configs = configs.filter(app=app)
+
+  if is_default:
+    configs = configs.filter(is_default=is_default.lower() == 'true')
+
+  if group_id:
+    configs = configs.filter(group=Group.objects.get(id=group_id))
+
+  if user_id:
+    configs = configs.filter(user=User.objects.get(id=user_id))
+
+  return JsonResponse({
+    'status': 0,
+    'configurations': [config.to_dict() for config in configs]
+  })
+
+
+@api_error_handler
+def get_default_configuration_for_user(request):
+  app = request.GET.get('app')
+  user_id = request.GET.get('user_id')
+
+  if not app or not user_id:
+    raise PopupException(_('get_default_configuration_for_user requires app and user_id'))
+
+  user = User.objects.get(id=user_id)
+
+  if not user:
+    raise PopupException(_('Could not find user with User ID: %s') % user_id)
+
+  config = DefaultConfiguration.objects.get_configuration_for_user(app, user)
+
+  return JsonResponse({
+    'status': 0,
+    'configuration': config.to_dict() if config is not None else None
+  })
+
+
+@api_error_handler
+@require_POST
+def save_default_configuration(request):
+  app = request.POST.get('app')
+  properties = request.POST.get('properties')
+  is_default = request.POST.get('is_default', 'false')
+  group_id = request.POST.get('group_id')
+  user_id = request.POST.get('user_id')
+
+  if not app or not properties or not (is_default or group_id or user_id):
+    raise PopupException(_('save_default_configuration requires app, properties, and is_default, group_id or user_id'))
+
+  if is_default and is_default.lower() == 'true':
+    kwargs = {'app': app, 'is_default': True}
+  elif group_id:
+    try:
+      group = Group.objects.get(id=int(group_id))
+      kwargs = {'app': app, 'is_default': False, 'group': group}
+    except Group.DoesNotExist, e:
+      raise PopupException(_('Could not find group with ID: %s') % group_id)
+  elif user_id:
+    try:
+      user = User.objects.get(id=int(user_id))
+      kwargs = {'app': app, 'is_default': False, 'user': user}
+    except User.DoesNotExist, e:
+      raise PopupException(_('Could not find user with ID: %s') % user_id)
+  else:
+    raise PopupException(_('Cannot find configuration for %(app)s with: is_default=%(is_default)s, group_id=%(group_id)s, user_id=%(user_id)s') %
+                         {'app': app, 'is_default': is_default, 'group_id': group_id, 'user_id': user_id})
+
+  config, created = DefaultConfiguration.objects.get_or_create(**kwargs)
+  # TODO: Validate properties?
+  config.properties = properties
+  config.save()
+
+  return JsonResponse({
+    'status': 0,
+    'configuration': config.to_dict()
+  })
+
+
+@api_error_handler
+@require_POST
+def delete_default_configuration(request):
+  app = request.POST.get('app')
+  is_default = request.POST.get('is_default', 'false')
+  group_id = request.POST.get('group_id')
+  user_id = request.POST.get('user_id')
+
+  if not app or not (is_default or group_id or user_id):
+    raise PopupException(_('save_default_configuration requires app and is_default, group_id or user_id'))
+
+  if is_default and is_default.lower() == 'true':
+    kwargs = {'app': app, 'is_default': True}
+  elif group_id:
+    try:
+      group = Group.objects.get(id=int(group_id))
+      kwargs = {'app': app, 'is_default': False, 'group': group}
+    except Group.DoesNotExist, e:
+      raise PopupException(_('Could not find group with ID: %s') % group_id)
+  elif user_id:
+    try:
+      user = User.objects.get(id=int(user_id))
+      kwargs = {'app': app, 'is_default': False, 'user': user}
+    except User.DoesNotExist, e:
+      raise PopupException(_('Could not find user with ID: %s') % user_id)
+  else:
+    raise PopupException(_('Cannot find configuration for %(app)s with: is_default=%(is_default)s, group_id=%(group_id)s, user_id=%(user_id)s') %
+                         {'app': app, 'is_default': is_default, 'group_id': group_id, 'user_id': user_id})
+
+  try:
+    DefaultConfiguration.objects.get(**kwargs).delete()
+  except DefaultConfiguration.DoesNotExist, e:
+    raise PopupException(_('Cannot find configuration for %(app)s with: is_default=%(is_default)s, group_id=%(group_id)s, user_id=%(user_id)s') %
+                         {'app': app, 'is_default': is_default, 'group_id': group_id, 'user_id': user_id})
+
+  return JsonResponse({
+    'status': 0,
+    'message': _('Successfully deleted the default configuration.')
+  })

+ 161 - 0
desktop/core/src/desktop/configuration/tests.py

@@ -0,0 +1,161 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+
+from nose.tools import assert_true, assert_false, assert_equal, assert_not_equal, assert_raises
+
+from django.contrib.auth.models import User
+
+from desktop.lib.django_test_util import make_logged_in_client
+from desktop.lib.test_utils import grant_access
+from desktop.models import DefaultConfiguration
+from useradmin.models import get_default_user_group
+
+
+class TestDefaultConfiguration(object):
+
+  def setUp(self):
+    self.client = make_logged_in_client(username="test_admin", groupname="default", recreate=False, is_superuser=True)
+    self.client_user = make_logged_in_client(username="test_user", groupname="default", recreate=False, is_superuser=False)
+
+    self.admin = User.objects.get(username="test_admin")
+    self.user = User.objects.get(username="test_user")
+
+    self.group = get_default_user_group()
+
+    grant_access(self.admin.username, self.admin.username, "desktop")
+    grant_access(self.user.username, self.user.username, "desktop")
+
+
+  def tearDown(self):
+    DefaultConfiguration.objects.all().delete()
+
+
+  def test_save_default_configuration(self):
+    app = 'hive'
+    is_default = True
+    properties = {
+        'settings': [{'key': 'hive.execution.engine', 'value': 'spark'}]
+    }
+
+    # Create new default configuration
+    configs = DefaultConfiguration.objects.filter(app=app, is_default=is_default)
+    assert_equal(configs.count(), 0)
+
+    response = self.client.post("/desktop/api/configurations/save", {
+        'app': 'hive',
+        'properties': json.dumps(properties),
+        'is_default': is_default})
+    content = json.loads(response.content)
+    assert_equal(content['status'], 0, content)
+    assert_true('configuration' in content, content)
+
+    config = DefaultConfiguration.objects.get(app=app, is_default=is_default)
+    assert_equal(config.properties_dict, properties, config.properties_dict)
+
+    # Update same default configuration
+    properties = {
+        'settings': [{'key': 'hive.execution.engine', 'value': 'mr'}]
+    }
+
+    response = self.client.post("/desktop/api/configurations/save", {
+        'app': 'hive',
+        'properties': json.dumps(properties),
+        'is_default': is_default})
+    content = json.loads(response.content)
+    assert_equal(content['status'], 0, content)
+    assert_true('configuration' in content, content)
+
+    config = DefaultConfiguration.objects.get(app=app, is_default=is_default)
+    assert_equal(config.properties_dict, properties, config.properties_dict)
+
+
+  def test_get_default_configurations(self):
+    app = 'hive'
+    properties = {
+        'settings': [{'key': 'hive.execution.engine', 'value': 'spark'}]
+    }
+
+    # No configurations returns null
+    response = self.client.get("/desktop/api/configurations/user", {
+        'app': 'hive',
+        'user_id': self.user.id})
+    content = json.loads(response.content)
+    assert_equal(content['status'], 0, content)
+    assert_equal(content['configuration'], None, content)
+
+    # Creating a default configuration returns default
+    response = self.client.post("/desktop/api/configurations/save", {
+        'app': 'hive',
+        'properties': json.dumps(properties),
+        'is_default': True})
+
+    response = self.client.get("/desktop/api/configurations/user", {
+        'app': 'hive',
+        'user_id': self.user.id})
+    content = json.loads(response.content)
+    assert_equal(content['status'], 0, content)
+    assert_equal(content['configuration']['app'], 'hive', content)
+    assert_equal(content['configuration']['is_default'], True, content)
+    assert_equal(content['configuration']['user'], None, content)
+    assert_equal(content['configuration']['group'], None, content)
+    assert_equal(content['configuration']['properties'], properties, content)
+
+    # Creating a group configuration returns group config
+    properties = {
+        'settings': [{'key': 'hive.execution.engine', 'value': 'mr'}]
+    }
+    response = self.client.post("/desktop/api/configurations/save", {
+        'app': 'hive',
+        'properties': json.dumps(properties),
+        'is_default': False,
+        'group_id': self.group.id})
+
+    response = self.client.get("/desktop/api/configurations/user", {
+        'app': 'hive',
+        'user_id': self.user.id})
+    content = json.loads(response.content)
+    assert_equal(content['status'], 0, content)
+    assert_equal(content['configuration']['app'], 'hive', content)
+    assert_equal(content['configuration']['is_default'], False, content)
+    assert_equal(content['configuration']['user'], None, content)
+    assert_equal(content['configuration']['group'], self.group.name, content)
+    assert_equal(content['configuration']['properties'], properties, content)
+
+    # Creating a user configuration returns user config
+    properties = {
+        'files': [{'type': 'JAR', 'path': '/user/test/udfs.jar'}],
+        'settings': [{'key': 'hive.execution.engine', 'value': 'spark'}]
+    }
+    response = self.client.post("/desktop/api/configurations/save", {
+        'app': 'hive',
+        'properties': json.dumps(properties),
+        'is_default': False,
+        'user_id': self.user.id})
+
+    response = self.client.get("/desktop/api/configurations/user", {
+        'app': 'hive',
+        'user_id': self.user.id})
+    content = json.loads(response.content)
+    assert_equal(content['status'], 0, content)
+    assert_equal(content['configuration']['app'], 'hive', content)
+    assert_equal(content['configuration']['is_default'], False, content)
+    assert_equal(content['configuration']['user'], self.user.username, content)
+    assert_equal(content['configuration']['group'], None, content)
+    assert_equal(content['configuration']['properties'], properties, content)

+ 146 - 0
desktop/core/src/desktop/migrations/0021_auto__add_defaultconfiguration__add_unique_defaultconfiguration_app_is.py

@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'DefaultConfiguration'
+        db.create_table(u'desktop_defaultconfiguration', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('app', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
+            ('properties', self.gf('django.db.models.fields.TextField')(default='{}')),
+            ('is_default', self.gf('django.db.models.fields.BooleanField')(default=True, db_index=True)),
+            ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.Group'], null=True, blank=True)),
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)),
+        ))
+        db.send_create_signal(u'desktop', ['DefaultConfiguration'])
+
+        # Adding unique constraint on 'DefaultConfiguration', fields ['app', 'is_default', 'group', 'user']
+        db.create_unique(u'desktop_defaultconfiguration', ['app', 'is_default', 'group_id', 'user_id'])
+
+
+    def backwards(self, orm):
+        # Removing unique constraint on 'DefaultConfiguration', fields ['app', 'is_default', 'group', 'user']
+        db.delete_unique(u'desktop_defaultconfiguration', ['app', 'is_default', 'group_id', 'user_id'])
+
+        # Deleting model 'DefaultConfiguration'
+        db.delete_table(u'desktop_defaultconfiguration')
+
+
+    models = {
+        u'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        u'auth.permission': {
+            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        u'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        u'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        u'desktop.defaultconfiguration': {
+            'Meta': {'unique_together': "(('app', 'is_default', 'group', 'user'),)", 'object_name': 'DefaultConfiguration'},
+            'app': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.Group']", 'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+            'properties': ('django.db.models.fields.TextField', [], {'default': "'{}'"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'})
+        },
+        u'desktop.document': {
+            'Meta': {'unique_together': "(('content_type', 'object_id'),)", 'object_name': 'Document'},
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''"}),
+            'extra': ('django.db.models.fields.TextField', [], {'default': "''"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+            'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'doc_owner'", 'to': u"orm['auth.User']"}),
+            'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['desktop.DocumentTag']", 'db_index': 'True', 'symmetrical': 'False'}),
+            'version': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+        },
+        u'desktop.document2': {
+            'Meta': {'ordering': "['-last_modified']", 'unique_together': "(('uuid', 'version', 'is_history'),)", 'object_name': 'Document2'},
+            'data': ('django.db.models.fields.TextField', [], {'default': "'{}'"}),
+            'dependencies': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'dependencies_rel_+'", 'db_index': 'True', 'to': u"orm['desktop.Document2']"}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''"}),
+            'extra': ('django.db.models.fields.TextField', [], {'default': "''"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_history': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+            'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'doc2_owner'", 'to': u"orm['auth.User']"}),
+            'parent_directory': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['desktop.Document2']"}),
+            'type': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'db_index': 'True'}),
+            'uuid': ('django.db.models.fields.CharField', [], {'default': "'7000816b-2fcd-4f9c-8664-c87241a386f5'", 'max_length': '36', 'db_index': 'True'}),
+            'version': ('django.db.models.fields.SmallIntegerField', [], {'default': '1', 'db_index': 'True'})
+        },
+        u'desktop.document2permission': {
+            'Meta': {'unique_together': "(('doc', 'perms'),)", 'object_name': 'Document2Permission'},
+            'doc': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['desktop.Document2']"}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'db_index': 'True', 'to': u"orm['auth.Group']", 'db_table': "'documentpermission2_groups'", 'symmetrical': 'False'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'perms': ('django.db.models.fields.CharField', [], {'default': "'read'", 'max_length': '10', 'db_index': 'True'}),
+            'users': ('django.db.models.fields.related.ManyToManyField', [], {'db_index': 'True', 'to': u"orm['auth.User']", 'db_table': "'documentpermission2_users'", 'symmetrical': 'False'})
+        },
+        u'desktop.documentpermission': {
+            'Meta': {'unique_together': "(('doc', 'perms'),)", 'object_name': 'DocumentPermission'},
+            'doc': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['desktop.Document']"}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'db_index': 'True', 'to': u"orm['auth.Group']", 'db_table': "'documentpermission_groups'", 'symmetrical': 'False'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'perms': ('django.db.models.fields.CharField', [], {'default': "'read'", 'max_length': '10'}),
+            'users': ('django.db.models.fields.related.ManyToManyField', [], {'db_index': 'True', 'to': u"orm['auth.User']", 'db_table': "'documentpermission_users'", 'symmetrical': 'False'})
+        },
+        u'desktop.documenttag': {
+            'Meta': {'unique_together': "(('owner', 'tag'),)", 'object_name': 'DocumentTag'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
+            'tag': ('django.db.models.fields.SlugField', [], {'max_length': '50'})
+        },
+        u'desktop.settings': {
+            'Meta': {'object_name': 'Settings'},
+            'collect_usage': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'tours_and_tutorials': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'})
+        },
+        u'desktop.userpreferences': {
+            'Meta': {'object_name': 'UserPreferences'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'key': ('django.db.models.fields.CharField', [], {'max_length': '20'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
+            'value': ('django.db.models.fields.TextField', [], {'max_length': '4096'})
+        }
+    }
+
+    complete_apps = ['desktop']

+ 66 - 0
desktop/core/src/desktop/models.py

@@ -55,6 +55,9 @@ def uuid_default():
   return str(uuid.uuid4())
 
 
+###################################################################################################
+# Custom Settings
+###################################################################################################
 class UserPreferences(models.Model):
   """Holds arbitrary key/value strings."""
   user = models.ForeignKey(auth_models.User)
@@ -72,6 +75,69 @@ class Settings(models.Model):
     return settings
 
 
+class DefaultConfigurationManager(models.Manager):
+
+  def get_configuration_for_user(self, app, user):
+    """
+    :param app: app name
+    :param user: User object
+    :return: DefaultConfiguration for user, or first group found, or default for app, or None
+    """
+    try:
+      return super(DefaultConfigurationManager, self).get(app=app, user=user)
+    except DefaultConfiguration.DoesNotExist:
+      pass
+
+    configs = super(DefaultConfigurationManager, self).get_queryset().filter(app=app, group__in=user.groups.all())
+    if configs.count() > 0:
+      return configs[0]
+
+    try:
+      return super(DefaultConfigurationManager, self).get(app=app, is_default=True)
+    except DefaultConfiguration.DoesNotExist:
+      pass
+
+    return None
+
+
+class DefaultConfiguration(models.Model):
+  """
+  Default values for configuration properties for a given app/editor
+  Can be designated as default for all users by is_default flag, or for a specific group or user
+  """
+  app = models.CharField(max_length=32, null=False, db_index=True, help_text=_t('App that this configuration belongs to.'))
+  properties = models.TextField(default='{}', help_text=_t('JSON-formatted default properties values.'))
+
+  is_default = models.BooleanField(default=False, db_index=True)
+  group = models.ForeignKey(auth_models.Group, blank=True, null=True, db_index=True)
+  user = models.ForeignKey(auth_models.User, blank=True, null=True, db_index=True)
+
+  objects = DefaultConfigurationManager()
+
+  class Meta:
+    unique_together = ('app', 'is_default', 'group', 'user')
+    ordering = ["app", "-is_default", "group", "user"]
+
+
+  @property
+  def properties_dict(self):
+    if not self.properties:
+      self.properties = json.dumps({})
+    return json.loads(self.properties)
+
+  def to_dict(self):
+    return {
+      'app': self.app,
+      'properties': self.properties_dict,
+      'is_default': self.is_default,
+      'group': self.group.name if self.group else None,
+      'user': self.user.username if self.user else None
+    }
+
+
+###################################################################################################
+# Document1
+###################################################################################################
 class DocumentTagManager(models.Manager):
 
   def get_tags(self, user):

+ 10 - 0
desktop/core/src/desktop/urls.py

@@ -123,6 +123,16 @@ dynamic_patterns += patterns('desktop.api2',
   (r'^desktop/api2/doc/import/?$', 'import_documents'),
 )
 
+# Default Configurations
+dynamic_patterns += patterns('desktop.configuration.api',
+  (r'^desktop/api/configurations/apps?$', 'get_configurable_apps'),
+
+  (r'^desktop/api/configurations/?$', 'search_default_configurations'),
+  (r'^desktop/api/configurations/user/?$', 'get_default_configuration_for_user'),
+  (r'^desktop/api/configurations/save/?$', 'save_default_configuration'),
+  (r'^desktop/api/configurations/delete/?$', 'delete_default_configuration'),
+)
+
 dynamic_patterns += patterns('useradmin.views',
   (r'^desktop/api/users/autocomplete', 'list_for_autocomplete'),
 )