commit 426e1fb9fab686cb1497a9da9a7d274579c584e4
Author: Kostas Papadimitriou <kpap@grnet.gr>
Date:   Thu May 2 16:17:40 2013 +0300

    cyclades ui: Expose and use the new astakos quota api
    
    - Removed /userdata/quota view in respect of astakos quotas api delegate
      views served in the same urls used by astakos api (/astakos/api/quotas,
      /astakos/api/resources). Base url is configurable using the
      UI_ACCOUNTS_API_URL (defaults to /astakos/api).
    - Refactored quotas js client to use backbone Collection/Model
      mechanisms.
    - Refresh quotas using the common api update mechanism used by vms and
      networks (deprecated UI_QUOTAS_UPDATE_INTERVAL setting)
    - Extended main view initialization steps to include quotas/resources
      loading before the initial layout renedring

diff --git a/snf-cyclades-app/conf/20-snf-cyclades-app-ui.conf b/snf-cyclades-app/conf/20-snf-cyclades-app-ui.conf
index 097a17e..2691143 100644
--- a/snf-cyclades-app/conf/20-snf-cyclades-app-ui.conf
+++ b/snf-cyclades-app/conf/20-snf-cyclades-app-ui.conf
@@ -39,9 +39,6 @@
 ## consecutive API calls (aligning changes-since attribute).
 #UI_CHANGES_SINCE_ALIGNMENT = 0
 #
-## How often to check for user usage changes
-#UI_QUOTAS_UPDATE_INTERVAL = 10000
-#
 ## URL to redirect not authenticated users
 #UI_LOGIN_URL = "/im/login"
 #
@@ -183,6 +180,8 @@
 ## The name of the grouped network view
 #UI_GROUPED_PUBLIC_NETWORK_NAME = 'Internet'
 #
+## Endpoint to make account specific requests (resources/quotas)
+#UI_ACCOUNTS_API_URL = '/astakos/api'
 #
 ################
 ## UI EXTENSIONS
diff --git a/snf-cyclades-app/synnefo/api/delegate.py b/snf-cyclades-app/synnefo/api/delegate.py
index fafd005..5b360bc 100644
--- a/snf-cyclades-app/synnefo/api/delegate.py
+++ b/snf-cyclades-app/synnefo/api/delegate.py
@@ -42,6 +42,8 @@ from django.conf import settings
 ASTAKOS_URL = getattr(settings, 'ASTAKOS_URL', None)
 USER_CATALOG_URL = urlparse.urljoin(ASTAKOS_URL, "user_catalogs")
 USER_FEEDBACK_URL = urlparse.urljoin(ASTAKOS_URL, "feedback")
+USER_QUOTA_URL = urlparse.urljoin(ASTAKOS_URL, "astakos/api/quotas")
+RESOURCES_URL = urlparse.urljoin(ASTAKOS_URL, "astakos/api/resources")
 
 from objpool.http import PooledHTTPConnection
 
@@ -68,6 +70,24 @@ def proxy(request, url, headers={}, body=None):
 
 
 @csrf_exempt
+def delegate_to_resources_service(request):
+    logger.debug("Delegate resources request to %s" % RESOURCES_URL)
+    token = request.META.get('HTTP_X_AUTH_TOKEN')
+    headers = {'X-Auth-Token': token}
+    return proxy(request, RESOURCES_URL, headers=headers,
+                 body=request.raw_post_data)
+
+
+@csrf_exempt
+def delegate_to_user_quota_service(request):
+    logger.debug("Delegate quotas request to %s" % USER_QUOTA_URL)
+    token = request.META.get('HTTP_X_AUTH_TOKEN')
+    headers = {'X-Auth-Token': token}
+    return proxy(request, USER_QUOTA_URL, headers=headers,
+                 body=request.raw_post_data)
+
+
+@csrf_exempt
 def delegate_to_feedback_service(request):
     logger.debug("Delegate feedback request to %s" % USER_FEEDBACK_URL)
     token = request.META.get('HTTP_X_AUTH_TOKEN')
