فهرست منبع

Merge branch 'develop' into template_var_multi_select

Torkel Ödegaard 10 سال پیش
والد
کامیت
01148ac1b9
30فایلهای تغییر یافته به همراه581 افزوده شده و 249 حذف شده
  1. 1 0
      CHANGELOG.md
  2. 3 3
      Godeps/Godeps.json
  3. 7 4
      Godeps/_workspace/src/github.com/Unknwon/macaron/README.md
  4. 0 37
      Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/README.md
  5. 0 45
      Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/bufferpool.go
  6. 69 12
      Godeps/_workspace/src/github.com/Unknwon/macaron/context.go
  7. 81 25
      Godeps/_workspace/src/github.com/Unknwon/macaron/context_test.go
  8. 1 1
      Godeps/_workspace/src/github.com/Unknwon/macaron/inject/inject_test.go
  9. 1 1
      Godeps/_workspace/src/github.com/Unknwon/macaron/logger_test.go
  10. 2 2
      Godeps/_workspace/src/github.com/Unknwon/macaron/macaron.go
  11. 37 5
      Godeps/_workspace/src/github.com/Unknwon/macaron/render.go
  12. 17 17
      Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler.go
  13. 1 1
      Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler_test.go
  14. 3 1
      Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore
  15. 34 2
      Godeps/_workspace/src/gopkg.in/ini.v1/README.md
  16. 32 2
      Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md
  17. 99 7
      Godeps/_workspace/src/gopkg.in/ini.v1/ini.go
  18. 45 6
      Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go
  19. 12 6
      Godeps/_workspace/src/gopkg.in/ini.v1/struct.go
  20. 21 23
      Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go
  21. 4 0
      pkg/cmd/web.go
  22. 1 1
      pkg/services/sqlstore/migrations/apikey_mig.go
  23. 2 2
      src/app/features/panel/panelDirective.js
  24. 12 0
      src/app/panels/graph/axisEditor.html
  25. 88 42
      src/app/panels/graph/graph.js
  26. 1 1
      src/app/panels/graph/graph.tooltip.js
  27. 5 1
      src/app/panels/graph/module.js
  28. 0 1
      src/app/partials/dasheditor.html
  29. 1 0
      src/css/less/grafana.less
  30. 1 1
      src/css/less/variables.dark.less

+ 1 - 0
CHANGELOG.md

