├── public ├── img │ ├── luban.png │ └── luban-brand.png ├── assets │ ├── font-awesome-4.5.0 │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ └── css │ │ │ └── font-awesome.min.css │ ├── AdminLTE-2.3.11 │ │ ├── css │ │ │ └── skins │ │ │ │ └── skin-black-light.min.css │ │ └── js │ │ │ └── app.min.js │ └── bootstrap-3.3.5 │ │ └── js │ │ └── bootstrap.min.js └── plugins │ └── select2 │ └── select2.min.css ├── Makefile ├── .gitignore ├── .bra.toml ├── templates ├── base │ ├── alert.tmpl │ ├── footer.tmpl │ └── head.tmpl ├── status │ ├── 404.tmpl │ └── 500.tmpl ├── builder │ ├── new.tmpl │ ├── list.tmpl │ └── edit.tmpl ├── dashboard.tmpl └── task │ ├── new_batch.tmpl │ ├── list.tmpl │ ├── new.tmpl │ └── view.tmpl ├── conf └── app.ini ├── README.md ├── pkg ├── context │ ├── auth.go │ └── context.go ├── form │ ├── builder.go │ ├── task.go │ └── form.go ├── setting │ ├── batch.go │ ├── matrix.go │ └── setting.go ├── template │ └── template.go └── tool │ └── tool.go ├── routes ├── dashboard.go ├── task.go ├── builder.go └── builder_api.go ├── models ├── error.go ├── matrix.go ├── models.go ├── user.go ├── builder.go └── task.go ├── luban.go └── LICENSE /public/img/luban.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubanstudio/luban/HEAD/public/img/luban.png -------------------------------------------------------------------------------- /public/img/luban-brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubanstudio/luban/HEAD/public/img/luban-brand.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build-dev: 2 | go install -v 3 | mv $(GOPATH)/bin/luban . 4 | 5 | clean: 6 | go clean -i ./... 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | luban.sublime-project 2 | luban.sublime-workspace 3 | luban 4 | custom 5 | data 6 | /log 7 | /.idea 8 | -------------------------------------------------------------------------------- /public/assets/font-awesome-4.5.0/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubanstudio/luban/HEAD/public/assets/font-awesome-4.5.0/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /public/assets/font-awesome-4.5.0/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubanstudio/luban/HEAD/public/assets/font-awesome-4.5.0/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/assets/font-awesome-4.5.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubanstudio/luban/HEAD/public/assets/font-awesome-4.5.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/assets/font-awesome-4.5.0/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubanstudio/luban/HEAD/public/assets/font-awesome-4.5.0/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/assets/font-awesome-4.5.0/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lubanstudio/luban/HEAD/public/assets/font-awesome-4.5.0/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /.bra.toml: -------------------------------------------------------------------------------- 1 | [run] 2 | init_cmds = [ 3 | ["make", "build-dev"], 4 | ["./luban"] 5 | ] 6 | watch_all = true 7 | watch_dirs = [ 8 | "models", 9 | "pkg", 10 | "routes" 11 | ] 12 | watch_exts = [".go"] 13 | cmds = [ 14 | ["make", "build-dev"], 15 | ["./luban"] 16 | ] -------------------------------------------------------------------------------- /templates/base/alert.tmpl: -------------------------------------------------------------------------------- 1 | {{if .Flash.ErrorMsg}} 2 |
3 | 4 | {{if .FlashTitle}}

{{.FlashTitle}}

{{end}} 5 | {{.Flash.ErrorMsg}} 6 |
7 | {{end}} -------------------------------------------------------------------------------- /conf/app.ini: -------------------------------------------------------------------------------- 1 | RUN_MODE = dev 2 | HTTP_PORT = 8086 3 | ARTIFACTS_PATH = data/artifacts 4 | 5 | [database] 6 | NAME = luban 7 | USER = root 8 | 9 | [oauth2] 10 | CLIENT_ID = 11 | CLIENT_SECRET = 12 | 13 | [project] 14 | NAME = 15 | CLONE_URL = 16 | COMMIT_URL = 17 | IMPORT_PATH = 18 | BRANCHES = 19 | PACK_ROOT = 20 | PACK_ENTRIES = 21 | PACK_FORMATS = -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](public/img/luban-brand.png) 2 | 3 | Luban is an on-demand building tasks dispatcher for Go. 4 | 5 | ## Purpose 6 | 7 | Go already supports cross-compilation but no luck if you uses CGO. This project aims to solve CGO problem by delegating build tasks to any available machines that supports native compilation with given OS, Arch and build tags. 8 | 9 | The machine does not have to be owned by you, which means anyone who is interesting on providing free CPU resources can take the build task and contribute to the final artifacts. -------------------------------------------------------------------------------- /templates/status/404.tmpl: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |
4 |

404

5 | 6 |
7 |
8 |

Oops! Page not found.

9 |

10 | We could not find the page you were looking for. 11 | Meanwhile, you may return to dashboard or try using the search form. 12 |

13 |
14 |
15 |
16 | {{template "base/footer" .}} -------------------------------------------------------------------------------- /templates/status/500.tmpl: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |
4 |

500

5 | 6 |
7 |
8 |

Oops! Something went wrong.

9 |

10 | We will work on fixing that right away. 11 | Meanwhile, you may return to dashboard or try using the search form. 12 |