diff --git a/snf-cyclades-app/synnefo/app_settings/urls.py b/snf-cyclades-app/synnefo/app_settings/urls.py
index 8e5eb84..078b5c6 100644
--- a/snf-cyclades-app/synnefo/app_settings/urls.py
+++ b/snf-cyclades-app/synnefo/app_settings/urls.py
@@ -51,5 +51,9 @@ if PROXY_USER_SERVICES:
     urlpatterns += patterns(
         '',
         (r'^feedback/?$', 'synnefo.api.delegate.delegate_to_feedback_service'),
-        (r'^user_catalogs/?$', 'synnefo.api.delegate.delegate_to_user_catalogs_service'))
-
+        (r'^user_catalogs/?$',
+         'synnefo.api.delegate.delegate_to_user_catalogs_service'),
+        (r'^astakos/api/resources/?$',
+         'synnefo.api.delegate.delegate_to_resources_service'),
+        (r'^astakos/api/quotas/?$',
+         'synnefo.api.delegate.delegate_to_user_quota_service'))
diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/models.js b/snf-cyclades-app/synnefo/ui/static/snf/js/models.js
index 7e628ad..d4bf3af 100644
--- a/snf-cyclades-app/synnefo/ui/static/snf/js/models.js
+++ b/snf-cyclades-app/synnefo/ui/static/snf/js/models.js
@@ -615,7 +615,7 @@
 
                 var _success = _.bind(function() {
                     if (success) { success() };
-                    snf.ui.main.load_user_quotas();
+                    synnefo.storage.quotas.get('cyclades.network.private').decrease();
                 }, this);
                 var _error = _.bind(function() {
                     this.set({state: previous_state, status: previous_status})
@@ -1441,7 +1441,7 @@
                                              // set state after successful call
                                              self.state('DESTROY');
                                              success.apply(this, arguments);
-                                             snf.ui.main.load_user_quotas();
+                                             synnefo.storage.quotas.get('cyclades.vm').decrease();
 
                                          },  
                                          error, 'destroy', params);
@@ -1693,7 +1693,7 @@
             
             var cb = function() {
               callback();
-              snf.ui.main.load_user_quotas();
+              synnefo.storage.quotas.get('cyclades.network.private').increase();
             }
             return this.api_call(this.path, "create", params, cb);
         },
@@ -2122,15 +2122,17 @@
                 }
             }
             
-            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
+            opts = {name: name, imageRef: image.id, flavorRef: flavor.id, 
+                    metadata:meta}
             opts = _.extend(opts, extra);
             
             var cb = function(data) {
-              snf.ui.main.load_user_quotas();
+              synnefo.storage.quotas.get('cyclades.vm').increase();
               callback(data);
             }
 
-            this.api_call(this.path, "create", {'server': opts}, undefined, undefined, cb, {critical: true});
+            this.api_call(this.path, "create", {'server': opts}, undefined, 
+                          undefined, cb, {critical: true});
         },
 
         load_missing_images: function(callback) {
@@ -2365,7 +2367,103 @@
             
             this.create(m.attributes, options);
         }
+    });
+
+  
+    models.Quota = models.Model.extend({
+
+        initialize: function() {
+            models.Quota.__super__.initialize.apply(this, arguments);
+            this.bind("change", this.check, this);
+            this.check();
+        },
+        
+        check: function() {
+            var usage, limit;
+            usage = this.get('usage');
+            limit = this.get('limit');
+            if (usage >= limit) {
+                this.trigger("available");
+            } else {
+                this.trigger("unavailable");
+            }
+        },
+
+        increase: function(val) {
+            if (val === undefined) { val = 1};
+            this.set({'usage': this.get('usage') + val})
+        },
+
+        decrease: function(val) {
+            if (val === undefined) { val = 1};
+            this.set({'usage': this.get('usage') - val})
+        },
+
+        can_consume: function() {
+            var usage, limit;
+            usage = this.get('usage');
+            limit = this.get('limit');
+            if (usage >= limit) {
+                return false
+            } else {
+                return true
+            }
+        },
+        
+        is_bytes: function() {
+            return this.get('resource').get('unit') == 'bytes';
+        },
+        
+        get_available: function() {
+            var value = this.get('limit') - this.get('usage');
+            if (value < 0) { return value }
+            return value
+        },
+
+        get_readable: function(key) {
+            var value;
+            if (key == 'available') {
+                value = this.get_available();
+            } else {
+                value = this.get(key)
+            }
+            if (!this.is_bytes()) {
+              return value + "";
+            }
+            return snf.util.readablizeBytes(value);
+        }
+    });
+
+    models.Quotas = models.Collection.extend({
+        model: models.Quota,
+        api_type: 'accounts',
+        path: 'quotas',
+        parse: function(resp) {
+            return _.map(resp.system, function(value, key) {
+                var available = (value.limit - value.usage) || 0;
+                return _.extend(value, {'name': key, 'id': key, 
+                          'available': available,
+                          'resource': snf.storage.resources.get(key)});
+            })
+        }
     })
