diff --git a/CHANGELOG.md b/CHANGELOG.md index 23cc05810eb..0ade6b78d33 100644 --- a/CHANGELOG.md +++ b/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 #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 #452](https://github.com/grafana/grafana/issues/452). Graph: Adds logarithmic scale option (log base 10) **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) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 323c3acfa79..79b49db2f0a 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -11,7 +11,7 @@ }, { "ImportPath": "github.com/Unknwon/macaron", - "Rev": "da7cbddc50b9d33e076fb1eabff13b55c3b85fc5" + "Rev": "93de4f3fad97bf246b838f828e2348f46f21f20a" }, { "ImportPath": "github.com/codegangsta/cli", @@ -72,8 +72,8 @@ }, { "ImportPath": "gopkg.in/ini.v1", - "Comment": "v0-10-g28ad8c4", - "Rev": "28ad8c408ba20e5c86b06d64cd2cc9248f640a83" + "Comment": "v0-16-g1772191", + "Rev": "177219109c97e7920c933e21c9b25f874357b237" } ] } diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/README.md b/Godeps/_workspace/src/github.com/Unknwon/macaron/README.md index 551790a7f6a..8b201624f61 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/README.md +++ b/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 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. -##### Current version: 0.5.0 +##### Current version: 0.5.4 ## Getting Started @@ -41,7 +41,7 @@ func main() { - Handy dependency injection powered by [inject](https://github.com/codegangsta/inject). - Better router layer and less reflection make faster speed. -## Middlewares +## Middlewares 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 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 - [YouGam](http://yougam.com): Online Forum - [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 - [API Reference](https://gowalker.org/github.com/Unknwon/macaron) - [Documentation](http://macaron.gogs.io) - [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 @@ -88,4 +91,4 @@ There are already many [middlewares](https://github.com/macaron-contrib) to simp ## License -This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text. \ No newline at end of file +This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text. diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/README.md b/Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/README.md deleted file mode 100644 index b51601c90b2..00000000000 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/README.md +++ /dev/null @@ -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 -} -``` diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/bufferpool.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/bufferpool.go deleted file mode 100644 index 93981b1907d..00000000000 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/bufferpool.go +++ /dev/null @@ -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 -} diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/context.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/context.go index fdaaf9d6775..abf7ac90257 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/context.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/context.go @@ -23,9 +23,11 @@ import ( "mime/multipart" "net/http" "net/url" + "os" "path" "path/filepath" "reflect" + "strconv" "strings" "time" @@ -182,6 +184,11 @@ func (ctx *Context) Query(name string) string { 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. func (ctx *Context) QueryStrings(name string) []string { if ctx.Req.Form == nil { @@ -210,9 +217,21 @@ func (ctx *Context) QueryInt64(name string) int64 { 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. -// e.g. ctx.Params(":uid") +// e.g. ctx.Params(":uid") or ctx.Params("uid") func (ctx *Context) Params(name string) string { + if len(name) == 0 { + return "" + } + if name[0] != '*' && name[0] != ':' { + name = ":" + name + } return ctx.params[name] } @@ -242,12 +261,20 @@ func (ctx *Context) ParamsInt64(name string) int64 { 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. func (ctx *Context) GetFile(name string) (multipart.File, *multipart.FileHeader, error) { return ctx.Req.FormFile(name) } // 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{}) { cookie := http.Cookie{} 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 v, ok := others[1].(string); ok && len(v) > 0 { cookie.Path = v } - } else { - cookie.Path = "/" } - // default empty if len(others) > 2 { if v, ok := others[2].(string); ok && len(v) > 0 { cookie.Domain = v } } - // default empty if len(others) > 3 { switch v := others[3].(type) { 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 v, ok := others[4].(bool); ok && v { cookie.HttpOnly = true @@ -322,6 +344,12 @@ func (ctx *Context) GetCookieInt64(name string) int64 { 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 // 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 } +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. func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) { modtime := time.Now() @@ -377,14 +413,35 @@ func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interfa 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) } +// 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. func (ctx *Context) ServeFile(file string, names ...string) { var name string diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/context_test.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/context_test.go index e5d8dd2dc02..c4b4752e12f 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/context_test.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/context_test.go @@ -76,35 +76,53 @@ func Test_Context(t *testing.T) { }) 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, "