13 | 14 |
15 |
16 |
17 | {{template "base/footer" .}} -------------------------------------------------------------------------------- /templates/base/footer.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /pkg/context/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package context 16 | 17 | import ( 18 | "gopkg.in/macaron.v1" 19 | ) 20 | 21 | func ReqAdmin() macaron.Handler { 22 | return func(ctx *Context) { 23 | if !ctx.User.IsAdmin { 24 | ctx.NotFound() 25 | return 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pkg/form/builder.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package form 16 | 17 | import ( 18 | "github.com/go-macaron/binding" 19 | "gopkg.in/macaron.v1" 20 | ) 21 | 22 | type NewBuilder struct { 23 | Name string `binding:"Required"` 24 | TrustLevel int 25 | } 26 | 27 | func (f *NewBuilder) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { 28 | return Validate(errs, ctx.Data, f) 29 | } 30 | -------------------------------------------------------------------------------- /routes/dashboard.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package routes 16 | 17 | import ( 18 | "github.com/lubanstudio/luban/models" 19 | "github.com/lubanstudio/luban/pkg/context" 20 | ) 21 | 22 | func Dashboard(ctx *context.Context) { 23 | ctx.Data["Title"] = "Dashboard" 24 | ctx.Data["PageIsDashboard"] = true 25 | 26 | ctx.Data["NumBuilders"] = models.CountBuilders() 27 | ctx.Data["NumTasks"] = models.CountTasks() 28 | 29 | ctx.HTML(200, "dashboard") 30 | } 31 | -------------------------------------------------------------------------------- /templates/builder/new.tmpl: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |

4 | Builders 5 |

6 |
7 |
8 |
9 |
10 |
11 |
12 |

New Builder

13 |
14 |
15 |
16 | {{template "base/alert" .}} 17 |
18 | 19 | 20 |
21 |
22 | 23 | 26 |
27 |
28 |
29 |
30 |
31 | {{template "base/footer" .}} -------------------------------------------------------------------------------- /pkg/form/task.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package form 16 | 17 | import ( 18 | "github.com/go-macaron/binding" 19 | "gopkg.in/macaron.v1" 20 | ) 21 | 22 | type NewTask struct { 23 | OS string `form:"os" binding:"Required"` 24 | Arch string `binding:"Required"` 25 | Tags []string 26 | Branch string `binding:"Required"` 27 | } 28 | 29 | func (f *NewTask) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { 30 | return Validate(errs, ctx.Data, f) 31 | } 32 | -------------------------------------------------------------------------------- /templates/dashboard.tmpl: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |

4 | Dashboard 5 |

6 |
7 |
8 |
9 |
10 |
11 |
12 |

{{.NumBuilders}}

13 | 14 |

Builders

15 |
16 |
17 | 18 |
19 | 20 | More info 21 | 22 |
23 |
24 |
25 |
26 |
27 |

{{.NumTasks}}

28 | 29 |

Build Tasks

30 |
31 |
32 | 33 |
34 | 35 | More info 36 | 37 |
38 |
39 |
40 |
41 | {{template "base/footer" .}} -------------------------------------------------------------------------------- /templates/task/new_batch.tmpl: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |

4 | Build Tasks 5 |

6 |
7 |
8 |
9 |
10 |
11 |
12 |

New Batch Tasks

13 |
14 |
15 |
16 | {{template "base/alert" .}} 17 |
18 | 19 | 24 |
25 |
26 | 27 | 30 |
31 |
32 |
33 |
34 |
35 | {{template "base/footer" .}} -------------------------------------------------------------------------------- /pkg/setting/batch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package setting 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "io/ioutil" 21 | "sort" 22 | ) 23 | 24 | var BatchTasks []*BatchTask 25 | 26 | type BatchTask struct { 27 | OS string `json:"os"` 28 | Arch string `json:"arch"` 29 | Tags []string `json:"tags"` 30 | } 31 | 32 | func loadBatchJobs() error { 33 | data, err := ioutil.ReadFile("custom/batch.json") 34 | if err != nil { 35 | return fmt.Errorf("ReadFile: %v", err) 36 | } 37 | 38 | if err = json.Unmarshal(data, &BatchTasks); err != nil { 39 | return fmt.Errorf("Unmarshal: %v", err) 40 | } 41 | 42 | for i := range BatchTasks { 43 | sort.Strings(BatchTasks[i].Tags) 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/template/template.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package template 16 | 17 | import ( 18 | "html/template" 19 | "time" 20 | 21 | "github.com/dustin/go-humanize" 22 | 23 | "github.com/lubanstudio/luban/pkg/setting" 24 | ) 25 | 26 | func NewFuncMap() []template.FuncMap { 27 | return []template.FuncMap{map[string]interface{}{ 28 | "AppVer": func() string { 29 | return setting.AppVer 30 | }, 31 | "DateFmtShort": func(t time.Time) string { 32 | return t.Format("Jan 02, 2006") 33 | }, 34 | "DateFmtLong": func(t time.Time) string { 35 | return t.Format(time.RFC1123Z) 36 | }, 37 | "TimeFmtShort": func(t time.Time) string { 38 | return t.Format("15:04:05") 39 | }, 40 | "NumCommas": humanize.Comma, 41 | }} 42 | } 43 | -------------------------------------------------------------------------------- /pkg/tool/tool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package tool 16 | 17 | import ( 18 | "crypto/sha1" 19 | "encoding/hex" 20 | 21 | "github.com/Unknwon/com" 22 | "github.com/satori/go.uuid" 23 | ) 24 | 25 | // EncodeSHA1 encodes string to SHA1 hex value. 26 | func EncodeSHA1(str string) string { 27 | h := sha1.New() 28 | h.Write([]byte(str)) 29 | return hex.EncodeToString(h.Sum(nil)) 30 | } 31 | 32 | // NewSecretToekn generates and returns a random secret token based on SHA1. 33 | func NewSecretToekn() string { 34 | return EncodeSHA1(uuid.NewV4().String()) 35 | } 36 | 37 | // Int64sToStrings converts a slice of int64 to a slice of string. 38 | func Int64sToStrings(ints []int64) []string { 39 | strs := make([]string, len(ints)) 40 | for i := range ints { 41 | strs[i] = com.ToStr(ints[i]) 42 | } 43 | return strs 44 | } 45 | -------------------------------------------------------------------------------- /models/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package models 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | type ErrBuilderExists struct { 23 | Name string 24 | } 25 | 26 | func IsErrBuilderExists(err error) bool { 27 | _, ok := err.(ErrBuilderExists) 28 | return ok 29 | } 30 | 31 | func (err ErrBuilderExists) Error() string { 32 | return fmt.Sprintf("Builder already exists [name: %s]", err.Name) 33 | } 34 | 35 | type ErrNoSuitableMatrix struct { 36 | OS string 37 | Arch string 38 | Tags []string 39 | } 40 | 41 | func IsErrNoSuitableMatrix(err error) bool { 42 | _, ok := err.(ErrNoSuitableMatrix) 43 | return ok 44 | } 45 | 46 | func (err ErrNoSuitableMatrix) Error() string { 47 | return fmt.Sprintf("no suitable matrix for the task [os: %s, arch: %s, tags: %s]", err.OS, err.Arch, strings.Join(err.Tags, ",")) 48 | } 49 | -------------------------------------------------------------------------------- /templates/task/list.tmpl: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |

4 | Build Tasks 5 |

6 |
7 |
8 |
9 |
10 |
11 |
12 |

Build Tasks

13 |
14 | New Task 15 | {{if .User.IsAdmin}} 16 | New Batch Tasks 17 | {{end}} 18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{range .Tasks}} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {{end}} 41 | 42 |
IDOSArchTagsStatus
{{.ID}}{{.OS}}{{.Arch}}{{if .Tags}}{{.Tags}}{{else}}{no tag}{{end}}{{.Status.ToString}}
43 |
44 |
45 |
46 |
47 |
48 | {{template "base/footer" .}} -------------------------------------------------------------------------------- /models/matrix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package models 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | type Matrix struct { 22 | ID int64 23 | BuilderID int64 24 | OS string 25 | Arch string 26 | Tags string 27 | } 28 | 29 | func UpdateBuilderMatrices(builderID int64, matrices []*Matrix) error { 30 | if err := x.Delete(new(Matrix), "builder_id = ?", builderID).Error; err != nil { 31 | return fmt.Errorf("delete old matrices: %v", err) 32 | } 33 | 34 | tx := x.Begin() 35 | defer releaseTransaction(tx) 36 | 37 | for _, matrix := range matrices { 38 | matrix.BuilderID = builderID 39 | if err := tx.Create(matrix).Error; err != nil { 40 | return fmt.Errorf("create matrix: %v", err) 41 | } 42 | } 43 | 44 | return tx.Commit().Error 45 | } 46 | 47 | func (b *Builder) UpdateMatrices(matrices []*Matrix) error { 48 | return UpdateBuilderMatrices(b.ID, matrices) 49 | } 50 | 51 | func FindMatrices(os, arch string) ([]*Matrix, error) { 52 | matrices := make([]*Matrix, 0, 10) 53 | return matrices, x.Where("os = ? AND arch= ?", os, arch).Find(&matrices).Error 54 | } 55 | -------------------------------------------------------------------------------- /templates/builder/list.tmpl: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |

4 | Builders 5 |

6 |
7 |
8 |
9 |
10 |
11 |
12 |

Builders

13 | {{if .User.IsAdmin}} 14 |
15 | New Builder 16 |
17 | {{end}} 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {{if .User.IsAdmin}} 29 | 30 | {{end}} 31 | 32 | {{range .Builders}} 33 | 34 | 35 | 36 | 37 | 38 | 39 | {{if $.User.IsAdmin}} 40 | 41 | {{end}} 42 | 43 | {{end}} 44 | 45 |
IDNameTrust LevelStatusOp.
{{.ID}}{{.Name}}{{.TrustLevel.ToString}}{{.Status}}
46 |
47 |
48 |
49 |
50 |
51 | {{template "base/footer" .}} -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package models 16 | 17 | import ( 18 | "fmt" 19 | 20 | _ "github.com/go-sql-driver/mysql" 21 | "github.com/jinzhu/gorm" 22 | log "gopkg.in/clog.v1" 23 | 24 | "github.com/lubanstudio/luban/pkg/setting" 25 | ) 26 | 27 | var x *gorm.DB 28 | 29 | func init() { 30 | var err error 31 | x, err = gorm.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=true", 32 | setting.Database.User, setting.Database.Password, setting.Database.Host, setting.Database.Name)) 33 | if err != nil { 34 | log.Fatal(4, "Fail to connect database: %s", err) 35 | } 36 | 37 | if err = x.Set("gorm:table_options", "ENGINE=InnoDB"). 38 | AutoMigrate(new(User), new(Builder), new(Matrix), new(Task)).Error; err != nil { 39 | log.Fatal(4, "Fail to auto migrate database: %s", err) 40 | } 41 | } 42 | 43 | func releaseTransaction(tx *gorm.DB) { 44 | if tx.Error != nil { 45 | tx.Rollback() 46 | } 47 | } 48 | 49 | func IsErrRecordNotFound(err error) bool { 50 | return err == gorm.ErrRecordNotFound 51 | } 52 | 53 | func Count(bean interface{}) int64 { 54 | var count int64 55 | x.Model(bean).Count(&count) 56 | return count 57 | } 58 | -------------------------------------------------------------------------------- /pkg/setting/matrix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package setting 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "io/ioutil" 21 | 22 | "github.com/Unknwon/com" 23 | ) 24 | 25 | var ( 26 | Matrices []*Matrix 27 | AllowedOSs []string 28 | AllowedArchs []string 29 | AllowedTags []string 30 | ) 31 | 32 | type Matrix struct { 33 | OS string `json:"os"` 34 | Archs []string `json:"archs"` 35 | Tags []string `json:"tags"` 36 | } 37 | 38 | func loadMatrices() error { 39 | data, err := ioutil.ReadFile("custom/matrices.json") 40 | if err != nil { 41 | return fmt.Errorf("ReadFile: %v", err) 42 | } 43 | 44 | if err = json.Unmarshal(data, &Matrices); err != nil { 45 | return fmt.Errorf("Unmarshal: %v", err) 46 | } 47 | 48 | for _, m := range Matrices { 49 | if !com.IsSliceContainsStr(AllowedOSs, m.OS) { 50 | AllowedOSs = append(AllowedOSs, m.OS) 51 | } 52 | 53 | for _, arch := range m.Archs { 54 | if !com.IsSliceContainsStr(AllowedArchs, arch) { 55 | AllowedArchs = append(AllowedArchs, arch) 56 | } 57 | } 58 | 59 | for _, tag := range m.Tags { 60 | if !com.IsSliceContainsStr(AllowedTags, tag) { 61 | AllowedTags = append(AllowedTags, tag) 62 | } 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /templates/task/new.tmpl: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |

4 | Build Tasks 5 |

6 |
7 |
8 |
9 |
10 |
11 |
12 |

New Task

13 |
14 |
15 |
16 | {{template "base/alert" .}} 17 |
18 | 19 | 24 |
25 |
26 | 27 | 32 |
33 |
34 | 35 | 38 |
39 |
40 | 41 | 46 |
47 |
48 | 49 | 52 |
53 |
54 |
55 |
56 |
57 | {{template "base/footer" .}} -------------------------------------------------------------------------------- /templates/base/head.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | {{.Title}} - Luban 19 | 20 | 21 | 22 |
23 |
24 | 28 | 29 | 46 |
47 | 48 |
49 | 63 |
64 | 65 |
66 | -------------------------------------------------------------------------------- /templates/builder/edit.tmpl: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |

4 | Builders 5 |

6 |
7 |
8 |
9 |
10 |
11 |
12 |

{{.Builder.Name}}

13 |
14 |
15 |
16 | {{template "base/alert" .}} 17 |
18 | 19 | 20 |
21 |
22 | 23 | 24 |

0=unapproved, 1=approved, 99=official

25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 35 |
36 |
37 | 38 |
39 |
40 |

Regeneeate Secret Token

41 |
42 |
43 |
Current secret token will be invalid after regenerated, make sure to update new token to your builder.
44 |
45 | 50 |
51 | 52 |
53 |
54 |

Delete Builder

55 |
56 |
57 |
All data related to this builder will be deleted permanently.
58 |
59 | 64 |
65 |
66 |
67 |
68 | {{template "base/footer" .}} -------------------------------------------------------------------------------- /routes/task.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package routes 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/lubanstudio/luban/models" 21 | "github.com/lubanstudio/luban/pkg/context" 22 | "github.com/lubanstudio/luban/pkg/form" 23 | "github.com/lubanstudio/luban/pkg/setting" 24 | ) 25 | 26 | func Tasks(c *context.Context) { 27 | c.Data["Title"] = "Tasks" 28 | 29 | tasks, err := models.ListTasks(1, 30) 30 | if err != nil { 31 | c.Handle(500, "ListTasks", err) 32 | return 33 | } 34 | c.Data["Tasks"] = tasks 35 | 36 | c.HTML(200, "task/list") 37 | } 38 | 39 | func NewTask(c *context.Context) { 40 | c.Data["Title"] = "New Task" 41 | form.AssignForm(form.NewTask{}, c.Data) 42 | c.HTML(200, "task/new") 43 | } 44 | 45 | func NewTaskPost(c *context.Context, form form.NewTask) { 46 | c.Data["Title"] = "New Task" 47 | 48 | if c.HasError() { 49 | c.HTML(200, "task/new") 50 | return 51 | } 52 | 53 | task, err := models.NewTask(c.User.ID, form.OS, form.Arch, form.Tags, form.Branch) 54 | if err != nil { 55 | if models.IsErrNoSuitableMatrix(err) { 56 | c.Data["Err_OS"] = true 57 | c.Data["Err_Arch"] = true 58 | c.Data["Err_Tags"] = true 59 | c.RenderWithErr(fmt.Sprintf("Fail to create task: %v", err), "task/new", form) 60 | } else { 61 | c.Handle(500, "NewTask", err) 62 | } 63 | return 64 | } 65 | 66 | c.Redirect(fmt.Sprintf("/tasks/%d", task.ID)) 67 | } 68 | 69 | func NewBatchTasks(c *context.Context) { 70 | c.Data["Title"] = "New Batch Tasks" 71 | c.Data["branch"] = "master" 72 | c.HTML(200, "task/new_batch") 73 | } 74 | 75 | func NewBatchTasksPost(c *context.Context) { 76 | if err := models.NewBatchTasks(c.User.ID, c.Query("branch")); err != nil { 77 | c.Flash.Error("NewBatchTasks: " + err.Error()) 78 | } 79 | c.Redirect("/tasks") 80 | } 81 | 82 | func ViewTask(c *context.Context) { 83 | c.Data["Title"] = c.Task.ID 84 | c.Data["PackFormats"] = setting.Project.PackFormats 85 | c.HTML(200, "task/view") 86 | } 87 | 88 | func ArchiveTask(c *context.Context) { 89 | if err := c.Task.Archive(); err != nil { 90 | c.RenderWithErr(fmt.Sprintf("Fail to archive task: %v", err), "task/view", nil) 91 | return 92 | } 93 | 94 | c.Redirect(fmt.Sprintf("/tasks/%d", c.Task.ID)) 95 | } 96 | -------------------------------------------------------------------------------- /templates/task/view.tmpl: -------------------------------------------------------------------------------- 1 | {{template "base/head" .}} 2 |
3 |

4 | Build Tasks 5 |

6 |
7 |
8 |
9 |
10 |
11 |
12 |

Task {{.Task.ID}}

13 |
14 |
15 |
16 | {{template "base/alert" .}} 17 |
18 | 19 | {{.Task.OS}} 20 |
21 |
22 | 23 | {{.Task.Arch}} 24 |
25 |
26 | 27 | {{if .Task.Tags}}{{.Task.Tags}}{{else}}{no tag}{{end}} 28 |
29 |
30 | 31 | {{.Task.Commit}} 32 |
33 |
34 | 35 | {{.Task.Status.ToString}} 36 |
37 |
38 | 39 | {{.Task.Poster.Username}} 40 |
41 |
42 | 43 | {{if .Task.BuilderID}}{{.Task.Builder.Name}}{{else}}{not assigned yet}{{end}} 44 |
45 |
46 | 47 | {{.Task.CreatedTime}} 48 |
49 |
50 | 51 | {{if .Task.Updated}}{{.Task.UpdatedTime}}{{else}}{never updated}{{end}} 52 |
53 | 54 | {{if eq .Task.Status 4}} 55 |
56 | 57 | {{range .PackFormats}} 58 | {{$.Task.ArtifactName .}}
59 | {{end}} 60 |
61 | 62 | {{if .User.IsAdmin}} 63 |
64 | 65 | Archive Task 66 |
67 | {{end}} 68 | {{end}} 69 |
70 |
71 |
72 |
73 |
74 |
75 | {{template "base/footer" .}} -------------------------------------------------------------------------------- /pkg/setting/setting.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package setting 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/Unknwon/com" 22 | log "gopkg.in/clog.v1" 23 | "gopkg.in/ini.v1" 24 | ) 25 | 26 | var ( 27 | AppVer string 28 | ProdMode bool 29 | 30 | HTTPPort int 31 | ArtifactsPath string 32 | 33 | Database struct { 34 | Host string 35 | Name string 36 | User string 37 | Password string 38 | } 39 | 40 | OAuth2 struct { 41 | ClientID string `ini:"CLIENT_ID"` 42 | ClientSecret string 43 | } 44 | 45 | Project struct { 46 | Name string 47 | CloneURL string `ini:"CLONE_URL"` 48 | CommitURL string `ini:"COMMIT_URL"` 49 | ImportPath string 50 | Branches []string 51 | PackRoot string 52 | PackEntries []string 53 | PackFormats []string 54 | } 55 | 56 | Cfg *ini.File 57 | ) 58 | 59 | func init() { 60 | err := log.New(log.CONSOLE, log.ConsoleConfig{}) 61 | if err != nil { 62 | fmt.Printf("Fail to create new logger: %v\n", err) 63 | os.Exit(1) 64 | } 65 | 66 | Cfg, err = ini.Load("conf/app.ini") 67 | if err != nil { 68 | log.Fatal(4, "Fail to load configuration: %s", err) 69 | } 70 | if com.IsFile("custom/app.ini") { 71 | if err = Cfg.Append("custom/app.ini"); err != nil { 72 | log.Fatal(4, "Fail to load custom configuration: %s", err) 73 | } 74 | } 75 | Cfg.NameMapper = ini.AllCapsUnderscore 76 | 77 | ProdMode = Cfg.Section("").Key("RUN_MODE").String() == "prod" 78 | if ProdMode { 79 | if err := log.New(log.FILE, log.FileConfig{ 80 | Level: log.INFO, 81 | Filename: "log/luban.log", 82 | FileRotationConfig: log.FileRotationConfig{ 83 | Rotate: true, 84 | Daily: true, 85 | MaxDays: 3, 86 | }, 87 | }); err != nil { 88 | log.Fatal(0, "Fail to create new logger: %v", err) 89 | } 90 | log.Delete(log.CONSOLE) 91 | } 92 | 93 | HTTPPort = Cfg.Section("").Key("HTTP_PORT").MustInt(8086) 94 | ArtifactsPath = Cfg.Section("").Key("ARTIFACTS_PATH").MustString("data/artifacts") 95 | 96 | if err = Cfg.Section("database").MapTo(&Database); err != nil { 97 | log.Fatal(4, "Fail to map section 'database': %v", err) 98 | } else if err = Cfg.Section("oauth2").MapTo(&OAuth2); err != nil { 99 | log.Fatal(4, "Fail to map section 'oauth2': %v", err) 100 | } else if err = Cfg.Section("project").MapTo(&Project); err != nil { 101 | log.Fatal(4, "Fail to map section 'project': %v", err) 102 | } 103 | 104 | if err = loadMatrices(); err != nil { 105 | log.Fatal(4, "loadMatrices: %v", err) 106 | } else if err = loadBatchJobs(); err != nil { 107 | log.Fatal(4, "loadBatchJobs: %v", err) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /pkg/context/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package context 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/go-macaron/oauth2" 22 | "github.com/go-macaron/session" 23 | log "gopkg.in/clog.v1" 24 | "gopkg.in/macaron.v1" 25 | 26 | "github.com/lubanstudio/luban/models" 27 | "github.com/lubanstudio/luban/pkg/form" 28 | ) 29 | 30 | type Context struct { 31 | *macaron.Context 32 | Flash *session.Flash 33 | Session session.Store 34 | 35 | User *models.User 36 | Builder *models.Builder 37 | Task *models.Task 38 | } 39 | 40 | // HasError returns true if error occurs in form validation. 41 | func (ctx *Context) HasError() bool { 42 | hasErr, ok := ctx.Data["HasError"] 43 | if !ok { 44 | return false 45 | } 46 | ctx.Data["FlashTitle"] = "Form Validation" 47 | ctx.Flash.ErrorMsg = ctx.Data["ErrorMsg"].(string) 48 | ctx.Data["Flash"] = ctx.Flash 49 | return hasErr.(bool) 50 | } 51 | 52 | // RenderWithErr used for page has form validation but need to prompt error to users. 53 | func (ctx *Context) RenderWithErr(msg string, tpl string, userForm interface{}) { 54 | if userForm != nil { 55 | form.AssignForm(userForm, ctx.Data) 56 | } 57 | ctx.Data["FlashTitle"] = "Form Validation" 58 | ctx.Flash.ErrorMsg = msg 59 | ctx.Data["Flash"] = ctx.Flash 60 | ctx.HTML(200, tpl) 61 | } 62 | 63 | func NotFound(ctx *Context) { 64 | ctx.Data["Title"] = "Page Not Found" 65 | ctx.HTML(404, "status/404") 66 | } 67 | 68 | func (ctx *Context) NotFound() { 69 | NotFound(ctx) 70 | } 71 | 72 | func (ctx *Context) Error(format string, a ...interface{}) { 73 | log.Error(3, format, a...) 74 | ctx.Context.Error(500, fmt.Sprintf(format, a...)) 75 | } 76 | 77 | // Handle handles and logs error by given status. 78 | func (ctx *Context) Handle(status int, title string, err error) { 79 | if err != nil { 80 | log.Error(4, "%s: %v", title, err) 81 | if macaron.Env != macaron.PROD { 82 | ctx.Data["ErrorMsg"] = err 83 | } 84 | } 85 | 86 | switch status { 87 | case 500: 88 | ctx.Data["Title"] = "Internal Server Error" 89 | } 90 | ctx.HTML(status, fmt.Sprintf("status/%d", status)) 91 | } 92 | 93 | func Contexter() macaron.Handler { 94 | return func(c *macaron.Context, sess session.Store, f *session.Flash, tokens oauth2.Tokens) { 95 | ctx := &Context{ 96 | Context: c, 97 | Flash: f, 98 | Session: sess, 99 | } 100 | c.Map(ctx) 101 | 102 | ctx.Data["Link"] = strings.TrimSuffix(ctx.Req.URL.Path, "/") 103 | 104 | if ctx.Session.Get(oauth2.KEY_TOKEN) != nil { 105 | user, isNew, err := models.GetOrCreateUserByOAuthID(tokens.Access()) 106 | if err != nil { 107 | ctx.Handle(500, "GetOrCreateUserByOAuthID", err) 108 | return 109 | } 110 | ctx.User = user 111 | ctx.Data["IsSigned"] = true 112 | ctx.Data["User"] = user 113 | 114 | if isNew { 115 | log.Info("New user authenticated: %s", user.Username) 116 | } else { 117 | log.Trace("Authenticated user: %s", user.Username) 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /routes/builder.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package routes 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/lubanstudio/luban/models" 21 | "github.com/lubanstudio/luban/pkg/context" 22 | "github.com/lubanstudio/luban/pkg/form" 23 | ) 24 | 25 | func Builders(ctx *context.Context) { 26 | ctx.Data["Title"] = "Builders" 27 | 28 | builders, err := models.ListBuilders() 29 | if err != nil { 30 | ctx.Handle(500, "ListBuilders", err) 31 | return 32 | } 33 | ctx.Data["Builders"] = builders 34 | 35 | ctx.HTML(200, "builder/list") 36 | } 37 | 38 | func NewBuilder(ctx *context.Context) { 39 | ctx.Data["Title"] = "New Builder" 40 | ctx.HTML(200, "builder/new") 41 | } 42 | 43 | func NewBuilderPost(ctx *context.Context, form form.NewBuilder) { 44 | ctx.Data["Title"] = "New Builder" 45 | 46 | if ctx.HasError() { 47 | ctx.HTML(200, "builder/new") 48 | return 49 | } 50 | 51 | builder, err := models.NewBuilder(form.Name) 52 | if err != nil { 53 | if models.IsErrBuilderExists(err) { 54 | ctx.Data["Err_Name"] = true 55 | ctx.RenderWithErr("Builder name has been used.", "builder/new", form) 56 | } else { 57 | ctx.Handle(500, "NewBuilder", err) 58 | } 59 | return 60 | } 61 | 62 | ctx.Redirect(fmt.Sprintf("/builders/%d/edit", builder.ID)) 63 | } 64 | 65 | func parseBuilderParams(ctx *context.Context) *models.Builder { 66 | builder, err := models.GetBuilderByID(ctx.ParamsInt64(":id")) 67 | if err != nil { 68 | if models.IsErrRecordNotFound(err) { 69 | ctx.NotFound() 70 | } else { 71 | ctx.Handle(500, "GetBuilderByID", err) 72 | } 73 | return nil 74 | } 75 | return builder 76 | } 77 | 78 | func EditBuilder(ctx *context.Context) { 79 | builder := parseBuilderParams(ctx) 80 | if ctx.Written() { 81 | return 82 | } 83 | ctx.Data["Builder"] = builder 84 | 85 | ctx.Data["Title"] = builder.Name + " - Builder" 86 | ctx.HTML(200, "builder/edit") 87 | } 88 | 89 | func EditBuilderPost(ctx *context.Context, form form.NewBuilder) { 90 | builder := parseBuilderParams(ctx) 91 | if ctx.Written() { 92 | return 93 | } 94 | ctx.Data["Builder"] = builder 95 | 96 | if ctx.HasError() { 97 | ctx.HTML(200, "builder/edit") 98 | return 99 | } 100 | 101 | builder.Name = form.Name 102 | builder.TrustLevel = models.ParseTrustLevel(form.TrustLevel) 103 | if err := builder.Save(); err != nil { 104 | if models.IsErrBuilderExists(err) { 105 | ctx.Data["Err_Name"] = true 106 | ctx.RenderWithErr("Builder name has been used.", "builder/edit", form) 107 | } else { 108 | ctx.Handle(500, "builder.Save", err) 109 | } 110 | return 111 | } 112 | 113 | ctx.Redirect(fmt.Sprintf("/builders/%d/edit", builder.ID)) 114 | } 115 | 116 | func RegenerateBuilderToken(ctx *context.Context) { 117 | if err := models.RegenerateBuilderToken(ctx.ParamsInt64(":id")); err != nil { 118 | ctx.Handle(500, "RegenerateBuilderToken", err) 119 | return 120 | } 121 | 122 | ctx.Redirect(fmt.Sprintf("/builders/%d/edit", ctx.ParamsInt64(":id"))) 123 | } 124 | 125 | func DeleteBuilder(ctx *context.Context) { 126 | if err := models.DeleteBuilderByID(ctx.ParamsInt64(":id")); err != nil { 127 | ctx.Handle(500, "DeleteBuilderByID", err) 128 | return 129 | } 130 | 131 | ctx.Redirect("/builders") 132 | } 133 | -------------------------------------------------------------------------------- /public/assets/AdminLTE-2.3.11/css/skins/skin-black-light.min.css: -------------------------------------------------------------------------------- 1 | .skin-black-light .main-header{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.skin-black-light .main-header .navbar-toggle{color:#333}.skin-black-light .main-header .navbar-brand{color:#333;border-right:1px solid #eee}.skin-black-light .main-header .navbar{background-color:#fff}.skin-black-light .main-header .navbar .nav>li>a{color:#333}.skin-black-light .main-header .navbar .nav>li>a:hover,.skin-black-light .main-header .navbar .nav>li>a:active,.skin-black-light .main-header .navbar .nav>li>a:focus,.skin-black-light .main-header .navbar .nav .open>a,.skin-black-light .main-header .navbar .nav .open>a:hover,.skin-black-light .main-header .navbar .nav .open>a:focus,.skin-black-light .main-header .navbar .nav>.active>a{background:#fff;color:#999}.skin-black-light .main-header .navbar .sidebar-toggle{color:#333}.skin-black-light .main-header .navbar .sidebar-toggle:hover{color:#999;background:#fff}.skin-black-light .main-header .navbar>.sidebar-toggle{color:#333;border-right:1px solid #eee}.skin-black-light .main-header .navbar .navbar-nav>li>a{border-right:1px solid #eee}.skin-black-light .main-header .navbar .navbar-custom-menu .navbar-nav>li>a,.skin-black-light .main-header .navbar .navbar-right>li>a{border-left:1px solid #eee;border-right-width:0}.skin-black-light .main-header>.logo{background-color:#fff;color:#333;border-bottom:0 solid transparent;border-right:1px solid #eee}.skin-black-light .main-header>.logo:hover{background-color:#fcfcfc}@media (max-width:767px){.skin-black-light .main-header>.logo{background-color:#222;color:#fff;border-bottom:0 solid transparent;border-right:none}.skin-black-light .main-header>.logo:hover{background-color:#1f1f1f}}.skin-black-light .main-header li.user-header{background-color:#222}.skin-black-light .content-header{background:transparent;box-shadow:none}.skin-black-light .wrapper,.skin-black-light .main-sidebar,.skin-black-light .left-side{background-color:#f9fafc}.skin-black-light .content-wrapper,.skin-black-light .main-footer{border-left:1px solid #d2d6de}.skin-black-light .user-panel>.info,.skin-black-light .user-panel>.info>a{color:#444}.skin-black-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-black-light .sidebar-menu>li.header{color:#848484;background:#f9fafc}.skin-black-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-black-light .sidebar-menu>li:hover>a,.skin-black-light .sidebar-menu>li.active>a{color:#000;background:#f4f4f5}.skin-black-light .sidebar-menu>li.active{border-left-color:#fff}.skin-black-light .sidebar-menu>li.active>a{font-weight:600}.skin-black-light .sidebar-menu>li>.treeview-menu{background:#f4f4f5}.skin-black-light .sidebar a{color:#444}.skin-black-light .sidebar a:hover{text-decoration:none}.skin-black-light .treeview-menu>li>a{color:#777}.skin-black-light .treeview-menu>li.active>a,.skin-black-light .treeview-menu>li>a:hover{color:#000}.skin-black-light .treeview-menu>li.active>a{font-weight:600}.skin-black-light .sidebar-form{border-radius:3px;border:1px solid #d2d6de;margin:10px 10px}.skin-black-light .sidebar-form input[type="text"],.skin-black-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px}.skin-black-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-black-light .sidebar-form input[type="text"]:focus,.skin-black-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-black-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-black-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-black-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #d2d6de}} -------------------------------------------------------------------------------- /pkg/form/form.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package form 16 | 17 | import ( 18 | "reflect" 19 | "strings" 20 | 21 | "github.com/Unknwon/com" 22 | "github.com/go-macaron/binding" 23 | ) 24 | 25 | type Form interface { 26 | binding.Validator 27 | } 28 | 29 | func init() { 30 | binding.SetNameMapper(com.ToSnakeCase) 31 | } 32 | 33 | // AssignForm assign form values back to the template data. 34 | func AssignForm(form interface{}, data map[string]interface{}) { 35 | typ := reflect.TypeOf(form) 36 | val := reflect.ValueOf(form) 37 | 38 | if typ.Kind() == reflect.Ptr { 39 | typ = typ.Elem() 40 | val = val.Elem() 41 | } 42 | 43 | for i := 0; i < typ.NumField(); i++ { 44 | field := typ.Field(i) 45 | 46 | fieldName := field.Tag.Get("form") 47 | // Allow ignored fields in the struct 48 | if fieldName == "-" { 49 | continue 50 | } else if len(fieldName) == 0 { 51 | fieldName = com.ToSnakeCase(field.Name) 52 | } 53 | 54 | data[fieldName] = val.Field(i).Interface() 55 | } 56 | } 57 | 58 | func getRuleBody(field reflect.StructField, prefix string) string { 59 | for _, rule := range strings.Split(field.Tag.Get("binding"), ";") { 60 | if strings.HasPrefix(rule, prefix) { 61 | return rule[len(prefix) : len(rule)-1] 62 | } 63 | } 64 | return "" 65 | } 66 | 67 | func GetSize(field reflect.StructField) string { 68 | return getRuleBody(field, "Size(") 69 | } 70 | 71 | func GetMinSize(field reflect.StructField) string { 72 | return getRuleBody(field, "MinSize(") 73 | } 74 | 75 | func GetMaxSize(field reflect.StructField) string { 76 | return getRuleBody(field, "MaxSize(") 77 | } 78 | 79 | func GetInclude(field reflect.StructField) string { 80 | return getRuleBody(field, "Include(") 81 | } 82 | 83 | func Validate(errs binding.Errors, data map[string]interface{}, f Form) binding.Errors { 84 | if errs.Len() == 0 { 85 | return errs 86 | } 87 | 88 | data["HasError"] = true 89 | AssignForm(f, data) 90 | 91 | typ := reflect.TypeOf(f) 92 | val := reflect.ValueOf(f) 93 | 94 | if typ.Kind() == reflect.Ptr { 95 | typ = typ.Elem() 96 | val = val.Elem() 97 | } 98 | 99 | for i := 0; i < typ.NumField(); i++ { 100 | field := typ.Field(i) 101 | 102 | fieldName := field.Tag.Get("form") 103 | // Allow ignored fields in the struct 104 | if fieldName == "-" { 105 | continue 106 | } 107 | 108 | if errs[0].FieldNames[0] == field.Name { 109 | data["Err_"+field.Name] = true 110 | 111 | name := field.Tag.Get("name") 112 | if len(name) == 0 { 113 | name = field.Name 114 | } 115 | 116 | switch errs[0].Classification { 117 | case binding.ERR_REQUIRED: 118 | data["ErrorMsg"] = name + " cannot be empty." 119 | case binding.ERR_ALPHA_DASH: 120 | data["ErrorMsg"] = name + " must be valid alpha or numeric or dash(-_) characters." 121 | case binding.ERR_ALPHA_DASH_DOT: 122 | data["ErrorMsg"] = name + " must be valid alpha or numeric or dash(-_) or dot characters." 123 | case binding.ERR_SIZE: 124 | data["ErrorMsg"] = name + " must be size " + GetSize(field) 125 | case binding.ERR_MIN_SIZE: 126 | data["ErrorMsg"] = name + " must contain at least " + GetMinSize(field) + " characters." 127 | case binding.ERR_MAX_SIZE: 128 | data["ErrorMsg"] = name + " must contain at most " + GetMaxSize(field) + " characters." 129 | case binding.ERR_EMAIL: 130 | data["ErrorMsg"] = name + " is not a valid email address." 131 | case binding.ERR_URL: 132 | data["ErrorMsg"] = name + " is not a valid URL." 133 | case binding.ERR_INCLUDE: 134 | data["ErrorMsg"] = name + " must contain substring '" + GetInclude(field) + "'." 135 | default: 136 | data["ErrorMsg"] = "Unknown error: " + errs[0].Classification 137 | } 138 | return errs 139 | } 140 | } 141 | return errs 142 | } 143 | -------------------------------------------------------------------------------- /models/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package models 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "time" 21 | 22 | "github.com/Unknwon/com" 23 | "github.com/parnurzeal/gorequest" 24 | ) 25 | 26 | type User struct { 27 | ID int64 28 | OAuthID string `gorm:"column:oauth_id;UNIQUE"` 29 | GitHubID string `gorm:"column:github_id;UNIQUE"` 30 | Username string 31 | AvatarURL string 32 | IsAdmin bool `gorm:"NOT NULL"` 33 | Created int64 34 | } 35 | 36 | func (u *User) BeforeCreate() { 37 | u.Created = time.Now().Unix() 38 | } 39 | 40 | func GetUserByID(id int64) (*User, error) { 41 | user := new(User) 42 | return user, x.Where("id = ?", id).First(user).Error 43 | } 44 | 45 | func GetUserByGitHubID(githubID string) (*User, error) { 46 | user := new(User) 47 | return user, x.Where("github_id = ?", githubID).First(user).Error 48 | } 49 | 50 | func GetUserByOAuthID(oauthID string) (*User, error) { 51 | user := new(User) 52 | return user, x.Where("oauth_id = ?", oauthID).First(user).Error 53 | } 54 | 55 | // GetOrCreateUserByGitHubID retrieves a user based on GitHub ID, 56 | // and creates a new user if does not exists. 57 | // It returns true if a new user created. 58 | func GetOrCreateUserByGitHubID(oauthID, githubID, username, avatarURL string) (*User, bool, error) { 59 | user, err := GetUserByGitHubID(githubID) 60 | if err != nil && !IsErrRecordNotFound(err) { 61 | return nil, false, fmt.Errorf("GetUserByGitHubID: %v", err) 62 | } 63 | 64 | isNew := false 65 | if IsErrRecordNotFound(err) { 66 | user.OAuthID = oauthID 67 | user.GitHubID = githubID 68 | user.Username = username 69 | user.AvatarURL = avatarURL 70 | if err = x.Create(user).Error; err != nil { 71 | return nil, false, fmt.Errorf("create new user: %v", err) 72 | } 73 | isNew = true 74 | } 75 | 76 | // Update OAuthID as needed 77 | if len(oauthID) > 0 && user.OAuthID != oauthID { 78 | user.OAuthID = oauthID 79 | user.AvatarURL = avatarURL 80 | if err = x.Save(user).Error; err != nil { 81 | return nil, false, fmt.Errorf("update user OAuthID: %v", err) 82 | } 83 | } 84 | 85 | // Make the first user be admin 86 | if Count(new(User)) == 1 { 87 | user.IsAdmin = true 88 | if err = x.Save(user).Error; err != nil { 89 | return nil, false, fmt.Errorf("set user as admin: %v", err) 90 | } 91 | } 92 | 93 | return user, isNew, nil 94 | } 95 | 96 | // GetOrCreateUserByOAuthID retrieves a user based on OAuth ID, 97 | // and creates a new user if does not exists. 98 | // It returns true if a new user created. 99 | func GetOrCreateUserByOAuthID(oauthID string) (*User, bool, error) { 100 | user, err := GetUserByOAuthID(oauthID) 101 | if err != nil && !IsErrRecordNotFound(err) { 102 | return nil, false, fmt.Errorf("GetUserByOAuthID: %v", err) 103 | } 104 | 105 | isNew := false 106 | if IsErrRecordNotFound(err) { 107 | // Fetch user info 108 | _, data, errs := gorequest.New().Get("https://api.github.com/user").Query("access_token=" + oauthID).EndBytes() 109 | if len(errs) > 0 { 110 | return nil, false, fmt.Errorf("request GitHub user info: %v", errs[0]) 111 | } 112 | infos := make(map[string]interface{}) 113 | if err = json.Unmarshal(data, &infos); err != nil { 114 | return nil, false, fmt.Errorf("decoding GitHub user info: %v", err) 115 | } 116 | if infos["id"] == nil || infos["login"] == nil { 117 | return nil, false, fmt.Errorf("'id' or 'login' not found in returned GitHub user info: %v", err) 118 | } 119 | user, isNew, err = GetOrCreateUserByGitHubID(oauthID, 120 | com.ToStr(infos["id"]), com.ToStr(infos["login"]), com.ToStr(infos["avatar_url"])) 121 | if err != nil { 122 | return nil, false, fmt.Errorf("GetOrCreateUserByGitHubID: %v", err) 123 | } 124 | } 125 | 126 | return user, isNew, nil 127 | } 128 | -------------------------------------------------------------------------------- /models/builder.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package models 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | "time" 21 | 22 | "github.com/Unknwon/com" 23 | 24 | "github.com/lubanstudio/luban/pkg/tool" 25 | ) 26 | 27 | type TrustLevel int 28 | 29 | const ( 30 | TRUST_LEVEL_UNAPPROVED TrustLevel = iota 31 | TRUST_LEVEL_APPROVED 32 | TRUST_LEVEL_OFFICIAL TrustLevel = 99 33 | ) 34 | 35 | func (l TrustLevel) ToString() string { 36 | switch l { 37 | case TRUST_LEVEL_APPROVED: 38 | return "Approved" 39 | case TRUST_LEVEL_OFFICIAL: 40 | return "Official" 41 | } 42 | return "Unapproved" 43 | } 44 | 45 | func ParseTrustLevel(n int) TrustLevel { 46 | switch n { 47 | case 1: 48 | return TRUST_LEVEL_APPROVED 49 | case 99: 50 | return TRUST_LEVEL_OFFICIAL 51 | default: 52 | return TRUST_LEVEL_UNAPPROVED 53 | } 54 | } 55 | 56 | type Builder struct { 57 | ID int64 58 | Name string 59 | Token string `gorm:"UNIQUE"` 60 | TrustLevel TrustLevel 61 | 62 | IsIdle bool `gorm:"NOT NULL"` 63 | LastHeartBeat int64 64 | Created int64 65 | 66 | TaskID int64 67 | } 68 | 69 | func (b *Builder) BeforeCreate() { 70 | b.Created = time.Now().Unix() 71 | } 72 | 73 | func (b *Builder) Status() string { 74 | if b.LastHeartBeat < time.Now().Add(-1*time.Minute).Unix() { 75 | return "Offline" 76 | } 77 | if b.IsIdle { 78 | return "Idle" 79 | } 80 | return "Busy" 81 | } 82 | 83 | func (b *Builder) CreatedTime() time.Time { 84 | return time.Unix(b.Created, 0) 85 | } 86 | 87 | // HeartBeat updates last active and status. 88 | func (b *Builder) HeartBeat(isIdle bool) error { 89 | b.LastHeartBeat = time.Now().Unix() 90 | b.IsIdle = isIdle 91 | return b.Save() 92 | } 93 | 94 | func (b *Builder) Save() error { 95 | if !IsErrRecordNotFound(x.Where("name = ? AND id != ?", b.Name, b.ID).First(new(Builder)).Error) { 96 | return ErrBuilderExists{b.Name} 97 | } 98 | return x.Save(b).Error 99 | } 100 | 101 | func NewBuilder(name string) (*Builder, error) { 102 | if !IsErrRecordNotFound(x.Where("name = ?", name).First(new(Builder)).Error) { 103 | return nil, ErrBuilderExists{name} 104 | } 105 | 106 | builder := &Builder{ 107 | Name: name, 108 | Token: tool.NewSecretToekn(), 109 | TrustLevel: 1, 110 | } 111 | return builder, x.Create(builder).Error 112 | } 113 | 114 | func GetBuilderByID(id int64) (*Builder, error) { 115 | builder := new(Builder) 116 | return builder, x.First(builder, id).Error 117 | } 118 | 119 | func GetBuilderByToken(token string) (*Builder, error) { 120 | builder := new(Builder) 121 | return builder, x.Where("token = ?", token).First(builder).Error 122 | } 123 | 124 | func ListBuilders() ([]*Builder, error) { 125 | builders := make([]*Builder, 0, 10) 126 | return builders, x.Find(&builders).Error 127 | } 128 | 129 | func CountBuilders() int64 { 130 | return Count(new(Builder)) 131 | } 132 | 133 | func RegenerateBuilderToken(id int64) error { 134 | return x.First(new(Builder), id).Update("token", tool.NewSecretToekn()).Error 135 | } 136 | 137 | // TODO: delete building history and matrices 138 | func DeleteBuilderByID(id int64) error { 139 | return x.Delete(new(Builder), id).Error 140 | } 141 | 142 | func MatchBuilders(os, arch string, tags []string) ([]int64, error) { 143 | matrices, err := FindMatrices(os, arch) 144 | if err != nil { 145 | return nil, fmt.Errorf("FindBuilders: %v", err) 146 | } 147 | if len(matrices) == 0 { 148 | return nil, ErrNoSuitableMatrix{os, arch, tags} 149 | } 150 | 151 | marked := make(map[int64]bool) 152 | builderIDs := make([]int64, 0, 5) 153 | CHECK_TAG: 154 | for _, m := range matrices { 155 | supportTags := strings.Split(m.Tags, ",") 156 | 157 | for _, tag := range tags { 158 | if !com.IsSliceContainsStr(supportTags, tag) { 159 | continue CHECK_TAG 160 | } 161 | } 162 | 163 | if !marked[m.BuilderID] { 164 | marked[m.BuilderID] = true 165 | builderIDs = append(builderIDs, m.BuilderID) 166 | } 167 | } 168 | return builderIDs, nil 169 | } 170 | -------------------------------------------------------------------------------- /luban.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "net/http" 20 | "path" 21 | 22 | "github.com/go-macaron/binding" 23 | "github.com/go-macaron/oauth2" 24 | "github.com/go-macaron/session" 25 | goauth2 "golang.org/x/oauth2" 26 | log "gopkg.in/clog.v1" 27 | "gopkg.in/macaron.v1" 28 | 29 | "github.com/lubanstudio/luban/models" 30 | "github.com/lubanstudio/luban/pkg/context" 31 | "github.com/lubanstudio/luban/pkg/form" 32 | "github.com/lubanstudio/luban/pkg/setting" 33 | "github.com/lubanstudio/luban/pkg/template" 34 | "github.com/lubanstudio/luban/routes" 35 | ) 36 | 37 | const APP_VER = "0.6.0.0610" 38 | 39 | func init() { 40 | setting.AppVer = APP_VER 41 | } 42 | 43 | func main() { 44 | log.Info("Luban %s", APP_VER) 45 | 46 | m := macaron.New() 47 | if !setting.ProdMode { 48 | m.Use(macaron.Logger()) 49 | } 50 | m.Use(macaron.Recovery()) 51 | m.Use(macaron.Static("public", macaron.StaticOptions{ 52 | SkipLogging: setting.ProdMode, 53 | })) 54 | m.Use(macaron.Renderer(macaron.RenderOptions{ 55 | Funcs: template.NewFuncMap(), 56 | IndentJSON: macaron.Env != macaron.PROD, 57 | })) 58 | m.Use(session.Sessioner(session.Options{ 59 | Provider: "file", 60 | ProviderConfig: "data/sessions", 61 | })) 62 | m.Use(oauth2.Github( 63 | &goauth2.Config{ 64 | ClientID: setting.OAuth2.ClientID, 65 | ClientSecret: setting.OAuth2.ClientSecret, 66 | }, 67 | )) 68 | m.Use(context.Contexter()) 69 | 70 | bindIgnErr := binding.BindIgnErr 71 | 72 | m.Get("/", func(ctx *macaron.Context) { ctx.Redirect("/dashboard") }) 73 | m.Group("", func() { 74 | m.Get("/dashboard", routes.Dashboard) 75 | 76 | m.Group("/tasks", func() { 77 | m.Get("", routes.Tasks) 78 | 79 | m.Group("", func() { 80 | m.Combo("/new").Get(routes.NewTask).Post(bindIgnErr(form.NewTask{}), routes.NewTaskPost) 81 | m.Combo("/new_batch", context.ReqAdmin()).Get(routes.NewBatchTasks).Post(routes.NewBatchTasksPost) 82 | 83 | m.Group("/:id", func() { 84 | m.Get("", routes.ViewTask) 85 | m.Get("/archive", context.ReqAdmin(), routes.ArchiveTask) 86 | }, func(ctx *context.Context) { 87 | task, err := models.GetTaskByID(ctx.ParamsInt64(":id")) 88 | if err != nil { 89 | if models.IsErrRecordNotFound(err) { 90 | ctx.NotFound() 91 | } else { 92 | ctx.Handle(500, "GetTaskByID", err) 93 | } 94 | return 95 | } 96 | ctx.Task = task 97 | ctx.Data["Task"] = ctx.Task 98 | }) 99 | }) 100 | }, func(ctx *context.Context) { 101 | ctx.Data["PageIsTask"] = true 102 | ctx.Data["AllowedOSs"] = setting.AllowedOSs 103 | ctx.Data["AllowedArchs"] = setting.AllowedArchs 104 | ctx.Data["AllowedTags"] = setting.AllowedTags 105 | ctx.Data["AllowedBranches"] = setting.Project.Branches 106 | }) 107 | 108 | m.Group("/builders", func() { 109 | m.Get("", routes.Builders) 110 | 111 | m.Group("", func() { 112 | m.Combo("/new").Get(routes.NewBuilder).Post(bindIgnErr(form.NewBuilder{}), routes.NewBuilderPost) 113 | 114 | m.Group("/:id", func() { 115 | m.Combo("/edit").Get(routes.EditBuilder).Post(bindIgnErr(form.NewBuilder{}), routes.EditBuilderPost) 116 | m.Post("/regenerate_token", routes.RegenerateBuilderToken) 117 | m.Post("/delete", routes.DeleteBuilder) 118 | }) 119 | }, context.ReqAdmin()) 120 | }, func(ctx *context.Context) { 121 | ctx.Data["PageIsBuilder"] = true 122 | }) 123 | 124 | }, oauth2.LoginRequired) 125 | 126 | m.Get("/artifacts/:name", func(ctx *context.Context) { 127 | http.ServeFile(ctx.Resp, ctx.Req.Request, path.Join(setting.ArtifactsPath, ctx.Params(":name"))) 128 | }) 129 | 130 | m.Group("/api/v1", func() { 131 | m.Group("/builder", func() { 132 | m.Post("/matrix", routes.UpdateMatrix) 133 | m.Post("/heartbeat", routes.HeartBeat) 134 | m.Post("/upload/artifact", routes.UploadArtifact) 135 | }, routes.RequireBuilderToken) 136 | }) 137 | 138 | m.NotFound(context.NotFound) 139 | 140 | go models.AssignTasks() 141 | 142 | listenAddr := fmt.Sprintf("0.0.0.0:%d", setting.HTTPPort) 143 | log.Info("Listening on %s", listenAddr) 144 | log.Fatal(4, "Fail to start server: %v", http.ListenAndServe(listenAddr, m)) 145 | } 146 | -------------------------------------------------------------------------------- /routes/builder_api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package routes 16 | 17 | import ( 18 | "encoding/json" 19 | "io" 20 | "os" 21 | "path" 22 | "sort" 23 | "strings" 24 | 25 | log "gopkg.in/clog.v1" 26 | 27 | "github.com/lubanstudio/luban/models" 28 | "github.com/lubanstudio/luban/pkg/context" 29 | "github.com/lubanstudio/luban/pkg/setting" 30 | ) 31 | 32 | func RequireBuilderToken(ctx *context.Context) { 33 | var err error 34 | ctx.Builder, err = models.GetBuilderByToken(ctx.Req.Header.Get("X-LUBAN-TOKEN")) 35 | if err != nil { 36 | if models.IsErrRecordNotFound(err) { 37 | ctx.Status(403) 38 | } else { 39 | ctx.Error("GetBuilderByToken: %v", err) 40 | } 41 | return 42 | } 43 | } 44 | 45 | func UpdateMatrix(ctx *context.Context) { 46 | data, err := ctx.Req.Body().Bytes() 47 | if err != nil { 48 | ctx.Error("Req.Body().Bytes: %v", err) 49 | return 50 | } 51 | 52 | rawMatrices := make([]*setting.Matrix, 0, 3) 53 | if err = json.Unmarshal(data, &rawMatrices); err != nil { 54 | ctx.Error("json.Unmarshal: %v", err) 55 | return 56 | } 57 | 58 | matrices := make([]*models.Matrix, 0, 5) 59 | for _, raw := range rawMatrices { 60 | sort.Strings(raw.Tags) 61 | for _, arch := range raw.Archs { 62 | matrices = append(matrices, &models.Matrix{ 63 | OS: raw.OS, 64 | Arch: arch, 65 | Tags: strings.Join(raw.Tags, ","), 66 | }) 67 | } 68 | } 69 | 70 | if err = ctx.Builder.UpdateMatrices(matrices); err != nil { 71 | ctx.Error("UpdateMatrices: %v", err) 72 | return 73 | } 74 | 75 | ctx.Status(204) 76 | } 77 | 78 | func HeartBeat(ctx *context.Context) { 79 | status := ctx.Req.Header.Get("X-LUBAN-STATUS") 80 | log.Trace("Hearrbeat from builder '%d': %s", ctx.Builder.ID, status) 81 | 82 | isIdle := status == "IDLE" 83 | if err := ctx.Builder.HeartBeat(isIdle && ctx.Builder.TaskID == 0); err != nil { 84 | log.Error(4, "HeartBeat [%d]: %v", ctx.Builder.ID, err) 85 | ctx.Error("HeartBeat: %v", err) 86 | return 87 | } 88 | 89 | if isIdle { 90 | // Response assgined task to builder if it's idle. 91 | if ctx.Builder.TaskID > 0 { 92 | isIdle = false 93 | task, err := models.GetTaskByID(ctx.Builder.TaskID) 94 | if err != nil { 95 | ctx.Error("GetTaskByID [%d]: %v", ctx.Builder.TaskID, err) 96 | return 97 | } 98 | 99 | ctx.Resp.Header().Set("X-LUBAN-TASK", "ASSIGN") 100 | ctx.JSON(200, map[string]interface{}{ 101 | "import_path": setting.Project.ImportPath, 102 | "pack_root": setting.Project.PackRoot, 103 | "pack_entries": setting.Project.PackEntries, 104 | "pack_formats": setting.Project.PackFormats, 105 | "task": map[string]interface{}{ 106 | "id": task.ID, 107 | "os": task.OS, 108 | "arch": task.Arch, 109 | "tags": task.Tags, 110 | "commit": task.Commit, 111 | }, 112 | }) 113 | } else { 114 | ctx.Status(204) 115 | } 116 | return 117 | } 118 | 119 | task, err := models.GetTaskByID(ctx.Builder.TaskID) 120 | if err != nil { 121 | ctx.Error("GetTaskByID [%d]: %v", ctx.Builder.TaskID, err) 122 | return 123 | } 124 | 125 | switch status { 126 | case "UPLOADING": 127 | task.Status = models.TASK_STATUS_UPLOADING 128 | if err = task.Save(); err != nil { 129 | ctx.Error("Save: %v", err) 130 | return 131 | } 132 | case "FAILED": 133 | if err = task.BuildFailed(); err != nil { 134 | ctx.Error("BuildFailed: %v", err) 135 | return 136 | } 137 | case "SUCCEED": 138 | if err = task.BuildSucceed(); err != nil { 139 | ctx.Error("BuildSucceed: %v", err) 140 | return 141 | } 142 | } 143 | 144 | ctx.Status(204) 145 | } 146 | 147 | func UploadArtifact(ctx *context.Context) { 148 | log.Trace("Receiving artifact from builder '%d' for task '%d'", ctx.Builder.ID, ctx.Builder.TaskID) 149 | 150 | task, err := models.GetTaskByID(ctx.Builder.TaskID) 151 | if err != nil { 152 | ctx.Error("GetTaskByID: %v", err) 153 | return 154 | } 155 | 156 | if err = ctx.Req.ParseMultipartForm(1024 * 1024 * 32); err != nil { 157 | ctx.Error("ParseMultipartForm: %v", err) 158 | return 159 | } 160 | 161 | savePath := path.Join("data/artifacts", task.ArtifactName(ctx.Req.Header.Get("X-LUBAN-FORMAT"))) 162 | os.MkdirAll(path.Dir(savePath), os.ModePerm) 163 | 164 | fw, err := os.Create(savePath) 165 | if err != nil { 166 | ctx.Error("Create: %v", err) 167 | return 168 | } 169 | defer fw.Close() 170 | 171 | fr, _, err := ctx.Req.FormFile("artifact") 172 | if err != nil { 173 | ctx.Error("FormFile: %v", err) 174 | return 175 | } 176 | defer fr.Close() 177 | 178 | if _, err = io.Copy(fw, fr); err != nil { 179 | ctx.Error("Copy: %v", err) 180 | return 181 | } 182 | 183 | ctx.Status(204) 184 | } 185 | -------------------------------------------------------------------------------- /models/task.go: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Unknwon 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package models 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "path" 21 | "sort" 22 | "strings" 23 | "time" 24 | 25 | "github.com/Unknwon/com" 26 | log "gopkg.in/clog.v1" 27 | 28 | "github.com/lubanstudio/luban/pkg/setting" 29 | "github.com/lubanstudio/luban/pkg/tool" 30 | ) 31 | 32 | type TaskStatus int 33 | 34 | const ( 35 | TASK_STATUS_PENDING TaskStatus = iota 36 | TASK_STATUS_BUILDING 37 | TASK_STATUS_UPLOADING 38 | TASK_STATUS_FAILED 39 | TASK_STATUS_SUCCEED 40 | TASK_STATUS_ARCHIVED TaskStatus = 99 41 | ) 42 | 43 | func (s TaskStatus) ToString() string { 44 | switch s { 45 | case TASK_STATUS_PENDING: 46 | return "Pending" 47 | case TASK_STATUS_BUILDING: 48 | return "Building" 49 | case TASK_STATUS_UPLOADING: 50 | return "Uploading" 51 | case TASK_STATUS_FAILED: 52 | return "Failed" 53 | case TASK_STATUS_SUCCEED: 54 | return "Succeed" 55 | case TASK_STATUS_ARCHIVED: 56 | return "Archived" 57 | } 58 | return "Unapproved" 59 | } 60 | 61 | type Task struct { 62 | ID int64 63 | OS string 64 | Arch string 65 | Tags string 66 | Commit string 67 | Status TaskStatus 68 | 69 | PosterID int64 70 | Poster *User `gorm:"-"` 71 | BuilderID int64 72 | Builder *Builder `gorm:"-"` 73 | Updated int64 74 | Created int64 75 | } 76 | 77 | func (t *Task) BeforeCreate() { 78 | t.Created = time.Now().Unix() 79 | } 80 | 81 | func (t *Task) AfterFind() (err error) { 82 | if t.PosterID > 0 { 83 | t.Poster, err = GetUserByID(t.PosterID) 84 | if err != nil { 85 | return fmt.Errorf("GetUserByID [%d]: %v", t.PosterID, err) 86 | } 87 | } 88 | 89 | if t.BuilderID > 0 { 90 | t.Builder, err = GetBuilderByID(t.BuilderID) 91 | if err != nil { 92 | return fmt.Errorf("GetBuilderByID [%d]: %v", t.BuilderID, err) 93 | } 94 | } 95 | return nil 96 | } 97 | 98 | func (t *Task) UpdatedTime() time.Time { 99 | return time.Unix(t.Updated, 0) 100 | } 101 | 102 | func (t *Task) CreatedTime() time.Time { 103 | return time.Unix(t.Created, 0) 104 | } 105 | 106 | func (t *Task) CommitURL() string { 107 | return com.Expand(setting.Project.CommitURL, map[string]string{"sha": t.Commit}) 108 | } 109 | 110 | func (t *Task) ArtifactName(format string) string { 111 | name := setting.Project.PackRoot + "_" + t.Commit[:10] + "_" + t.OS + "_" + t.Arch 112 | if len(t.Tags) > 0 { 113 | name += "_" + strings.Replace(t.Tags, ",", "_", -1) 114 | } 115 | return name + "." + format 116 | } 117 | 118 | func (t *Task) Save() error { 119 | return x.Save(t).Error 120 | } 121 | 122 | func (t *Task) AssignBuilder(builderID int64) (err error) { 123 | tx := x.Begin() 124 | defer releaseTransaction(tx) 125 | 126 | t.BuilderID = builderID 127 | t.Status = TASK_STATUS_BUILDING 128 | t.Updated = time.Now().Unix() 129 | if err = tx.Exec("UPDATE builders SET is_idle = ?,task_id = ? WHERE id = ?", false, t.ID, builderID).Error; err != nil { 130 | return fmt.Errorf("set builder to busy: %v", err) 131 | } else if err = tx.Save(t).Error; err != nil { 132 | return fmt.Errorf("save task: %v", err) 133 | } 134 | 135 | return tx.Commit().Error 136 | } 137 | 138 | func (t *Task) buildFinish(status TaskStatus) error { 139 | builder, err := GetBuilderByID(t.BuilderID) 140 | if err != nil { 141 | return fmt.Errorf("GetBuilderByID: %v", err) 142 | } 143 | 144 | tx := x.Begin() 145 | defer releaseTransaction(tx) 146 | 147 | t.Status = status 148 | t.Updated = time.Now().Unix() 149 | if err = tx.Save(t).Error; err != nil { 150 | return fmt.Errorf("Save.(task): %v", err) 151 | } 152 | 153 | builder.IsIdle = true 154 | builder.TaskID = 0 155 | if err = tx.Save(builder).Error; err != nil { 156 | return fmt.Errorf("Save.(builder): %v", err) 157 | } 158 | 159 | return tx.Commit().Error 160 | } 161 | 162 | func (t *Task) BuildFailed() error { 163 | return t.buildFinish(TASK_STATUS_FAILED) 164 | } 165 | 166 | func (t *Task) BuildSucceed() error { 167 | return t.buildFinish(TASK_STATUS_SUCCEED) 168 | } 169 | 170 | func (t *Task) Archive() error { 171 | t.Status = TASK_STATUS_ARCHIVED 172 | if err := t.Save(); err != nil { 173 | return err 174 | } 175 | 176 | for _, format := range setting.Project.PackFormats { 177 | os.Remove(path.Join(setting.ArtifactsPath, t.ArtifactName(format))) 178 | } 179 | return nil 180 | } 181 | 182 | func GetCommitOfBranch(branch string) (string, error) { 183 | fmt.Println("git", "ls-remote", setting.Project.CloneURL, branch) 184 | // Get latest commit ID on given branch. 185 | stdout, stderr, err := com.ExecCmd("git", "ls-remote", setting.Project.CloneURL, branch) 186 | if err != nil { 187 | return "", fmt.Errorf("get latest commit of branch '%s': %v - %s", branch, err, stderr) 188 | } 189 | if len(stdout) < 40 { 190 | return "", fmt.Errorf("not enough length of commit ID: %s", stdout) 191 | } 192 | return stdout[:40], nil 193 | } 194 | 195 | func NewTask(doerID int64, os, arch string, tags []string, branch string) (*Task, error) { 196 | sort.Strings(tags) 197 | 198 | // Make sure there is a matrix can take the job. 199 | builderIDs, err := MatchBuilders(os, arch, tags) 200 | if err != nil { 201 | if IsErrNoSuitableMatrix(err) { 202 | return nil, err 203 | } 204 | return nil, fmt.Errorf("MatchBuilders: %v", err) 205 | } 206 | if len(builderIDs) == 0 { 207 | return nil, ErrNoSuitableMatrix{os, arch, tags} 208 | } 209 | 210 | commit, err := GetCommitOfBranch(branch) 211 | if err != nil { 212 | return nil, fmt.Errorf("GetCommitOfBranch: %v", err) 213 | } 214 | 215 | // Check to prevent duplicated tasks 216 | task := new(Task) 217 | if err = x.Where("os=? AND arch=? AND tags=? AND commit=? AND status!=? AND status!=?", 218 | os, arch, strings.Join(tags, ","), commit, TASK_STATUS_FAILED, TASK_STATUS_ARCHIVED).First(task).Error; err == nil { 219 | return task, nil 220 | } else if !IsErrRecordNotFound(err) { 221 | return nil, fmt.Errorf("check existing task: %v", err) 222 | } 223 | 224 | task = &Task{ 225 | OS: os, 226 | Arch: arch, 227 | Tags: strings.Join(tags, ","), 228 | Commit: commit, 229 | PosterID: doerID, 230 | } 231 | return task, x.Create(task).Error 232 | } 233 | 234 | func NewBatchTasks(doerID int64, branch string) error { 235 | commit, err := GetCommitOfBranch(branch) 236 | if err != nil { 237 | return fmt.Errorf("GetCommitOfBranch: %v", err) 238 | } 239 | 240 | // Check to prevent duplicated tasks 241 | for _, t := range setting.BatchTasks { 242 | task := new(Task) 243 | if err = x.Where("os=? AND arch=? AND tags=? AND commit=? AND status!=? AND status!=?", 244 | t.OS, t.Arch, strings.Join(t.Tags, ","), commit, TASK_STATUS_FAILED, TASK_STATUS_ARCHIVED).First(task).Error; err != nil { 245 | if !IsErrRecordNotFound(err) { 246 | return fmt.Errorf("check existing task: %v", err) 247 | } 248 | } 249 | 250 | task = &Task{ 251 | OS: t.OS, 252 | Arch: t.Arch, 253 | Tags: strings.Join(t.Tags, ","), 254 | Commit: commit, 255 | PosterID: doerID, 256 | } 257 | if err = x.Create(task).Error; err != nil { 258 | return fmt.Errorf("create new task: %v", err) 259 | } 260 | } 261 | 262 | return nil 263 | } 264 | 265 | func GetTaskByID(id int64) (*Task, error) { 266 | task := new(Task) 267 | return task, x.First(task, id).Error 268 | } 269 | 270 | func ListTasks(page, pageSize int64) ([]*Task, error) { 271 | tasks := make([]*Task, 0, 10) 272 | return tasks, x.Limit(pageSize).Offset((page - 1) * pageSize).Order("id DESC").Find(&tasks).Error 273 | } 274 | 275 | func ListPendingTasks() ([]*Task, error) { 276 | tasks := make([]*Task, 0, 10) 277 | return tasks, x.Where("status = ?", TASK_STATUS_PENDING).Find(&tasks).Error 278 | } 279 | 280 | func CountTasks() int64 { 281 | return Count(new(Task)) 282 | } 283 | 284 | func AssignTasks() { 285 | defer func() { 286 | log.Trace("Finish assigning tasks.") 287 | time.AfterFunc(30*time.Second, AssignTasks) 288 | }() 289 | 290 | log.Trace("Start assigning tasks...") 291 | tasks, err := ListPendingTasks() 292 | if err != nil { 293 | log.Error(4, "ListPendingTasks: %v", err) 294 | return 295 | } 296 | 297 | for _, t := range tasks { 298 | var tags []string 299 | if len(t.Tags) > 0 { 300 | tags = strings.Split(t.Tags, ",") 301 | } 302 | builderIDs, err := MatchBuilders(t.OS, t.Arch, tags) 303 | if err != nil { 304 | if !IsErrNoSuitableMatrix(err) { 305 | log.Error(4, "MatchBuilders [task_id: %d]: %v", t.ID, err) 306 | } 307 | continue 308 | } 309 | 310 | builder := new(Builder) 311 | if err = x.Where("is_idle = ? AND id IN (?)", true, tool.Int64sToStrings(builderIDs)).First(builder).Error; err != nil { 312 | if !IsErrRecordNotFound(err) { 313 | log.Error(4, "find idle builder [task_id: %s]: %v", t.ID, err) 314 | } 315 | continue 316 | } 317 | 318 | if err = t.AssignBuilder(builder.ID); err != nil { 319 | log.Error(4, "AssignBuilder [task_id: %s, builder_id: %d]: %v", t.ID, builder.ID, err) 320 | continue 321 | } 322 | 323 | log.Trace("Assigned task '%d' to builder '%d'", t.ID, builder.ID) 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /public/assets/AdminLTE-2.3.11/js/app.min.js: -------------------------------------------------------------------------------- 1 | /*! AdminLTE app.js 2 | * ================ 3 | * Main JS application file for AdminLTE v2. This file 4 | * should be included in all pages. It controls some layout 5 | * options and implements exclusive AdminLTE plugins. 6 | * 7 | * @Author Almsaeed Studio 8 | * @Support 9 | * @Email 10 | * @version 2.3.8 11 | * @license MIT 12 | */ 13 | function _init(){"use strict";$.AdminLTE.layout={activate:function(){var a=this;a.fix(),a.fixSidebar(),$("body, html, .wrapper").css("height","auto"),$(window,".wrapper").resize(function(){a.fix(),a.fixSidebar()})},fix:function(){$(".layout-boxed > .wrapper").css("overflow","hidden");var a=$(".main-footer").outerHeight()||0,b=$(".main-header").outerHeight()+a,c=$(window).height(),d=$(".sidebar").height()||0;if($("body").hasClass("fixed"))$(".content-wrapper, .right-side").css("min-height",c-a);else{var e;c>=d?($(".content-wrapper, .right-side").css("min-height",c-b),e=c-b):($(".content-wrapper, .right-side").css("min-height",d),e=d);var f=$($.AdminLTE.options.controlSidebarOptions.selector);"undefined"!=typeof f&&f.height()>e&&$(".content-wrapper, .right-side").css("min-height",f.height())}},fixSidebar:function(){return $("body").hasClass("fixed")?("undefined"==typeof $.fn.slimScroll&&window.console&&window.console.error("Error: the fixed layout requires the slimscroll plugin!"),void($.AdminLTE.options.sidebarSlimScroll&&"undefined"!=typeof $.fn.slimScroll&&($(".sidebar").slimScroll({destroy:!0}).height("auto"),$(".sidebar").slimScroll({height:$(window).height()-$(".main-header").height()+"px",color:"rgba(0,0,0,0.2)",size:"3px"})))):void("undefined"!=typeof $.fn.slimScroll&&$(".sidebar").slimScroll({destroy:!0}).height("auto"))}},$.AdminLTE.pushMenu={activate:function(a){var b=$.AdminLTE.options.screenSizes;$(document).on("click",a,function(a){a.preventDefault(),$(window).width()>b.sm-1?$("body").hasClass("sidebar-collapse")?$("body").removeClass("sidebar-collapse").trigger("expanded.pushMenu"):$("body").addClass("sidebar-collapse").trigger("collapsed.pushMenu"):$("body").hasClass("sidebar-open")?$("body").removeClass("sidebar-open").removeClass("sidebar-collapse").trigger("collapsed.pushMenu"):$("body").addClass("sidebar-open").trigger("expanded.pushMenu")}),$(".content-wrapper").click(function(){$(window).width()<=b.sm-1&&$("body").hasClass("sidebar-open")&&$("body").removeClass("sidebar-open")}),($.AdminLTE.options.sidebarExpandOnHover||$("body").hasClass("fixed")&&$("body").hasClass("sidebar-mini"))&&this.expandOnHover()},expandOnHover:function(){var a=this,b=$.AdminLTE.options.screenSizes.sm-1;$(".main-sidebar").hover(function(){$("body").hasClass("sidebar-mini")&&$("body").hasClass("sidebar-collapse")&&$(window).width()>b&&a.expand()},function(){$("body").hasClass("sidebar-mini")&&$("body").hasClass("sidebar-expanded-on-hover")&&$(window).width()>b&&a.collapse()})},expand:function(){$("body").removeClass("sidebar-collapse").addClass("sidebar-expanded-on-hover")},collapse:function(){$("body").hasClass("sidebar-expanded-on-hover")&&$("body").removeClass("sidebar-expanded-on-hover").addClass("sidebar-collapse")}},$.AdminLTE.tree=function(a){var b=this,c=$.AdminLTE.options.animationSpeed;$(document).off("click",a+" li a").on("click",a+" li a",function(a){var d=$(this),e=d.next();if(e.is(".treeview-menu")&&e.is(":visible")&&!$("body").hasClass("sidebar-collapse"))e.slideUp(c,function(){e.removeClass("menu-open")}),e.parent("li").removeClass("active");else if(e.is(".treeview-menu")&&!e.is(":visible")){var f=d.parents("ul").first(),g=f.find("ul:visible").slideUp(c);g.removeClass("menu-open");var h=d.parent("li");e.slideDown(c,function(){e.addClass("menu-open"),f.find("li.active").removeClass("active"),h.addClass("active"),b.layout.fix()})}e.is(".treeview-menu")&&a.preventDefault()})},$.AdminLTE.controlSidebar={activate:function(){var a=this,b=$.AdminLTE.options.controlSidebarOptions,c=$(b.selector),d=$(b.toggleBtnSelector);d.on("click",function(d){d.preventDefault(),c.hasClass("control-sidebar-open")||$("body").hasClass("control-sidebar-open")?a.close(c,b.slide):a.open(c,b.slide)});var e=$(".control-sidebar-bg");a._fix(e),$("body").hasClass("fixed")?a._fixForFixed(c):$(".content-wrapper, .right-side").height() .box-body, > .box-footer, > form >.box-body, > form > .box-footer");c.hasClass("collapsed-box")?(a.children(":first").removeClass(b.icons.open).addClass(b.icons.collapse),d.slideDown(b.animationSpeed,function(){c.removeClass("collapsed-box")})):(a.children(":first").removeClass(b.icons.collapse).addClass(b.icons.open),d.slideUp(b.animationSpeed,function(){c.addClass("collapsed-box")}))},remove:function(a){var b=a.parents(".box").first();b.slideUp(this.animationSpeed)}}}if("undefined"==typeof jQuery)throw new Error("AdminLTE requires jQuery");$.AdminLTE={},$.AdminLTE.options={navbarMenuSlimscroll:!0,navbarMenuSlimscrollWidth:"3px",navbarMenuHeight:"200px",animationSpeed:500,sidebarToggleSelector:"[data-toggle='offcanvas']",sidebarPushMenu:!0,sidebarSlimScroll:!0,sidebarExpandOnHover:!1,enableBoxRefresh:!0,enableBSToppltip:!0,BSTooltipSelector:"[data-toggle='tooltip']",enableFastclick:!1,enableControlTreeView:!0,enableControlSidebar:!0,controlSidebarOptions:{toggleBtnSelector:"[data-toggle='control-sidebar']",selector:".control-sidebar",slide:!0},enableBoxWidget:!0,boxWidgetOptions:{boxWidgetIcons:{collapse:"fa-minus",open:"fa-plus",remove:"fa-times"},boxWidgetSelectors:{remove:'[data-widget="remove"]',collapse:'[data-widget="collapse"]'}},directChat:{enable:!0,contactToggleSelector:'[data-widget="chat-pane-toggle"]'},colors:{lightBlue:"#3c8dbc",red:"#f56954",green:"#00a65a",aqua:"#00c0ef",yellow:"#f39c12",blue:"#0073b7",navy:"#001F3F",teal:"#39CCCC",olive:"#3D9970",lime:"#01FF70",orange:"#FF851B",fuchsia:"#F012BE",purple:"#8E24AA",maroon:"#D81B60",black:"#222222",gray:"#d2d6de"},screenSizes:{xs:480,sm:768,md:992,lg:1200}},$(function(){"use strict";$("body").removeClass("hold-transition"),"undefined"!=typeof AdminLTEOptions&&$.extend(!0,$.AdminLTE.options,AdminLTEOptions);var a=$.AdminLTE.options;_init(),$.AdminLTE.layout.activate(),a.enableControlTreeView&&$.AdminLTE.tree(".sidebar"),a.enableControlSidebar&&$.AdminLTE.controlSidebar.activate(),a.navbarMenuSlimscroll&&"undefined"!=typeof $.fn.slimscroll&&$(".navbar .menu").slimscroll({height:a.navbarMenuHeight,alwaysVisible:!1,size:a.navbarMenuSlimscrollWidth}).css("width","100%"),a.sidebarPushMenu&&$.AdminLTE.pushMenu.activate(a.sidebarToggleSelector),a.enableBSToppltip&&$("body").tooltip({selector:a.BSTooltipSelector,container:"body"}),a.enableBoxWidget&&$.AdminLTE.boxWidget.activate(),a.enableFastclick&&"undefined"!=typeof FastClick&&FastClick.attach(document.body),a.directChat.enable&&$(document).on("click",a.directChat.contactToggleSelector,function(){var a=$(this).parents(".direct-chat").first();a.toggleClass("direct-chat-contacts-open")}),$('.btn-group[data-toggle="btn-toggle"]').each(function(){var a=$(this);$(this).find(".btn").on("click",function(b){a.find(".btn.active").removeClass("active"),$(this).addClass("active"),b.preventDefault()})})}),function(a){"use strict";a.fn.boxRefresh=function(b){function c(a){a.append(f),e.onLoadStart.call(a)}function d(a){a.find(f).remove(),e.onLoadDone.call(a)}var e=a.extend({trigger:".refresh-btn",source:"",onLoadStart:function(a){return a},onLoadDone:function(a){return a}},b),f=a('
');return this.each(function(){if(""===e.source)return void(window.console&&window.console.log("Please specify a source first - boxRefresh()"));var b=a(this),f=b.find(e.trigger).first();f.on("click",function(a){a.preventDefault(),c(b),b.find(".box-body").load(e.source,function(){d(b)})})})}}(jQuery),function(a){"use strict";a.fn.activateBox=function(){a.AdminLTE.boxWidget.activate(this)},a.fn.toggleBox=function(){var b=a(a.AdminLTE.boxWidget.selectors.collapse,this);a.AdminLTE.boxWidget.collapse(b)},a.fn.removeBox=function(){var b=a(a.AdminLTE.boxWidget.selectors.remove,this);a.AdminLTE.boxWidget.remove(b)}}(jQuery),function(a){"use strict";a.fn.todolist=function(b){var c=a.extend({onCheck:function(a){return a},onUncheck:function(a){return a}},b);return this.each(function(){"undefined"!=typeof a.fn.iCheck?(a("input",this).on("ifChecked",function(){var b=a(this).parents("li").first();b.toggleClass("done"),c.onCheck.call(b)}),a("input",this).on("ifUnchecked",function(){var b=a(this).parents("li").first();b.toggleClass("done"),c.onUncheck.call(b)})):a("input",this).on("change",function(){var b=a(this).parents("li").first();b.toggleClass("done"),a("input",b).is(":checked")?c.onCheck.call(b):c.onUncheck.call(b)})})}}(jQuery); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /public/plugins/select2/select2.min.css: -------------------------------------------------------------------------------- 1 | .select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;height:1px !important;margin:-1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} 2 | -------------------------------------------------------------------------------- /public/assets/font-awesome-4.5.0/css/font-awesome.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.5.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.5.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.5.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.5.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.5.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"} 5 | -------------------------------------------------------------------------------- /public/assets/bootstrap-3.3.5/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.4 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";var b=a.fn.jquery.split(" ")[0].split(".");if(b[0]<2&&b[1]<9||1==b[0]&&9==b[1]&&b[2]<1)throw new Error("Bootstrap's JavaScript requires jQuery version 1.9.1 or higher")}(jQuery),+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.3.4",d.TRANSITION_DURATION=150,d.prototype.close=function(b){function c(){g.detach().trigger("closed.bs.alert").remove()}var e=a(this),f=e.attr("data-target");f||(f=e.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,""));var g=a(f);b&&b.preventDefault(),g.length||(g=e.closest(".alert")),g.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(g.removeClass("in"),a.support.transition&&g.hasClass("fade")?g.one("bsTransitionEnd",c).emulateTransitionEnd(d.TRANSITION_DURATION):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.3.4",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),setTimeout(a.proxy(function(){d[e](null==f[b]?this.options[b]:f[b]),"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}else this.$element.attr("aria-pressed",!this.$element.hasClass("active"));a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()}).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',function(b){a(b.target).closest(".btn").toggleClass("focus",/^focus(in)?$/.test(b.type))})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=null,this.sliding=null,this.interval=null,this.$active=null,this.$items=null,this.options.keyboard&&this.$element.on("keydown.bs.carousel",a.proxy(this.keydown,this)),"hover"==this.options.pause&&!("ontouchstart"in document.documentElement)&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.3.4",c.TRANSITION_DURATION=600,c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0,keyboard:!0},c.prototype.keydown=function(a){if(!/input|textarea/i.test(a.target.tagName)){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()}},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.getItemForDirection=function(a,b){var c=this.getItemIndex(b),d="prev"==a&&0===c||"next"==a&&c==this.$items.length-1;if(d&&!this.options.wrap)return b;var e="prev"==a?-1:1,f=(c+e)%this.$items.length;return this.$items.eq(f)},c.prototype.to=function(a){var b=this,c=this.getItemIndex(this.$active=this.$element.find(".item.active"));return a>this.$items.length-1||0>a?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){b.to(a)}):c==a?this.pause().cycle():this.slide(a>c?"next":"prev",this.$items.eq(a))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,d){var e=this.$element.find(".item.active"),f=d||this.getItemForDirection(b,e),g=this.interval,h="next"==b?"left":"right",i=this;if(f.hasClass("active"))return this.sliding=!1;var j=f[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:h});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,g&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(f)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:h});return a.support.transition&&this.$element.hasClass("slide")?(f.addClass(b),f[0].offsetWidth,e.addClass(h),f.addClass(h),e.one("bsTransitionEnd",function(){f.removeClass([b,h].join(" ")).addClass("active"),e.removeClass(["active",h].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(c.TRANSITION_DURATION)):(e.removeClass("active"),f.addClass("active"),this.sliding=!1,this.$element.trigger(m)),g&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this};var e=function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}};a(document).on("click.bs.carousel.data-api","[data-slide]",e).on("click.bs.carousel.data-api","[data-slide-to]",e),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){var c,d=b.attr("data-target")||(c=b.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"");return a(d)}function c(b){return this.each(function(){var c=a(this),e=c.data("bs.collapse"),f=a.extend({},d.DEFAULTS,c.data(),"object"==typeof b&&b);!e&&f.toggle&&/show|hide/.test(b)&&(f.toggle=!1),e||c.data("bs.collapse",e=new d(this,f)),"string"==typeof b&&e[b]()})}var d=function(b,c){this.$element=a(b),this.options=a.extend({},d.DEFAULTS,c),this.$trigger=a('[data-toggle="collapse"][href="#'+b.id+'"],[data-toggle="collapse"][data-target="#'+b.id+'"]'),this.transitioning=null,this.options.parent?this.$parent=this.getParent():this.addAriaAndCollapsedClass(this.$element,this.$trigger),this.options.toggle&&this.toggle()};d.VERSION="3.3.4",d.TRANSITION_DURATION=350,d.DEFAULTS={toggle:!0},d.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},d.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b,e=this.$parent&&this.$parent.children(".panel").children(".in, .collapsing");if(!(e&&e.length&&(b=e.data("bs.collapse"),b&&b.transitioning))){var f=a.Event("show.bs.collapse");if(this.$element.trigger(f),!f.isDefaultPrevented()){e&&e.length&&(c.call(e,"hide"),b||e.data("bs.collapse",null));var g=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[g](0).attr("aria-expanded",!0),this.$trigger.removeClass("collapsed").attr("aria-expanded",!0),this.transitioning=1;var h=function(){this.$element.removeClass("collapsing").addClass("collapse in")[g](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return h.call(this);var i=a.camelCase(["scroll",g].join("-"));this.$element.one("bsTransitionEnd",a.proxy(h,this)).emulateTransitionEnd(d.TRANSITION_DURATION)[g](this.$element[0][i])}}}},d.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse in").attr("aria-expanded",!1),this.$trigger.addClass("collapsed").attr("aria-expanded",!1),this.transitioning=1;var e=function(){this.transitioning=0,this.$element.removeClass("collapsing").addClass("collapse").trigger("hidden.bs.collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(e,this)).emulateTransitionEnd(d.TRANSITION_DURATION):e.call(this)}}},d.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()},d.prototype.getParent=function(){return a(this.options.parent).find('[data-toggle="collapse"][data-parent="'+this.options.parent+'"]').each(a.proxy(function(c,d){var e=a(d);this.addAriaAndCollapsedClass(b(e),e)},this)).end()},d.prototype.addAriaAndCollapsedClass=function(a,b){var c=a.hasClass("in");a.attr("aria-expanded",c),b.toggleClass("collapsed",!c).attr("aria-expanded",c)};var e=a.fn.collapse;a.fn.collapse=c,a.fn.collapse.Constructor=d,a.fn.collapse.noConflict=function(){return a.fn.collapse=e,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(d){var e=a(this);e.attr("data-target")||d.preventDefault();var f=b(e),g=f.data("bs.collapse"),h=g?"toggle":e.data();c.call(f,h)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=a(this),e=c(d),f={relatedTarget:this};e.hasClass("open")&&(e.trigger(b=a.Event("hide.bs.dropdown",f)),b.isDefaultPrevented()||(d.attr("aria-expanded","false"),e.removeClass("open").trigger("hidden.bs.dropdown",f)))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.3.4",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(this.options.viewport.selector||this.options.viewport),this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c&&c.$tip&&c.$tip.is(":visible")?void(c.hoverState="in"):(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.options.container?a(this.options.container):this.$element.parent(),p=this.getPosition(o);h="bottom"==h&&k.bottom+m>p.bottom?"top":"top"==h&&k.top-mp.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.width&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type)})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||!/destroy|hide/.test(b))&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.4",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.4",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.4",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return c>e?"top":!1;if("bottom"==this.affixed)return null!=c?e+this.unpin<=f.top?!1:"bottom":a-d>=e+g?!1:"bottom";var h=null==this.affixed,i=h?e:f.top,j=h?g:b;return null!=c&&c>=e?"top":null!=d&&i+j>=a-d?"bottom":!1},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=a(document.body).height();"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); --------------------------------------------------------------------------------