+
+    models.Resource = models.Model.extend({
+        api_type: 'accounts',
+        path: 'resources'
+    });
+
+    models.Resources = models.Collection.extend({
+        api_type: 'accounts',
+        path: 'resources',
+        model: models.Network,
+
+        parse: function(resp) {
+            return _.map(resp, function(value, key) {
+                return _.extend(value, {'name': key, 'id': key});
+            })
+        }
+    });
     
     // storage initialization
     snf.storage.images = new models.Images();
@@ -2374,9 +2472,7 @@
     snf.storage.vms = new models.VMS();
     snf.storage.keys = new models.PublicKeys();
     snf.storage.nics = new models.NICs();
-
-    //snf.storage.vms.fetch({update:true});
-    //snf.storage.images.fetch({update:true});
-    //snf.storage.flavors.fetch({update:true});
+    snf.storage.resources = new models.Resources();
+    snf.storage.quotas = new models.Quotas();
 
 })(this);
diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_create_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_create_view.js
index 5f2fcd8..b51e7ff 100644
--- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_create_view.js
+++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_create_view.js
@@ -678,10 +678,8 @@
               image_excluded = storage.flavors.unavailable_values_for_image(this.current_image);
             }
 
-            if (snf.user.quota) {
-              quotas = this.get_vm_params_quotas();
-              user_excluded = storage.flavors.unavailable_values_for_quotas(quotas);
-            }
+            quotas = this.get_vm_params_quotas();
+            user_excluded = storage.flavors.unavailable_values_for_quotas(quotas);
 
             unavailable.disk = user_excluded.disk.concat(image_excluded.disk);
             unavailable.ram = user_excluded.ram.concat(image_excluded.ram);
@@ -691,10 +689,11 @@
         },
         
         get_vm_params_quotas: function() {
+          var quotas = synnefo.storage.quotas;
           var quota = {
-            'ram': snf.user.quota.get_available('cyclades.ram'),
-            'cpu': snf.user.quota.get_available('cyclades.cpu'),
-            'disk': snf.user.quota.get_available('cyclades.disk')
+            'ram': quotas.get('cyclades.ram').get('available'),
+            'cpu': quotas.get('cyclades.cpu').get('available'),
+            'disk': quotas.get('cyclades.disk').get('available')
           }
           return quota;
         },
@@ -769,7 +768,7 @@
                 };
             }, this));
 