Hello Unknwon

") }) - resp := httptest.NewRecorder() - req, err := http.NewRequest("GET", "/html", nil) - So(err, ShouldBeNil) - m.ServeHTTP(resp, req) - So(resp.Body.String(), ShouldEqual, "

Hello Unknwon

") + 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, "

Hello Unknwon

") }) - resp = httptest.NewRecorder() - req, err = http.NewRequest("GET", "/html2", nil) - So(err, ShouldBeNil) - m.ServeHTTP(resp, req) - So(resp.Body.String(), ShouldEqual, "

Hello Unknwon

") + 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

Hello Unknwon

foot") + }) }) Convey("Parse from and query", func() { m.Get("/query", func(ctx *Context) string { var buf bytes.Buffer - buf.WriteString(ctx.Query("name") + " ") + buf.WriteString(ctx.QueryTrim("name") + " ") buf.WriteString(ctx.QueryEscape("name") + " ") buf.WriteString(com.ToStr(ctx.QueryInt("int")) + " ") buf.WriteString(com.ToStr(ctx.QueryInt64("int64")) + " ") + buf.WriteString(com.ToStr(ctx.QueryFloat64("float64")) + " ") return buf.String() }) m.Get("/query2", func(ctx *Context) string { @@ -115,10 +133,10 @@ func Test_Context(t *testing.T) { }) 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) 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() 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() { - m.Get("/:name/:int/:int64", func(ctx *Context) string { + m.Get("/:name/:int/:int64/:float64", func(ctx *Context) string { 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.ParamsEscape(":name") + " ") buf.WriteString(com.ToStr(ctx.ParamsInt(":int")) + " ") buf.WriteString(com.ToStr(ctx.ParamsInt64(":int64")) + " ") + buf.WriteString(com.ToStr(ctx.ParamsFloat64(":float64")) + " ") return buf.String() }) 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) 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() { @@ -158,26 +178,29 @@ func Test_Context(t *testing.T) { Convey("Set and get cookie", func() { 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() req, err := http.NewRequest("GET", "/set", nil) So(err, ShouldBeNil) 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 { ctx.GetCookie("404") So(ctx.GetCookieInt("uid"), ShouldEqual, 1) So(ctx.GetCookieInt64("uid"), ShouldEqual, 1) + So(ctx.GetCookieFloat64("balance"), ShouldEqual, 1.25) return ctx.GetCookie("user") }) resp = httptest.NewRecorder() req, err = http.NewRequest("GET", "/get", nil) 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) So(resp.Body.String(), ShouldEqual, "Unknwon") }) @@ -231,6 +254,39 @@ func Test_Context(t *testing.T) { 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() { m.Get("/content", func(ctx *Context) { ctx.ServeContent("content1", bytes.NewReader([]byte("Hello world!"))) diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/inject/inject_test.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/inject/inject_test.go index a6d85ef5483..748572ac2bf 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/inject/inject_test.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/inject/inject_test.go @@ -1,5 +1,5 @@ // Copyright 2013 Martini Authors -// Copyright 2014 Unknown +// Copyright 2014 Unknwon // // 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 diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/logger_test.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/logger_test.go index e4951dc28cc..c81f70237f9 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/logger_test.go +++ b/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) }) - if !isWindows { + if ColorLog { Convey("Color console output", t, func() { m := Classic() m.Get("/:code:int", func(ctx *Context) (int, string) { diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/macaron.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/macaron.go index 414e8e80b65..adbe9e3692d 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/macaron.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/macaron.go @@ -29,7 +29,7 @@ import ( "github.com/Unknwon/macaron/inject" ) -const _VERSION = "0.5.0.0116" +const _VERSION = "0.5.4.0318" func Version() string { 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. func Config() *ini.File { if cfg == nil { - return &ini.File{} + return ini.Empty() } return cfg } diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/render.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/render.go index bc13888dcc6..b0558c95742 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/render.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/render.go @@ -1,4 +1,5 @@ // Copyright 2013 Martini Authors +// Copyright 2013 oxtoacart // Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may @@ -32,10 +33,39 @@ import ( "time" "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 ( ContentType = "Content-Type" ContentLength = "Content-Length" @@ -50,7 +80,7 @@ const ( var ( // 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 helperFuncs = template.FuncMap{ @@ -392,8 +422,10 @@ func (r *TplRender) RW() http.ResponseWriter { } func (r *TplRender) JSON(status int, v interface{}) { - var result []byte - var err error + var ( + result []byte + err error + ) if r.Opt.IndentJSON { result, err = json.MarshalIndent(v, "", " ") } else { diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler.go index 8a2c5063e2d..ea1e0447646 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler.go @@ -1,5 +1,5 @@ // Copyright 2013 Martini Authors -// Copyright 2014 Unknown +// Copyright 2014 Unknwon // // 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 @@ -28,32 +28,32 @@ import ( // that are passed into this function. type ReturnHandler func(*Context, []reflect.Value) -func defaultReturnHandler() ReturnHandler { - return func(ctx *Context, vals []reflect.Value) { - rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil))) - res := rv.Interface().(http.ResponseWriter) - var responseVal reflect.Value - if len(vals) > 1 && vals[0].Kind() == reflect.Int { - res.WriteHeader(int(vals[0].Int())) - responseVal = vals[1] - } else if len(vals) > 0 { - responseVal = vals[0] - } - if canDeref(responseVal) { - responseVal = responseVal.Elem() - } - if isByteSlice(responseVal) { - res.Write(responseVal.Bytes()) - } else { - res.Write([]byte(responseVal.String())) - } - } +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 canDeref(val reflect.Value) bool { - return val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr +func defaultReturnHandler() ReturnHandler { + return func(ctx *Context, vals []reflect.Value) { + rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil))) + res := rv.Interface().(http.ResponseWriter) + var respVal reflect.Value + if len(vals) > 1 && vals[0].Kind() == reflect.Int { + res.WriteHeader(int(vals[0].Int())) + respVal = vals[1] + } else if len(vals) > 0 { + respVal = vals[0] + } + if canDeref(respVal) { + respVal = respVal.Elem() + } + if isByteSlice(respVal) { + res.Write(respVal.Bytes()) + } else { + res.Write([]byte(respVal.String())) + } + } } diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler_test.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler_test.go index 27cc9d953f0..02325b218b0 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler_test.go +++ b/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 // not use this file except in compliance with the License. You may obtain diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore b/Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore index 610ecf3242c..53b64215a3b 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore @@ -1 +1,3 @@ -testdata/conf_out.ini \ No newline at end of file +testdata/conf_out.ini +ini.sublime-project +ini.sublime-workspace diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/README.md b/Godeps/_workspace/src/gopkg.in/ini.v1/README.md index 626a6ae5c53..6d771819656 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/README.md +++ b/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") ``` +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. ```go @@ -58,7 +64,7 @@ When you're pretty sure the section exists, following code could make your life 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: @@ -117,6 +123,9 @@ val := cfg.Section("").Key("key name").String() To get value with types: ```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("FLOAT64").Float64() 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("INT").InInt(5, []int{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. +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: ```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 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. diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md b/Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md index 2dc38df4a42..c455cb67297 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md @@ -27,6 +27,12 @@ cfg, err := ini.Load([]byte("raw data"), "filename") ``` +或者从一个空白的文件开始: + +```go +cfg := ini.Empty() +``` + 当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。 ```go @@ -53,7 +59,7 @@ section, err := cfg.GetSection("") section := cfg.Section("") ``` -如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会返回一个空的分区对象。 +如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您。 创建一个分区: @@ -112,6 +118,9 @@ val := cfg.Section("").Key("key name").String() 获取其它类型的值: ```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("FLOAT64").Float64() 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("INT").InInt(5, []int{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): ```go @@ -290,6 +310,16 @@ func main() { } ``` +结构的字段怎么设置默认值呢?很简单,只要在映射之前对指定字段进行赋值就可以了。如果键未找到或者类型错误,该值不会发生改变。 + +```go +// ... +p := &Person{ + Name: "Joe", +} +// ... +``` + #### 名称映射器(Name Mapper) 为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。 diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/ini.go b/Godeps/_workspace/src/gopkg.in/ini.v1/ini.go index be45f25e4ac..6674baf0b00 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/ini.go +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/ini.go @@ -35,7 +35,7 @@ const ( // Maximum allowed depth when recursively substituing variable names. _DEPTH_VALUES = 99 - _VERSION = "1.0.1" + _VERSION = "1.2.6" ) func Version() string { @@ -144,9 +144,24 @@ func (k *Key) String() string { 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. func (k *Key) Bool() (bool, error) { - return strconv.ParseBool(k.String()) + return parseBool(k.String()) } // 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) } +// 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. func (k *Key) Strings(delim string) []string { str := k.String() @@ -440,7 +501,10 @@ func (s *Section) GetKey(name string) (*Key, error) { func (s *Section) Key(name string) *Key { key, err := s.GetKey(name) 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 } @@ -555,6 +619,13 @@ func Load(source interface{}, others ...interface{}) (_ *File, err error) { 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. func (f *File) NewSection(name string) (*Section, error) { if len(name) == 0 { @@ -575,6 +646,16 @@ func (f *File) NewSection(name string) (*Section, error) { 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. func (f *File) GetSection(name string) (*Section, error) { if len(name) == 0 { @@ -597,7 +678,10 @@ func (f *File) GetSection(name string) (*Section, error) { func (f *File) Section(name string) *Section { sec, err := f.GetSection(name) 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 } @@ -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. func (f *File) parse(reader io.Reader) error { buf := bufio.NewReader(reader) @@ -776,7 +868,6 @@ func (f *File) parse(reader io.Reader) error { val = lineRight[qLen:] + "\n" for { next, err := buf.ReadString('\n') - val += next if err != nil { if err != io.EOF { return err @@ -785,9 +876,10 @@ func (f *File) parse(reader io.Reader) error { } pos = strings.LastIndex(next, valQuote) if pos > -1 { - val = val[:len(val)-len(valQuote)-1] + val += next[:pos] break } + val += next if isEnd { 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] } } else { - val = strings.TrimSpace(lineRight[0:]) + val = strings.TrimSpace(cutComment(lineRight[0:])) } k, err := section.NewKey(kname, val) diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go b/Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go index aafe91826e5..c6daf81ca52 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go +++ b/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 # Bio can be written in multiple lines. [author] -NAME = Unknwon +NAME = Unknwon # Succeeding comment E-MAIL = fake@localhost GITHUB = https://github.com/%(NAME)s BIO = """Gopher. Coding addict. Good man. -""" +""" # Succeeding comment [package] CLONE_URL = https://%(IMPORT_PATH)s @@ -62,6 +62,7 @@ UNUSED_KEY = should be deleted [types] STRING = str BOOL = true +BOOL_FALSE = false FLOAT64 = 1.25 INT = 10 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 with empty data", func() { - cfg, err := Load([]byte("")) - So(err, ShouldBeNil) - So(cfg, ShouldNotBeNil) + So(Empty(), ShouldNotBeNil) }) Convey("Load with multiple data sources", func() { @@ -203,6 +202,10 @@ func Test_Values(t *testing.T) { So(err, ShouldBeNil) So(v1, ShouldBeTrue) + v1, err = sec.Key("BOOL_FALSE").Bool() + So(err, ShouldBeNil) + So(v1, ShouldBeFalse) + v2, err := sec.Key("FLOAT64").Float64() So(err, ShouldBeNil) 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() { sec := cfg.Section("array") 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() { - 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() { @@ -321,6 +348,14 @@ func Test_Values(t *testing.T) { cfg.DeleteSection("") 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() { @@ -340,6 +375,10 @@ func Test_Values(t *testing.T) { So(s, ShouldBeNil) }) + Convey("Create new sections with empty name", func() { + So(cfg.NewSections(""), ShouldNotBeNil) + }) + Convey("Get section that not exists", func() { s, err := cfg.GetSection("404") So(err, ShouldNotBeNil) diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/struct.go b/Godeps/_workspace/src/gopkg.in/ini.v1/struct.go index 1875faf7695..09ea816442b 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/struct.go +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/struct.go @@ -29,7 +29,7 @@ type NameMapper func(string) string var ( // AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE. AllCapsUnderscore NameMapper = func(raw string) string { - newstr := make([]rune, 0, 10) + newstr := make([]rune, 0, len(raw)) for i, chr := range raw { if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { if i > 0 { @@ -42,7 +42,7 @@ var ( } // TitleUnderscore converts to format title_underscore. TitleUnderscore NameMapper = func(raw string) string { - newstr := make([]rune, 0, 10) + newstr := make([]rune, 0, len(raw)) for i, chr := range raw { if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { if i > 0 { @@ -75,32 +75,38 @@ func parseDelim(actual string) string { 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 { switch kind { case reflect.String: + if len(key.String()) == 0 { + return nil + } field.SetString(key.String()) case reflect.Bool: boolVal, err := key.Bool() if err != nil { - return err + return nil } field.SetBool(boolVal) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: intVal, err := key.Int64() if err != nil { - return err + return nil } field.SetInt(intVal) case reflect.Float64: floatVal, err := key.Float64() if err != nil { - return err + return nil } field.SetFloat(floatVal) case reflectTime: timeVal, err := key.Time() if err != nil { - return err + return nil } field.Set(reflect.ValueOf(timeVal)) case reflect.Slice: diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go b/Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go index 97cf7452705..f6fad196ae0 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go @@ -78,27 +78,17 @@ type unsupport4 struct { *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 } const _INVALID_DATA_CONF_STRUCT = ` +Name = Age = age Male = 123 Money = money @@ -154,12 +144,20 @@ func Test_Struct(t *testing.T) { 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") }) } diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index d8f8295d874..6619e5b1e0e 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -11,6 +11,7 @@ import ( "path" "path/filepath" "strconv" + "time" "github.com/Unknwon/macaron" "github.com/codegangsta/cli" @@ -68,6 +69,9 @@ func mapStatic(m *macaron.Macaron, dir string, prefix string) { macaron.StaticOptions{ SkipLogging: true, Prefix: prefix, + Expires: func() string { + return time.Now().UTC().Format(http.TimeFormat) + }, }, )) } diff --git a/pkg/services/sqlstore/migrations/apikey_mig.go b/pkg/services/sqlstore/migrations/apikey_mig.go index e25b57c77ef..a02e759f551 100644 --- a/pkg/services/sqlstore/migrations/apikey_mig.go +++ b/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: "org_id", Type: DB_BigInt, 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: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, diff --git a/src/app/features/panel/panelDirective.js b/src/app/features/panel/panelDirective.js index f8048d53637..7330bb627de 100644 --- a/src/app/features/panel/panelDirective.js +++ b/src/app/features/panel/panelDirective.js @@ -30,8 +30,8 @@ function (angular, $, config) { link: function(scope, elem) { 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); }); } diff --git a/src/app/panels/graph/axisEditor.html b/src/app/panels/graph/axisEditor.html index edf61a04cd3..40391995fc4 100644 --- a/src/app/panels/graph/axisEditor.html +++ b/src/app/panels/graph/axisEditor.html @@ -30,6 +30,12 @@ empty-to-null ng-model="panel.grid.leftMin" ng-change="render()" ng-model-onblur> +
  • + Scale type +
  • +
  • + +
  • Label
  • @@ -69,6 +75,12 @@ empty-to-null ng-model="panel.grid.rightMin" ng-change="render()" ng-model-onblur> +
  • + Scale type +
  • +
  • + +
  • Label
  • diff --git a/src/app/panels/graph/graph.js b/src/app/panels/graph/graph.js index 967991e5da8..5e2066466ca 100755 --- a/src/app/panels/graph/graph.js +++ b/src/app/panels/graph/graph.js @@ -27,6 +27,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) { var dashboard = scope.dashboard; var data, annotations; var sortedSeries; + var graphHeight; var legendSideLastValue = null; scope.crosshairEmiter = false; @@ -64,19 +65,19 @@ function (angular, $, kbn, moment, _, GraphTooltip) { function setElementHeight() { 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) { - 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; } catch(e) { // IE throws errors sometimes @@ -349,6 +350,8 @@ function (angular, $, kbn, moment, _, GraphTooltip) { position: 'left', show: scope.panel['y-axis'], min: scope.panel.grid.leftMin, + index: 1, + scale: scope.panel.grid.leftScale, 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})) { var secondY = _.clone(defaults); + secondY.index = 2, + secondY.scale = scope.panel.grid.rightScale; secondY.position = 'right'; secondY.min = scope.panel.grid.rightMin; secondY.max = scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.rightMax; options.yaxes.push(secondY); + + applyLogScale(options.yaxes[1], data); configureAxisMode(options.yaxes[1], scope.panel.y_formats[1]); } + applyLogScale(options.yaxes[0], data); 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) { axis.tickFormatter = function(val, axis) { 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'; 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) { - 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' : ''; diff --git a/src/app/panels/graph/graph.tooltip.js b/src/app/panels/graph/graph.tooltip.js index d4690bea96d..724588c4fd5 100644 --- a/src/app/panels/graph/graph.tooltip.js +++ b/src/app/panels/graph/graph.tooltip.js @@ -99,7 +99,7 @@ function ($) { var group, value, timestamp, hoverInfo, i, series, seriesHtml; if(dashboard.sharedCrosshair){ - scope.appEvent('setCrosshair', { pos: pos, scope: scope }); + scope.appEvent('setCrosshair', { pos: pos, scope: scope }); } if (seriesList.length === 0) { diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index d14d30fd300..485e7593b73 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -53,10 +53,12 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { y_formats : ['short', 'short'], // grid options grid : { + leftScale: 1, leftMax: null, rightMax: null, leftMin: null, rightMin: null, + rightScale: 1, threshold1: null, threshold2: null, threshold1Color: 'rgba(216, 200, 27, 0.27)', @@ -95,7 +97,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { // tooltip options tooltip : { value_type: 'cumulative', - shared: false, + shared: true, }, // time overrides timeFrom: null, @@ -114,6 +116,8 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { _.defaults($scope.panel.grid, _d.grid); _.defaults($scope.panel.legend, _d.legend); + $scope.scaleTypes = {'linear': 1, 'log (base 10)': 2}; + $scope.hiddenSeries = {}; $scope.seriesList = []; $scope.unitFormats = kbn.getUnitFormats(); diff --git a/src/app/partials/dasheditor.html b/src/app/partials/dasheditor.html index a9de9af2acb..e6ab6070f9f 100644 --- a/src/app/partials/dasheditor.html +++ b/src/app/partials/dasheditor.html @@ -31,7 +31,6 @@ -
    diff --git a/src/css/less/grafana.less b/src/css/less/grafana.less index b6c127c5319..579415f17f8 100644 --- a/src/css/less/grafana.less +++ b/src/css/less/grafana.less @@ -62,6 +62,7 @@ .main-view-container { overflow: hidden; height: 0; + padding: 0; .row-control-inner { display: none; } diff --git a/src/css/less/variables.dark.less b/src/css/less/variables.dark.less index 6aab758ad82..9a7613ff311 100644 --- a/src/css/less/variables.dark.less +++ b/src/css/less/variables.dark.less @@ -28,7 +28,7 @@ // grafana Variables // ------------------------- @grafanaPanelBackground: @grayDarker; -@grafanaPanelBorder: solid 1px @grayDark; +@grafanaPanelBorder: solid 1px @grayDark; @grafanaTriggerBorder: solid 1px #555; // Graphite Target Editor