")
- .addClass("slider"+(@settings.classSuffix || ""))
- .css
- position: "relative"
- userSelect: "none"
- boxSizing: "border-box"
- .insertBefore @input
- @slider.attr("id", @input.attr("id") + "-slider") if @input.attr("id")
-
- @track = @createDivElement("track")
- .css
- width: "100%"
-
- if @settings.highlight
- # Create the highlighting track on top of the track
- @highlightTrack = @createDivElement("highlight-track")
- .css
- width: "0"
-
- # Create the slider drag target
- @dragger = @createDivElement("dragger")
-
- # Adjust dimensions now elements are in the DOM
- @slider.css
- minHeight: @dragger.outerHeight()
- marginLeft: @dragger.outerWidth()/2
- marginRight: @dragger.outerWidth()/2
-
- @track.css
- marginTop: @track.outerHeight()/-2
-
- if @settings.highlight
- @highlightTrack.css
- marginTop: @track.outerHeight()/-2
-
- @dragger.css
- marginTop: @dragger.outerWidth()/-2
- marginLeft: @dragger.outerWidth()/-2
-
- # Hook up drag/drop mouse events
- @track
- .mousedown (e) =>
- @trackEvent(e)
-
- if @settings.highlight
- @highlightTrack
- .mousedown (e) =>
- @trackEvent(e)
-
- @dragger
- .mousedown (e) =>
- return unless e.which == 1
-
- # We've started moving
- @dragging = true
- @dragger.addClass "dragging"
-
- # Update the slider position
- @domDrag(e.pageX, e.pageY)
-
- false
-
- $("body")
- .mousemove (e) =>
- if @dragging
- # Update the slider position
- @domDrag(e.pageX, e.pageY)
-
- # Always show a pointer when dragging
- $("body").css cursor: "pointer"
-
-
- .mouseup (e) =>
- if @dragging
- # Finished dragging
- @dragging = false
- @dragger.removeClass "dragging"
-
- # Revert the cursor
- $("body").css cursor: "auto"
-
- # Set slider initial position
- @pagePos = 0
-
- # Fill in initial slider value
- if @input.val() == ""
- @value = @getRange().min
- @input.val(@value)
- else
- @value = @nearestValidValue(@input.val())
-
- @setSliderPositionFromValue(@value)
-
- # We are ready to go
- ratio = @valueToRatio(@value)
- @input.trigger "slider:ready",
- value: @value
- ratio: ratio
- position: ratio * @slider.outerWidth()
- el: @slider
-
- # Create the basis of the track-div(s)
- createDivElement: (classname) ->
- item = $("
")
- .addClass(classname)
- .css
- position: "absolute"
- top: "50%"
- userSelect: "none"
- cursor: "pointer"
- .appendTo @slider
- return item
-
-
- # Set the ratio (value between 0 and 1) of the slider.
- # Exposed via el.slider("setRatio", ratio)
- setRatio: (ratio) ->
- # Range-check the ratio
- ratio = Math.min(1, ratio)
- ratio = Math.max(0, ratio)
-
- # Work out the value
- value = @ratioToValue(ratio)
-
- # Update the position of the slider on the screen
- @setSliderPositionFromValue(value)
-
- # Trigger value changed events
- @valueChanged(value, ratio, "setRatio")
-
- # Set the value of the slider
- # Exposed via el.slider("setValue", value)
- setValue: (value) ->
- # Snap value to nearest step or allowedValue
- value = @nearestValidValue(value)
-
- # Work out the ratio
- ratio = @valueToRatio(value)
-
- # Update the position of the slider on the screen
- @setSliderPositionFromValue(value)
-
- # Trigger value changed events
- @valueChanged(value, ratio, "setValue")
-
- # Respond to an event on a track
- trackEvent: (e) ->
- return unless e.which == 1
-
- @domDrag(e.pageX, e.pageY, true)
- @dragging = true
- false
-
- # Respond to a dom drag event
- domDrag: (pageX, pageY, animate=false) ->
- # Normalize position within allowed range
- pagePos = pageX - @slider.offset().left
- pagePos = Math.min(@slider.outerWidth(), pagePos)
- pagePos = Math.max(0, pagePos)
-
- # If the element position has changed, do stuff
- if @pagePos != pagePos
- @pagePos = pagePos
-
- # Set the percentage value of the slider
- ratio = pagePos / @slider.outerWidth()
-
- # Trigger value changed events
- value = @ratioToValue(ratio)
- @valueChanged(value, ratio, "domDrag")
-
- # Update the position of the slider on the screen
- if @settings.snap
- @setSliderPositionFromValue(value, animate)
- else
- @setSliderPosition(pagePos, animate)
-
- # Set the slider position given a slider canvas position
- setSliderPosition: (position, animate=false) ->
- if animate and @settings.animate
- @dragger.animate left: position, 200
- @highlightTrack.animate width: position, 200 if @settings.highlight
- else
- @dragger.css left: position
- @highlightTrack.css width: position if @settings.highlight
-
- # Set the slider position given a value
- setSliderPositionFromValue: (value, animate=false) ->
- # Get the slide ratio from the value
- ratio = @valueToRatio(value)
-
- # Set the slider position
- @setSliderPosition(ratio * @slider.outerWidth(), animate)
-
- # Get the valid range of values
- getRange: ->
- if @settings.allowedValues
- min: Math.min(@settings.allowedValues...)
- max: Math.max(@settings.allowedValues...)
- else if @settings.range
- min: parseFloat(@settings.range[0])
- max: parseFloat(@settings.range[1])
- else
- min: 0
- max: 1
-
- # Find the nearest valid value, checking allowedValues and step settings
- nearestValidValue: (rawValue) ->
- range = @getRange()
-
- # Range-check the value
- rawValue = Math.min(range.max, rawValue)
- rawValue = Math.max(range.min, rawValue)
-
- # Apply allowedValues or step settings
- if @settings.allowedValues
- closest = null
- $.each @settings.allowedValues, ->
- if closest == null || Math.abs(this - rawValue) < Math.abs(closest - rawValue)
- closest = this
-
- return closest
- else if @settings.step
- maxSteps = (range.max - range.min) / @settings.step
- steps = Math.floor((rawValue - range.min) / @settings.step)
- steps += 1 if (rawValue - range.min) % @settings.step > @settings.step / 2 and steps < maxSteps
-
- return steps * @settings.step + range.min
- else
- return rawValue
-
- # Convert a value to a ratio
- valueToRatio: (value) ->
- if @settings.equalSteps
- # Get slider ratio for equal-step
- for allowedVal, idx in @settings.allowedValues
- if !closest? || Math.abs(allowedVal - value) < Math.abs(closest - value)
- closest = allowedVal
- closestIdx = idx
-
- if @settings.snapMid
- (closestIdx+0.5)/@settings.allowedValues.length
- else
- (closestIdx)/(@settings.allowedValues.length - 1)
-
- else
- # Get slider ratio for continuous values
- range = @getRange()
- (value - range.min) / (range.max - range.min)
-
- # Convert a ratio to a valid value
- ratioToValue: (ratio) ->
- if @settings.equalSteps
- steps = @settings.allowedValues.length
- step = Math.round(ratio * steps - 0.5)
- idx = Math.min(step, @settings.allowedValues.length - 1)
-
- @settings.allowedValues[idx]
- else
- range = @getRange()
- rawValue = ratio * (range.max - range.min) + range.min
-
- @nearestValidValue(rawValue)
-
- # Trigger value changed events
- valueChanged: (value, ratio, trigger) ->
- return if value.toString() == @value.toString()
-
- # Save the new value
- @value = value
-
- # Construct event data and fire event
- eventData =
- value: value
- ratio: ratio
- position: ratio * @slider.outerWidth()
- trigger: trigger
- el: @slider
-
- @input
- .val(value)
- .trigger($.Event("change", eventData))
- .trigger("slider:changed", eventData)
-
-
- #
- # Expose as jQuery Plugin
- #
-
- $.extend $.fn, simpleSlider: (settingsOrMethod, params...) ->
- publicMethods = ["setRatio", "setValue"]
-
- $(this).each ->
- if settingsOrMethod and settingsOrMethod in publicMethods
- obj = $(this).data("slider-object")
-
- obj[settingsOrMethod].apply(obj, params)
- else
- settings = settingsOrMethod
- $(this).data "slider-object", new SimpleSlider($(this), settings)
-
-
- #
- # Attach unobtrusive JS hooks
- #
-
- $ ->
- $("[data-slider]").each ->
- $el = $(this)
-
- # Build options object from data attributes
- settings = {}
-
- allowedValues = $el.data "slider-values"
- settings.allowedValues = (parseFloat(x) for x in allowedValues.split(",")) if allowedValues
- settings.range = $el.data("slider-range").split(",") if $el.data("slider-range")
- settings.step = $el.data("slider-step") if $el.data("slider-step")
- settings.snap = $el.data("slider-snap")
- settings.equalSteps = $el.data("slider-equal-steps")
- settings.theme = $el.data("slider-theme") if $el.data("slider-theme")
- settings.highlight = $el.data("slider-highlight") if $el.attr("data-slider-highlight")
- settings.animate = $el.data("slider-animate") if $el.data("slider-animate")?
-
- # Activate the plugin
- $el.simpleSlider settings
-
-) @jQuery or @Zepto, this
diff --git a/circle/dashboard/static/dashboard/loopj-jquery-simple-slider-fa64f59/js/simple-slider.min.js b/circle/dashboard/static/dashboard/loopj-jquery-simple-slider-fa64f59/js/simple-slider.min.js
deleted file mode 100644
index b6a7341..0000000
--- a/circle/dashboard/static/dashboard/loopj-jquery-simple-slider-fa64f59/js/simple-slider.min.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * jQuery Simple Slider: Unobtrusive Numerical Slider
- * Version 1.0.0
- *
- * Copyright (c) 2013 James Smith (http://loopj.com)
- *
- * Licensed under the MIT license (http://mit-license.org/)
- *
- */
-
-var __slice=[].slice,__indexOf=[].indexOf||function(e){for(var t=0,n=this.length;t
").addClass("slider"+(this.settings.classSuffix||"")).css({position:"relative",userSelect:"none",boxSizing:"border-box"}).insertBefore(this.input),this.input.attr("id")&&this.slider.attr("id",this.input.attr("id")+"-slider"),this.track=this.createDivElement("track").css({width:"100%"}),this.settings.highlight&&(this.highlightTrack=this.createDivElement("highlight-track").css({width:"0"})),this.dragger=this.createDivElement("dragger"),this.slider.css({minHeight:this.dragger.outerHeight(),marginLeft:this.dragger.outerWidth()/2,marginRight:this.dragger.outerWidth()/2}),this.track.css({marginTop:this.track.outerHeight()/-2}),this.settings.highlight&&this.highlightTrack.css({marginTop:this.track.outerHeight()/-2}),this.dragger.css({marginTop:this.dragger.outerWidth()/-2,marginLeft:this.dragger.outerWidth()/-2}),this.track.mousedown(function(e){return i.trackEvent(e)}),this.settings.highlight&&this.highlightTrack.mousedown(function(e){return i.trackEvent(e)}),this.dragger.mousedown(function(e){if(e.which!==1)return;return i.dragging=!0,i.dragger.addClass("dragging"),i.domDrag(e.pageX,e.pageY),!1}),e("body").mousemove(function(t){if(i.dragging)return i.domDrag(t.pageX,t.pageY),e("body").css({cursor:"pointer"})}).mouseup(function(t){if(i.dragging)return i.dragging=!1,i.dragger.removeClass("dragging"),e("body").css({cursor:"auto"})}),this.pagePos=0,this.input.val()===""?(this.value=this.getRange().min,this.input.val(this.value)):this.value=this.nearestValidValue(this.input.val()),this.setSliderPositionFromValue(this.value),r=this.valueToRatio(this.value),this.input.trigger("slider:ready",{value:this.value,ratio:r,position:r*this.slider.outerWidth(),el:this.slider})}return t.prototype.createDivElement=function(t){var n;return n=e("").addClass(t).css({position:"absolute",top:"50%",userSelect:"none",cursor:"pointer"}).appendTo(this.slider),n},t.prototype.setRatio=function(e){var t;return e=Math.min(1,e),e=Math.max(0,e),t=this.ratioToValue(e),this.setSliderPositionFromValue(t),this.valueChanged(t,e,"setRatio")},t.prototype.setValue=function(e){var t;return e=this.nearestValidValue(e),t=this.valueToRatio(e),this.setSliderPositionFromValue(e),this.valueChanged(e,t,"setValue")},t.prototype.trackEvent=function(e){if(e.which!==1)return;return this.domDrag(e.pageX,e.pageY,!0),this.dragging=!0,!1},t.prototype.domDrag=function(e,t,n){var r,i,s;n==null&&(n=!1),r=e-this.slider.offset().left,r=Math.min(this.slider.outerWidth(),r),r=Math.max(0,r);if(this.pagePos!==r)return this.pagePos=r,i=r/this.slider.outerWidth(),s=this.ratioToValue(i),this.valueChanged(s,i,"domDrag"),this.settings.snap?this.setSliderPositionFromValue(s,n):this.setSliderPosition(r,n)},t.prototype.setSliderPosition=function(e,t){t==null&&(t=!1);if(t&&this.settings.animate){this.dragger.animate({left:e},200);if(this.settings.highlight)return this.highlightTrack.animate({width:e},200)}else{this.dragger.css({left:e});if(this.settings.highlight)return this.highlightTrack.css({width:e})}},t.prototype.setSliderPositionFromValue=function(e,t){var n;return t==null&&(t=!1),n=this.valueToRatio(e),this.setSliderPosition(n*this.slider.outerWidth(),t)},t.prototype.getRange=function(){return this.settings.allowedValues?{min:Math.min.apply(Math,this.settings.allowedValues),max:Math.max.apply(Math,this.settings.allowedValues)}:this.settings.range?{min:parseFloat(this.settings.range[0]),max:parseFloat(this.settings.range[1])}:{min:0,max:1}},t.prototype.nearestValidValue=function(t){var n,r,i,s;return i=this.getRange(),t=Math.min(i.max,t),t=Math.max(i.min,t),this.settings.allowedValues?(n=null,e.each(this.settings.allowedValues,function(){if(n===null||Math.abs(this-t)
this.settings.step/2&&s=0?(s=e(this).data("slider-object"),s[i].apply(s,t)):(o=i,e(this).data("slider-object",new n(e(this),o)))})}}),e(function(){return e("[data-slider]").each(function(){var t,n,r,i;return t=e(this),r={},n=t.data("slider-values"),n&&(r.allowedValues=function(){var e,t,r,s;r=n.split(","),s=[];for(e=0,t=r.length;e .dragger, .slider > .dragger:hover {
+ border-radius: 0px;
+ -moz-border-radius: 0px;
+ -webkit-border-radius: 0px;
+ width: 8px;
+ height: 24px;
+ margin-top: -12px!important;
+ text-shadow: 0 1px 0 #fff;
+ background-image: -webkit-gradient(linear, left 0%, left 100%, from(#428bca), to(#3071a9));
+ background-image: -webkit-linear-gradient(top, #428bca, 0%, #3071a9, 100%);
+ background-image: -moz-linear-gradient(top, #428bca 0%, #3071a9 100%);
+ background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%);
+ background-repeat: repeat-x;
+ border-color: #2d6ca2;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);
+}
+.slider > .dragger:hover {
+ background-color: #3071a9;
+ background-image: none;
+ border-color: #2d6ca2;
+}
+
+.slider > .highlight-track {
+ height: 20px;
+ top: 50%;
+}
+.slider + .output {
+
+}
+
+.slider > .track, .slider > .highlight-track {
+border-radius: 5px;
+}
+
+/* review this later */
+.slider {
+ width: 100%;
+}
diff --git a/circle/dashboard/static/dashboard/loopj-jquery-simple-slider-fa64f59/js/simple-slider.js b/circle/dashboard/static/dashboard/loopj-jquery-simple-slider/js/simple-slider.js
similarity index 80%
rename from circle/dashboard/static/dashboard/loopj-jquery-simple-slider-fa64f59/js/simple-slider.js
rename to circle/dashboard/static/dashboard/loopj-jquery-simple-slider/js/simple-slider.js
index 1b917f6..5426cbd 100644
--- a/circle/dashboard/static/dashboard/loopj-jquery-simple-slider-fa64f59/js/simple-slider.js
+++ b/circle/dashboard/static/dashboard/loopj-jquery-simple-slider/js/simple-slider.js
@@ -1,7 +1,10 @@
/*
jQuery Simple Slider
- Copyright (c) 2012 James Smith (http://loopj.com)
+ Copyright (c) 2012, 2013 James Smith (http://loopj.com)
+ Copyright (c) 2013 Maarten van Grootel (http://maatenvangrootel.nl)
+ Copyright (c) 2013 Nathan Hunzaker (http://natehunzaker.com)
+ Copyright (c) 2013 Erik J. Nedwidek (http://github.com/nedwidek)
Licensed under the MIT license (http://mit-license.org/)
*/
@@ -23,8 +26,12 @@ var __slice = [].slice,
classPrefix: null,
classSuffix: null,
theme: null,
- highlight: false
+ highlight: false,
+ showScale: false
};
+ if(typeof options == 'undefined') {
+ options = this.loadDataOptions();
+ }
this.settings = $.extend({}, this.defaultOptions, options);
if (this.settings.theme) {
this.settings.classSuffix = "-" + this.settings.theme;
@@ -106,6 +113,20 @@ var __slice = [].slice,
}
this.setSliderPositionFromValue(this.value);
ratio = this.valueToRatio(this.value);
+ if (this.settings.showScale) {
+ this.scale = this.createDivElement("scale");
+ this.minScale = this.createSpanElement("min-scale", this.scale);
+ this.maxScale = this.createSpanElement("max-scale", this.scale);
+
+ range = this.getRange();
+
+ this.minScale.html(range.min);
+ this.maxScale.html(range.max);
+
+ this.scale.css('marginTop', function(index, currentValue) {
+ return (parseInt(currentValue, 10) + this.previousSibling.offsetHeight / 2) + 'px';
+ });
+ }
this.input.trigger("slider:ready", {
value: this.value,
ratio: ratio,
@@ -114,6 +135,44 @@ var __slice = [].slice,
});
}
+ SimpleSlider.prototype.loadDataOptions = function() {
+ var options = {};
+ allowedValues = this.input.data("slider-values");
+ if (allowedValues) {
+ options.allowedValues = (function() {
+ var _i, _len, _ref, _results;
+ _ref = allowedValues.split(",");
+ _results = [];
+ for (_i = 0, _len = _ref.length; _i < _len; _i++) {
+ x = _ref[_i];
+ _results.push(parseFloat(x));
+ }
+ return _results;
+ })();
+ }
+ if (this.input.data("slider-range")) {
+ options.range = this.input.data("slider-range").split(",");
+ }
+ if (this.input.data("slider-step")) {
+ options.step = this.input.data("slider-step");
+ }
+ options.snap = this.input.data("slider-snap");
+ options.equalSteps = this.input.data("slider-equal-steps");
+ if (this.input.data("slider-theme")) {
+ options.theme = this.input.data("slider-theme");
+ }
+ if (this.input.attr("data-slider-highlight")) {
+ options.highlight = this.input.data("slider-highlight");
+ }
+ if (this.input.data("slider-animate") != null) {
+ options.animate = this.input.data("slider-animate");
+ }
+ if (this.input.data("slider-showscale") != null) {
+ options.showScale = this.input.data("slider-showscale");
+ }
+ return options;
+ }
+
SimpleSlider.prototype.createDivElement = function(classname) {
var item;
item = $("").addClass(classname).css({
@@ -125,6 +184,12 @@ var __slice = [].slice,
return item;
};
+ SimpleSlider.prototype.createSpanElement = function(classname, parent) {
+ var item;
+ item = $("
").addClass(classname).appendTo(parent);
+ return item;
+ };
+
SimpleSlider.prototype.setRatio = function(ratio) {
var value;
ratio = Math.min(1, ratio);
@@ -322,42 +387,14 @@ var __slice = [].slice,
});
}
});
+
+ /*
return $(function() {
return $("[data-slider]").each(function() {
var $el, allowedValues, settings, x;
$el = $(this);
- settings = {};
- allowedValues = $el.data("slider-values");
- if (allowedValues) {
- settings.allowedValues = (function() {
- var _i, _len, _ref, _results;
- _ref = allowedValues.split(",");
- _results = [];
- for (_i = 0, _len = _ref.length; _i < _len; _i++) {
- x = _ref[_i];
- _results.push(parseFloat(x));
- }
- return _results;
- })();
- }
- if ($el.data("slider-range")) {
- settings.range = $el.data("slider-range").split(",");
- }
- if ($el.data("slider-step")) {
- settings.step = $el.data("slider-step");
- }
- settings.snap = $el.data("slider-snap");
- settings.equalSteps = $el.data("slider-equal-steps");
- if ($el.data("slider-theme")) {
- settings.theme = $el.data("slider-theme");
- }
- if ($el.attr("data-slider-highlight")) {
- settings.highlight = $el.data("slider-highlight");
- }
- if ($el.data("slider-animate") != null) {
- settings.animate = $el.data("slider-animate");
- }
- return $el.simpleSlider(settings);
+ return $el.simpleSlider();
});
});
+ */
})(this.jQuery || this.Zepto, this);
diff --git a/circle/dashboard/static/dashboard/loopj-jquery-simple-slider/js/simple-slider.min.js b/circle/dashboard/static/dashboard/loopj-jquery-simple-slider/js/simple-slider.min.js
new file mode 100644
index 0000000..fbdb6e3
--- /dev/null
+++ b/circle/dashboard/static/dashboard/loopj-jquery-simple-slider/js/simple-slider.min.js
@@ -0,0 +1,11 @@
+/*
+ * jQuery Simple Slider: Unobtrusive Numerical Slider
+ * Version 1.0.0
+ *
+ * Copyright (c) 2013 James Smith (http://loopj.com)
+ *
+ * Licensed under the MIT license (http://mit-license.org/)
+ *
+ */
+
+var __slice=[].slice,__indexOf=[].indexOf||function(c){for(var b=0,a=this.length;b").addClass("slider"+(this.settings.classSuffix||"")).css({position:"relative",userSelect:"none",boxSizing:"border-box"}).insertBefore(this.input);if(this.input.attr("id")){this.slider.attr("id",this.input.attr("id")+"-slider")}this.track=this.createDivElement("track").css({width:"100%"});if(this.settings.highlight){this.highlightTrack=this.createDivElement("highlight-track").css({width:"0"})}this.dragger=this.createDivElement("dragger");this.slider.css({minHeight:this.dragger.outerHeight(),marginLeft:this.dragger.outerWidth()/2,marginRight:this.dragger.outerWidth()/2});this.track.css({marginTop:this.track.outerHeight()/-2});if(this.settings.highlight){this.highlightTrack.css({marginTop:this.track.outerHeight()/-2})}this.dragger.css({marginTop:this.dragger.outerWidth()/-2,marginLeft:this.dragger.outerWidth()/-2});this.track.mousedown(function(i){return h.trackEvent(i)});if(this.settings.highlight){this.highlightTrack.mousedown(function(i){return h.trackEvent(i)})}this.dragger.mousedown(function(i){if(i.which!==1){return}h.dragging=true;h.dragger.addClass("dragging");h.domDrag(i.pageX,i.pageY);return false});c("body").mousemove(function(i){if(h.dragging){h.domDrag(i.pageX,i.pageY);return c("body").css({cursor:"pointer"})}}).mouseup(function(i){if(h.dragging){h.dragging=false;h.dragger.removeClass("dragging");return c("body").css({cursor:"auto"})}});this.pagePos=0;if(this.input.val()===""){this.value=this.getRange().min;this.input.val(this.value)}else{this.value=this.nearestValidValue(this.input.val())}this.setSliderPositionFromValue(this.value);g=this.valueToRatio(this.value);if(this.settings.showScale){this.scale=this.createDivElement("scale");this.minScale=this.createSpanElement("min-scale",this.scale);this.maxScale=this.createSpanElement("max-scale",this.scale);range=this.getRange();this.minScale.html(range.min);this.maxScale.html(range.max);this.scale.css("marginTop",function(i,j){return(parseInt(j,10)+this.previousSibling.offsetHeight/2)+"px"})}this.input.trigger("slider:ready",{value:this.value,ratio:g,position:g*this.slider.outerWidth(),el:this.slider})}d.prototype.loadDataOptions=function(){var e={};allowedValues=this.input.data("slider-values");if(allowedValues){e.allowedValues=(function(){var i,g,h,f;h=allowedValues.split(",");f=[];for(i=0,g=h.length;i").addClass(f).css({position:"absolute",top:"50%",userSelect:"none",cursor:"pointer"}).appendTo(this.slider);return e};d.prototype.createSpanElement=function(g,e){var f;f=c("").addClass(g).appendTo(e);return f};d.prototype.setRatio=function(e){var f;e=Math.min(1,e);e=Math.max(0,e);f=this.ratioToValue(e);this.setSliderPositionFromValue(f);return this.valueChanged(f,e,"setRatio")};d.prototype.setValue=function(f){var e;f=this.nearestValidValue(f);e=this.valueToRatio(f);this.setSliderPositionFromValue(f);return this.valueChanged(f,e,"setValue")};d.prototype.trackEvent=function(f){if(f.which!==1){return}this.domDrag(f.pageX,f.pageY,true);this.dragging=true;return false};d.prototype.domDrag=function(i,g,e){var f,h,j;if(e==null){e=false}f=i-this.slider.offset().left;f=Math.min(this.slider.outerWidth(),f);f=Math.max(0,f);if(this.pagePos!==f){this.pagePos=f;h=f/this.slider.outerWidth();j=this.ratioToValue(h);this.valueChanged(j,h,"domDrag");if(this.settings.snap){return this.setSliderPositionFromValue(j,e)}else{return this.setSliderPosition(f,e)}}};d.prototype.setSliderPosition=function(e,f){if(f==null){f=false}if(f&&this.settings.animate){this.dragger.animate({left:e},200);if(this.settings.highlight){return this.highlightTrack.animate({width:e},200)}}else{this.dragger.css({left:e});if(this.settings.highlight){return this.highlightTrack.css({width:e})}}};d.prototype.setSliderPositionFromValue=function(g,e){var f;if(e==null){e=false}f=this.valueToRatio(g);return this.setSliderPosition(f*this.slider.outerWidth(),e)};d.prototype.getRange=function(){if(this.settings.allowedValues){return{min:Math.min.apply(Math,this.settings.allowedValues),max:Math.max.apply(Math,this.settings.allowedValues)}}else{if(this.settings.range){return{min:parseFloat(this.settings.range[0]),max:parseFloat(this.settings.range[1])}}else{return{min:0,max:1}}}};d.prototype.nearestValidValue=function(i){var h,g,f,e;f=this.getRange();i=Math.min(f.max,i);i=Math.max(f.min,i);if(this.settings.allowedValues){h=null;c.each(this.settings.allowedValues,function(){if(h===null||Math.abs(this-i)this.settings.step/2&&e=0){h=c(this).data("slider-object");return h[f].apply(h,e)}else{g=f;return c(this).data("slider-object",new b(c(this),g))}})}});return c(function(){return c("[data-slider]").each(function(){var e,g,f,d;e=c(this);return e.simpleSlider()})})})(this.jQuery||this.Zepto,this);
diff --git a/circle/dashboard/static/dashboard/store.js b/circle/dashboard/static/dashboard/store.js
new file mode 100644
index 0000000..602f601
--- /dev/null
+++ b/circle/dashboard/static/dashboard/store.js
@@ -0,0 +1,60 @@
+$(function() {
+ $("#store-list-container").on("click", ".store-list-item", function() {
+ if($(this).data("item-type") == "D") {
+ $("#store-list-up-icon").removeClass("fa-reply").addClass("fa-refresh fa-spin");
+ var url = $(this).prop("href");
+ $.get(url, function(result) {
+ $("#store-list-container").html(result);
+ noJS();
+ $("[title]").tooltip();
+ history.pushState({}, "", url);
+ });
+ } else {
+ $(this).next(".store-list-file-infos").stop().slideToggle();
+ }
+ return false;
+ });
+
+ /* how upload works
+ * - user clicks on a "fake" browse button, this triggers a click event on the file upload
+ * - if the file input changes it adds the name of the file to form (or number of files if multiple is enabled)
+ * - and finally when we click on the upload button (this event handler) it firsts ask the store api where to upload
+ * then changes the form's action attr before sending the form itself
+ */
+ $("#store-list-container").on("click", '#store-upload-form button[type="submit"]', function() {
+ var current_dir = $("#store-upload-form").find('[name="current_dir"]').val();
+ $.get($("#store-upload-form").data("action") + "?current_dir=" + current_dir, function(result) {
+ $('#store-upload-form button[type="submit"] i').addClass("fa-spinner fa-spin");
+ $("#store-upload-form").get(0).setAttribute("action", result['url']);
+ $("#store-upload-form").submit();
+ });
+
+ return false;
+ });
+
+ /* "fake" browse button */
+ $("#store-list-container").on("click", "#store-upload-browse", function() {
+ $('#store-upload-form input[type="file"]').click();
+ });
+
+ $("#store-list-container").on("change", "#store-upload-file", function() {
+ var input = $(this);
+ var numFiles = input.get(0).files ? input.get(0).files.length : 1;
+ var label = input.val().replace(/\\/g, '/').replace(/.*\//, '');
+ input.trigger('fileselect', [numFiles, label]);
+ });
+
+ $("#store-list-container").on("fileselect", "#store-upload-file", function(event, numFiles, label) {
+ var input = $("#store-upload-filename");
+ var log = numFiles > 1 ? numFiles + ' files selected' : label;
+ if(input.length) {
+ input.val(log);
+ }
+ if(log) {
+ $('#store-upload-form button[type="submit"]').prop("disabled", false);
+ } else {
+ $('#store-upload-form button[type="submit"]').prop("disabled", true);
+ }
+
+ });
+});
diff --git a/circle/dashboard/static/dashboard/vm-common.js b/circle/dashboard/static/dashboard/vm-common.js
index 548777a..bc3f0ef 100644
--- a/circle/dashboard/static/dashboard/vm-common.js
+++ b/circle/dashboard/static/dashboard/vm-common.js
@@ -3,7 +3,7 @@
$(function() {
/* vm operations */
- $('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface').on('click', '.operation.btn', function(e) {
+ $('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface').on('click', '.operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin');
$.ajax({
diff --git a/circle/dashboard/static/dashboard/vm-create.js b/circle/dashboard/static/dashboard/vm-create.js
index b0926f1..8806993 100644
--- a/circle/dashboard/static/dashboard/vm-create.js
+++ b/circle/dashboard/static/dashboard/vm-create.js
@@ -2,13 +2,17 @@ var vlans = [];
var disks = [];
$(function() {
- vmCustomizeLoaded();
+ if($(".vm-create-template-list").length) {
+ vmCreateLoaded();
+ } else {
+ vmCustomizeLoaded();
+ }
});
function vmCreateLoaded() {
$(".vm-create-template-details").hide();
- $(".vm-create-template-summary").click(function() {
+ $(".vm-create-template-summary").unbind("click").click(function() {
$(this).next(".vm-create-template-details").slideToggle();
});
@@ -64,9 +68,18 @@ function vmCreateLoaded() {
return false;
});
+ $('.progress-bar').each(function() {
+ var min = $(this).attr('aria-valuemin');
+ var max = $(this).attr('aria-valuemax');
+ var now = $(this).attr('aria-valuenow');
+ var siz = (now-min)*100/(max-min);
+ $(this).css('width', siz+'%');
+ });
+
}
function vmCustomizeLoaded() {
+ $("[title]").tooltip();
/* network thingies */
/* add network */
@@ -92,7 +105,7 @@ function vmCustomizeLoaded() {
/* add dummy text if no more networks are available */
if($('#vm-create-network-add-select option').length < 1) {
$('#vm-create-network-add-button').attr('disabled', true);
- $('#vm-create-network-add-select').html('');
+ $('#vm-create-network-add-select').html('');
}
return false;
@@ -124,7 +137,7 @@ function vmCustomizeLoaded() {
/* remove the selection from the multiple select */
$('#vm-create-network-add-vlan option[value="' + vlan_pk + '"]').prop('selected', false);
if ($('#vm-create-network-list').children('span').length < 1) {
- $('#vm-create-network-list').append('Not added to any network!');
+ $('#vm-create-network-list').append(gettext("Not added to any network"));
}
});
return false;
@@ -155,7 +168,7 @@ function vmCustomizeLoaded() {
// if all networks are added add a dummy and disable the add button
if($("#vm-create-network-add-select option").length < 1) {
- $("#vm-create-network-add-select").html('');
+ $("#vm-create-network-add-select").html('');
$('#vm-create-network-add-button').attr('disabled', true);
}
@@ -170,54 +183,6 @@ function vmCustomizeLoaded() {
/* ----- end of networks thingies ----- */
-
- /* add disk */
- $('#vm-create-disk-add-button').click(function() {
- var disk_pk = $('#vm-create-disk-add-select :selected').val();
- var name = $('#vm-create-disk-add-select :selected').text();
-
- if ($('#vm-create-disk-list').children('span').length < 1) {
- $('#vm-create-disk-list').html('');
- }
- $('#vm-create-disk-list').append(
- vmCreateDiskLabel(disk_pk, name)
- );
-
- /* select the disk from the multiple select */
- $('#vm-create-disk-add-form option[value="' + disk_pk + '"]').prop('selected', true);
-
- $('option:selected', $('#vm-create-disk-add-select')).remove();
-
- /* add dummy text if no more disks are available */
- if($('#vm-create-disk-add-select option').length < 1) {
- $('#vm-create-disk-add-button').attr('disabled', true);
- $('#vm-create-disk-add-select').html('');
- }
-
- return false;
- });
-
-
- /* remove disk */
- // event for disk remove button (icon, X)
- $('body').on('click', '.vm-create-remove-disk', function() {
- var disk_pk = ($(this).parent('span').prop('id')).replace('disk-', '')
-
- $(this).parent('span').fadeOut(500, function() {
- /* remove the disk label */
- $(this).remove();
-
- var disk_name = $(this).text();
-
- /* remove the selection from the multiple select */
- $('#vm-create-disk-add-form option[value="' + disk_pk + '"]').prop('selected', false);
- if ($('#vm-create-disk-list').children('span').length < 1) {
- $('#vm-create-disk-list').append('No disks are added!');
- }
- });
- return false;
- });
-
/* copy disks from hidden select */
$('#vm-create-disk-add-form option').each(function() {
var text = $(this).text();
@@ -244,6 +209,14 @@ function vmCustomizeLoaded() {
/* start vm button clicks */
$('#vm-create-customized-start').click(function() {
+ var error = false;
+ $(".cpu-count-input, .ram-input, #id_name, #id_amount ").each(function() {
+ if(!$(this)[0].checkValidity()) {
+ error = true;
+ }
+ });
+ if(error) return true;
+
$.ajax({
url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')},
@@ -284,6 +257,7 @@ function vmCustomizeLoaded() {
/* for no js stuff */
$('.no-js-hidden').show();
$('.js-hidden').hide();
+
}
@@ -294,5 +268,5 @@ function vmCreateNetworkLabel(pk, name, managed) {
function vmCreateDiskLabel(pk, name) {
var style = "float: left; margin: 5px 5px 5px 0;";
- return ' ' + name + ' ';
+ return ' ' + name + ' ';
}
diff --git a/circle/dashboard/static/dashboard/vm-details.js b/circle/dashboard/static/dashboard/vm-details.js
index 2a32a2c..3129f9d 100644
--- a/circle/dashboard/static/dashboard/vm-details.js
+++ b/circle/dashboard/static/dashboard/vm-details.js
@@ -1,5 +1,6 @@
var show_all = false;
var in_progress = false;
+var activity_hash = 5;
$(function() {
/* do we need to check for new activities */
@@ -27,6 +28,15 @@ $(function() {
/* save resources */
$('#vm-details-resources-save').click(function() {
+ var error = false;
+ $(".cpu-count-input, .ram-input").each(function() {
+ if(!$(this)[0].checkValidity()) {
+ error = true;
+ }
+ });
+ if(error) return true;
+
+
$('i.fa-floppy-o', this).removeClass("fa-floppy-o").addClass("fa-refresh fa-spin");
var vm = $(this).data("vm");
$.ajax({
@@ -34,8 +44,12 @@ $(function() {
url: "/dashboard/vm/" + vm + "/op/resources_change/",
data: $('#vm-details-resources-form').serialize(),
success: function(data, textStatus, xhr) {
+ if(data.success) {
+ $('a[href="#activity"]').trigger("click");
+ } else {
+ addMessage(data.messages.join("
"), "danger");
+ }
$("#vm-details-resources-save i").removeClass('fa-refresh fa-spin').addClass("fa-floppy-o");
- $('a[href="#activity"]').trigger("click");
},
error: function(xhr, textStatus, error) {
$("#vm-details-resources-save i").removeClass('fa-refresh fa-spin').addClass("fa-floppy-o");
@@ -307,6 +321,10 @@ $(function() {
$("#vm-details-connection-string").focus();
});
+ $("a.operation-password_reset").click(function() {
+ if(Boolean($(this).data("disabled"))) return false;
+ });
+
});
@@ -327,6 +345,7 @@ function removePort(data) {
}
});
+
}
function decideActivityRefresh() {
@@ -341,17 +360,6 @@ function decideActivityRefresh() {
return check;
}
-/* unescapes html got via the request, also removes whitespaces and replaces all ' with " */
-function unescapeHTML(html) {
- return html.replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&').replace(/–/g, "–").replace(/\//g, "").replace(/'/g, '"').replace(/'/g, "'").replace(/ /g, '');
-}
-
-/* the html page contains some tags that were modified via js (titles for example), we delete these
- also some html tags are closed with / */
-function changeHTML(html) {
- return html.replace(/data-original-title/g, "title").replace(/title=""/g, "").replace(/\//g, '').replace(/ /g, '');
-}
-
function checkNewActivity(runs) {
var instance = location.href.split('/'); instance = instance[instance.length - 2];
@@ -360,19 +368,24 @@ function checkNewActivity(runs) {
url: '/dashboard/vm/' + instance + '/activity/',
data: {'show_all': show_all},
success: function(data) {
- if(show_all) { /* replace on longer string freezes the spinning stuff */
+ var new_activity_hash = (data['activities'] + "").hashCode();
+ if(new_activity_hash != activity_hash) {
$("#activity-refresh").html(data['activities']);
- } else {
- a = unescapeHTML(data['activities']);
- b = changeHTML($("#activity-refresh").html());
- if(a != b)
- $("#activity-refresh").html(data['activities']);
}
+ activity_hash = new_activity_hash;
+
$("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip();
- $("#vm-details-state i").prop("class", "fa " + data['icon']);
+ /* changing the status text */
+ var icon = $("#vm-details-state i");
+ if(data['is_new_state']) {
+ if(!icon.hasClass("fa-spin"))
+ icon.prop("class", "fa fa-spinner fa-spin");
+ } else {
+ icon.prop("class", "fa " + data['icon']);
+ }
$("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
if(data['status'] == "RUNNING") {
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
@@ -380,12 +393,12 @@ function checkNewActivity(runs) {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
}
- if(data['status'] == "STOPPED") {
- $(".enabled-when-stopped").prop("disabled", false);
- $(".hide-when-stopped").hide();
+ if(data['status'] == "STOPPED" || data['status'] == "PENDING") {
+ $(".change-resources-button").prop("disabled", false);
+ $(".change-resources-help").hide();
} else {
- $(".enabled-when-stopped").prop("disabled", true);
- $(".hide-when-stopped").show();
+ $(".change-resources-button").prop("disabled", true);
+ $(".change-resources-help").show();
}
if(runs > 0 && decideActivityRefresh()) {
@@ -403,3 +416,14 @@ function checkNewActivity(runs) {
}
});
}
+
+String.prototype.hashCode = function() {
+ var hash = 0, i, chr, len;
+ if (this.length == 0) return hash;
+ for (i = 0, len = this.length; i < len; i++) {
+ chr = this.charCodeAt(i);
+ hash = ((hash << 5) - hash) + chr;
+ hash |= 0; // Convert to 32bit integer
+ }
+ return hash;
+};
diff --git a/circle/dashboard/static/grafikon.png b/circle/dashboard/static/grafikon.png
deleted file mode 100644
index 744a092..0000000
Binary files a/circle/dashboard/static/grafikon.png and /dev/null differ
diff --git a/circle/dashboard/store_api.py b/circle/dashboard/store_api.py
new file mode 100644
index 0000000..07f9441
--- /dev/null
+++ b/circle/dashboard/store_api.py
@@ -0,0 +1,195 @@
+from os.path import splitext
+import json
+import logging
+from urlparse import urljoin
+from datetime import datetime
+
+from django.http import Http404
+from django.conf import settings
+from requests import get, post, codes
+from sizefield.utils import filesizeformat
+
+logger = logging.getLogger(__name__)
+
+
+class StoreApiException(Exception):
+ pass
+
+
+class NotOkException(StoreApiException):
+ def __init__(self, status, *args, **kwargs):
+ self.status = status
+ super(NotOkException, self).__init__(*args, **kwargs)
+
+
+class NoStoreException(StoreApiException):
+ pass
+
+
+class Store(object):
+
+ def __init__(self, user, default_timeout=0.5):
+ self.request_args = {'verify': settings.STORE_VERIFY_SSL}
+ if settings.STORE_SSL_AUTH:
+ self.request_args['cert'] = (settings.STORE_CLIENT_CERT,
+ settings.STORE_CLIENT_KEY)
+ if settings.STORE_BASIC_AUTH:
+ self.request_args['auth'] = (settings.STORE_CLIENT_USER,
+ settings.STORE_CLIENT_PASSWORD)
+ self.username = "u-%d" % user.pk
+ self.default_timeout = default_timeout
+ self.store_url = settings.STORE_URL
+ if not self.store_url:
+ raise NoStoreException
+
+ def _request(self, url, method=get, timeout=None,
+ raise_status_code=True, **kwargs):
+ url = urljoin(self.store_url, url)
+ if timeout is None:
+ timeout = self.default_timeout
+ payload = json.dumps(kwargs) if kwargs else None
+ try:
+ headers = {'content-type': 'application/json'}
+ response = method(url, data=payload, headers=headers,
+ timeout=timeout, **self.request_args)
+ except Exception:
+ logger.exception("Error in store %s loading %s",
+ unicode(method), url)
+ raise
+ else:
+ if raise_status_code and response.status_code != codes.ok:
+ if response.status_code == 404:
+ raise Http404()
+ else:
+ raise NotOkException(response.status_code)
+ return response
+
+ def _request_cmd(self, cmd, **kwargs):
+ return self._request(self.username, post, CMD=cmd, **kwargs)
+
+ def list(self, path, process=True):
+ r = self._request_cmd("LIST", PATH=path)
+ result = r.json()
+ if process:
+ return self._process_list(result)
+ else:
+ return result
+
+ def toplist(self, process=True):
+ r = self._request_cmd("TOPLIST")
+ result = r.json()
+ if process:
+ return self._process_list(result)
+ else:
+ return result
+
+ def request_download(self, path):
+ r = self._request_cmd("DOWNLOAD", PATH=path, timeout=10)
+ return r.json()['LINK']
+
+ def request_upload(self, path):
+ r = self._request_cmd("UPLOAD", PATH=path)
+ return r.json()['LINK']
+
+ def remove(self, path):
+ self._request_cmd("REMOVE", PATH=path)
+
+ def new_folder(self, path):
+ self._request_cmd("NEW_FOLDER", PATH=path)
+
+ def rename(self, old_path, new_name):
+ self._request_cmd("RENAME", PATH=old_path, NEW_NAME=new_name)
+
+ def get_quota(self): # no CMD? :o
+ r = self._request(self.username)
+ quota = r.json()
+ quota.update({
+ 'readable_used': filesizeformat(float(quota['used'])),
+ 'readable_soft': filesizeformat(float(quota['soft'])),
+ 'readable_hard': filesizeformat(float(quota['hard'])),
+ })
+ return quota
+
+ def set_quota(self, quota):
+ self._request("/quota/" + self.username, post, QUOTA=quota)
+
+ def user_exist(self):
+ try:
+ self._request(self.username)
+ return True
+ except NotOkException:
+ return False
+
+ def create_user(self, password, keys, quota):
+ self._request("/new/" + self.username, method=post,
+ SMBPASSWD=password, KEYS=keys, QUOTA=quota)
+
+ @staticmethod
+ def _process_list(content):
+ for d in content:
+ d['human_readable_date'] = datetime.utcfromtimestamp(float(
+ d['MTIME']))
+ delta = (datetime.utcnow() -
+ d['human_readable_date']).total_seconds()
+ d['is_new'] = 0 < delta < 5
+ d['human_readable_size'] = (
+ "directory" if d['TYPE'] == "D" else
+ filesizeformat(float(d['SIZE'])))
+
+ if d['DIR'] == ".":
+ d['directory'] = "/"
+ else:
+ d['directory'] = "/" + d['DIR'] + "/"
+
+ d['path'] = d['directory']
+ d['path'] += d['NAME']
+ if d['TYPE'] == "D":
+ d['path'] += "/"
+
+ d['ext'] = splitext(d['path'])[1]
+ d['icon'] = ("folder-open" if not d['TYPE'] == "F"
+ else file_icons.get(d['ext'], "file-o"))
+
+ return sorted(content, key=lambda k: k['TYPE'])
+
+
+file_icons = {
+ '.txt': "file-text-o",
+ '.pdf': "file-pdf-o",
+
+ '.jpg': "file-image-o",
+ '.jpeg': "file-image-o",
+ '.png': "file-image-o",
+ '.gif': "file-image-o",
+
+ '.avi': "file-video-o",
+ '.mkv': "file-video-o",
+ '.mp4': "file-video-o",
+ '.mov': "file-video-o",
+
+ '.mp3': "file-sound-o",
+ '.flac': "file-sound-o",
+ '.wma': "file-sound-o",
+
+ '.pptx': "file-powerpoint-o",
+ '.ppt': "file-powerpoint-o",
+ '.doc': "file-word-o",
+ '.docx': "file-word-o",
+ '.xlsx': "file-excel-o",
+ '.xls': "file-excel-o",
+
+ '.rar': "file-archive-o",
+ '.zip': "file-archive-o",
+ '.7z': "file-archive-o",
+ '.tar': "file-archive-o",
+ '.gz': "file-archive-o",
+
+ '.py': "file-code-o",
+ '.html': "file-code-o",
+ '.js': "file-code-o",
+ '.css': "file-code-o",
+ '.c': "file-code-o",
+ '.cpp': "file-code-o",
+ '.h': "file-code-o",
+ '.sh': "file-code-o",
+}
diff --git a/circle/dashboard/tables.py b/circle/dashboard/tables.py
index 66f16de..e8188c9 100644
--- a/circle/dashboard/tables.py
+++ b/circle/dashboard/tables.py
@@ -27,43 +27,6 @@ from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey
-class VmListTable(Table):
- pk = TemplateColumn(
- template_name='dashboard/vm-list/column-id.html',
- verbose_name="ID",
- attrs={'th': {'class': 'vm-list-table-thin'}},
- )
-
- name = TemplateColumn(
- template_name="dashboard/vm-list/column-name.html"
- )
-
- admin = TemplateColumn(
- template_name='dashboard/vm-list/column-admin.html',
- attrs={'th': {'class': 'vm-list-table-admin'}},
- )
- details = TemplateColumn(
- template_name='dashboard/vm-list/column-details.html',
- attrs={'th': {'class': 'vm-list-table-thin'}},
- )
- actions = TemplateColumn(
- template_name='dashboard/vm-list/column-actions.html',
- attrs={'th': {'class': 'vm-list-table-thin'}},
- )
- time_of_suspend = TemplateColumn(
- '{{ record.time_of_suspend|timeuntil }}',
- verbose_name=_("Suspend in"))
- time_of_delete = TemplateColumn(
- '{{ record.time_of_delete|timeuntil }}',
- verbose_name=_("Delete in"))
-
- class Meta:
- model = Instance
- attrs = {'class': ('table table-bordered table-striped table-hover '
- 'vm-list-table')}
- fields = ('pk', 'name', 'state', 'time_of_suspend', 'time_of_delete', )
-
-
class NodeListTable(Table):
pk = Column(
diff --git a/circle/dashboard/templates/base.html b/circle/dashboard/templates/base.html
index 4498b18..aefa330 100644
--- a/circle/dashboard/templates/base.html
+++ b/circle/dashboard/templates/base.html
@@ -68,6 +68,7 @@