-            this.$("#create-vm-flavor-options .flavor-options.ram li").each(_.bind(function(i, el){
+            this.$("#create-vm-flavor-options .flavor-options.mem li").each(_.bind(function(i, el){
                 var el_value = $(el).data("value");
                 if (this.unavailable_values.ram.indexOf(el_value) > -1) {
                     $(el).addClass("disabled");
@@ -941,11 +940,11 @@
         },
         
         update_quota_display: function() {
-          if (!snf.user.quota || !snf.user.quota.data) { return };
 
+          var quotas = synnefo.storage.quotas;
           _.each(["disk", "ram", "cpu"], function(type) {
-            var available_dsp = snf.user.quota.get_available_readable(type);
-            var available = snf.user.quota.get_available(type);
+            var available_dsp = quotas.get('cyclades.'+type).get_readable('available');
+            var available = quotas.get('cyclades.'+type).get('available');
             var content = "({0} left)".format(available_dsp);
             if (available <= 0) { content = "(None left)" }
             
diff --git a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_main_view.js b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_main_view.js
index 5c9bad1..94f4e93 100644
--- a/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_main_view.js
+++ b/snf-cyclades-app/synnefo/ui/static/snf/js/ui/web/ui_main_view.js
@@ -231,9 +231,12 @@
                 this.fix_position();
             }, this));
 
-            storage.vms.bind("change:pending_action", _.bind(this.handle_action_add, this, "vms"));
-            storage.vms.bind("change:reboot_required", _.bind(this.handle_action_add, this, "reboots"));
-            storage.networks.bind("change:actions", _.bind(this.handle_action_add, this, "nets"));
+            storage.vms.bind("change:pending_action", 
+                             _.bind(this.handle_action_add, this, "vms"));
+            storage.vms.bind("change:reboot_required", 
+                             _.bind(this.handle_action_add, this, "reboots"));
+            storage.networks.bind("change:actions", 
+                                  _.bind(this.handle_action_add, this, "nets"));
         },
 
         handle_action_add: function(type, model, action) {
@@ -532,6 +535,7 @@
             storage.vms.bind("add", _.bind(this.check_empty, this));
             storage.vms.bind("change:status", _.bind(this.check_empty, this));
             storage.vms.bind("reset", _.bind(this.check_empty, this));
+            storage.quotas.bind("change", _.bind(this.update_create_buttons_status, this));
             
         },
         
@@ -592,7 +596,7 @@
             $(".css-panes").show();
         },
         
-        items_to_load: 4,
+        items_to_load: 6,
         completed_items: 0,
         check_status: function(loaded) {
             this.completed_items++;
@@ -629,6 +633,7 @@
                 self.update_status("networks", 1);
                 self.check_status();
             }});
+
         },  
 
         init_intervals: function() {
@@ -642,11 +647,13 @@
             
             this._networks = storage.networks.get_fetcher.apply(storage.networks, _.clone(fetcher_params));
             this._vms = storage.vms.get_fetcher.apply(storage.vms, _.clone(fetcher_params));
+            this._quotas = storage.quotas.get_fetcher.apply(storage.quotas, _.clone(fetcher_params));
         },
 
         stop_intervals: function() {
             if (this._networks) { this._networks.stop(); }
             if (this._vms) { this._vms.stop(); }
+            if (this._quotas) { this._quotas.stop(); }
             this.intervals_stopped = true;
         },
 
@@ -665,6 +672,13 @@
                 this.init_intervals();
             }
 
+            if (this._quotas) {
+                this._quotas.stop();
+                this._quotas.start();
+            } else {
+                this.init_intervals();
+            }
+
             this.intervals_stopped = false;
         },
 
@@ -683,7 +697,6 @@
             snf.config.update_hidden_views = uhv;
 
             window.setTimeout(function() {
-                self.update_status("layout", 0);
                 self.load_initialize_overlays();
             }, 20);
         },
@@ -732,6 +745,18 @@
                 self.update_status("flavors", 1);
                 self.check_status()
             }});
+
+            this.update_status("resources", 0);
+            storage.resources.fetch({refresh:true, update:false, success: function(){
+                self.update_status("resources", 1);
+                self.update_status("quotas", 0);
+                self.check_status();
+                storage.quotas.fetch({refresh:true, update:true, success: function() {
+                  self.update_status("quotas", 1);
+                  self.update_status("layout", 1);
+                  self.check_status()
+                }})
+            }})
         },
 
         update_status: function(ns, state) {
@@ -779,72 +804,11 @@
             }
         },
         