@@ -7,6 +7,7 @@
 - [Issue #171](https://github.com/grafana/grafana/issues/171).   Panel: Different time periods, panels can override dashboard relative time and/or add a time shift
 - [Issue #171](https://github.com/grafana/grafana/issues/171).   Panel: Different time periods, panels can override dashboard relative time and/or add a time shift
 - [Issue #1488](https://github.com/grafana/grafana/issues/1488). Dashboard: Clone dashboard / Save as
 - [Issue #1488](https://github.com/grafana/grafana/issues/1488). Dashboard: Clone dashboard / Save as
 - [Issue #1458](https://github.com/grafana/grafana/issues/1458). User: persisted user option for dark or light theme  (no longer an option on a dashboard)
 - [Issue #1458](https://github.com/grafana/grafana/issues/1458). User: persisted user option for dark or light theme  (no longer an option on a dashboard)
+- [Issue #452](https://github.com/grafana/grafana/issues/452).   Graph: Adds logarithmic scale option (log base 10)
 
 
 **Enhancements**
 **Enhancements**
 - [Issue #1366](https://github.com/grafana/grafana/issues/1366). Graph & Singlestat: Support for additional units, Fahrenheit (°F) and Celsius (°C), Humidity (%H), kW, watt-hour (Wh), kilowatt-hour (kWh), velocities (m/s, km/h, mpg, knot)
 - [Issue #1366](https://github.com/grafana/grafana/issues/1366). Graph & Singlestat: Support for additional units, Fahrenheit (°F) and Celsius (°C), Humidity (%H), kW, watt-hour (Wh), kilowatt-hour (kWh), velocities (m/s, km/h, mpg, knot)

+ 3 - 3
Godeps/Godeps.json

@@ -11,7 +11,7 @@
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/Unknwon/macaron",
 			"ImportPath": "github.com/Unknwon/macaron",
-			"Rev": "da7cbddc50b9d33e076fb1eabff13b55c3b85fc5"
+			"Rev": "93de4f3fad97bf246b838f828e2348f46f21f20a"
 		},
 		},
 		{
 		{
 			"ImportPath": "github.com/codegangsta/cli",
 			"ImportPath": "github.com/codegangsta/cli",
@@ -72,8 +72,8 @@
 		},
 		},
 		{
 		{
 			"ImportPath": "gopkg.in/ini.v1",
 			"ImportPath": "gopkg.in/ini.v1",
-			"Comment": "v0-10-g28ad8c4",
-			"Rev": "28ad8c408ba20e5c86b06d64cd2cc9248f640a83"
+			"Comment": "v0-16-g1772191",
+			"Rev": "177219109c97e7920c933e21c9b25f874357b237"
 		}
 		}
 	]
 	]
 }
 }

+ 7 - 4
Godeps/_workspace/src/github.com/Unknwon/macaron/README.md

@@ -1,11 +1,11 @@
 Macaron [![Build Status](https://drone.io/github.com/Unknwon/macaron/status.png)](https://drone.io/github.com/Unknwon/macaron/latest) [![](http://gocover.io/_badge/github.com/Unknwon/macaron)](http://gocover.io/github.com/Unknwon/macaron)
 Macaron [![Build Status](https://drone.io/github.com/Unknwon/macaron/status.png)](https://drone.io/github.com/Unknwon/macaron/latest) [![](http://gocover.io/_badge/github.com/Unknwon/macaron)](http://gocover.io/github.com/Unknwon/macaron)
 =======================
 =======================
 
 
-![Macaron Logo](macaronlogo.png)
+![Macaron Logo](https://raw.githubusercontent.com/Unknwon/macaron/master/macaronlogo.png)
 
 
 Package macaron is a high productive and modular design web framework in Go.
 Package macaron is a high productive and modular design web framework in Go.
 
 
-##### Current version: 0.5.0
+##### Current version: 0.5.4
 
 
 ## Getting Started
 ## Getting Started
 
 
@@ -41,7 +41,7 @@ func main() {
 - Handy dependency injection powered by [inject](https://github.com/codegangsta/inject).
 - Handy dependency injection powered by [inject](https://github.com/codegangsta/inject).
 - Better router layer and less reflection make faster speed.
 - Better router layer and less reflection make faster speed.
 
 
-## Middlewares 
+## Middlewares
 
 
 Middlewares allow you easily plugin/unplugin features for your Macaron applications.
 Middlewares allow you easily plugin/unplugin features for your Macaron applications.
 
 
@@ -70,15 +70,18 @@ There are already many [middlewares](https://github.com/macaron-contrib) to simp
 
 
 - [Gogs](https://github.com/gogits/gogs): Go Git Service
 - [Gogs](https://github.com/gogits/gogs): Go Git Service
 - [Gogs Web](https://github.com/gogits/gogsweb): Gogs official website
 - [Gogs Web](https://github.com/gogits/gogsweb): Gogs official website
+- [Go Walker](https://gowalker.org): Go online API documentation
 - [Switch](https://github.com/gpmgo/switch): Gopm registry
 - [Switch](https://github.com/gpmgo/switch): Gopm registry
 - [YouGam](http://yougam.com): Online Forum
 - [YouGam](http://yougam.com): Online Forum
 - [Car Girl](http://qcnl.gzsy.com/): Online campaign
 - [Car Girl](http://qcnl.gzsy.com/): Online campaign
+- [Critical Stack Intel](https://intel.criticalstack.com/): A 100% free intel marketplace from Critical Stack, Inc.
 
 
 ## Getting Help
 ## Getting Help
 
 
 - [API Reference](https://gowalker.org/github.com/Unknwon/macaron)
 - [API Reference](https://gowalker.org/github.com/Unknwon/macaron)
 - [Documentation](http://macaron.gogs.io)
 - [Documentation](http://macaron.gogs.io)
 - [FAQs](http://macaron.gogs.io/docs/faqs)
 - [FAQs](http://macaron.gogs.io/docs/faqs)
+- [![Join the chat at https://gitter.im/Unknwon/macaron](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Unknwon/macaron?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 
 
 ## Credits
 ## Credits
 
 
@@ -88,4 +91,4 @@ There are already many [middlewares](https://github.com/macaron-contrib) to simp
 
 
 ## License
 ## License
 
 
-This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text.
+This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text.

+ 0 - 37
Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/README.md

@@ -1,37 +0,0 @@
-# bpool [![GoDoc](https://godoc.org/github.com/oxtoacart/bpool?status.png)](https://godoc.org/github.com/oxtoacart/bpool)
-
-Package bpool implements leaky pools of byte arrays and Buffers as bounded channels. It is based on the leaky buffer example from the Effective Go documentation: http://golang.org/doc/effective_go.html#leaky_buffer
-
-## Install
-
-`go get github.com/oxtoacart/bpool`
-
-## Documentation
-
-See [godoc.org](http://godoc.org/github.com/oxtoacart/bpool) or use `godoc github.com/oxtoacart/bpool`
-
-## Example
-
-```go
-
-var bufpool *bpol.BufferPool
-
-func main() {
-
-    bufpool = bpool.NewBufferPool(48)
-
-}
-
-func someFunction() error {
-
-     // Get a buffer from the pool
-     buf := bufpool.Get()
-     ...
-     ...
-     ...
-     // Return the buffer to the pool
-     bufpool.Put(buf)
-     
-     return nil
-}
-```

+ 0 - 45
Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/bufferpool.go

@@ -1,45 +0,0 @@
-package bpool
-
-import (
-	"bytes"
-)
-
-/*
-BufferPool implements a pool of bytes.Buffers in the form of a bounded
-channel.
-*/
-type BufferPool struct {
-	c chan *bytes.Buffer
-}
-
-/*
-NewBufferPool creates a new BufferPool bounded to the given size.
-*/
-func NewBufferPool(size int) (bp *BufferPool) {
-	return &BufferPool{
-		c: make(chan *bytes.Buffer, size),
-	}
-}
-
-/*
-Get gets a Buffer from the BufferPool, or creates a new one if none are available
-in the pool.
-*/
-func (bp *BufferPool) Get() (b *bytes.Buffer) {
-	select {
-	case b = <-bp.c:
-	// reuse existing buffer
-	default:
-		// create new buffer
-		b = bytes.NewBuffer([]byte{})
-	}
-	return
-}
-
-/*
-Put returns the given Buffer to the BufferPool.
-*/
-func (bp *BufferPool) Put(b *bytes.Buffer) {
-	b.Reset()
-	bp.c <- b
-}

+ 69 - 12
Godeps/_workspace/src/github.com/Unknwon/macaron/context.go

@@ -23,9 +23,11 @@ import (
 	"mime/multipart"
 	"mime/multipart"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
+	"os"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
 	"reflect"
 	"reflect"
+	"strconv"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
@@ -182,6 +184,11 @@ func (ctx *Context) Query(name string) string {
 	return ctx.Req.Form.Get(name)
 	return ctx.Req.Form.Get(name)
 }
 }
 
 
+// QueryTrim querys and trims spaces form parameter.
+func (ctx *Context) QueryTrim(name string) string {
+	return strings.TrimSpace(ctx.Query(name))
+}
+
 // QueryStrings returns a list of results by given query name.
 // QueryStrings returns a list of results by given query name.
 func (ctx *Context) QueryStrings(name string) []string {
 func (ctx *Context) QueryStrings(name string) []string {
 	if ctx.Req.Form == nil {
 	if ctx.Req.Form == nil {
@@ -210,9 +217,21 @@ func (ctx *Context) QueryInt64(name string) int64 {
 	return com.StrTo(ctx.Query(name)).MustInt64()
 	return com.StrTo(ctx.Query(name)).MustInt64()
 }
 }
 
 
+// QueryFloat64 returns query result in float64 type.
+func (ctx *Context) QueryFloat64(name string) float64 {
+	v, _ := strconv.ParseFloat(ctx.Query(name), 64)
+	return v
+}
+
 // Params returns value of given param name.
 // Params returns value of given param name.
-// e.g. ctx.Params(":uid")
+// e.g. ctx.Params(":uid") or ctx.Params("uid")
 func (ctx *Context) Params(name string) string {
 func (ctx *Context) Params(name string) string {
+	if len(name) == 0 {
+		return ""
+	}
+	if name[0] != '*' && name[0] != ':' {
+		name = ":" + name
+	}
 	return ctx.params[name]
 	return ctx.params[name]
 }
 }
 
 
@@ -242,12 +261,20 @@ func (ctx *Context) ParamsInt64(name string) int64 {
 	return com.StrTo(ctx.Params(name)).MustInt64()
 	return com.StrTo(ctx.Params(name)).MustInt64()
 }
 }
 
 
+// ParamsFloat64 returns params result in int64 type.
+// e.g. ctx.ParamsFloat64(":uid")
+func (ctx *Context) ParamsFloat64(name string) float64 {
+	v, _ := strconv.ParseFloat(ctx.Params(name), 64)
+	return v
+}
+
 // GetFile returns information about user upload file by given form field name.
 // GetFile returns information about user upload file by given form field name.
 func (ctx *Context) GetFile(name string) (multipart.File, *multipart.FileHeader, error) {
 func (ctx *Context) GetFile(name string) (multipart.File, *multipart.FileHeader, error) {
 	return ctx.Req.FormFile(name)
 	return ctx.Req.FormFile(name)
 }
 }
 
 
 // SetCookie sets given cookie value to response header.
 // SetCookie sets given cookie value to response header.
+// FIXME: IE support? http://golanghome.com/post/620#reply2
 func (ctx *Context) SetCookie(name string, value string, others ...interface{}) {
 func (ctx *Context) SetCookie(name string, value string, others ...interface{}) {
 	cookie := http.Cookie{}
 	cookie := http.Cookie{}
 	cookie.Name = name
 	cookie.Name = name
@@ -264,23 +291,19 @@ func (ctx *Context) SetCookie(name string, value string, others ...interface{})
 		}
 		}
 	}
 	}
 
 
-	// default "/"
+	cookie.Path = "/"
 	if len(others) > 1 {
 	if len(others) > 1 {
 		if v, ok := others[1].(string); ok && len(v) > 0 {
 		if v, ok := others[1].(string); ok && len(v) > 0 {
 			cookie.Path = v
 			cookie.Path = v
 		}
 		}
-	} else {
-		cookie.Path = "/"
 	}
 	}
 
 
-	// default empty
 	if len(others) > 2 {
 	if len(others) > 2 {
 		if v, ok := others[2].(string); ok && len(v) > 0 {
 		if v, ok := others[2].(string); ok && len(v) > 0 {
 			cookie.Domain = v
 			cookie.Domain = v
 		}
 		}
 	}
 	}
 
 
-	// default empty
 	if len(others) > 3 {
 	if len(others) > 3 {
 		switch v := others[3].(type) {
 		switch v := others[3].(type) {
 		case bool:
 		case bool:
@@ -292,7 +315,6 @@ func (ctx *Context) SetCookie(name string, value string, others ...interface{})
 		}
 		}
 	}
 	}
 
 
-	// default false. for session cookie default true
 	if len(others) > 4 {
 	if len(others) > 4 {
 		if v, ok := others[4].(bool); ok && v {
 		if v, ok := others[4].(bool); ok && v {
 			cookie.HttpOnly = true
 			cookie.HttpOnly = true
@@ -322,6 +344,12 @@ func (ctx *Context) GetCookieInt64(name string) int64 {
 	return com.StrTo(ctx.GetCookie(name)).MustInt64()
 	return com.StrTo(ctx.GetCookie(name)).MustInt64()
 }
 }
 
 
+// GetCookieFloat64 returns cookie result in float64 type.
+func (ctx *Context) GetCookieFloat64(name string) float64 {
+	v, _ := strconv.ParseFloat(ctx.GetCookie(name), 64)
+	return v
+}
+
 var defaultCookieSecret string
 var defaultCookieSecret string
 
 
 // SetDefaultCookieSecret sets global default secure cookie secret.
 // SetDefaultCookieSecret sets global default secure cookie secret.
@@ -368,6 +396,14 @@ func (ctx *Context) GetSuperSecureCookie(secret, key string) (string, bool) {
 	return string(text), err == nil
 	return string(text), err == nil
 }
 }
 
 
+func (ctx *Context) setRawContentHeader() {
+	ctx.Resp.Header().Set("Content-Description", "Raw content")
+	ctx.Resp.Header().Set("Content-Type", "text/plain")
+	ctx.Resp.Header().Set("Expires", "0")
+	ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
+	ctx.Resp.Header().Set("Pragma", "public")
+}
+
 // ServeContent serves given content to response.
 // ServeContent serves given content to response.
 func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) {
 func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) {
 	modtime := time.Now()
 	modtime := time.Now()
@@ -377,14 +413,35 @@ func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interfa
 			modtime = v
 			modtime = v
 		}
 		}
 	}
 	}
-	ctx.Resp.Header().Set("Content-Description", "Raw content")
-	ctx.Resp.Header().Set("Content-Type", "text/plain")
-	ctx.Resp.Header().Set("Expires", "0")
-	ctx.Resp.Header().Set("Cache-Control", "must-revalidate")
-	ctx.Resp.Header().Set("Pragma", "public")
+
+	ctx.setRawContentHeader()
 	http.ServeContent(ctx.Resp, ctx.Req.Request, name, modtime, r)
 	http.ServeContent(ctx.Resp, ctx.Req.Request, name, modtime, r)
 }
 }
 
 
+// ServeFileContent serves given file as content to response.
+func (ctx *Context) ServeFileContent(file string, names ...string) {
+	var name string
+	if len(names) > 0 {
+		name = names[0]
+	} else {
+		name = path.Base(file)
+	}
+
+	f, err := os.Open(file)
+	if err != nil {
+		if Env == PROD {
+			http.Error(ctx.Resp, "Internal Server Error", 500)
+		} else {
+			http.Error(ctx.Resp, err.Error(), 500)
+		}
+		return
+	}
+	defer f.Close()
+
+	ctx.setRawContentHeader()
+	http.ServeContent(ctx.Resp, ctx.Req.Request, name, time.Now(), f)
+}
+
 // ServeFile serves given file to response.
 // ServeFile serves given file to response.
 func (ctx *Context) ServeFile(file string, names ...string) {
 func (ctx *Context) ServeFile(file string, names ...string) {
 	var name string
 	var name string

+ 81 - 25
Godeps/_workspace/src/github.com/Unknwon/macaron/context_test.go

@@ -76,35 +76,53 @@ func Test_Context(t *testing.T) {
 		})
 		})
 
 
 		Convey("Render HTML", func() {
 		Convey("Render HTML", func() {
-			m.Get("/html", func(ctx *Context) {
-				ctx.HTML(304, "hello", "Unknwon") // 304 for logger test.
+
+			Convey("Normal HTML", func() {
+				m.Get("/html", func(ctx *Context) {
+					ctx.HTML(304, "hello", "Unknwon") // 304 for logger test.
+				})
+
+				resp := httptest.NewRecorder()
+				req, err := http.NewRequest("GET", "/html", nil)
+				So(err, ShouldBeNil)
+				m.ServeHTTP(resp, req)
+				So(resp.Body.String(), ShouldEqual, "<h1>Hello Unknwon</h1>")
 			})
 			})
 
 
-			resp := httptest.NewRecorder()
-			req, err := http.NewRequest("GET", "/html", nil)
-			So(err, ShouldBeNil)
-			m.ServeHTTP(resp, req)
-			So(resp.Body.String(), ShouldEqual, "<h1>Hello Unknwon</h1>")
+			Convey("HTML template set", func() {
+				m.Get("/html2", func(ctx *Context) {
+					ctx.Data["Name"] = "Unknwon"
+					ctx.HTMLSet(200, "basic2", "hello2")
+				})
 
 
-			m.Get("/html2", func(ctx *Context) {
-				ctx.Data["Name"] = "Unknwon"
-				ctx.HTMLSet(200, "basic2", "hello2")
+				resp := httptest.NewRecorder()
+				req, err := http.NewRequest("GET", "/html2", nil)
+				So(err, ShouldBeNil)
+				m.ServeHTTP(resp, req)
+				So(resp.Body.String(), ShouldEqual, "<h1>Hello Unknwon</h1>")
 			})
 			})
 
 
-			resp = httptest.NewRecorder()
-			req, err = http.NewRequest("GET", "/html2", nil)
-			So(err, ShouldBeNil)
-			m.ServeHTTP(resp, req)
-			So(resp.Body.String(), ShouldEqual, "<h1>Hello Unknwon</h1>")
+			Convey("With layout", func() {
+				m.Get("/layout", func(ctx *Context) {
+					ctx.HTML(200, "hello", "Unknwon", HTMLOptions{"layout"})
+				})
+
+				resp := httptest.NewRecorder()
+				req, err := http.NewRequest("GET", "/layout", nil)
+				So(err, ShouldBeNil)
+				m.ServeHTTP(resp, req)
+				So(resp.Body.String(), ShouldEqual, "head<h1>Hello Unknwon</h1>foot")
+			})
 		})
 		})
 
 
 		Convey("Parse from and query", func() {
 		Convey("Parse from and query", func() {
 			m.Get("/query", func(ctx *Context) string {
 			m.Get("/query", func(ctx *Context) string {
 				var buf bytes.Buffer
 				var buf bytes.Buffer
-				buf.WriteString(ctx.Query("name") + " ")
+				buf.WriteString(ctx.QueryTrim("name") + " ")
 				buf.WriteString(ctx.QueryEscape("name") + " ")
 				buf.WriteString(ctx.QueryEscape("name") + " ")
 				buf.WriteString(com.ToStr(ctx.QueryInt("int")) + " ")
 				buf.WriteString(com.ToStr(ctx.QueryInt("int")) + " ")
 				buf.WriteString(com.ToStr(ctx.QueryInt64("int64")) + " ")
 				buf.WriteString(com.ToStr(ctx.QueryInt64("int64")) + " ")
+				buf.WriteString(com.ToStr(ctx.QueryFloat64("float64")) + " ")
 				return buf.String()
 				return buf.String()
 			})
 			})
 			m.Get("/query2", func(ctx *Context) string {
 			m.Get("/query2", func(ctx *Context) string {
@@ -115,10 +133,10 @@ func Test_Context(t *testing.T) {
 			})
 			})
 
 
 			resp := httptest.NewRecorder()
 			resp := httptest.NewRecorder()
-			req, err := http.NewRequest("GET", "/query?name=Unknwon&int=12&int64=123", nil)
+			req, err := http.NewRequest("GET", "/query?name=Unknwon&int=12&int64=123&float64=1.25", nil)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			m.ServeHTTP(resp, req)
 			m.ServeHTTP(resp, req)
-			So(resp.Body.String(), ShouldEqual, "Unknwon Unknwon 12 123 ")
+			So(resp.Body.String(), ShouldEqual, "Unknwon Unknwon 12 123 1.25 ")
 
 
 			resp = httptest.NewRecorder()
 			resp = httptest.NewRecorder()
 			req, err = http.NewRequest("GET", "/query2?list=item1&list=item2", nil)
 			req, err = http.NewRequest("GET", "/query2?list=item1&list=item2", nil)
@@ -128,21 +146,23 @@ func Test_Context(t *testing.T) {
 		})
 		})
 
 
 		Convey("URL parameter", func() {
 		Convey("URL parameter", func() {
-			m.Get("/:name/:int/:int64", func(ctx *Context) string {
+			m.Get("/:name/:int/:int64/:float64", func(ctx *Context) string {
 				var buf bytes.Buffer
 				var buf bytes.Buffer
-				ctx.SetParams(":name", ctx.Params(":name"))
+				ctx.SetParams("name", ctx.Params("name"))
+				buf.WriteString(ctx.Params(""))
 				buf.WriteString(ctx.Params(":name") + " ")
 				buf.WriteString(ctx.Params(":name") + " ")
 				buf.WriteString(ctx.ParamsEscape(":name") + " ")
 				buf.WriteString(ctx.ParamsEscape(":name") + " ")
 				buf.WriteString(com.ToStr(ctx.ParamsInt(":int")) + " ")
 				buf.WriteString(com.ToStr(ctx.ParamsInt(":int")) + " ")
 				buf.WriteString(com.ToStr(ctx.ParamsInt64(":int64")) + " ")
 				buf.WriteString(com.ToStr(ctx.ParamsInt64(":int64")) + " ")
+				buf.WriteString(com.ToStr(ctx.ParamsFloat64(":float64")) + " ")
 				return buf.String()
 				return buf.String()
 			})
 			})
 
 
 			resp := httptest.NewRecorder()
 			resp := httptest.NewRecorder()
-			req, err := http.NewRequest("GET", "/user/1/13", nil)
+			req, err := http.NewRequest("GET", "/user/1/13/1.24", nil)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			m.ServeHTTP(resp, req)
 			m.ServeHTTP(resp, req)
-			So(resp.Body.String(), ShouldEqual, "user user 1 13 ")
+			So(resp.Body.String(), ShouldEqual, "user user 1 13 1.24 ")
 		})
 		})
 
 
 		Convey("Get file", func() {
 		Convey("Get file", func() {
@@ -158,26 +178,29 @@ func Test_Context(t *testing.T) {
 
 
 		Convey("Set and get cookie", func() {
 		Convey("Set and get cookie", func() {
 			m.Get("/set", func(ctx *Context) {
 			m.Get("/set", func(ctx *Context) {
-				ctx.SetCookie("user", "Unknwon", 1)
+				ctx.SetCookie("user", "Unknwon", 1, "/", "localhost", true, true)
+				ctx.SetCookie("user", "Unknwon", int32(1), "/", "localhost", 1)
+				ctx.SetCookie("user", "Unknwon", int64(1))
 			})
 			})
 
 
 			resp := httptest.NewRecorder()
 			resp := httptest.NewRecorder()
 			req, err := http.NewRequest("GET", "/set", nil)
 			req, err := http.NewRequest("GET", "/set", nil)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			m.ServeHTTP(resp, req)
 			m.ServeHTTP(resp, req)
-			So(resp.Header().Get("Set-Cookie"), ShouldEqual, "user=Unknwon; Path=/; Max-Age=1")
+			So(resp.Header().Get("Set-Cookie"), ShouldEqual, "user=Unknwon; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure")
 
 
 			m.Get("/get", func(ctx *Context) string {
 			m.Get("/get", func(ctx *Context) string {
 				ctx.GetCookie("404")
 				ctx.GetCookie("404")
 				So(ctx.GetCookieInt("uid"), ShouldEqual, 1)
 				So(ctx.GetCookieInt("uid"), ShouldEqual, 1)
 				So(ctx.GetCookieInt64("uid"), ShouldEqual, 1)
 				So(ctx.GetCookieInt64("uid"), ShouldEqual, 1)
+				So(ctx.GetCookieFloat64("balance"), ShouldEqual, 1.25)
 				return ctx.GetCookie("user")
 				return ctx.GetCookie("user")
 			})
 			})
 
 
 			resp = httptest.NewRecorder()
 			resp = httptest.NewRecorder()
 			req, err = http.NewRequest("GET", "/get", nil)
 			req, err = http.NewRequest("GET", "/get", nil)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
-			req.Header.Set("Cookie", "user=Unknwon; uid=1")
+			req.Header.Set("Cookie", "user=Unknwon; uid=1; balance=1.25")
 			m.ServeHTTP(resp, req)
 			m.ServeHTTP(resp, req)
 			So(resp.Body.String(), ShouldEqual, "Unknwon")
 			So(resp.Body.String(), ShouldEqual, "Unknwon")
 		})
 		})
@@ -231,6 +254,39 @@ func Test_Context(t *testing.T) {
 			So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
 			So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
 		})
 		})
 
 
+		Convey("Serve file content", func() {
+			m.Get("/file", func(ctx *Context) {
+				ctx.ServeFileContent("fixtures/custom_funcs/index.tmpl")
+			})
+
+			resp := httptest.NewRecorder()
+			req, err := http.NewRequest("GET", "/file", nil)
+			So(err, ShouldBeNil)
+			m.ServeHTTP(resp, req)
+			So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
+
+			m.Get("/file2", func(ctx *Context) {
+				ctx.ServeFileContent("fixtures/custom_funcs/index.tmpl", "ok.tmpl")
+			})
+
+			resp = httptest.NewRecorder()
+			req, err = http.NewRequest("GET", "/file2", nil)
+			So(err, ShouldBeNil)
+			m.ServeHTTP(resp, req)
+			So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}")
+
+			m.Get("/file3", func(ctx *Context) {
+				ctx.ServeFileContent("404.tmpl")
+			})
+
+			resp = httptest.NewRecorder()
+			req, err = http.NewRequest("GET", "/file3", nil)
+			So(err, ShouldBeNil)
+			m.ServeHTTP(resp, req)
+			So(resp.Body.String(), ShouldEqual, "open 404.tmpl: no such file or directory\n")
+			So(resp.Code, ShouldEqual, 500)
+		})
+
 		Convey("Serve content", func() {
 		Convey("Serve content", func() {
 			m.Get("/content", func(ctx *Context) {
 			m.Get("/content", func(ctx *Context) {
 				ctx.ServeContent("content1", bytes.NewReader([]byte("Hello world!")))
 				ctx.ServeContent("content1", bytes.NewReader([]byte("Hello world!")))

+ 1 - 1
Godeps/_workspace/src/github.com/Unknwon/macaron/inject/inject_test.go

@@ -1,5 +1,5 @@
 // Copyright 2013 Martini Authors
 // Copyright 2013 Martini Authors
-// Copyright 2014 Unknown
+// Copyright 2014 Unknwon
 //
 //
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // not use this file except in compliance with the License. You may obtain
 // not use this file except in compliance with the License. You may obtain

+ 1 - 1
Godeps/_workspace/src/github.com/Unknwon/macaron/logger_test.go

@@ -46,7 +46,7 @@ func Test_Logger(t *testing.T) {
 		So(len(buf.String()), ShouldBeGreaterThan, 0)
 		So(len(buf.String()), ShouldBeGreaterThan, 0)
 	})
 	})
 
 
-	if !isWindows {
+	if ColorLog {
 		Convey("Color console output", t, func() {
 		Convey("Color console output", t, func() {
 			m := Classic()
 			m := Classic()
 			m.Get("/:code:int", func(ctx *Context) (int, string) {
 			m.Get("/:code:int", func(ctx *Context) (int, string) {

+ 2 - 2
Godeps/_workspace/src/github.com/Unknwon/macaron/macaron.go

@@ -29,7 +29,7 @@ import (
 	"github.com/Unknwon/macaron/inject"
 	"github.com/Unknwon/macaron/inject"
 )
 )
 
 
-const _VERSION = "0.5.0.0116"
+const _VERSION = "0.5.4.0318"
 
 
 func Version() string {
 func Version() string {
 	return _VERSION
 	return _VERSION
@@ -267,7 +267,7 @@ func SetConfig(source interface{}, others ...interface{}) (_ *ini.File, err erro
 // It returns an empty object if there is no one available.
 // It returns an empty object if there is no one available.
 func Config() *ini.File {
 func Config() *ini.File {
 	if cfg == nil {
 	if cfg == nil {
-		return &ini.File{}
+		return ini.Empty()
 	}
 	}
 	return cfg
 	return cfg
 }
 }

+ 37 - 5
Godeps/_workspace/src/github.com/Unknwon/macaron/render.go

@@ -1,4 +1,5 @@
 // Copyright 2013 Martini Authors
 // Copyright 2013 Martini Authors
+// Copyright 2013 oxtoacart
 // Copyright 2014 Unknwon
 // Copyright 2014 Unknwon
 //
 //
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
@@ -32,10 +33,39 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/Unknwon/com"
 	"github.com/Unknwon/com"
-
-	"github.com/Unknwon/macaron/bpool"
 )
 )
 
 
+// BufferPool implements a pool of bytes.Buffers in the form of a bounded channel.
+type BufferPool struct {
+	c chan *bytes.Buffer
+}
+
+// NewBufferPool creates a new BufferPool bounded to the given size.
+func NewBufferPool(size int) (bp *BufferPool) {
+	return &BufferPool{
+		c: make(chan *bytes.Buffer, size),
+	}
+}
+
+// Get gets a Buffer from the BufferPool, or creates a new one if none are available
+// in the pool.
+func (bp *BufferPool) Get() (b *bytes.Buffer) {
+	select {
+	case b = <-bp.c:
+	// reuse existing buffer
+	default:
+		// create new buffer
+		b = bytes.NewBuffer([]byte{})
+	}
+	return
+}
+
+// Put returns the given Buffer to the BufferPool.
+func (bp *BufferPool) Put(b *bytes.Buffer) {
+	b.Reset()
+	bp.c <- b
+}
+
 const (
 const (
 	ContentType    = "Content-Type"
 	ContentType    = "Content-Type"
 	ContentLength  = "Content-Length"
 	ContentLength  = "Content-Length"
@@ -50,7 +80,7 @@ const (
 
 
 var (
 var (
 	// Provides a temporary buffer to execute templates into and catch errors.
 	// Provides a temporary buffer to execute templates into and catch errors.
-	bufpool = bpool.NewBufferPool(64)
+	bufpool = NewBufferPool(64)
 
 
 	// Included helper functions for use when rendering html
 	// Included helper functions for use when rendering html
 	helperFuncs = template.FuncMap{
 	helperFuncs = template.FuncMap{
@@ -392,8 +422,10 @@ func (r *TplRender) RW() http.ResponseWriter {
 }
 }
 
 
 func (r *TplRender) JSON(status int, v interface{}) {
 func (r *TplRender) JSON(status int, v interface{}) {
-	var result []byte
-	var err error
+	var (
+		result []byte
+		err    error
+	)
 	if r.Opt.IndentJSON {
 	if r.Opt.IndentJSON {
 		result, err = json.MarshalIndent(v, "", "  ")
 		result, err = json.MarshalIndent(v, "", "  ")
 	} else {
 	} else {

+ 17 - 17
Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler.go

@@ -1,5 +1,5 @@
 // Copyright 2013 Martini Authors
 // Copyright 2013 Martini Authors
-// Copyright 2014 Unknown
+// Copyright 2014 Unknwon
 //
 //
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // not use this file except in compliance with the License. You may obtain
 // not use this file except in compliance with the License. You may obtain
@@ -28,32 +28,32 @@ import (
 // that are passed into this function.
 // that are passed into this function.
 type ReturnHandler func(*Context, []reflect.Value)
 type ReturnHandler func(*Context, []reflect.Value)
 
 
+func canDeref(val reflect.Value) bool {
+	return val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr
+}
+
+func isByteSlice(val reflect.Value) bool {
+	return val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8
+}
+
 func defaultReturnHandler() ReturnHandler {
 func defaultReturnHandler() ReturnHandler {
 	return func(ctx *Context, vals []reflect.Value) {
 	return func(ctx *Context, vals []reflect.Value) {
 		rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
 		rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
 		res := rv.Interface().(http.ResponseWriter)
 		res := rv.Interface().(http.ResponseWriter)
-		var responseVal reflect.Value
+		var respVal reflect.Value
 		if len(vals) > 1 && vals[0].Kind() == reflect.Int {
 		if len(vals) > 1 && vals[0].Kind() == reflect.Int {
 			res.WriteHeader(int(vals[0].Int()))
 			res.WriteHeader(int(vals[0].Int()))
-			responseVal = vals[1]
+			respVal = vals[1]
 		} else if len(vals) > 0 {
 		} else if len(vals) > 0 {
-			responseVal = vals[0]
+			respVal = vals[0]
 		}
 		}
-		if canDeref(responseVal) {
-			responseVal = responseVal.Elem()
+		if canDeref(respVal) {
+			respVal = respVal.Elem()
 		}
 		}
-		if isByteSlice(responseVal) {
-			res.Write(responseVal.Bytes())
+		if isByteSlice(respVal) {
+			res.Write(respVal.Bytes())
 		} else {
 		} else {
-			res.Write([]byte(responseVal.String()))
+			res.Write([]byte(respVal.String()))
 		}
 		}
 	}
 	}
 }
 }
-
-func isByteSlice(val reflect.Value) bool {
-	return val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8
-}
-
-func canDeref(val reflect.Value) bool {
-	return val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr
-}

+ 1 - 1
Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler_test.go

@@ -1,4 +1,4 @@
-// Copyright 2014 Unknown
+// Copyright 2014 Unknwon
 //
 //
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // Licensed under the Apache License, Version 2.0 (the "License"): you may
 // not use this file except in compliance with the License. You may obtain
 // not use this file except in compliance with the License. You may obtain

+ 3 - 1
Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore

@@ -1 +1,3 @@
-testdata/conf_out.ini
+testdata/conf_out.ini
+ini.sublime-project
+ini.sublime-workspace

+ 34 - 2
Godeps/_workspace/src/gopkg.in/ini.v1/README.md

@@ -32,6 +32,12 @@ A **Data Source** is either raw data in type `[]byte` or a file name with type `
 cfg, err := ini.Load([]byte("raw data"), "filename")
 cfg, err := ini.Load([]byte("raw data"), "filename")
 ```
 ```
 
 
+Or start with an empty object:
+
+```go
+cfg := ini.Empty()
+```
+
 When you cannot decide how many data sources to load at the beginning, you still able to **Append()** them later.
 When you cannot decide how many data sources to load at the beginning, you still able to **Append()** them later.
 
 
 ```go
 ```go
@@ -58,7 +64,7 @@ When you're pretty sure the section exists, following code could make your life
 section := cfg.Section("")
 section := cfg.Section("")
 ```
 ```
 
 
-What happens when the section somehow does not exists? Won't panic, it returns an empty section object.
+What happens when the section somehow does not exist? Don't panic, it automatically creates and returns a new section to you.
 
 
 To create a new section:
 To create a new section:
 
 
@@ -117,6 +123,9 @@ val := cfg.Section("").Key("key name").String()
 To get value with types:
 To get value with types:
 
 
 ```go
 ```go
+// For boolean values:
+// true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On
+// false when value is: 0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off
 v, err = cfg.Section("").Key("BOOL").Bool()
 v, err = cfg.Section("").Key("BOOL").Bool()
 v, err = cfg.Section("").Key("FLOAT64").Float64()
 v, err = cfg.Section("").Key("FLOAT64").Float64()
 v, err = cfg.Section("").Key("INT").Int()
 v, err = cfg.Section("").Key("INT").Int()
@@ -176,11 +185,22 @@ v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"})
 v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
 v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
 v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
 v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
 v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
 v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
-v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3})
+v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
+v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
 ```
 ```
 
 
 Default value will be presented if value of key is not in candidates you given, and default value does not need be one of candidates.
 Default value will be presented if value of key is not in candidates you given, and default value does not need be one of candidates.
 
 
+To validate value in a given range:
+
+```go
+vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
+vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
+vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
+vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
+vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
+```
+
 To auto-split value into slice:
 To auto-split value into slice:
 
 
 ```go
 ```go
@@ -295,6 +315,18 @@ func main() {
 }
 }
 ```
 ```
 
 
+Can I have default value for field? Absolutely.
+
+Assign it before you map to struct. It will keep the value as it is if the key is not presented or got wrong type.
+
+```go
+// ...
+p := &Person{
+	Name: "Joe",
+}
+// ...
+```
+
 #### Name Mapper
 #### Name Mapper
 
 
 To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual secion and key name.
 To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual secion and key name.

+ 32 - 2
Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md

@@ -27,6 +27,12 @@
 cfg, err := ini.Load([]byte("raw data"), "filename")
 cfg, err := ini.Load([]byte("raw data"), "filename")
 ```
 ```
 
 
+或者从一个空白的文件开始:
+
+```go
+cfg := ini.Empty()
+```
+
 当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。
 当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。
 
 
 ```go
 ```go
@@ -53,7 +59,7 @@ section, err := cfg.GetSection("")
 section := cfg.Section("")
 section := cfg.Section("")
 ```
 ```
 
 
-如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会返回一个空的分区对象
+如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您
 
 
 创建一个分区:
 创建一个分区:
 
 
@@ -112,6 +118,9 @@ val := cfg.Section("").Key("key name").String()
 获取其它类型的值:
 获取其它类型的值:
 
 
 ```go
 ```go
+// 布尔值的规则:
+// true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On
+// false 当值为:0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off
 v, err = cfg.Section("").Key("BOOL").Bool()
 v, err = cfg.Section("").Key("BOOL").Bool()
 v, err = cfg.Section("").Key("FLOAT64").Float64()
 v, err = cfg.Section("").Key("FLOAT64").Float64()
 v, err = cfg.Section("").Key("INT").Int()
 v, err = cfg.Section("").Key("INT").Int()
@@ -171,11 +180,22 @@ v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"})
 v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
 v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75})
 v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
 v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30})
 v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
 v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30})
-v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3})
+v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3})
+v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339
 ```
 ```
 
 
 如果获取到的值不是候选值的任意一个,则会返回默认值,而默认值不需要是候选值中的一员。
 如果获取到的值不是候选值的任意一个,则会返回默认值,而默认值不需要是候选值中的一员。
 
 
+验证获取的值是否在指定范围内:
+
+```go
+vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2)
+vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20)
+vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20)
+vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime)
+vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339
+```
+
 自动分割键值为切片(slice):
 自动分割键值为切片(slice):
 
 
 ```go
 ```go
@@ -290,6 +310,16 @@ func main() {
 }
 }
 ```
 ```
 
 
+结构的字段怎么设置默认值呢?很简单,只要在映射之前对指定字段进行赋值就可以了。如果键未找到或者类型错误,该值不会发生改变。
+
+```go
+// ...
+p := &Person{
+	Name: "Joe",
+}
+// ...
+```
+
 #### 名称映射器(Name Mapper)
 #### 名称映射器(Name Mapper)
 
 
 为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。
 为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。

+ 99 - 7
Godeps/_workspace/src/gopkg.in/ini.v1/ini.go

@@ -35,7 +35,7 @@ const (
 	// Maximum allowed depth when recursively substituing variable names.
 	// Maximum allowed depth when recursively substituing variable names.
 	_DEPTH_VALUES = 99
 	_DEPTH_VALUES = 99
 
 
-	_VERSION = "1.0.1"
+	_VERSION = "1.2.6"
 )
 )
 
 
 func Version() string {
 func Version() string {
@@ -144,9 +144,24 @@ func (k *Key) String() string {
 	return val
 	return val
 }
 }
 
 
+// parseBool returns the boolean value represented by the string.
+//
+// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On,
+// 0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off.
+// Any other value returns an error.
+func parseBool(str string) (value bool, err error) {
+	switch str {
+	case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "ON", "on", "On":
+		return true, nil
+	case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "OFF", "off", "Off":
+		return false, nil
+	}
+	return false, fmt.Errorf("parsing \"%s\": invalid syntax", str)
+}
+
 // Bool returns bool type value.
 // Bool returns bool type value.
 func (k *Key) Bool() (bool, error) {
 func (k *Key) Bool() (bool, error) {
-	return strconv.ParseBool(k.String())
+	return parseBool(k.String())
 }
 }
 
 
 // Float64 returns float64 type value.
 // Float64 returns float64 type value.
@@ -305,6 +320,52 @@ func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time {
 	return k.InTimeFormat(time.RFC3339, defaultVal, candidates)
 	return k.InTimeFormat(time.RFC3339, defaultVal, candidates)
 }
 }
 
 
+// RangeFloat64 checks if value is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 {
+	val := k.MustFloat64()
+	if val < min || val > max {
+		return defaultVal
+	}
+	return val
+}
+
+// RangeInt checks if value is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeInt(defaultVal, min, max int) int {
+	val := k.MustInt()
+	if val < min || val > max {
+		return defaultVal
+	}
+	return val
+}
+
+// RangeInt64 checks if value is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeInt64(defaultVal, min, max int64) int64 {
+	val := k.MustInt64()
+	if val < min || val > max {
+		return defaultVal
+	}
+	return val
+}
+
+// RangeTimeFormat checks if value with given format is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time {
+	val := k.MustTimeFormat(format)
+	if val.Unix() < min.Unix() || val.Unix() > max.Unix() {
+		return defaultVal
+	}
+	return val
+}
+
+// RangeTime checks if value with RFC3339 format is in given range inclusively,
+// and returns default value if it's not.
+func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time {
+	return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max)
+}
+
 // Strings returns list of string devide by given delimiter.
 // Strings returns list of string devide by given delimiter.
 func (k *Key) Strings(delim string) []string {
 func (k *Key) Strings(delim string) []string {
 	str := k.String()
 	str := k.String()
@@ -440,7 +501,10 @@ func (s *Section) GetKey(name string) (*Key, error) {
 func (s *Section) Key(name string) *Key {
 func (s *Section) Key(name string) *Key {
 	key, err := s.GetKey(name)
 	key, err := s.GetKey(name)
 	if err != nil {
 	if err != nil {
-		return &Key{}
+		// It's OK here because the only possible error is empty key name,
+		// but if it's empty, this piece of code won't be executed.
+		key, _ = s.NewKey(name, "")
+		return key
 	}
 	}
 	return key
 	return key
 }
 }
@@ -555,6 +619,13 @@ func Load(source interface{}, others ...interface{}) (_ *File, err error) {
 	return f, f.Reload()
 	return f, f.Reload()
 }
 }
 
 
+// Empty returns an empty file object.
+func Empty() *File {
+	// Ignore error here, we sure our data is good.
+	f, _ := Load([]byte(""))
+	return f
+}
+
 // NewSection creates a new section.
 // NewSection creates a new section.
 func (f *File) NewSection(name string) (*Section, error) {
 func (f *File) NewSection(name string) (*Section, error) {
 	if len(name) == 0 {
 	if len(name) == 0 {
@@ -575,6 +646,16 @@ func (f *File) NewSection(name string) (*Section, error) {
 	return f.sections[name], nil
 	return f.sections[name], nil
 }
 }
 
 
+// NewSections creates a list of sections.
+func (f *File) NewSections(names ...string) (err error) {
+	for _, name := range names {
+		if _, err = f.NewSection(name); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 // GetSection returns section by given name.
 // GetSection returns section by given name.
 func (f *File) GetSection(name string) (*Section, error) {
 func (f *File) GetSection(name string) (*Section, error) {
 	if len(name) == 0 {
 	if len(name) == 0 {
@@ -597,7 +678,10 @@ func (f *File) GetSection(name string) (*Section, error) {
 func (f *File) Section(name string) *Section {
 func (f *File) Section(name string) *Section {
 	sec, err := f.GetSection(name)
 	sec, err := f.GetSection(name)
 	if err != nil {
 	if err != nil {
-		return newSection(f, name)
+		// It's OK here because the only possible error is empty section name,
+		// but if it's empty, this piece of code won't be executed.
+		sec, _ = f.NewSection(name)
+		return sec
 	}
 	}
 	return sec
 	return sec
 }
 }
@@ -638,6 +722,14 @@ func (f *File) DeleteSection(name string) {
 	}
 	}
 }
 }
 
 
+func cutComment(str string) string {
+	i := strings.Index(str, "#")
+	if i == -1 {
+		return str
+	}
+	return str[:i]
+}
+
 // parse parses data through an io.Reader.
 // parse parses data through an io.Reader.
 func (f *File) parse(reader io.Reader) error {
 func (f *File) parse(reader io.Reader) error {
 	buf := bufio.NewReader(reader)
 	buf := bufio.NewReader(reader)
@@ -776,7 +868,6 @@ func (f *File) parse(reader io.Reader) error {
 				val = lineRight[qLen:] + "\n"
 				val = lineRight[qLen:] + "\n"
 				for {
 				for {
 					next, err := buf.ReadString('\n')
 					next, err := buf.ReadString('\n')
-					val += next
 					if err != nil {
 					if err != nil {
 						if err != io.EOF {
 						if err != io.EOF {
 							return err
 							return err
@@ -785,9 +876,10 @@ func (f *File) parse(reader io.Reader) error {
 					}
 					}
 					pos = strings.LastIndex(next, valQuote)
 					pos = strings.LastIndex(next, valQuote)
 					if pos > -1 {
 					if pos > -1 {
-						val = val[:len(val)-len(valQuote)-1]
+						val += next[:pos]
 						break
 						break
 					}
 					}
+					val += next
 					if isEnd {
 					if isEnd {
 						return fmt.Errorf("error parsing line: missing closing key quote from '%s' to '%s'", line, next)
 						return fmt.Errorf("error parsing line: missing closing key quote from '%s' to '%s'", line, next)
 					}
 					}
@@ -796,7 +888,7 @@ func (f *File) parse(reader io.Reader) error {
 				val = lineRight[qLen : pos+qLen]
 				val = lineRight[qLen : pos+qLen]
 			}
 			}
 		} else {
 		} else {
-			val = strings.TrimSpace(lineRight[0:])
+			val = strings.TrimSpace(cutComment(lineRight[0:]))
 		}
 		}
 
 
 		k, err := section.NewKey(kname, val)
 		k, err := section.NewKey(kname, val)

+ 45 - 6
Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go

@@ -40,13 +40,13 @@ IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s
 # Information about package author
 # Information about package author
 # Bio can be written in multiple lines.
 # Bio can be written in multiple lines.
 [author]
 [author]
-NAME = Unknwon
+NAME = Unknwon  # Succeeding comment
 E-MAIL = fake@localhost
 E-MAIL = fake@localhost
 GITHUB = https://github.com/%(NAME)s
 GITHUB = https://github.com/%(NAME)s
 BIO = """Gopher.
 BIO = """Gopher.
 Coding addict.
 Coding addict.
 Good man.
 Good man.
-"""
+"""  # Succeeding comment
 
 
 [package]
 [package]
 CLONE_URL = https://%(IMPORT_PATH)s
 CLONE_URL = https://%(IMPORT_PATH)s
@@ -62,6 +62,7 @@ UNUSED_KEY = should be deleted
 [types]
 [types]
 STRING = str
 STRING = str
 BOOL = true
 BOOL = true
+BOOL_FALSE = false
 FLOAT64 = 1.25
 FLOAT64 = 1.25
 INT = 10
 INT = 10
 TIME = 2015-01-01T20:17:05Z
 TIME = 2015-01-01T20:17:05Z
@@ -88,9 +89,7 @@ func Test_Load(t *testing.T) {
 	Convey("Load from data sources", t, func() {
 	Convey("Load from data sources", t, func() {
 
 
 		Convey("Load with empty data", func() {
 		Convey("Load with empty data", func() {
-			cfg, err := Load([]byte(""))
-			So(err, ShouldBeNil)
-			So(cfg, ShouldNotBeNil)
+			So(Empty(), ShouldNotBeNil)
 		})
 		})
 
 
 		Convey("Load with multiple data sources", func() {
 		Convey("Load with multiple data sources", func() {
@@ -203,6 +202,10 @@ func Test_Values(t *testing.T) {
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			So(v1, ShouldBeTrue)
 			So(v1, ShouldBeTrue)
 
 
+			v1, err = sec.Key("BOOL_FALSE").Bool()
+			So(err, ShouldBeNil)
+			So(v1, ShouldBeFalse)
+
 			v2, err := sec.Key("FLOAT64").Float64()
 			v2, err := sec.Key("FLOAT64").Float64()
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 			So(v2, ShouldEqual, 1.25)
 			So(v2, ShouldEqual, 1.25)
@@ -265,6 +268,30 @@ func Test_Values(t *testing.T) {
 			})
 			})
 		})
 		})
 
 
+		Convey("Get values in range", func() {
+			sec := cfg.Section("types")
+			So(sec.Key("FLOAT64").RangeFloat64(0, 1, 2), ShouldEqual, 1.25)
+			So(sec.Key("INT").RangeInt(0, 10, 20), ShouldEqual, 10)
+			So(sec.Key("INT").RangeInt64(0, 10, 20), ShouldEqual, 10)
+
+			minT, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z")
+			So(err, ShouldBeNil)
+			midT, err := time.Parse(time.RFC3339, "2013-01-01T01:00:00Z")
+			So(err, ShouldBeNil)
+			maxT, err := time.Parse(time.RFC3339, "9999-01-01T01:00:00Z")
+			So(err, ShouldBeNil)
+			t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z")
+			So(err, ShouldBeNil)
+			So(sec.Key("TIME").RangeTime(t, minT, maxT).String(), ShouldEqual, t.String())
+
+			Convey("Get value in range with default value", func() {
+				So(sec.Key("FLOAT64").RangeFloat64(5, 0, 1), ShouldEqual, 5)
+				So(sec.Key("INT").RangeInt(7, 0, 5), ShouldEqual, 7)
+				So(sec.Key("INT").RangeInt64(7, 0, 5), ShouldEqual, 7)
+				So(sec.Key("TIME").RangeTime(t, minT, midT).String(), ShouldEqual, t.String())
+			})
+		})
+
 		Convey("Get values into slice", func() {
 		Convey("Get values into slice", func() {
 			sec := cfg.Section("array")
 			sec := cfg.Section("array")
 			So(strings.Join(sec.Key("STRINGS").Strings(","), ","), ShouldEqual, "en,zh,de")
 			So(strings.Join(sec.Key("STRINGS").Strings(","), ","), ShouldEqual, "en,zh,de")
@@ -304,7 +331,7 @@ func Test_Values(t *testing.T) {
 		})
 		})
 
 
 		Convey("Get key strings", func() {
 		Convey("Get key strings", func() {
-			So(strings.Join(cfg.Section("types").KeyStrings(), ","), ShouldEqual, "STRING,BOOL,FLOAT64,INT,TIME")
+			So(strings.Join(cfg.Section("types").KeyStrings(), ","), ShouldEqual, "STRING,BOOL,BOOL_FALSE,FLOAT64,INT,TIME")
 		})
 		})
 
 
 		Convey("Delete a key", func() {
 		Convey("Delete a key", func() {
@@ -321,6 +348,14 @@ func Test_Values(t *testing.T) {
 			cfg.DeleteSection("")
 			cfg.DeleteSection("")
 			So(cfg.SectionStrings()[0], ShouldNotEqual, DEFAULT_SECTION)
 			So(cfg.SectionStrings()[0], ShouldNotEqual, DEFAULT_SECTION)
 		})
 		})
+
+		Convey("Create new sections", func() {
+			cfg.NewSections("test", "test2")
+			_, err := cfg.GetSection("test")
+			So(err, ShouldBeNil)
+			_, err = cfg.GetSection("test2")
+			So(err, ShouldBeNil)
+		})
 	})
 	})
 
 
 	Convey("Test getting and setting bad values", t, func() {
 	Convey("Test getting and setting bad values", t, func() {
@@ -340,6 +375,10 @@ func Test_Values(t *testing.T) {
 			So(s, ShouldBeNil)
 			So(s, ShouldBeNil)
 		})
 		})
 
 
+		Convey("Create new sections with empty name", func() {
+			So(cfg.NewSections(""), ShouldNotBeNil)
+		})
+
 		Convey("Get section that not exists", func() {
 		Convey("Get section that not exists", func() {
 			s, err := cfg.GetSection("404")
 			s, err := cfg.GetSection("404")
 			So(err, ShouldNotBeNil)
 			So(err, ShouldNotBeNil)

+ 12 - 6
Godeps/_workspace/src/gopkg.in/ini.v1/struct.go

@@ -29,7 +29,7 @@ type NameMapper func(string) string
 var (
 var (
 	// AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE.
 	// AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE.
 	AllCapsUnderscore NameMapper = func(raw string) string {
 	AllCapsUnderscore NameMapper = func(raw string) string {
-		newstr := make([]rune, 0, 10)
+		newstr := make([]rune, 0, len(raw))
 		for i, chr := range raw {
 		for i, chr := range raw {
 			if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
 			if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
 				if i > 0 {
 				if i > 0 {
@@ -42,7 +42,7 @@ var (
 	}
 	}
 	// TitleUnderscore converts to format title_underscore.
 	// TitleUnderscore converts to format title_underscore.
 	TitleUnderscore NameMapper = func(raw string) string {
 	TitleUnderscore NameMapper = func(raw string) string {
-		newstr := make([]rune, 0, 10)
+		newstr := make([]rune, 0, len(raw))
 		for i, chr := range raw {
 		for i, chr := range raw {
 			if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
 			if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
 				if i > 0 {
 				if i > 0 {
@@ -75,32 +75,38 @@ func parseDelim(actual string) string {
 
 
 var reflectTime = reflect.TypeOf(time.Now()).Kind()
 var reflectTime = reflect.TypeOf(time.Now()).Kind()
 
 
+// setWithProperType sets proper value to field based on its type,
+// but it does not return error for failing parsing,
+// because we want to use default value that is already assigned to strcut.
 func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim string) error {
 func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim string) error {
 	switch kind {
 	switch kind {
 	case reflect.String:
 	case reflect.String:
+		if len(key.String()) == 0 {
+			return nil
+		}
 		field.SetString(key.String())
 		field.SetString(key.String())
 	case reflect.Bool:
 	case reflect.Bool:
 		boolVal, err := key.Bool()
 		boolVal, err := key.Bool()
 		if err != nil {
 		if err != nil {
-			return err
+			return nil
 		}
 		}
 		field.SetBool(boolVal)
 		field.SetBool(boolVal)
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
 		intVal, err := key.Int64()
 		intVal, err := key.Int64()
 		if err != nil {
 		if err != nil {
-			return err
+			return nil
 		}
 		}
 		field.SetInt(intVal)
 		field.SetInt(intVal)
 	case reflect.Float64:
 	case reflect.Float64:
 		floatVal, err := key.Float64()
 		floatVal, err := key.Float64()
 		if err != nil {
 		if err != nil {
-			return err
+			return nil
 		}
 		}
 		field.SetFloat(floatVal)
 		field.SetFloat(floatVal)
 	case reflectTime:
 	case reflectTime:
 		timeVal, err := key.Time()
 		timeVal, err := key.Time()
 		if err != nil {
 		if err != nil {
-			return err
+			return nil
 		}
 		}
 		field.Set(reflect.ValueOf(timeVal))
 		field.Set(reflect.ValueOf(timeVal))
 	case reflect.Slice:
 	case reflect.Slice:

+ 21 - 23
Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go

@@ -78,27 +78,17 @@ type unsupport4 struct {
 	*unsupport3 `ini:"Others"`
 	*unsupport3 `ini:"Others"`
 }
 }
 
 
-type invalidInt struct {
-	Age int
-}
-
-type invalidBool struct {
-	Male bool
-}
-
-type invalidFloat struct {
-	Money float64
-}
-
-type invalidTime struct {
-	Born time.Time
-}
-
-type emptySlice struct {
+type defaultValue struct {
+	Name   string
+	Age    int
+	Male   bool
+	Money  float64
+	Born   time.Time
 	Cities []string
 	Cities []string
 }
 }
 
 
 const _INVALID_DATA_CONF_STRUCT = `
 const _INVALID_DATA_CONF_STRUCT = `
+Name = 
 Age = age
 Age = age
 Male = 123
 Male = 123
 Money = money
 Money = money
@@ -154,12 +144,20 @@ func Test_Struct(t *testing.T) {
 		So(MapTo(&testStruct{}, "hi"), ShouldNotBeNil)
 		So(MapTo(&testStruct{}, "hi"), ShouldNotBeNil)
 	})
 	})
 
 
-	Convey("Map to wrong types", t, func() {
-		So(MapTo(&invalidInt{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldNotBeNil)
-		So(MapTo(&invalidBool{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldNotBeNil)
-		So(MapTo(&invalidFloat{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldNotBeNil)
-		So(MapTo(&invalidTime{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldNotBeNil)
-		So(MapTo(&emptySlice{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldBeNil)
+	Convey("Map to wrong types and gain default values", t, func() {
+		cfg, err := Load([]byte(_INVALID_DATA_CONF_STRUCT))
+		So(err, ShouldBeNil)
+
+		t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z")
+		So(err, ShouldBeNil)
+		dv := &defaultValue{"Joe", 10, true, 1.25, t, []string{"HangZhou", "Boston"}}
+		So(cfg.MapTo(dv), ShouldBeNil)
+		So(dv.Name, ShouldEqual, "Joe")
+		So(dv.Age, ShouldEqual, 10)
+		So(dv.Male, ShouldBeTrue)
+		So(dv.Money, ShouldEqual, 1.25)
+		So(dv.Born.String(), ShouldEqual, t.String())
+		So(strings.Join(dv.Cities, ","), ShouldEqual, "HangZhou,Boston")
 	})
 	})
 }
 }
 
 

+ 4 - 0
pkg/cmd/web.go

@@ -11,6 +11,7 @@ import (
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
 	"strconv"
 	"strconv"
+	"time"
 
 
 	"github.com/Unknwon/macaron"
 	"github.com/Unknwon/macaron"
 	"github.com/codegangsta/cli"
 	"github.com/codegangsta/cli"
@@ -68,6 +69,9 @@ func mapStatic(m *macaron.Macaron, dir string, prefix string) {
 		macaron.StaticOptions{
 		macaron.StaticOptions{
 			SkipLogging: true,
 			SkipLogging: true,
 			Prefix:      prefix,
 			Prefix:      prefix,
+			Expires: func() string {
+				return time.Now().UTC().Format(http.TimeFormat)
+			},
 		},
 		},
 	))
 	))
 }
 }

+ 1 - 1
pkg/services/sqlstore/migrations/apikey_mig.go

@@ -42,7 +42,7 @@ func addApiKeyMigrations(mg *Migrator) {
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
 			{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
 			{Name: "org_id", Type: DB_BigInt, Nullable: false},
 			{Name: "org_id", Type: DB_BigInt, Nullable: false},
 			{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false},
-			{Name: "key", Type: DB_Varchar, Length: 64, Nullable: false},
+			{Name: "key", Type: DB_Varchar, Length: 255, Nullable: false},
 			{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "created", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},
 			{Name: "updated", Type: DB_DateTime, Nullable: false},

+ 2 - 2
src/app/features/panel/panelDirective.js

@@ -30,8 +30,8 @@ function (angular, $, config) {
         link: function(scope, elem) {
         link: function(scope, elem) {
           var panelContainer = elem.find('.panel-container');
           var panelContainer = elem.find('.panel-container');
 
 
-          scope.$watchGroup(['fullscreen', 'panel.height', 'row.height'], function() {
-            panelContainer.css({ minHeight: scope.panel.height || scope.row.height, display: 'block' });
+          scope.$watchGroup(['fullscreen', 'height', 'panel.height', 'row.height'], function() {
+            panelContainer.css({ minHeight: scope.height || scope.panel.height || scope.row.height, display: 'block' });
             elem.toggleClass('panel-fullscreen', scope.fullscreen ? true : false);
             elem.toggleClass('panel-fullscreen', scope.fullscreen ? true : false);
           });
           });
         }
         }

+ 12 - 0
src/app/panels/graph/axisEditor.html

@@ -30,6 +30,12 @@
 					empty-to-null ng-model="panel.grid.leftMin"
 					empty-to-null ng-model="panel.grid.leftMin"
 					ng-change="render()" ng-model-onblur>
 					ng-change="render()" ng-model-onblur>
 				</li>
 				</li>
+				<li class="tight-form-item">
+					Scale type
+				</li>
+				<li>
+					<select class="input-small tight-form-input" style="width: 113px" ng-model="panel.grid.leftScale" ng-options="v as k for (k, v) in scaleTypes" ng-change="render()"></select>
+				</li>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
 					Label
 					Label
 				</li>
 				</li>
@@ -69,6 +75,12 @@
 					empty-to-null ng-model="panel.grid.rightMin"
 					empty-to-null ng-model="panel.grid.rightMin"
 					ng-change="render()" ng-model-onblur>
 					ng-change="render()" ng-model-onblur>
 				</li>
 				</li>
+				<li class="tight-form-item">
+					Scale type
+				</li>
+				<li>
+					<select class="input-small tight-form-input" style="width: 113px" ng-model="panel.grid.rightScale" ng-options="v as k for (k, v) in scaleTypes" ng-change="render()"></select>
+				</li>
 				<li class="tight-form-item">
 				<li class="tight-form-item">
 					Label
 					Label
 				</li>
 				</li>

+ 88 - 42
src/app/panels/graph/graph.js

@@ -27,6 +27,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
         var dashboard = scope.dashboard;
         var dashboard = scope.dashboard;
         var data, annotations;
         var data, annotations;
         var sortedSeries;
         var sortedSeries;
+        var graphHeight;
         var legendSideLastValue = null;
         var legendSideLastValue = null;
         scope.crosshairEmiter = false;
         scope.crosshairEmiter = false;
 
 
@@ -64,19 +65,19 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
 
 
         function setElementHeight() {
         function setElementHeight() {
           try {
           try {
-            var height = scope.height || scope.panel.height || scope.row.height;
-            if (_.isString(height)) {
-              height = parseInt(height.replace('px', ''), 10);
+            graphHeight = scope.height || scope.panel.height || scope.row.height;
+            if (_.isString(graphHeight)) {
+              graphHeight = parseInt(graphHeight.replace('px', ''), 10);
             }
             }
 
 
-            height -= 5; // padding
-            height -= scope.panel.title ? 24 : 9; // subtract panel title bar
+            graphHeight -= 5; // padding
+            graphHeight -= scope.panel.title ? 24 : 9; // subtract panel title bar
 
 
             if (scope.panel.legend.show && !scope.panel.legend.rightSide) {
             if (scope.panel.legend.show && !scope.panel.legend.rightSide) {
-              height = height - 26; // subtract one line legend
+              graphHeight = graphHeight - 26; // subtract one line legend
             }
             }
 
 
-            elem.css('height', height + 'px');
+            elem.css('height', graphHeight + 'px');
 
 
             return true;
             return true;
           } catch(e) { // IE throws errors sometimes
           } catch(e) { // IE throws errors sometimes
@@ -349,6 +350,8 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
             position: 'left',
             position: 'left',
             show: scope.panel['y-axis'],
             show: scope.panel['y-axis'],
             min: scope.panel.grid.leftMin,
             min: scope.panel.grid.leftMin,
+            index: 1,
+            scale: scope.panel.grid.leftScale,
             max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.leftMax,
             max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.leftMax,
           };
           };
 
 
@@ -356,16 +359,59 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
 
 
           if (_.findWhere(data, {yaxis: 2})) {
           if (_.findWhere(data, {yaxis: 2})) {
             var secondY = _.clone(defaults);
             var secondY = _.clone(defaults);
+            secondY.index = 2,
+            secondY.scale = scope.panel.grid.rightScale;
             secondY.position = 'right';
             secondY.position = 'right';
             secondY.min = scope.panel.grid.rightMin;
             secondY.min = scope.panel.grid.rightMin;
             secondY.max = scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.rightMax;
             secondY.max = scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.rightMax;
             options.yaxes.push(secondY);
             options.yaxes.push(secondY);
+
+            applyLogScale(options.yaxes[1], data);
             configureAxisMode(options.yaxes[1], scope.panel.y_formats[1]);
             configureAxisMode(options.yaxes[1], scope.panel.y_formats[1]);
           }
           }
 
 
+          applyLogScale(options.yaxes[0], data);
           configureAxisMode(options.yaxes[0], scope.panel.y_formats[0]);
           configureAxisMode(options.yaxes[0], scope.panel.y_formats[0]);
         }
         }
 
 
+        function applyLogScale(axis, data) {
+          if (axis.scale !== 2) {
+            return;
+          }
+
+          var series, i;
+          var max = axis.max;
+
+          if (max === null) {
+            for (i = 0; i < data.length; i++) {
+              series = data[i];
+              if (series.yaxis === axis.index) {
+                if (max < series.stats.max) {
+                  max = series.stats.max;
+                }
+              }
+            }
+
+            if (max === null) {
+              max = 10000000000;
+            }
+          }
+
+          axis.ticks = [0, 1];
+          var tick = 1;
+
+          while (true) {
+            tick = tick*10;
+            axis.ticks.push(tick);
+            if (tick > max) {
+              break;
+            }
+          }
+
+          axis.transform = function(v) { return Math.log(v+0.1); };
+          axis.inverseTransform  = function (v) { return Math.pow(10,v); };
+        }
+
         function configureAxisMode(axis, format) {
         function configureAxisMode(axis, format) {
           axis.tickFormatter = function(val, axis) {
           axis.tickFormatter = function(val, axis) {
             return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
             return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
@@ -411,44 +457,44 @@ function (angular, $, kbn, moment, _, GraphTooltip) {
           url += scope.panel['y-axis'] ? '' : '&hideYAxis=true';
           url += scope.panel['y-axis'] ? '' : '&hideYAxis=true';
 
 
           switch(scope.panel.y_formats[0]) {
           switch(scope.panel.y_formats[0]) {
-          case 'bytes':
-            url += '&yUnitSystem=binary';
-            break;
-          case 'bits':
-            url += '&yUnitSystem=binary';
-            break;
-          case 'bps':
-            url += '&yUnitSystem=si';
-            break;
-          case 'Bps':
-            url += '&yUnitSystem=si';
-            break;
-          case 'short':
-            url += '&yUnitSystem=si';
-            break;
-          case 'joule':
-            url += '&yUnitSystem=si';
-            break;
-          case 'watt':
-            url += '&yUnitSystem=si';
-            break;
-          case 'ev':
-            url += '&yUnitSystem=si';
-            break;
-          case 'none':
-            url += '&yUnitSystem=none';
-            break;
+            case 'bytes':
+              url += '&yUnitSystem=binary';
+              break;
+            case 'bits':
+              url += '&yUnitSystem=binary';
+              break;
+            case 'bps':
+              url += '&yUnitSystem=si';
+              break;
+            case 'Bps':
+              url += '&yUnitSystem=si';
+              break;
+            case 'short':
+              url += '&yUnitSystem=si';
+              break;
+            case 'joule':
+              url += '&yUnitSystem=si';
+              break;
+            case 'watt':
+              url += '&yUnitSystem=si';
+              break;
+            case 'ev':
+              url += '&yUnitSystem=si';
+              break;
+            case 'none':
+              url += '&yUnitSystem=none';
+              break;
           }
           }
 
 
           switch(scope.panel.nullPointMode) {
           switch(scope.panel.nullPointMode) {
-          case 'connected':
-            url += '&lineMode=connected';
-            break;
-          case 'null':
-            break; // graphite default lineMode
-          case 'null as zero':
-            url += "&drawNullAsZero=true";
-            break;
+            case 'connected':
+              url += '&lineMode=connected';
+              break;
+            case 'null':
+              break; // graphite default lineMode
+            case 'null as zero':
+              url += "&drawNullAsZero=true";
+              break;
           }
           }
 
 
           url += scope.panel.steppedLine ? '&lineMode=staircase' : '';
           url += scope.panel.steppedLine ? '&lineMode=staircase' : '';

+ 1 - 1
src/app/panels/graph/graph.tooltip.js

@@ -99,7 +99,7 @@ function ($) {
       var group, value, timestamp, hoverInfo, i, series, seriesHtml;
       var group, value, timestamp, hoverInfo, i, series, seriesHtml;
 
 
       if(dashboard.sharedCrosshair){
       if(dashboard.sharedCrosshair){
-        scope.appEvent('setCrosshair',  { pos: pos, scope: scope });
+        scope.appEvent('setCrosshair', { pos: pos, scope: scope });
       }
       }
 
 
       if (seriesList.length === 0) {
       if (seriesList.length === 0) {

+ 5 - 1
src/app/panels/graph/module.js

@@ -53,10 +53,12 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
       y_formats    : ['short', 'short'],
       y_formats    : ['short', 'short'],
       // grid options
       // grid options
       grid          : {
       grid          : {
+        leftScale: 1,
         leftMax: null,
         leftMax: null,
         rightMax: null,
         rightMax: null,
         leftMin: null,
         leftMin: null,
         rightMin: null,
         rightMin: null,
+        rightScale: 1,
         threshold1: null,
         threshold1: null,
         threshold2: null,
         threshold2: null,
         threshold1Color: 'rgba(216, 200, 27, 0.27)',
         threshold1Color: 'rgba(216, 200, 27, 0.27)',
@@ -95,7 +97,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
       // tooltip options
       // tooltip options
       tooltip       : {
       tooltip       : {
         value_type: 'cumulative',
         value_type: 'cumulative',
-        shared: false,
+        shared: true,
       },
       },
       // time overrides
       // time overrides
       timeFrom: null,
       timeFrom: null,
@@ -114,6 +116,8 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) {
     _.defaults($scope.panel.grid, _d.grid);
     _.defaults($scope.panel.grid, _d.grid);
     _.defaults($scope.panel.legend, _d.legend);
     _.defaults($scope.panel.legend, _d.legend);
 
 
+    $scope.scaleTypes = {'linear': 1, 'log (base 10)': 2};
+
     $scope.hiddenSeries = {};
     $scope.hiddenSeries = {};
     $scope.seriesList = [];
     $scope.seriesList = [];
     $scope.unitFormats = kbn.getUnitFormats();
     $scope.unitFormats = kbn.getUnitFormats();

+ 0 - 1
src/app/partials/dasheditor.html

@@ -31,7 +31,6 @@
 					</div>
 					</div>
 					<editor-opt-bool text="Hide controls (CTRL+H)" model="dashboard.hideControls"></editor-opt-bool>
 					<editor-opt-bool text="Hide controls (CTRL+H)" model="dashboard.hideControls"></editor-opt-bool>
           <editor-opt-bool text="Shared Crosshair (CTRL+O)" model="dashboard.sharedCrosshair"></editor-opt-bool>
           <editor-opt-bool text="Shared Crosshair (CTRL+O)" model="dashboard.sharedCrosshair"></editor-opt-bool>
-					<editor-opt-bool text="Editable" model="dashboard.editable"></editor-opt-bool>
 				</div>
 				</div>
 			</div>
 			</div>
 			<div class="editor-row">
 			<div class="editor-row">

+ 1 - 0
src/css/less/grafana.less

@@ -62,6 +62,7 @@
   .main-view-container {
   .main-view-container {
     overflow: hidden;
     overflow: hidden;
     height: 0;
     height: 0;
+    padding: 0;
     .row-control-inner {
     .row-control-inner {
       display: none;
       display: none;
     }
     }

+ 1 - 1
src/css/less/variables.dark.less

@@ -28,7 +28,7 @@
 // grafana Variables
 // grafana Variables
 // -------------------------
 // -------------------------
 @grafanaPanelBackground: 	@grayDarker;
 @grafanaPanelBackground: 	@grayDarker;
-@grafanaPanelBorder: 		solid 1px @grayDark;
+@grafanaPanelBorder: 		  solid 1px @grayDark;
 @grafanaTriggerBorder:		solid 1px #555;
 @grafanaTriggerBorder:		solid 1px #555;
 
 
 // Graphite Target Editor
 // Graphite Target Editor