Bläddra i källkod

HUE-2325 [core] Refactor config API and update README and tests

Jenny Kim 9 år sedan
förälder
incheckning
ac022bf

+ 267 - 23
desktop/core/src/desktop/configuration/README.md

@@ -1,18 +1,35 @@
 # DefaultConfiguration API
 ## API Endpoints
 
-* [/desktop/api/configurations/apps/](#get_configurable_apps)
-* [/desktop/api/configurations/user/](#get_default_configuration_for_user)
-* [/desktop/api/configurations/save/](#save_default_configuration)
-* [desktop/api/configurations/delete/](#delete_default_configuration)
+### [Default and Group Configurations](#default_and_group_configurations)
+* GET [/desktop/api/configurations/](#default_configurations)
+* POST [/desktop/api/configurations/](#update_default_group_configurations)
 
-### <a name="get_configurable_apps"></a> GET /desktop/api/configurations/apps
-Returns a JSON response with `status` and `apps` where apps contains a dictionary of all configurable apps and their defined configuration, as well as any default and group saved configurations.
+### [User-Specific Configuration for App](#user_specific_configuration)
 
-Each record in `apps` will map to a dictionary that contains a **required** `properties` record which maps to a list of defined properties for the app. Optionally, the app may also contain a `default` list of properties, and/or `group` properties where each configured group ID is returned with corresponding properties.
+* GET [/desktop/api/configurations/user/](#app_configuration_for_user)
+* POST [/desktop/api/configurations/user/](#save_app_configuration_for_user)
+
+### Delete a Saved Configuration
+
+* POST [desktop/api/configurations/delete/](#delete_default_configuration)
+
+----
+
+## <a name="default_and_group_configurations"></a> Default and Group Configurations
+
+### <a name="default_configurations"></a> GET /desktop/api/configurations/
+**Returns all configurable apps and their defined, default, and group configurations**
+
+Returns a JSON response with `status` and `configuration` where configuration contains a dictionary of all configurable apps and their defined configuration, as well as any default and group saved configurations.
+
+Each record in `configuration` will map to a dictionary that contains a **required** `properties` record which maps to a list of defined properties for the app. Optionally, the app may also contain a `default` list of properties, and/or `group` properties where each configured group ID is returned with corresponding properties.
 
 #### Example Request
-GET /desktop/api/configurations/apps
+GET /desktop/api/configurations/
+
+##### Parameters:
+None
 
 #### Example Response
 ```
@@ -164,40 +181,264 @@ GET /desktop/api/configurations/apps
 }
 ```
 
+### <a name="update_default_group_configurations"></a> POST /desktop/api/configurations/
+**Override (delete and replace) all default and group configurations with the updated configuration sent in request**
+
+Assumes that the `configuration` parameter contains a JSON of all apps and their new `default` and `groups` configurations.
+Only processes `default` and `groups`; ignores the defined `properties` and `users` if they are in the `configuration` object.
+
+#### Parameters
+
+* (**Required**) configuration: JSON dictionary where the key is the app name and the value is another dictionary that can contain `default` and/or `groups` properties. If no `default` or `groups` keys are found, all saved default and group configurations are deleted without replacement.
+
+
+#### Example Request
+POST /desktop/api/configurations/
+
+```
+"configuration": {
+    "hive": {
+        "default": [
+            {
+                "multiple": true,
+                "value": [
+                    {
+                        "path": "/user/test/myudfs.jar",
+                        "type": "jar"
+                    }
+                ],
+                "nice_name": "Files",
+                "key": "files",
+                "help_text": "Add one or more files, jars, or archives to the list of resources.",
+                "type": "hdfs-files"
+            },
+            {
+                "multiple": true,
+                "value": [
+                    {
+                        "class_name": "org.hue.udf.MyUpper",
+                        "name": "myUpper"
+                    }
+                ],
+                "nice_name": "Functions",
+                "key": "functions",
+                "help_text": "Add one or more registered UDFs (requires function name and fully-qualified classname).",
+                "type": "functions"
+            },
+            {
+                "multiple": true,
+                "value": [
+                    {
+                        "key": "mapreduce.job.queuename",
+                        "value": "mr"
+                    }
+                ],
+                "nice_name": "Settings",
+                "key": "settings",
+                "help_text": "Hive and Hadoop configuration properties.",
+                "type": "settings",
+                "options": [
+                    "hive.map.aggr",
+                    "hive.exec.compress.output",
+                    "hive.exec.parallel",
+                    "hive.execution.engine",
+                    "mapreduce.job.queuename"
+                ]
+            }
+        ]
+    }
+}
+```
+
+#### Example Response
+```
+{
+    "status": 0,
+    "configuration": {
+        "hive": {
+            "properties": [
+                {
+                    "multiple": true,
+                    "value": [],
+                    "nice_name": "Files",
+                    "key": "files",
+                    "help_text": "Add one or more files, jars, or archives to the list of resources.",
+                    "type": "hdfs-files"
+                },
+                {
+                    "multiple": true,
+                    "value": [],
+                    "nice_name": "Functions",
+                    "key": "functions",
+                    "help_text": "Add one or more registered UDFs (requires function name and fully-qualified class name).",
+                    "type": "functions"
+                },
+                {
+                    "multiple": true,
+                    "value": [],
+                    "nice_name": "Settings",
+                    "key": "settings",
+                    "help_text": "Hive and Hadoop configuration properties.",
+                    "type": "settings",
+                    "options": [
+                        "hive.map.aggr",
+                        "hive.exec.compress.output",
+                        "hive.exec.parallel",
+                        "hive.execution.engine",
+                        "mapreduce.job.queuename"
+                    ]
+                }
+            ],
+            "default": [
+                {
+                    "multiple": true,
+                    "value": [
+                        {
+                            "path": "/user/test/myudfs.jar",
+                            "type": "jar"
+                        }
+                    ],
+                    "nice_name": "Files",
+                    "key": "files",
+                    "help_text": "Add one or more files, jars, or archives to the list of resources.",
+                    "type": "hdfs-files"
+                },
+                {
+                    "multiple": true,
+                    "value": [
+                        {
+                            "class_name": "org.hue.udf.MyUpper",
+                            "name": "myUpper"
+                        }
+                    ],
+                    "nice_name": "Functions",
+                    "key": "functions",
+                    "help_text": "Add one or more registered UDFs (requires function name and fully-qualified class name).",
+                    "type": "functions"
+                },
+                {
+                    "multiple": true,
+                    "value": [
+                        {
+                            "key": "mapreduce.job.queuename",
+                            "value": "mr"
+                        }
+                    ],
+                    "nice_name": "Settings",
+                    "key": "settings",
+                    "help_text": "Hive and Hadoop configuration properties.",
+                    "type": "settings",
+                    "options": [
+                        "hive.map.aggr",
+                        "hive.exec.compress.output",
+                        "hive.exec.parallel",
+                        "hive.execution.engine",
+                        "mapreduce.job.queuename"
+                    ]
+                }
+            ]
+        }
+    }
+}
+```
+
+----
 
-### <a name="get_default_configuration_for_user"></a> GET /desktop/api/configurations/user
-Returns a JSON response with configuration for a given `app` type and user designated by `user_id`.
+## <a name="user_specific_configuration"></a> User-Specific Configuration for App
+### <a name="app_configuration_for_user"></a> GET /desktop/api/configurations/user/
+** Returns the configuration that should be used for a given user and app. Checks in order of user, group, default precedence. **
+
+#### Parameters
+
+* (**Required**) app: app type (e.g. - hive, impala, jdbc, etc.)
+* (**Required**) user_id: User ID for user
 
-If no saved configuration is found in the DB (i.e. - no default, group or user specific saved configuration), null is returned.
 
 #### Example Request
-GET /desktop/api/configurations/user
+GET /desktop/api/configurations/user/?app=hive&user_id=1
 
 #### Example Response
-TODO: write me
+
+```
+{
+    "status": 0,
+    "configuration": {
+        "is_default": false,
+        "app": "hive",
+        "group": "default",
+        "properties": [
+            {
+                "multiple": true,
+                "value": [
+                    {
+                        "path": "/user/test/myudfs.jar",
+                        "type": "jar"
+                    }
+                ],
+                "nice_name": "Files",
+                "key": "files",
+                "help_text": "Add one or more files, jars, or archives to the list of resources.",
+                "type": "hdfs-files"
+            },
+            {
+                "multiple": true,
+                "value": [
+                    {
+                        "class_name": "org.hue.udf.MyUpper",
+                        "name": "myUpper"
+                    }
+                ],
+                "nice_name": "Functions",
+                "key": "functions",
+                "help_text": "Add one or more registered UDFs (requires function name and fully-qualified class name).",
+                "type": "functions"
+            },
+            {
+                "multiple": true,
+                "value": [
+                    {
+                        "key": "mapreduce.job.queuename",
+                        "value": "mr"
+                    }
+                ],
+                "nice_name": "Settings",
+                "key": "settings",
+                "help_text": "Hive and Hadoop configuration properties.",
+                "type": "settings",
+                "options": [
+                    "hive.map.aggr",
+                    "hive.exec.compress.output",
+                    "hive.exec.parallel",
+                    "hive.execution.engine",
+                    "mapreduce.job.queuename"
+                ]
+            }
+        ],
+        "user": null
+    }
+}
+```
 
 
-### <a name="save_default_configuration"></a> POST /desktop/api/configurations/save/
-Saves a configuration for either `default` (all users), a specific `group` designated by `group_id` or a specific `user` designated by `user_id`.
+### <a name="save_app_configuration_for_user"></a> POST /desktop/api/configurations/user/
+Saves a single configuration for a specific `user` designated by `user_id` and `app`. This will create or update an existing user-specific configuration.
 
 #### Parameters
 
 * (**Required**) app: app type (e.g. - hive, impala, jdbc, etc.)
 * (**Required**) properties: JSON of saved properties
-* (**Optional**) is_default: boolean indicating if this is a default configuration or not
-* (**Optional**) group_id: group ID if this is a saved configuration for a specific group
-* (**Optional**) user_id: user ID if this is a saved configuration for a specific user
+* (**Required**) user_id: user ID if this is a saved configuration for a specific user
 
-Either `is_default`, `group_id` or `user_id` must be passed
+`properties` is the properties list, NOT the full `configuration` JSON
 
 
 #### Example Request
-POST /desktop/api/configurations/apps
+POST /desktop/api/configurations/user
 
 ```
 {
     "app": "hive",
-    "group_id": 1,
+    "user_id": 1,
     "properties": [
         {
             "multiple": true,
@@ -256,7 +497,7 @@ POST /desktop/api/configurations/apps
     "configuration": {
         "is_default": false,
         "app": "hive",
-        "group": "default",
+        "group": null,
         "properties": [
             {
                 "multiple": true,
@@ -305,11 +546,14 @@ POST /desktop/api/configurations/apps
                 ]
             }
         ],
-        "user": null
+        "user": 1
     }
 }
 ```
 
+---
+
+
 ### <a name="delete_default_configuration"></a> GET /desktop/api/configurations/delete
 TODO: Write me
 

+ 130 - 129
desktop/core/src/desktop/configuration/api.py

@@ -15,9 +15,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import json
 import logging
 
 from django.contrib.auth.models import Group, User
+from django.db import transaction
+from django.db.models import Q
 from django.utils.translation import ugettext as _
 from django.views.decorators.http import require_POST
 
@@ -54,157 +57,74 @@ def api_error_handler(func):
 
   return decorator
 
-def get_configurable():
-  # TODO: Use metaclasses to self-register configurable apps
-  app_configs = {}
-  config_classes = [HiveConfiguration, ImpalaConfiguration, SparkConfiguration]
-
-  # Optional configurable classes from installed apps
-  if OozieWorkflowConfiguration is not None:
-    config_classes.append(OozieWorkflowConfiguration)
-
-  for config_cls in config_classes:
-    if not hasattr(config_cls, 'APP_NAME') or not hasattr(config_cls, 'PROPERTIES'):
-      LOG.exception('Configurable classes must define APP_NAME and PROPERTIES.')
-    app_name = config_cls.APP_NAME
-    app_configs[app_name] = {
-      'properties': config_cls.PROPERTIES
-    }
-
-    # Get default config
-    if DefaultConfiguration.objects.filter(app=app_name, is_default=True).exists():
-      default_config = DefaultConfiguration.objects.get(app=app_name, is_default=True)
-      app_configs[app_name].update({'default': default_config.properties_list})
-
-    # Get group configs
-    if DefaultConfiguration.objects.filter(app=app_name, group__isnull=False).exists():
-      app_configs[app_name].update({'groups': {}})
-      for grp_config in DefaultConfiguration.objects.filter(app=app_name, group__isnull=False).all():
-        app_configs[app_name]['groups'].update({grp_config.group.id: grp_config.properties_list})
-
-  return {
-    'status': 0,
-    'apps': app_configs
-  }
 
 @api_error_handler
-def get_configurable_apps(request):
-  # TODO: Use metaclasses to self-register configurable apps
-  app_configs = {}
-  config_classes = [HiveConfiguration, ImpalaConfiguration]
+def default_configurations(request):
+  if request.method == 'GET':  # get configurable apps
+    configurations = _get_default_configurations()
 
-  for config_cls in config_classes:
-    if not hasattr(config_cls, 'APP_NAME') or not hasattr(config_cls, 'PROPERTIES'):
-      LOG.exception('Configurable classes must define APP_NAME and PROPERTIES.')
-    app_name = config_cls.APP_NAME
-    app_configs[app_name] = {
-      'properties': config_cls.PROPERTIES
+    response = {
+      'status': 0,
+      'configuration': configurations
     }
+  elif request.method == 'POST':  # save/overwrite app configurations
+    configurations = json.loads(request.POST.get('configuration'))
+    updated_configurations = _update_default_and_group_configurations(configurations)
 
-    # Get default config
-    if DefaultConfiguration.objects.filter(app=app_name, is_default=True).exists():
-      default_config = DefaultConfiguration.objects.get(app=app_name, is_default=True)
-      app_configs[app_name].update({'default': default_config.properties_list})
-
-    # Get group configs
-    if DefaultConfiguration.objects.filter(app=app_name, group__isnull=False).exists():
-      app_configs[app_name].update({'groups': {}})
-      for grp_config in DefaultConfiguration.objects.filter(app=app_name, group__isnull=False).all():
-        app_configs[app_name]['groups'].update({grp_config.group.id: grp_config.properties_list})
-
-  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')
+    response = {
+      'status': 0,
+      'configuration': updated_configurations
+    }
+  else:
+    raise PopupException(_('%s method is not supported') % request.method)
 
-  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]
-  })
+  return JsonResponse(response)
 
 
 @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)
+def app_configuration_for_user(request):
+  if request.method == 'GET':  # get app configuration for user (checks in order of user, group, default precedence)
+    app = request.GET.get('app')
+    user_id = request.GET.get('user_id')
 
-  if not user:
-    raise PopupException(_('Could not find user with User ID: %s') % user_id)
+    if not app or not user_id:
+      raise PopupException(_('app_configuration_for_user requires app and user_id'))
 
-  config = DefaultConfiguration.objects.get_configuration_for_user(app, user)
+    user = User.objects.get(id=user_id)
 
-  return JsonResponse({
-    'status': 0,
-    'configuration': config.to_dict() if config is not None else None
-  })
+    if not user:
+      raise PopupException(_('Could not find user with User ID: %s') % user_id)
 
+    config = DefaultConfiguration.objects.get_configuration_for_user(app, user)
 
-@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')
+    response = {
+      'status': 0,
+      'configuration': config.to_dict() if config is not None else None
+    }
+  elif request.method == 'POST':  # save user-specific configuration for app
+    app = request.POST.get('app')
+    user_id = request.POST.get('user_id')
+    properties = json.loads(request.POST.get('properties'))
 
-  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 not app or not user_id or not properties:
+      raise PopupException(_('app_configuration_for_user requires app, user_id, and properties'))
 
-  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()
+    config = _save_configuration(app, properties, is_default=False, user=user)
+    LOG.info('Saved user configuration for app: %s and group_id: %s' % (app, user_id))
 
-  return JsonResponse({
-    'status': 0,
-    'configuration': config.to_dict()
-  })
+    response = {
+      'status': 0,
+      'configuration': config.to_dict()
+    }
+  else:
+    raise PopupException(_('%s method is not supported') % request.method)
+
+  return JsonResponse(response)
 
 
 @api_error_handler
@@ -246,3 +166,84 @@ def delete_default_configuration(request):
     'status': 0,
     'message': _('Successfully deleted the default configuration.')
   })
+
+
+def _get_default_configurations():
+  """
+  :return: Dictionary where key is app name and values include the defined "properties" list and any saved "default"
+    configuration or "groups" configurations
+  """
+  # TODO: Use metaclasses to self-register configurable apps
+  app_configs = {}
+  config_classes = _get_configurable_classes()
+
+  for config_cls in config_classes:
+    if not hasattr(config_cls, 'APP_NAME') or not hasattr(config_cls, 'PROPERTIES'):
+      LOG.exception('Configurable classes must define APP_NAME and PROPERTIES.')
+    app_name = config_cls.APP_NAME
+    app_configs[app_name] = {
+      'properties': config_cls.PROPERTIES
+    }
+
+    # Get default config
+    if DefaultConfiguration.objects.filter(app=app_name, is_default=True).exists():
+      default_config = DefaultConfiguration.objects.get(app=app_name, is_default=True)
+      app_configs[app_name].update({'default': default_config.properties_list})
+
+    # Get group configs
+    if DefaultConfiguration.objects.filter(app=app_name, group__isnull=False).exists():
+      app_configs[app_name].update({'groups': {}})
+      for grp_config in DefaultConfiguration.objects.filter(app=app_name, group__isnull=False).all():
+        app_configs[app_name]['groups'].update({grp_config.group.id: grp_config.properties_list})
+
+  return app_configs
+
+
+def _get_configurable_classes():
+  config_classes = [HiveConfiguration, ImpalaConfiguration, SparkConfiguration]
+
+  # Optional configurable classes from installed apps
+  if OozieWorkflowConfiguration is not None:
+    config_classes.append(OozieWorkflowConfiguration)
+
+  return config_classes
+
+
+def _update_default_and_group_configurations(configurations):
+  """
+  Overrides (deletes and updates) saved app configs based on the given configurations dict. Wrapped in an atomic
+    transaction block so that it is an all-or-nothing operation.
+  :param configurations: Dictionary of app to configuration objects. Only processes "default" and "groups" configs
+  :return: updated configurations dict
+  """
+  with transaction.atomic():
+    # delete all previous default and group configurations
+    DefaultConfiguration.objects.filter(Q(is_default=True) | Q(group__isnull=False)).delete()
+
+    for app, configs in configurations.items():
+      if 'default' in configs:
+        properties = configs['default']
+        _save_configuration(app, properties, is_default=True)
+        LOG.info('Saved default configuration for app: %s' % app)
+
+      if 'groups' in configs:
+        for group_id, properties in configs['groups'].items():
+          try:
+            group = Group.objects.get(id=int(group_id))
+          except Group.DoesNotExist, e:
+            raise PopupException(_('Could not find group with ID: %s') % group_id)
+          _save_configuration(app, properties, is_default=False, group=group)
+          LOG.info('Saved group configuration for app: %s and group_id: %s' % (app, group_id))
+
+  return _get_default_configurations()
+
+
+def _save_configuration(app, properties, is_default=False, group=None, user=None):
+  if not (is_default or group or user):
+    raise PopupException(_('_save_configuration requires app, properties, and is_default, group_id or user_id'))
+
+  kwargs = {'app': app, 'is_default': is_default, 'group': group, 'user': user}
+  config, created = DefaultConfiguration.objects.get_or_create(**kwargs)
+  config.properties = json.dumps(properties)
+  config.save()
+  return config

+ 141 - 103
desktop/core/src/desktop/configuration/tests.py

@@ -8,7 +8,7 @@
 # "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
+#   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,
@@ -47,115 +47,142 @@ class TestDefaultConfiguration(object):
     DefaultConfiguration.objects.all().delete()
 
 
-  def test_save_default_configuration(self):
-    app = 'hive'
-    is_default = True
-    properties = [
-        {
-          "multiple": True,
-          "value": [],
-          "nice_name": "Settings",
-          "key": "settings",
-          "help_text": "Impala configuration properties.",
-          "type": "settings",
-          "options": []
-        }
-    ]
+  def test_update_default_and_group_configurations(self):
+    configuration = {
+      'hive': {
+        'default': [
+          {
+            'multiple': True,
+            'value': [],
+            'nice_name': 'Settings',
+            'key': 'settings',
+            'help_text': 'Hive configuration properties.',
+            'type': 'settings',
+            'options': []
+          }
+        ]
+      }
+    }
 
-    # Create new default configuration
-    configs = DefaultConfiguration.objects.filter(app=app, is_default=is_default)
+    # Verify no default configuration found for app
+    configs = DefaultConfiguration.objects.filter(app='hive', is_default=True)
     assert_equal(configs.count(), 0)
 
-    response = self.client.post("/desktop/api/configurations/save", {
-        'app': 'hive',
-        'properties': json.dumps(properties),
-        'is_default': is_default})
+    # Save configuration
+    response = self.client.post("/desktop/api/configurations/", {'configuration': json.dumps(configuration)})
     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_list, properties, config.properties_list)
-
-    # Update same default configuration
-    properties = {
-        'settings': [{'key': 'hive.execution.engine', 'value': 'mr'}]
+    config = DefaultConfiguration.objects.get(app='hive', is_default=True)
+    assert_equal(config.properties_list, configuration['hive']['default'], config.properties_list)
+
+    # Update with group configuration
+    configuration = {
+      'hive': {
+        'default': [
+          {
+            'multiple': True,
+            'value': [{'key': 'hive.execution.engine', 'value': 'mr'}],
+            'nice_name': 'Settings',
+            'key': 'settings',
+            'help_text': 'Hive configuration properties.',
+            'type': 'settings',
+            'options': []
+          }
+        ],
+        'groups': {
+          str(self.group.id): [
+            {
+              'multiple': True,
+              'value': [{'key': 'hive.execution.engine', 'value': 'spark'}],
+              'nice_name': 'Settings',
+              'key': 'settings',
+              'help_text': 'Hive configuration properties.',
+              'type': 'settings',
+              'options': []
+            }
+          ]
+        }
+      }
     }
 
-    response = self.client.post("/desktop/api/configurations/save", {
-        'app': 'hive',
-        'properties': json.dumps(properties),
-        'is_default': is_default})
+    response = self.client.post("/desktop/api/configurations/", {'configuration': json.dumps(configuration)})
     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_list, properties, config.properties_list)
+    config = DefaultConfiguration.objects.get(app='hive', is_default=True)
+    assert_equal(config.properties_list, configuration['hive']['default'], config.properties_list)
+
+    config = DefaultConfiguration.objects.get(app='hive', group=self.group)
+    assert_equal(config.properties_list, configuration['hive']['groups'][str(self.group.id)], config.properties_list)
 
 
   def test_get_default_configurations(self):
     app = 'hive'
     properties = [
-        {
-            "multiple": True,
-            "value": [{
-                "path": "/user/test/myudfs.jar",
-                "type": "jar"
-            }],
-            "nice_name": "Files",
-            "key": "files",
-            "help_text": "Add one or more files, jars, or archives to the list of resources.",
-            "type": "hdfs-files"
-        },
-        {
-            "multiple": True,
-            "value": [{
-                "class_name": "org.hue.udf.MyUpper",
-                "name": "myUpper"
-            }],
-            "nice_name": "Functions",
-            "key": "functions",
-            "help_text": "Add one or more registered UDFs (requires function name and fully-qualified class name).",
-            "type": "functions"
-        },
-        {
-            "multiple": True,
-            "value": [{
-                "key": "mapreduce.job.queuename",
-                "value": "mr"
-            }],
-            "nice_name": "Settings",
-            "key": "settings",
-            "help_text": "Hive and Hadoop configuration properties.",
-            "type": "settings",
-            "options": [
-                "hive.map.aggr",
-                "hive.exec.compress.output",
-                "hive.exec.parallel",
-                "hive.execution.engine",
-                "mapreduce.job.queuename"
-            ]
-        }
+      {
+        "multiple": True,
+        "value": [{
+          "path": "/user/test/myudfs.jar",
+          "type": "jar"
+        }],
+        "nice_name": "Files",
+        "key": "files",
+        "help_text": "Add one or more files, jars, or archives to the list of resources.",
+        "type": "hdfs-files"
+      },
+      {
+        "multiple": True,
+        "value": [{
+          "class_name": "org.hue.udf.MyUpper",
+          "name": "myUpper"
+        }],
+        "nice_name": "Functions",
+        "key": "functions",
+        "help_text": "Add one or more registered UDFs (requires function name and fully-qualified class name).",
+        "type": "functions"
+      },
+      {
+        "multiple": True,
+        "value": [{
+          "key": "mapreduce.job.queuename",
+          "value": "mr"
+        }],
+        "nice_name": "Settings",
+        "key": "settings",
+        "help_text": "Hive and Hadoop configuration properties.",
+        "type": "settings",
+        "options": [
+          "hive.map.aggr",
+          "hive.exec.compress.output",
+          "hive.exec.parallel",
+          "hive.execution.engine",
+          "mapreduce.job.queuename"
+        ]
+      }
     ]
+    configuration = {
+      app: {
+        'default': properties
+      }
+    }
 
     # No configurations returns null
     response = self.client.get("/desktop/api/configurations/user", {
-        'app': 'hive',
-        'user_id': self.user.id})
+      '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.post("/desktop/api/configurations/", {'configuration': json.dumps(configuration)})
 
     response = self.client.get("/desktop/api/configurations/user", {
-        'app': 'hive',
-        'user_id': self.user.id})
+      '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)
@@ -165,44 +192,55 @@ class TestDefaultConfiguration(object):
     assert_equal(content['configuration']['properties'], properties, content)
 
     # Creating a group configuration returns group config
-    properties = {
-        'settings': [{'key': 'hive.execution.engine', 'value': 'mr'}]
+    group_properties = [{
+      'multiple': True,
+      'value': [{'key': 'hive.execution.engine', 'value': 'spark'}],
+      'nice_name': 'Settings',
+      'key': 'settings',
+      'help_text': 'Hive configuration properties.',
+      'type': 'settings',
+      'options': []
+    }]
+    configuration = {
+      app: {
+        'default': properties,
+        'groups': {
+          str(self.group.id): group_properties
+        }
+      }
     }
-    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.post("/desktop/api/configurations/", {'configuration': json.dumps(configuration)})
 
     response = self.client.get("/desktop/api/configurations/user", {
-        'app': 'hive',
-        'user_id': self.user.id})
+      '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)
+    assert_equal(content['configuration']['properties'], group_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})
+    user_properties = [{
+      'files': [{'type': 'JAR', 'path': '/user/test/udfs.jar'}],
+      'settings': [{'key': 'hive.execution.engine', 'value': 'spark'}]
+    }]
+    response = self.client.post("/desktop/api/configurations/user", {
+      'app': 'hive',
+      'user_id': self.user.id,
+      'properties': json.dumps(user_properties)
+    })
 
     response = self.client.get("/desktop/api/configurations/user", {
-        'app': 'hive',
-        'user_id': self.user.id})
+      '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)
+    assert_equal(content['configuration']['properties'], user_properties, content)

+ 2 - 5
desktop/core/src/desktop/urls.py

@@ -126,11 +126,8 @@ dynamic_patterns += patterns('desktop.api2',
 
 # 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/?$', 'default_configurations'),
+  (r'^desktop/api/configurations/user/?$', 'app_configuration_for_user'),
   (r'^desktop/api/configurations/delete/?$', 'delete_default_configuration'),
 )