-        quota_handlers_initialized: false,
-
-        load_user_quotas: function(repeat) {
-          var main_view = this;
-          if (!snf.user.quota) {
-            snf.user.quota = new snf.quota.Quota("cyclades");
-            main_view.init_quotas_handlers();
-      
-          }
-
-          snf.api.sync('read', undefined, {
-            url: synnefo.config.quota_url, 
-            success: function(d) {
-              snf.user.quota.load(d);
-            },
-            complete: function() {
-                if (repeat) {
-                  setTimeout(function(){
-                      main_view.load_user_quotas(1);
-                  }, synnefo.config.quotas_update_interval || 10000);
-                }
-            }
-          });
-        },
-        
-        check_quotas: function(type) {
-          var storage = synnefo.storage[type];
-          var consumed = storage.length;
-          var quotakey = {
-            'networks': 'cyclades.network.private',
-            'vms': 'cyclades.vm'
-          }
-          if (type == "networks") {
-            consumed = storage.filter(function(net){
-              return !net.is_public() && !net.is_deleted();
-            }).length;
-          }
-          
-          var limit = snf.user.quota.get_limit(quotakey[type]);
-          if (snf.user.quota && snf.user.quota.data && consumed >= limit) {
-            storage.trigger("quota_reached");
-          } else {
-            storage.trigger("quota_free");
-          }
-        },
-
-        init_quotas_handlers: function() {
-          var self = this, event;
-          snf.user.quota.bind("cyclades.vm.quota.changed", function() {
-            this.check_quotas("vms");
-          }, this);
-
-          var event = "cyclades.network.private.quota.changed";
-          snf.user.quota.bind(event, function() {
-            this.check_quotas("networks");
-          }, this);
-        },
-
         // initial view based on user cookie
         show_initial_view: function() {
           this.set_vm_view_handlers();
-          this.load_user_quotas(1);
           this.hide_loading_view();
-          
           bb.history.start();
-
           this.trigger("ready");
         },
 
@@ -853,6 +817,25 @@
               this.router.vm_details_view(vm.id);
             }
         },
+        
+        update_create_buttons_status: function() {
+            var nets = storage.quotas.get('cyclades.network.private');
+            var vms = storage.quotas.get('cyclades.vm');
+            
+            if (!nets || !vms) { return }
+
+            if (!nets.can_consume()) {
+                $("#networks-pane a.createbutton").addClass("disabled");
+            } else {
+                $("#networks-pane a.createbutton").removeClass("disabled");
+            }
+
+            if (!vms.can_consume()) {
+                $("#createcontainer #create").addClass("disabled");
+            } else {
+                $("#createcontainer #create").removeClass("disabled");
+            }
+        },
 
         set_vm_view_handlers: function() {
             var self = this;
@@ -861,17 +844,6 @@
                 if ($(this).hasClass("disabled")) { return }
                 self.router.vm_create_view();
             });
-
-            synnefo.storage.vms.bind("quota_reached", function(){
-              $("#createcontainer #create").addClass("disabled");
-              $("#createcontainer #create").attr("title", "Machines limit reached");
-            });
-
-            synnefo.storage.vms.bind("quota_free", function(){
-              $("#createcontainer #create").removeClass("disabled");
-              $("#createcontainer #create").attr("title", "");
-            });
-
         },
 
         check_empty: function() {
diff --git a/snf-cyclades-app/synnefo/ui/templates/home.html b/snf-cyclades-app/synnefo/ui/templates/home.html
index 17e2549..71ee935 100644
--- a/snf-cyclades-app/synnefo/ui/templates/home.html
+++ b/snf-cyclades-app/synnefo/ui/templates/home.html
@@ -558,6 +558,8 @@
     <div id="loading-view" class="hidden clearfix">
         <div class="header clearfix images off">Loading images...<span></span></div>
         <div class="header clearfix flavors off">Loading flavors...<span></span></div>
+        <div class="header clearfix resources off">Loading resources...<span></span></div>
+        <div class="header clearfix quotas off">Loading quotas...<span></span></div>
         <div class="header clearfix vms off">Loading machines...<span></span></div>
         <div class="header clearfix networks off">Loading networks...<span></span></div>
         <div class="header clearfix layout off">Rendering layout...<span></span></div>
@@ -611,7 +613,8 @@
    			// TODO: make it dynamic
             synnefo.config.api_urls = {
                 'compute':  {{ compute_api_url|safe }}, 
-                'glance': {{ glance_api_url|safe }}
+                'glance': {{ glance_api_url|safe }},
+                'accounts': {{ accounts_api_url|safe }},
             };
             
             // TODO: configurable userdata urls in models.js
@@ -649,7 +652,6 @@
             synnefo.config.grouped_public_network_name = {{ grouped_public_network_name|safe }};
             synnefo.config.vm_hostname_format = {{ vm_hostname_format|safe }};
             synnefo.config.automatic_network_range_format = {{ automatic_network_range_format|safe }};
-            synnefo.config.quota_url = '{% url synnefo.ui.views.user_quota %}';
             synnefo.config.custom_image_help_url = '{{ custom_image_help_url|safe }}';
             synnefo.config.quotas_update_interval = {{ quotas_update_interval }};
             
diff --git a/snf-cyclades-app/synnefo/ui/urls.py b/snf-cyclades-app/synnefo/ui/urls.py
index da50dde..ddf48aa 100644
--- a/snf-cyclades-app/synnefo/ui/urls.py
+++ b/snf-cyclades-app/synnefo/ui/urls.py
@@ -37,7 +37,6 @@ import os
 
 urlpatterns = patterns('',
     url(r'^$', 'synnefo.ui.views.home', name='ui_index'),
-    url(r'^userquota$', 'synnefo.ui.views.user_quota', name='ui_userquota'),
     url(r'userdata/', include('synnefo.ui.userdata.urls'))
 )
 
diff --git a/snf-cyclades-app/synnefo/ui/views.py b/snf-cyclades-app/synnefo/ui/views.py
index 50c6016..6de33dd 100644
--- a/snf-cyclades-app/synnefo/ui/views.py
+++ b/snf-cyclades-app/synnefo/ui/views.py
@@ -62,7 +62,6 @@ UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT = \
     getattr(settings, "UI_UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT", 3)
 UPDATE_INTERVAL_FAST = getattr(settings, "UI_UPDATE_INTERVAL_FAST", 2500)
 UPDATE_INTERVAL_MAX = getattr(settings, "UI_UPDATE_INTERVAL_MAX", 10000)
-QUOTAS_UPDATE_INTERVAL = getattr(settings, "UI_QUOTAS_UPDATE_INTERVAL", 10000)
 
 # predefined values settings
 VM_IMAGE_COMMON_METADATA = \
@@ -161,6 +160,7 @@ GROUPED_PUBLIC_NETWORK_NAME = \
 USER_CATALOG_URL = getattr(settings, 'UI_USER_CATALOG_URL', '/user_catalogs')
 FEEDBACK_POST_URL = getattr(settings, 'UI_FEEDBACK_POST_URL', '/feedback')
 TRANSLATE_UUIDS = not getattr(settings, 'TRANSLATE_UUIDS', False)
+ACCOUNTS_API_URL = getattr(settings, 'UI_ACCOUNTS_API_URL', '/astakos/api')
 
 
 def template(name, request, context):
@@ -189,16 +189,16 @@ def home(request):
                'compute_api_url': json.dumps(COMPUTE_API_URL),
                'user_catalog_url': json.dumps(USER_CATALOG_URL),
                'feedback_post_url': json.dumps(FEEDBACK_POST_URL),
+               'accounts_api_url': json.dumps(ACCOUNTS_API_URL),
                'translate_uuids': json.dumps(TRANSLATE_UUIDS),
                # update interval settings
                'update_interval': UPDATE_INTERVAL,
                'update_interval_increase': UPDATE_INTERVAL_INCREASE,
                'update_interval_increase_after_calls':
-               UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT,
+                UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT,
                'update_interval_fast': UPDATE_INTERVAL_FAST,
                'update_interval_max': UPDATE_INTERVAL_MAX,
                'changes_since_alignment': CHANGES_SINCE_ALIGNMENT,
-               'quotas_update_interval': QUOTAS_UPDATE_INTERVAL,
                'image_icons': IMAGE_ICONS,
                'logout_redirect': LOGOUT_URL,
                'login_redirect': LOGIN_URL,
@@ -255,14 +255,6 @@ def machines_console(request):
     return template('machines_console', request, context)
 
 
-def user_quota(request):
-    get_user(request, settings.ASTAKOS_URL, usage=True)
-
-    response = json.dumps(request.user['usage'])
-
-    return HttpResponse(response, mimetype="application/json")
-
-
 def js_tests(request):
     return template('tests', request, {})
 
