├── .gitignore ├── .project ├── .settings └── com.palantir.typescript.prefs ├── LICENSE ├── README.md ├── app.go ├── go ├── dto │ ├── 0_BoardSearchDTO.go │ ├── 0_BoardUserDTO.go │ ├── 0_BoardUserSearchDTO.go │ ├── 0_IdVersionDTO.go │ ├── 0_IdentityDTO.go │ ├── 0_LoginDTO.go │ ├── 0_NotificationSearchDTO.go │ ├── 0_UserDTO.go │ └── 0_UserSearchDTO.go ├── entity │ ├── 0_Board.go │ ├── 0_Lane.go │ ├── 0_Notification.go │ ├── 0_Role.go │ ├── 0_Task.go │ ├── 0_User.go │ └── EntityAudit.go ├── impl │ ├── 0_AppCtx.go │ ├── 0_Roles.go │ ├── Configuration.go │ └── TaskBoardService.go ├── lov │ └── 0_ERole.go ├── service │ ├── 0_ITaskBoardService.go │ └── 0_TaskBoardServiceIn.go └── tables │ ├── 0_TBoard.go │ ├── 0_TLane.go │ ├── 0_TNotification.go │ ├── 0_TRole.go │ ├── 0_TTask.go │ └── 0_TUser.go ├── main.go ├── sql ├── 01_create.sql ├── 02_taskboard.sql └── 03_populate.sql ├── taskboard.ini ├── taskboard.ini.template ├── taskboard_test.go ├── ts ├── app.ts ├── controllers.ts ├── infra.ts ├── toolkit.ts └── typings │ ├── angularjs │ ├── angular-cookies.d.ts │ ├── angular-route.d.ts │ └── angular.d.ts │ ├── bootstrap │ └── bootstrap.d.ts │ ├── jquery │ └── jquery.d.ts │ └── jqueryui │ └── jqueryui.d.ts └── www ├── css ├── angular-markdown-editor.css ├── app.css ├── bootstrap-markdown.min.css ├── bootstrap.min.css ├── bootstrap.min.css.map ├── font-awesome.css.map ├── font-awesome.min.css ├── highlight-github.min.css ├── images │ ├── sort-asc.png │ ├── sort-desc.png │ ├── sort-none.png │ ├── ui-bg_flat_75_ffffff_40x100.png │ ├── ui-bg_highlight-soft_75_cccccc_1x100.png │ └── ui-icons_222222_256x240.png ├── jquery-ui.css └── toastr.min.css ├── favicon.ico ├── fonts ├── glyphicons-halflings-regular.eot ├── glyphicons-halflings-regular.svg ├── glyphicons-halflings-regular.ttf ├── glyphicons-halflings-regular.woff └── glyphicons-halflings-regular.woff2 ├── img ├── check.png ├── delete.png ├── edit.png └── remove.png ├── index.html ├── js ├── .gitignore ├── gen │ ├── app.js │ ├── controllers.js │ ├── infra.js │ └── toolkit.js └── lib │ ├── angular-highlightjs.min.js │ ├── angular-markdown-editor.js │ ├── angular-marked.min.js │ ├── angular-route.min.js │ ├── angular-route.min.js.map │ ├── angular-sanitize.min.js │ ├── angular-sanitize.min.js.map │ ├── angular-ui.min.js │ ├── angular.min.js │ ├── angular.min.js.map │ ├── bootstrap-markdown.js │ ├── bootstrap.min.js │ ├── highlight.min.js │ ├── jquery-ui.min.js │ ├── jquery.bootstrap.confirm.popover.js │ ├── jquery.min.js │ ├── jquery.min.map │ ├── jquery.ui.touch-punch.min.js │ ├── marked.min.js │ ├── ngStorage.min.js │ ├── poller.js │ ├── resizable-tables.js │ └── toastr.min.js └── partials ├── about.html ├── board.html ├── user.html ├── users.html └── welcome.html /.gitignore: -------------------------------------------------------------------------------- 1 | /taskboard 2 | *.log 3 | /bower_components -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | KanbanBoard-ts 4 | 5 | 6 | 7 | 8 | 9 | com.palantir.typescript.typeScriptBuilder 10 | 11 | 12 | 13 | 14 | 15 | com.palantir.typescript.typeScriptNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.settings/com.palantir.typescript.prefs: -------------------------------------------------------------------------------- 1 | build.path.sourceFolder=ts 2 | compiler.codeGenTarget=ECMASCRIPT3 3 | compiler.compileOnSave=true 4 | compiler.generateDeclarationFiles=false 5 | compiler.mapSourceFiles=false 6 | compiler.moduleGenTarget=UNSPECIFIED 7 | compiler.noImplicitAny=false 8 | compiler.noLib=false 9 | compiler.optionDirOption=www/js 10 | compiler.outputDirOption=www/js/gen 11 | compiler.outputFileOption= 12 | compiler.removeComments=false 13 | eclipse.preferences.version=1 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Paulo Quintans Pereira 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | taskboard 2 | ========= 3 | 4 | A very simple kanban taskboard. 5 | 6 | This is a working in progress but it is ready for a test drive. 7 | 8 | It uses AngularJS for the frontend and Go(golang) for the backend. The data is stored in a MySQL database. 9 | goSQL is used for all database operations. 10 | The backend supplies JSON services that the frontend consumes. 11 | 12 | Features 13 | - 14 | * Multiple boards 15 | * Drag n Drop of tasks 16 | * “Real time” refresh between multiple browsers looking to the same board 17 | * Configurable e-mail notifications when a task is droped in a column 18 | * Authentication 19 | * Authorization 20 | 21 | Dependencies 22 | - 23 | Go 1.2 24 | ``` 25 | go get github.com/quintans/taskboard 26 | ``` 27 | 28 | Installation 29 | - 30 | 1. Install a **MySQL** database. 31 | 1. Execute **sql/create.sql** to create a database and user. 32 | 1. Connect to the created dabase with the created user and execute **sql/taskboard.sql** and **sql/populate.sql** 33 | 1. Copy **taskboard.ini.template** to **taskboard.ini** and change taskboard.ini to reflect the execution environment. 34 | 1. Compile and execute 35 | 36 | How To 37 | - 38 | 1. Login with admin/admin 39 | 1. From the Board menu choose "New Board" 40 | 1. From the Board menu choose "New Column" 41 | 1. Double click on the column name to edit its name 42 | 1. Click the **+** icon of a column to add a task 43 | 1. Double click in a task to edit 44 | 1. Drag a task to another column or position and observe other browsers, open in the same board, upating. 45 | 1. Click in the gear icon of the task to add e-mail notification (do not forget to configure the smtp server in the taskboard.ini file) 46 | 1. To choose another board, choose "Open Board" from the Board menu. 47 | 1. Non admin users can only add tasks if they belong to the board. Add them with the menu option Board > Board Users. 48 | 1. To add new users to the application you must be logged with admin. Then add them eith the menu option Admin > Manage Users. 49 | 50 | Todo 51 | - 52 | * Tests 53 | * Sanitize HTML 54 | * SSL 55 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quintans/maze" 5 | "github.com/quintans/toolkit/log" 6 | 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | // App is the main application 12 | type App struct { 13 | ContextFactory func(w http.ResponseWriter, r *http.Request, filters []*maze.Filter) maze.IContext 14 | Limit func(ctx maze.IContext) error 15 | AuthenticationFilter func(ctx maze.IContext) error 16 | ResponseBuffer func(ctx maze.IContext) error 17 | TransactionFilter func(ctx maze.IContext) error 18 | LoginFilter func(ctx maze.IContext) error 19 | PingFilter func(ctx maze.IContext) error 20 | Poll http.Handler 21 | IpPort string 22 | JsonRpc *maze.JsonRpc 23 | ContentDir string 24 | } 25 | 26 | func (app *App) Start() { 27 | startTime := time.Now() 28 | 29 | logger := log.LoggerFor("taskboad") 30 | 31 | // Filters will be executed in order 32 | fh := maze.NewMaze(app.ContextFactory) 33 | 34 | // limits size 35 | fh.Push("/*", app.Limit) 36 | // security 37 | fh.Push("/rest/*", app.AuthenticationFilter, app.ResponseBuffer) 38 | // json services will be the most used so, they are at the front 39 | fh.Add(app.JsonRpc.Build("/rest/taskboard")...) 40 | 41 | fh.Push("/login", app.ResponseBuffer, app.TransactionFilter, app.LoginFilter) 42 | // this endponint is called as the expiration time of the token approaches 43 | fh.Push("/ping", app.ResponseBuffer, app.AuthenticationFilter, app.TransactionFilter, app.PingFilter) 44 | 45 | // delivering static content and preventing malicious access 46 | fh.Static("/*", app.ContentDir) 47 | 48 | http.Handle("/", fh) 49 | http.Handle("/feed", app.Poll) 50 | 51 | logger.Infof("Listening at %s", app.IpPort) 52 | logger.Debugf("Started in %f secs", time.Since(startTime).Seconds()) 53 | if err := http.ListenAndServe(app.IpPort, nil); err != nil { 54 | panic(err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /go/dto/0_BoardSearchDTO.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/DTO.ftl 4 | */ 5 | package dto 6 | 7 | import ( 8 | "github.com/quintans/toolkit/web/app" 9 | ) 10 | 11 | func NewBoardSearchDTO() BoardSearchDTO { 12 | this := BoardSearchDTO{} 13 | return this 14 | } 15 | 16 | type BoardSearchDTO struct { 17 | app.Criteria 18 | 19 | Name *string `json:"name"` 20 | } 21 | -------------------------------------------------------------------------------- /go/dto/0_BoardUserDTO.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/DTO.ftl 4 | */ 5 | package dto 6 | 7 | import ( 8 | ) 9 | 10 | func NewBoardUserDTO() BoardUserDTO { 11 | this := BoardUserDTO{} 12 | return this 13 | } 14 | 15 | type BoardUserDTO struct { 16 | IdVersionDTO 17 | 18 | Name *string `json:"name"` 19 | Belongs bool `json:"belongs"` 20 | } 21 | -------------------------------------------------------------------------------- /go/dto/0_BoardUserSearchDTO.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/DTO.ftl 4 | */ 5 | package dto 6 | 7 | import ( 8 | "github.com/quintans/toolkit/web/app" 9 | ) 10 | 11 | func NewBoardUserSearchDTO() BoardUserSearchDTO { 12 | this := BoardUserSearchDTO{} 13 | return this 14 | } 15 | 16 | type BoardUserSearchDTO struct { 17 | app.Criteria 18 | 19 | BoardId int64 `json:"boardId"` 20 | Name *string `json:"name"` 21 | } 22 | -------------------------------------------------------------------------------- /go/dto/0_IdVersionDTO.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/DTO.ftl 4 | */ 5 | package dto 6 | 7 | import ( 8 | "github.com/quintans/toolkit/ext" 9 | ) 10 | 11 | func NewIdVersionDTO() IdVersionDTO { 12 | this := IdVersionDTO{} 13 | this.Version = ext.Int64(1) 14 | return this 15 | } 16 | 17 | type IdVersionDTO struct { 18 | Id *int64 `json:"id"` 19 | Version *int64 `json:"version"` 20 | } 21 | -------------------------------------------------------------------------------- /go/dto/0_IdentityDTO.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/DTO.ftl 4 | */ 5 | package dto 6 | 7 | import ( 8 | ) 9 | 10 | func NewIdentityDTO() IdentityDTO { 11 | this := IdentityDTO{} 12 | return this 13 | } 14 | 15 | type IdentityDTO struct { 16 | Id *int64 `json:"id"` 17 | Name *string `json:"name"` 18 | Roles []*string `json:"roles"` 19 | } 20 | -------------------------------------------------------------------------------- /go/dto/0_LoginDTO.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/DTO.ftl 4 | */ 5 | package dto 6 | 7 | import ( 8 | ) 9 | 10 | func NewLoginDTO() LoginDTO { 11 | this := LoginDTO{} 12 | return this 13 | } 14 | 15 | type LoginDTO struct { 16 | Username *string `json:"username"` 17 | Password *string `json:"password"` 18 | } 19 | -------------------------------------------------------------------------------- /go/dto/0_NotificationSearchDTO.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/DTO.ftl 4 | */ 5 | package dto 6 | 7 | import ( 8 | "github.com/quintans/toolkit/web/app" 9 | ) 10 | 11 | func NewNotificationSearchDTO() NotificationSearchDTO { 12 | this := NotificationSearchDTO{} 13 | return this 14 | } 15 | 16 | type NotificationSearchDTO struct { 17 | app.Criteria 18 | 19 | TaskId *int64 `json:"taskId"` 20 | Email *string `json:"email"` 21 | } 22 | -------------------------------------------------------------------------------- /go/dto/0_UserDTO.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/DTO.ftl 4 | */ 5 | package dto 6 | 7 | import ( 8 | ) 9 | 10 | func NewUserDTO() UserDTO { 11 | this := UserDTO{} 12 | return this 13 | } 14 | 15 | type UserDTO struct { 16 | IdVersionDTO 17 | 18 | Name *string `json:"name"` 19 | Username *string `json:"username"` 20 | Password *string `json:"password"` 21 | Admin bool `json:"admin"` 22 | } 23 | -------------------------------------------------------------------------------- /go/dto/0_UserSearchDTO.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/DTO.ftl 4 | */ 5 | package dto 6 | 7 | import ( 8 | "github.com/quintans/toolkit/web/app" 9 | ) 10 | 11 | func NewUserSearchDTO() UserSearchDTO { 12 | this := UserSearchDTO{} 13 | return this 14 | } 15 | 16 | type UserSearchDTO struct { 17 | app.Criteria 18 | 19 | Name *string `json:"name"` 20 | } 21 | -------------------------------------------------------------------------------- /go/entity/0_Board.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/Entity.ftl 4 | */ 5 | package entity; 6 | import ( 7 | "github.com/quintans/toolkit" 8 | "github.com/quintans/toolkit/web/app" 9 | "github.com/quintans/toolkit/ext" 10 | ) 11 | 12 | var _ toolkit.Hasher = &Board{} 13 | 14 | func NewBoard() *Board { 15 | this := new(Board) 16 | return this 17 | } 18 | 19 | type Board struct { 20 | EntityAudit 21 | 22 | //ATTRIBUTES 23 | Name *string `json:"name"` 24 | Description *string `json:"description"` 25 | // ASSOCIATIONS 26 | // lanes 27 | Lanes []*Lane `json:"lanes"` 28 | // users 29 | Users []*User `json:"users"` 30 | } 31 | 32 | func (this *Board) SetDescription(description *string) { 33 | this.Description = description 34 | this.Mark("Description") 35 | } 36 | 37 | func (this *Board) SetName(name *string) { 38 | this.Name = name 39 | this.Mark("Name") 40 | } 41 | 42 | func (this *Board) Clone() interface{} { 43 | clone := NewBoard() 44 | clone.Copy(this) 45 | return clone 46 | } 47 | 48 | func (this *Board) Copy(entity *Board) { 49 | if entity != nil { 50 | this.EntityAudit.Copy(entity.EntityAudit) 51 | // attributes 52 | this.Name = app.CopyString(entity.Name) 53 | this.Description = app.CopyString(entity.Description) 54 | // associations 55 | this.Lanes = make([]*Lane, len(entity.Lanes), cap(entity.Lanes)) 56 | copy(this.Lanes, entity.Lanes) 57 | this.Users = make([]*User, len(entity.Users), cap(entity.Users)) 58 | copy(this.Users, entity.Users) 59 | } 60 | } 61 | 62 | func (this *Board) String() string { 63 | sb := toolkit.NewStrBuffer() 64 | sb.Add("{Id: ", this.Id, ", Version: ", this.Version) 65 | sb.Add(", Name: ", this.Name) 66 | sb.Add(", Description: ", this.Description) 67 | sb.Add("}") 68 | return sb.String() 69 | } 70 | 71 | func (this *Board) Equals(e interface{}) bool { 72 | if this == e { 73 | return true 74 | } 75 | 76 | switch t := e.(type) { 77 | case *Board: 78 | return this.Id != nil && t.Id != nil && *this.Id == *t.Id 79 | } 80 | return false 81 | } 82 | 83 | func (this *Board) HashCode() int { 84 | result := toolkit.HashType(toolkit.HASH_SEED, this) 85 | result = toolkit.HashLong(result, ext.DefInt64(this.Id, 0)) 86 | return result 87 | } 88 | -------------------------------------------------------------------------------- /go/entity/0_Lane.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/Entity.ftl 4 | */ 5 | package entity; 6 | import ( 7 | "github.com/quintans/toolkit" 8 | "github.com/quintans/toolkit/web/app" 9 | "github.com/quintans/toolkit/ext" 10 | ) 11 | 12 | var _ toolkit.Hasher = &Lane{} 13 | 14 | func NewLane() *Lane { 15 | this := new(Lane) 16 | return this 17 | } 18 | 19 | type Lane struct { 20 | EntityAudit 21 | 22 | //ATTRIBUTES 23 | Name *string `json:"name"` 24 | Position *int64 `json:"position"` 25 | BoardId *int64 `json:"boardId"` 26 | // ASSOCIATIONS 27 | // board 28 | Board *Board `json:"board"` 29 | // tasks 30 | Tasks []*Task `json:"tasks"` 31 | // notifications 32 | Notifications []*Notification `json:"notifications"` 33 | } 34 | 35 | func (this *Lane) SetBoardId(boardId *int64) { 36 | this.BoardId = boardId 37 | this.Mark("BoardId") 38 | } 39 | 40 | func (this *Lane) SetPosition(position *int64) { 41 | this.Position = position 42 | this.Mark("Position") 43 | } 44 | 45 | func (this *Lane) SetName(name *string) { 46 | this.Name = name 47 | this.Mark("Name") 48 | } 49 | 50 | func (this *Lane) Clone() interface{} { 51 | clone := NewLane() 52 | clone.Copy(this) 53 | return clone 54 | } 55 | 56 | func (this *Lane) Copy(entity *Lane) { 57 | if entity != nil { 58 | this.EntityAudit.Copy(entity.EntityAudit) 59 | // attributes 60 | this.Name = app.CopyString(entity.Name) 61 | this.Position = app.CopyInteger(entity.Position) 62 | // associations 63 | this.Board = entity.Board 64 | this.Tasks = make([]*Task, len(entity.Tasks), cap(entity.Tasks)) 65 | copy(this.Tasks, entity.Tasks) 66 | this.Notifications = make([]*Notification, len(entity.Notifications), cap(entity.Notifications)) 67 | copy(this.Notifications, entity.Notifications) 68 | } 69 | } 70 | 71 | func (this *Lane) String() string { 72 | sb := toolkit.NewStrBuffer() 73 | sb.Add("{Id: ", this.Id, ", Version: ", this.Version) 74 | sb.Add(", Name: ", this.Name) 75 | sb.Add(", Position: ", this.Position) 76 | sb.Add(", *BoardId: ", *this.BoardId) 77 | sb.Add("}") 78 | return sb.String() 79 | } 80 | 81 | func (this *Lane) Equals(e interface{}) bool { 82 | if this == e { 83 | return true 84 | } 85 | 86 | switch t := e.(type) { 87 | case *Lane: 88 | return this.Id != nil && t.Id != nil && *this.Id == *t.Id 89 | } 90 | return false 91 | } 92 | 93 | func (this *Lane) HashCode() int { 94 | result := toolkit.HashType(toolkit.HASH_SEED, this) 95 | result = toolkit.HashLong(result, ext.DefInt64(this.Id, 0)) 96 | return result 97 | } 98 | -------------------------------------------------------------------------------- /go/entity/0_Notification.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/Entity.ftl 4 | */ 5 | package entity; 6 | import ( 7 | "github.com/quintans/toolkit" 8 | "github.com/quintans/toolkit/ext" 9 | ) 10 | 11 | var _ toolkit.Hasher = &Notification{} 12 | 13 | func NewNotification() *Notification { 14 | this := new(Notification) 15 | return this 16 | } 17 | 18 | type Notification struct { 19 | EntityAudit 20 | 21 | //ATTRIBUTES 22 | Email string `json:"email"` 23 | TaskId int64 `json:"taskId"` 24 | LaneId int64 `json:"laneId"` 25 | // ASSOCIATIONS 26 | // task 27 | Task *Task `json:"task"` 28 | // lane 29 | Lane *Lane `json:"lane"` 30 | } 31 | 32 | func (this *Notification) SetEmail(email string) { 33 | this.Email = email 34 | this.Mark("Email") 35 | } 36 | 37 | func (this *Notification) SetTaskId(taskId int64) { 38 | this.TaskId = taskId 39 | this.Mark("TaskId") 40 | } 41 | 42 | func (this *Notification) SetLaneId(laneId int64) { 43 | this.LaneId = laneId 44 | this.Mark("LaneId") 45 | } 46 | 47 | func (this *Notification) Clone() interface{} { 48 | clone := NewNotification() 49 | clone.Copy(this) 50 | return clone 51 | } 52 | 53 | func (this *Notification) Copy(entity *Notification) { 54 | if entity != nil { 55 | this.EntityAudit.Copy(entity.EntityAudit) 56 | // attributes 57 | this.Email = entity.Email 58 | // associations 59 | this.Task = entity.Task 60 | this.Lane = entity.Lane 61 | } 62 | } 63 | 64 | func (this *Notification) String() string { 65 | sb := toolkit.NewStrBuffer() 66 | sb.Add("{Id: ", this.Id, ", Version: ", this.Version) 67 | sb.Add(", Email: ", this.Email) 68 | sb.Add(", TaskId: ", this.TaskId) 69 | sb.Add(", LaneId: ", this.LaneId) 70 | sb.Add("}") 71 | return sb.String() 72 | } 73 | 74 | func (this *Notification) Equals(e interface{}) bool { 75 | if this == e { 76 | return true 77 | } 78 | 79 | switch t := e.(type) { 80 | case *Notification: 81 | return this.Id != nil && t.Id != nil && *this.Id == *t.Id 82 | } 83 | return false 84 | } 85 | 86 | func (this *Notification) HashCode() int { 87 | result := toolkit.HashType(toolkit.HASH_SEED, this) 88 | result = toolkit.HashLong(result, ext.DefInt64(this.Id, 0)) 89 | return result 90 | } 91 | -------------------------------------------------------------------------------- /go/entity/0_Role.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/Entity.ftl 4 | */ 5 | package entity; 6 | import ( 7 | "github.com/quintans/toolkit" 8 | "github.com/quintans/taskboard/go/lov" 9 | "github.com/quintans/toolkit/ext" 10 | ) 11 | 12 | var _ toolkit.Hasher = &Role{} 13 | 14 | func NewRole() *Role { 15 | this := new(Role) 16 | return this 17 | } 18 | 19 | type Role struct { 20 | EntityAudit 21 | 22 | //ATTRIBUTES 23 | Kind lov.ERole `json:"kind"` 24 | UserId *int64 `json:"userId"` 25 | // ASSOCIATIONS 26 | // user 27 | User *User `json:"user"` 28 | } 29 | 30 | func (this *Role) SetUserId(userId *int64) { 31 | this.UserId = userId 32 | this.Mark("UserId") 33 | } 34 | 35 | func (this *Role) SetKind(kind lov.ERole) { 36 | this.Kind = kind 37 | this.Mark("Kind") 38 | } 39 | 40 | func (this *Role) Clone() interface{} { 41 | clone := NewRole() 42 | clone.Copy(this) 43 | return clone 44 | } 45 | 46 | func (this *Role) Copy(entity *Role) { 47 | if entity != nil { 48 | this.EntityAudit.Copy(entity.EntityAudit) 49 | // attributes 50 | this.Kind = entity.Kind 51 | // associations 52 | this.User = entity.User 53 | } 54 | } 55 | 56 | func (this *Role) String() string { 57 | sb := toolkit.NewStrBuffer() 58 | sb.Add("{Id: ", this.Id, ", Version: ", this.Version) 59 | sb.Add(", Kind: ", this.Kind) 60 | sb.Add(", *UserId: ", *this.UserId) 61 | sb.Add("}") 62 | return sb.String() 63 | } 64 | 65 | func (this *Role) Equals(e interface{}) bool { 66 | if this == e { 67 | return true 68 | } 69 | 70 | switch t := e.(type) { 71 | case *Role: 72 | return this.Id != nil && t.Id != nil && *this.Id == *t.Id 73 | } 74 | return false 75 | } 76 | 77 | func (this *Role) HashCode() int { 78 | result := toolkit.HashType(toolkit.HASH_SEED, this) 79 | result = toolkit.HashLong(result, ext.DefInt64(this.Id, 0)) 80 | return result 81 | } 82 | -------------------------------------------------------------------------------- /go/entity/0_Task.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/Entity.ftl 4 | */ 5 | package entity; 6 | import ( 7 | "github.com/quintans/toolkit" 8 | "github.com/quintans/toolkit/web/app" 9 | "github.com/quintans/toolkit/ext" 10 | ) 11 | 12 | var _ toolkit.Hasher = &Task{} 13 | 14 | func NewTask() *Task { 15 | this := new(Task) 16 | return this 17 | } 18 | 19 | type Task struct { 20 | EntityAudit 21 | 22 | //ATTRIBUTES 23 | Title *string `json:"title"` 24 | Detail *string `json:"detail"` 25 | HeadColor *string `json:"headColor"` 26 | BodyColor *string `json:"bodyColor"` 27 | Position *int64 `json:"position"` 28 | Reference *string `json:"reference"` 29 | Spent *int64 `json:"spent"` 30 | Remaining *int64 `json:"remaining"` 31 | UserId *int64 `json:"userId"` 32 | LaneId *int64 `json:"laneId"` 33 | // ASSOCIATIONS 34 | // user 35 | User *User `json:"user"` 36 | // lane 37 | Lane *Lane `json:"lane"` 38 | // notifications 39 | Notifications []*Notification `json:"notifications"` 40 | } 41 | 42 | func (this *Task) SetBodyColor(bodyColor *string) { 43 | this.BodyColor = bodyColor 44 | this.Mark("BodyColor") 45 | } 46 | 47 | func (this *Task) SetPosition(position *int64) { 48 | this.Position = position 49 | this.Mark("Position") 50 | } 51 | 52 | func (this *Task) SetReference(reference *string) { 53 | this.Reference = reference 54 | this.Mark("Reference") 55 | } 56 | 57 | func (this *Task) SetUserId(userId *int64) { 58 | this.UserId = userId 59 | this.Mark("UserId") 60 | } 61 | 62 | func (this *Task) SetHeadColor(headColor *string) { 63 | this.HeadColor = headColor 64 | this.Mark("HeadColor") 65 | } 66 | 67 | func (this *Task) SetSpent(spent *int64) { 68 | this.Spent = spent 69 | this.Mark("Spent") 70 | } 71 | 72 | func (this *Task) SetTitle(title *string) { 73 | this.Title = title 74 | this.Mark("Title") 75 | } 76 | 77 | func (this *Task) SetRemaining(remaining *int64) { 78 | this.Remaining = remaining 79 | this.Mark("Remaining") 80 | } 81 | 82 | func (this *Task) SetLaneId(laneId *int64) { 83 | this.LaneId = laneId 84 | this.Mark("LaneId") 85 | } 86 | 87 | func (this *Task) SetDetail(detail *string) { 88 | this.Detail = detail 89 | this.Mark("Detail") 90 | } 91 | 92 | func (this *Task) Clone() interface{} { 93 | clone := NewTask() 94 | clone.Copy(this) 95 | return clone 96 | } 97 | 98 | func (this *Task) Copy(entity *Task) { 99 | if entity != nil { 100 | this.EntityAudit.Copy(entity.EntityAudit) 101 | // attributes 102 | this.Title = app.CopyString(entity.Title) 103 | this.Detail = app.CopyString(entity.Detail) 104 | this.HeadColor = app.CopyString(entity.HeadColor) 105 | this.BodyColor = app.CopyString(entity.BodyColor) 106 | this.Position = app.CopyInteger(entity.Position) 107 | this.Reference = app.CopyString(entity.Reference) 108 | this.Spent = app.CopyInteger(entity.Spent) 109 | this.Remaining = app.CopyInteger(entity.Remaining) 110 | // associations 111 | this.User = entity.User 112 | this.Lane = entity.Lane 113 | this.Notifications = make([]*Notification, len(entity.Notifications), cap(entity.Notifications)) 114 | copy(this.Notifications, entity.Notifications) 115 | } 116 | } 117 | 118 | func (this *Task) String() string { 119 | sb := toolkit.NewStrBuffer() 120 | sb.Add("{Id: ", this.Id, ", Version: ", this.Version) 121 | sb.Add(", Title: ", this.Title) 122 | sb.Add(", Detail: ", this.Detail) 123 | sb.Add(", HeadColor: ", this.HeadColor) 124 | sb.Add(", BodyColor: ", this.BodyColor) 125 | sb.Add(", Position: ", this.Position) 126 | sb.Add(", Reference: ", this.Reference) 127 | sb.Add(", Spent: ", this.Spent) 128 | sb.Add(", Remaining: ", this.Remaining) 129 | sb.Add(", *UserId: ", *this.UserId) 130 | sb.Add(", *LaneId: ", *this.LaneId) 131 | sb.Add("}") 132 | return sb.String() 133 | } 134 | 135 | func (this *Task) Equals(e interface{}) bool { 136 | if this == e { 137 | return true 138 | } 139 | 140 | switch t := e.(type) { 141 | case *Task: 142 | return this.Id != nil && t.Id != nil && *this.Id == *t.Id 143 | } 144 | return false 145 | } 146 | 147 | func (this *Task) HashCode() int { 148 | result := toolkit.HashType(toolkit.HASH_SEED, this) 149 | result = toolkit.HashLong(result, ext.DefInt64(this.Id, 0)) 150 | return result 151 | } 152 | -------------------------------------------------------------------------------- /go/entity/0_User.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/Entity.ftl 4 | */ 5 | package entity; 6 | import ( 7 | "github.com/quintans/toolkit" 8 | "github.com/quintans/toolkit/web/app" 9 | "github.com/quintans/toolkit/ext" 10 | ) 11 | 12 | var _ toolkit.Hasher = &User{} 13 | 14 | func NewUser() *User { 15 | this := new(User) 16 | return this 17 | } 18 | 19 | type User struct { 20 | EntityAudit 21 | 22 | //ATTRIBUTES 23 | Name *string `json:"name"` 24 | Username *string `json:"username"` 25 | Password *string `json:"password"` 26 | // ASSOCIATIONS 27 | // boards 28 | Boards []*Board `json:"boards"` 29 | // roles 30 | Roles []*Role `json:"roles"` 31 | // tasks 32 | Tasks []*Task `json:"tasks"` 33 | } 34 | 35 | func (this *User) SetUsername(username *string) { 36 | this.Username = username 37 | this.Mark("Username") 38 | } 39 | 40 | func (this *User) SetName(name *string) { 41 | this.Name = name 42 | this.Mark("Name") 43 | } 44 | 45 | func (this *User) SetPassword(password *string) { 46 | this.Password = password 47 | this.Mark("Password") 48 | } 49 | 50 | func (this *User) Clone() interface{} { 51 | clone := NewUser() 52 | clone.Copy(this) 53 | return clone 54 | } 55 | 56 | func (this *User) Copy(entity *User) { 57 | if entity != nil { 58 | this.EntityAudit.Copy(entity.EntityAudit) 59 | // attributes 60 | this.Name = app.CopyString(entity.Name) 61 | this.Username = app.CopyString(entity.Username) 62 | this.Password = app.CopyString(entity.Password) 63 | // associations 64 | this.Boards = make([]*Board, len(entity.Boards), cap(entity.Boards)) 65 | copy(this.Boards, entity.Boards) 66 | this.Roles = make([]*Role, len(entity.Roles), cap(entity.Roles)) 67 | copy(this.Roles, entity.Roles) 68 | this.Tasks = make([]*Task, len(entity.Tasks), cap(entity.Tasks)) 69 | copy(this.Tasks, entity.Tasks) 70 | } 71 | } 72 | 73 | func (this *User) String() string { 74 | sb := toolkit.NewStrBuffer() 75 | sb.Add("{Id: ", this.Id, ", Version: ", this.Version) 76 | sb.Add(", Name: ", this.Name) 77 | sb.Add(", Username: ", this.Username) 78 | sb.Add(", Password: ", this.Password) 79 | sb.Add("}") 80 | return sb.String() 81 | } 82 | 83 | func (this *User) Equals(e interface{}) bool { 84 | if this == e { 85 | return true 86 | } 87 | 88 | switch t := e.(type) { 89 | case *User: 90 | return this.Id != nil && t.Id != nil && *this.Id == *t.Id 91 | } 92 | return false 93 | } 94 | 95 | func (this *User) HashCode() int { 96 | result := toolkit.HashType(toolkit.HASH_SEED, this) 97 | result = toolkit.HashLong(result, ext.DefInt64(this.Id, 0)) 98 | return result 99 | } 100 | -------------------------------------------------------------------------------- /go/entity/EntityAudit.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | // "github.com/quintans/goSQL/db" 5 | . "github.com/quintans/toolkit/ext" 6 | "github.com/quintans/toolkit/web/app" 7 | ) 8 | 9 | type EntityAudit struct { 10 | app.EntityBase 11 | 12 | Creation *Date `json:"creation"` 13 | Modification *Date `json:"modification"` 14 | UserCreationId *int64 `json:"userCreationId"` 15 | UserCreation *User `json:"userCreation"` 16 | UserModificationId *int64 `json:"userModificationId"` 17 | UserModification *User `json:"userModification"` 18 | } 19 | 20 | func (this *EntityAudit) Copy(entity EntityAudit) { 21 | this.EntityBase.Copy(entity.EntityBase) 22 | 23 | this.Id = CloneInt64(entity.Id) 24 | this.Creation = CloneDate(entity.Creation) 25 | this.Modification = CloneDate(entity.Modification) 26 | this.UserCreation = entity.UserCreation 27 | this.UserCreationId = CloneInt64(entity.UserCreationId) 28 | this.UserModificationId = CloneInt64(entity.UserModificationId) 29 | this.UserModification = entity.UserModification 30 | } 31 | 32 | const ATTR_USERID = "_userid_" 33 | 34 | /* 35 | func (this *EntityAudit) PreInsert(store db.IDb) error { 36 | this.Creation = NOW() 37 | uid, _ := store.GetAttribute(ATTR_USERID) 38 | if uid != nil { 39 | this.UserCreationId = uid.(*int64) 40 | } 41 | return nil 42 | } 43 | 44 | func (this *EntityAudit) PreUpdate(store db.IDb) error { 45 | this.Modification = NOW() 46 | uid, _ := store.GetAttribute(ATTR_USERID) 47 | if uid != nil { 48 | this.UserModificationId = uid.(*int64) 49 | } 50 | return nil 51 | } 52 | */ 53 | -------------------------------------------------------------------------------- /go/impl/0_AppCtx.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Warning: Generated code! do not change! 3 | * Generated by: go/AppCtx.ftl 4 | */ 5 | package impl 6 | 7 | import ( 8 | "encoding/json" 9 | "net/http" 10 | 11 | "github.com/quintans/goSQL/db" 12 | "github.com/quintans/maze" 13 | "github.com/quintans/taskboard/go/service" 14 | ) 15 | 16 | func NewAppCtx( 17 | w http.ResponseWriter, 18 | r *http.Request, 19 | filters []*maze.Filter, 20 | taskBoardService service.ITaskBoardService, 21 | ) *AppCtx { 22 | var this = new(AppCtx) 23 | this.Context = maze.NewContext(w, r, filters) 24 | this.taskBoardService = taskBoardService 25 | return this 26 | } 27 | 28 | var _ maze.IContext = &AppCtx{} 29 | 30 | type AppCtx struct { 31 | *maze.Context 32 | 33 | Principal *Principal 34 | Store db.IDb 35 | taskBoardService service.ITaskBoardService 36 | } 37 | 38 | func (this *AppCtx) Proceed() error { 39 | return this.Next(this) 40 | } 41 | 42 | func (this *AppCtx) GetTaskBoardService() service.ITaskBoardService { 43 | return this.taskBoardService 44 | } 45 | 46 | func (this *AppCtx) SetTaskBoardService(taskBoardService service.ITaskBoardService) { 47 | this.taskBoardService = taskBoardService 48 | } 49 | 50 | func (this *AppCtx) BuildJsonRpcTaskBoardService(transaction func(ctx maze.IContext) error) *maze.JsonRpc { 51 | // JSON-RPC services 52 | var rpc = maze.NewJsonRpc(this.taskBoardService, transaction) // json-rpc builder 53 | rpc.SetActionFilters("WhoAmI", authorize("USER")) 54 | rpc.SetActionFilters("FetchBoardUsers", authorize("USER")) 55 | rpc.SetActionFilters("FetchBoardAllUsers", authorize("ADMIN")) 56 | rpc.SetActionFilters("FetchBoards", authorize("USER")) 57 | rpc.SetActionFilters("FetchBoardById", authorize("USER")) 58 | rpc.SetActionFilters("FullyLoadBoardById", authorize("USER")) 59 | rpc.SetActionFilters("SaveBoard", authorize("ADMIN")) 60 | rpc.SetActionFilters("DeleteBoard", authorize("ADMIN")) 61 | rpc.SetActionFilters("AddLane", authorize("ADMIN")) 62 | rpc.SetActionFilters("SaveLane", authorize("ADMIN")) 63 | rpc.SetActionFilters("DeleteLastLane", authorize("ADMIN")) 64 | rpc.SetActionFilters("SaveUser", authorize("ADMIN")) 65 | rpc.SetActionFilters("FetchUsers", authorize("ADMIN")) 66 | rpc.SetActionFilters("DeleteUser", authorize("ADMIN")) 67 | rpc.SetActionFilters("AddUserToBoard", authorize("ADMIN")) 68 | rpc.SetActionFilters("RemoveUserFromBoard", authorize("ADMIN")) 69 | rpc.SetActionFilters("SaveUserName", authorize("USER")) 70 | rpc.SetActionFilters("ChangeUserPassword", authorize("USER")) 71 | rpc.SetActionFilters("SaveTask", authorize("USER")) 72 | rpc.SetActionFilters("MoveTask", authorize("USER")) 73 | rpc.SetActionFilters("FetchNotifications", authorize("USER")) 74 | rpc.SetActionFilters("SaveNotification", authorize("USER")) 75 | rpc.SetActionFilters("DeleteNotification", authorize("USER")) 76 | return rpc 77 | } 78 | 79 | func (this *AppCtx) Reply(value interface{}) error { 80 | // JSON Vulnerability Protection. 81 | // AngularJS will automatically strip the prefix before processing it as JSON. 82 | result, err := json.Marshal(value) 83 | if err != nil { 84 | return err 85 | } 86 | if _, err = this.Response.Write([]byte(")]}',\n")); err != nil { 87 | return err 88 | } 89 | _, err = this.Response.Write(result) 90 | return err 91 | } 92 | 93 | func authorize(roles ...string) func(ctx maze.IContext) error { 94 | return func(ctx maze.IContext) error { 95 | user := ctx.(*AppCtx).Principal 96 | for _, r := range roles { 97 | for _, role := range user.Roles { 98 | if r == string(role) { 99 | if err := ctx.Proceed(); err != nil { 100 | return err 101 | } 102 | return nil 103 | } 104 | } 105 | } 106 | http.Error(ctx.GetResponse(), "Unauthorized", http.StatusUnauthorized) 107 | return nil 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /go/impl/0_Roles.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Warning: Generated code! do not change! 3 | * Generated by: go/Roles.ftl 4 | */ 5 | package impl 6 | 7 | const ( 8 | ROLE_ADMIN = "ADMIN" 9 | ROLE_USER = "USER" 10 | ) 11 | -------------------------------------------------------------------------------- /go/impl/Configuration.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "path/filepath" 7 | "sync" 8 | 9 | "github.com/dgrijalva/jwt-go" 10 | "github.com/quintans/goSQL/db" 11 | "github.com/quintans/goSQL/dbx" 12 | trx "github.com/quintans/goSQL/translators" 13 | "github.com/quintans/maze" 14 | tk "github.com/quintans/toolkit" 15 | "github.com/quintans/toolkit/log" 16 | "github.com/quintans/toolkit/web" 17 | "github.com/quintans/toolkit/web/poller" 18 | 19 | _ "github.com/go-sql-driver/mysql" 20 | "github.com/msbranco/goconfig" 21 | 22 | "github.com/quintans/taskboard/go/entity" 23 | "github.com/quintans/taskboard/go/lov" 24 | "github.com/quintans/taskboard/go/service" 25 | T "github.com/quintans/taskboard/go/tables" 26 | 27 | "compress/gzip" 28 | "crypto/rand" 29 | "database/sql" 30 | "encoding/base64" 31 | "encoding/json" 32 | "fmt" 33 | "net/http" 34 | "os" 35 | "regexp" 36 | "runtime" 37 | "runtime/debug" 38 | "strings" 39 | "time" 40 | ) 41 | 42 | const ( 43 | UNKNOWN = "TB00" 44 | DBFAIL = "TB01" 45 | DBLOCK = "TB02" 46 | 47 | PRINCIPAL_KEY = "principal" 48 | JWT_TIMEOUT = 15 49 | ) 50 | 51 | var ( 52 | logger *log.Logger 53 | TM db.ITransactionManager 54 | 55 | ContentDir string 56 | IpPort string 57 | HttpsOnly bool 58 | postLimit int64 59 | 60 | Poll *poller.Poller 61 | 62 | SmtpUser string 63 | SmtpPass string 64 | SmtpHost string 65 | SmtpPort string 66 | SmtpFrom string 67 | 68 | TaskBoardService service.ITaskBoardService 69 | 70 | ErrNoTokenInRequest = errors.New("no token present in request") 71 | ) 72 | 73 | var varguard = regexp.MustCompile("\\${[^{}]+}") 74 | 75 | func replaceOsEnv(val string) string { 76 | // finds all keys in the OS environment 77 | values := make(map[string]string) 78 | keys := varguard.FindAllString(val, -1) 79 | for _, k := range keys { 80 | v := os.Getenv(k[2 : len(k)-1]) 81 | values[k] = v 82 | } 83 | // replace the keys found 84 | for k, v := range values { 85 | val = strings.Replace(val, k, v, -1) 86 | } 87 | return val 88 | } 89 | 90 | func init() { 91 | TaskBoardService = new(TaskBoardServiceImpl) 92 | 93 | c, err := goconfig.ReadConfigFile("taskboard.ini") 94 | if err != nil { 95 | panic(err) 96 | } 97 | /* 98 | * ======= 99 | * WEB 100 | * ======= 101 | */ 102 | ContentDir, _ = c.GetString("web", "dir") 103 | ContentDir = replaceOsEnv(ContentDir) 104 | fmt.Println("[web]dir=", ContentDir) 105 | IpPort, _ = c.GetString("web", "ip_port") 106 | IpPort = replaceOsEnv(IpPort) 107 | fmt.Println("[web]ip_port=", IpPort) 108 | HttpsOnly, _ = c.GetBool("web", "https_only") 109 | fmt.Println("[web]https_only=", HttpsOnly) 110 | postLimit, _ = c.GetInt64("web", "post_limit") 111 | fmt.Println("[web]post_limit=", postLimit) 112 | 113 | /* 114 | * ======= 115 | * LOG 116 | * ======= 117 | */ 118 | //// log configuration - its the same the default 119 | level, _ := c.GetString("log", "level") 120 | level = replaceOsEnv(level) 121 | fmt.Println("[log]level=", level) 122 | file, _ := c.GetString("log", "file") 123 | file = replaceOsEnv(file) 124 | fmt.Println("[log]file=", file) 125 | count, _ := c.GetInt64("log", "count") 126 | fmt.Println("[log]count=", count) 127 | size, _ := c.GetInt64("log", "size") 128 | fmt.Println("[log]size=", size) 129 | logToConsole, _ := c.GetBool("log", "console") 130 | fmt.Println("[log]console=", logToConsole) 131 | 132 | logLevel := log.ParseLevel(level, log.ERROR) 133 | var show = logLevel <= log.INFO 134 | 135 | // setting root writers 136 | writers := make([]log.LogWriter, 0) 137 | writers = append(writers, log.NewRollingFileAppender(file, size, int(count), true)) 138 | if logToConsole { 139 | writers = append(writers, log.NewConsoleAppender(false)) 140 | } 141 | log.Register("/", logLevel, writers...).ShowCaller(show) 142 | 143 | //log.Register("/", logLevel, log.NewRollingFileAppender(file, size, int(count), true)) 144 | 145 | //master.SetLevel("pqp", log.LogLevel(level)) 146 | logger = log.LoggerFor("taskboad/biz/impl") 147 | 148 | // smtp 149 | SmtpHost, _ = c.GetString("smtp", "host") 150 | SmtpHost = replaceOsEnv(SmtpHost) 151 | fmt.Println("[smtp]host=", SmtpHost) 152 | SmtpPort, _ = c.GetString("smtp", "port") 153 | SmtpPort = replaceOsEnv(SmtpPort) 154 | fmt.Println("[smtp]port=", SmtpPort) 155 | SmtpUser, _ = c.GetString("smtp", "user") 156 | SmtpUser = replaceOsEnv(SmtpUser) 157 | fmt.Println("[smtp]user=", SmtpUser) 158 | SmtpPass, _ = c.GetString("smtp", "pass") 159 | SmtpPass = replaceOsEnv(SmtpPass) 160 | SmtpFrom, _ = c.GetString("smtp", "from") 161 | SmtpFrom = replaceOsEnv(SmtpFrom) 162 | fmt.Println("[smtp]from=", SmtpFrom) 163 | 164 | /* 165 | * ======================= 166 | * BEGIN DATABASE CONFIG 167 | * ======================= 168 | */ 169 | // database configuration 170 | driverName, _ := c.GetString("database", "driver_name") 171 | driverName = replaceOsEnv(driverName) 172 | fmt.Println("[database]driver_name=", driverName) 173 | dataSourceName, _ := c.GetString("database", "data_source_name") 174 | dataSourceName = replaceOsEnv(dataSourceName) 175 | fmt.Println("[database]data_source_name=", dataSourceName) 176 | statementCache, _ := c.GetInt64("database", "statement_cache") 177 | fmt.Println("[database]statement_cache=", statementCache) 178 | idle, _ := c.GetInt64("database", "idle_connections") 179 | fmt.Println("[database]idle_connections=", idle) 180 | 181 | appDB, err := sql.Open(driverName, dataSourceName) 182 | if err != nil { 183 | panic(err) 184 | } 185 | 186 | if idle > 0 { 187 | appDB.SetMaxIdleConns(int(idle)) 188 | } 189 | 190 | // wake up the database pool 191 | err = appDB.Ping() 192 | if err != nil { 193 | panic(err) 194 | } 195 | 196 | var translator = trx.NewMySQL5Translator() 197 | TM = db.NewTransactionManager( 198 | // database 199 | appDB, 200 | // databse context factory - called for each transaction 201 | func(inTx *bool, c dbx.IConnection) db.IDb { 202 | return db.NewDb(inTx, c, translator) 203 | }, 204 | // statement cache 205 | int(statementCache), 206 | ) 207 | /* 208 | * ======================= 209 | * END DATABASE CONFIG 210 | * ======================= 211 | */ 212 | 213 | Poll = poller.NewPoller(30 * time.Second) 214 | } 215 | 216 | func GenerateRandomBytes(n int) ([]byte, error) { 217 | b := make([]byte, n) 218 | _, err := rand.Read(b) 219 | return b, err 220 | } 221 | 222 | func GenerateRandomString(s int) (string, error) { 223 | b, err := GenerateRandomBytes(s) 224 | return base64.URLEncoding.EncodeToString(b), err 225 | } 226 | 227 | type Principal struct { 228 | UserId int64 229 | Username string 230 | Roles []lov.ERole 231 | Version int64 232 | } 233 | 234 | func (this *Principal) HasRole(role lov.ERole) bool { 235 | for _, r := range this.Roles { 236 | if r == role { 237 | return true 238 | } 239 | } 240 | return false 241 | } 242 | 243 | var secret, _ = GenerateRandomBytes(64) 244 | 245 | func KeyFunction(token *jwt.Token) (interface{}, error) { 246 | return secret, nil 247 | } 248 | 249 | func serializePrincipal(p *Principal) (string, error) { 250 | principal, err := json.Marshal(*p) 251 | if err != nil { 252 | return "", err 253 | } 254 | // Create JWT token 255 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 256 | PRINCIPAL_KEY: string(principal), 257 | // Expiration time in minutes 258 | "exp": time.Now().Add(time.Minute * JWT_TIMEOUT).Unix(), 259 | }) 260 | return token.SignedString(secret) 261 | } 262 | 263 | func deserializePrincipal(r *http.Request) *Principal { 264 | token, err := parseFromRequest(r, KeyFunction) 265 | if err == nil && token.Valid { 266 | if claims, ok := token.Claims.(jwt.MapClaims); ok { 267 | p := claims[PRINCIPAL_KEY].(string) 268 | principal := &Principal{} 269 | json.Unmarshal([]byte(p), principal) 270 | return principal 271 | } 272 | } 273 | 274 | return nil 275 | } 276 | 277 | // Try to find the token in an http.Request. 278 | // This method will call ParseMultipartForm if there's no token in the header. 279 | // Currently, it looks in the Authorization header as well as 280 | // looking for an 'access_token' request parameter in req.Form. 281 | func parseFromRequest(req *http.Request, keyFunc jwt.Keyfunc) (token *jwt.Token, err error) { 282 | 283 | // Look for an Authorization header 284 | if ah := req.Header.Get("Authorization"); ah != "" { 285 | // Should be a bearer token 286 | if len(ah) > 6 && strings.ToUpper(ah[0:6]) == "BEARER" { 287 | return jwt.Parse(ah[7:], keyFunc) 288 | } 289 | } 290 | 291 | // Look for "access_token" parameter 292 | req.ParseMultipartForm(10e6) 293 | if tokStr := req.Form.Get("access_token"); tokStr != "" { 294 | return jwt.Parse(tokStr, keyFunc) 295 | } 296 | 297 | return nil, ErrNoTokenInRequest 298 | } 299 | 300 | func TransactionFilter(ctx maze.IContext) error { 301 | return TM.Transaction(func(DB db.IDb) error { 302 | appCtx := ctx.(*AppCtx) 303 | appCtx.Store = DB 304 | p := appCtx.Principal 305 | if p != nil { 306 | appCtx.Store.SetAttribute(entity.ATTR_USERID, p.UserId) 307 | } 308 | 309 | return ctx.Proceed() 310 | }) 311 | } 312 | 313 | func NoTransactionFilter(ctx maze.IContext) error { 314 | return TM.NoTransaction(func(DB db.IDb) error { 315 | appCtx := ctx.(*AppCtx) 316 | appCtx.Store = DB 317 | 318 | return ctx.Proceed() 319 | }) 320 | } 321 | 322 | func PingFilter(ctx maze.IContext) error { 323 | ctx.GetResponse().Header().Set("Content-Type", "text/html; charset=utf-8") 324 | 325 | // TODO what happens if this is called right after the user submited a password change 326 | // and the reply was not yet delivered/processed 327 | 328 | app := ctx.(*AppCtx) 329 | p := app.Principal 330 | // check if version is valid - if the user changed the password the version changes 331 | var uid int64 332 | store := app.Store 333 | ok, err := store.Query(T.USER).Column(T.USER_C_ID). 334 | Where(T.USER_C_ID.Matches(p.UserId).And(T.USER_C_VERSION.Matches(p.Version))). 335 | SelectInto(&uid) 336 | if !ok { 337 | // version is different 338 | logger.Debugf("Unable to revalidate the token, because the user no longer exists or it was changed (different version)") 339 | http.Error(ctx.GetResponse(), "Unauthorized", http.StatusUnauthorized) 340 | return nil 341 | } else if err != nil { 342 | return err 343 | } 344 | tokenString, err := serializePrincipal(p) 345 | if err != nil { 346 | return err 347 | } 348 | ctx.GetResponse().Write([]byte(tokenString)) 349 | 350 | return nil 351 | } 352 | 353 | func LoginFilter(ctx maze.IContext) error { 354 | ctx.GetResponse().Header().Set("Content-Type", "text/html; charset=utf-8") 355 | 356 | //logger.Debugf("serving static(): " + ctx.GetRequest().URL.Path) 357 | username := ctx.GetRequest().FormValue("username") 358 | pass := ctx.GetRequest().FormValue("password") 359 | var err error 360 | if username != "" && pass != "" { 361 | store := ctx.(*AppCtx).Store 362 | // usernames are stored in lowercase 363 | username = strings.ToLower(username) 364 | var user entity.User 365 | var ok bool 366 | if ok, err = store.Query(T.USER). 367 | All(). 368 | Inner(T.USER_A_ROLES).Fetch(). 369 | Where( 370 | db.And(T.USER_C_USERNAME.Matches(username), 371 | T.USER_C_PASSWORD.Matches(pass))). 372 | SelectTree(&user); ok && err == nil { 373 | // role array 374 | roles := make([]lov.ERole, len(user.Roles)) 375 | for k, v := range user.Roles { 376 | roles[k] = v.Kind 377 | } 378 | tokenString, err := serializePrincipal(&Principal{ 379 | *user.Id, 380 | *user.Username, 381 | roles, 382 | *user.Version, 383 | }) 384 | if err != nil { 385 | return err 386 | } 387 | ctx.GetResponse().Write([]byte(tokenString)) 388 | } 389 | } 390 | 391 | return err 392 | } 393 | 394 | func AuthenticationFilter(ctx maze.IContext) error { 395 | p := deserializePrincipal(ctx.GetRequest()) 396 | if p != nil { 397 | // for authorizations and business logic 398 | ctx.(*AppCtx).Principal = p 399 | return ctx.Proceed() 400 | } else { 401 | logger.Debugf("Unable to proceed: invalid token!") 402 | http.Error(ctx.GetResponse(), "Unauthorized", http.StatusUnauthorized) 403 | } 404 | 405 | return nil 406 | } 407 | 408 | func ContextFactory(w http.ResponseWriter, r *http.Request, filters []*maze.Filter) maze.IContext { 409 | return NewAppCtx(w, r, filters, TaskBoardService) 410 | } 411 | 412 | // Gzip Compression 413 | type gzipResponseWriter struct { 414 | io.Writer 415 | http.ResponseWriter 416 | } 417 | 418 | func (w gzipResponseWriter) Write(b []byte) (int, error) { 419 | return w.Writer.Write(b) 420 | } 421 | 422 | func isHttps(r *http.Request) bool { 423 | return r.URL.Scheme == "https" || 424 | strings.HasPrefix(r.Proto, "HTTPS") || 425 | r.Header.Get("X-Forwarded-Proto") == "https" 426 | } 427 | 428 | // Create a Pool that contains previously used Writers and 429 | // can create new ones if we run out. 430 | var zippers = sync.Pool{New: func() interface{} { 431 | return gzip.NewWriter(nil) 432 | }} 433 | 434 | var zipexts = []string{".html", ".js", ".css", ".svg", ".xml"} 435 | 436 | // Limit limits the body of a post, compress response and format eventual errors 437 | func Limit(ctx maze.IContext) (err error) { 438 | r := ctx.GetRequest() 439 | // https only -- redirect in openshift 440 | if HttpsOnly && !isHttps(r) { 441 | url := "https://" + r.Host + r.RequestURI 442 | logger.Debugf("redirecting to %s", url) 443 | http.Redirect(ctx.GetResponse(), r, url, http.StatusMovedPermanently) 444 | return 445 | } 446 | 447 | /* 448 | Very Important: Before compressing the response, the "Content-Type" header must be properly set! 449 | */ 450 | // encodes only text files 451 | var zip bool 452 | var ext = filepath.Ext(r.URL.Path) 453 | for _, v := range zipexts { 454 | if v == ext { 455 | zip = true 456 | break 457 | } 458 | } 459 | // TODO gzip encoding should occour only after a size threshold 460 | if zip && strings.Contains(fmt.Sprint(r.Header["Accept-Encoding"]), "gzip") { 461 | appCtx := ctx.(*AppCtx) 462 | w := appCtx.Response 463 | w.Header().Set("Content-Encoding", "gzip") 464 | 465 | // Get a Writer from the Pool 466 | gz := zippers.Get().(*gzip.Writer) 467 | // When done, put the Writer back in to the Pool 468 | defer zippers.Put(gz) 469 | 470 | // We use Reset to set the writer we want to use. 471 | gz.Reset(w) 472 | defer gz.Close() 473 | 474 | appCtx.Response = gzipResponseWriter{Writer: gz, ResponseWriter: w} 475 | } 476 | 477 | defer func() { 478 | if r := recover(); r != nil { 479 | if e, ok := r.(runtime.Error); ok { 480 | logger.Errorf("%s\n========== Begin Stack Trace ==========\n%s\n========== End Stack Trace ==========\n", e, debug.Stack()) 481 | } 482 | err = formatError(ctx.GetResponse(), r.(error)) 483 | } 484 | }() 485 | 486 | logger.Debugf("requesting %s", r.URL.Path) 487 | r.Body = http.MaxBytesReader(ctx.GetResponse(), r.Body, postLimit) 488 | err = ctx.Proceed() 489 | if err != nil { 490 | err = formatError(ctx.GetResponse(), err) 491 | } 492 | return err 493 | } 494 | 495 | func formatError(w http.ResponseWriter, err error) error { 496 | switch t := err.(type) { 497 | case *web.HttpFail: 498 | if t.Status == http.StatusInternalServerError { 499 | jsonError(w, t.GetCode(), t.GetMessage()) 500 | } else { 501 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 502 | http.Error(w, t.Message, t.Status) 503 | } 504 | case *dbx.PersistenceFail: 505 | logger.Errorf("%s", err) 506 | jsonError(w, t.GetCode(), t.GetMessage()) 507 | case *dbx.OptimisticLockFail: 508 | logger.Errorf("%s", err) 509 | jsonError(w, t.GetCode(), t.GetMessage()) 510 | case tk.Fault: 511 | jsonError(w, t.GetCode(), t.GetMessage()) 512 | default: 513 | jsonError(w, UNKNOWN, err.Error()) 514 | } 515 | return nil 516 | } 517 | 518 | func jsonError(w http.ResponseWriter, code string, msg string) { 519 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 520 | jmsg, _ := json.Marshal(msg) 521 | http.Error(w, fmt.Sprintf(`{"code":"%s", "message":%s}`, code, jmsg), http.StatusInternalServerError) 522 | } 523 | -------------------------------------------------------------------------------- /go/lov/0_ERole.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Warning: Generated code! do not change! 3 | * Generated by: go/LOV.ftl 4 | */ 5 | package lov; 6 | 7 | import ( 8 | "database/sql" 9 | "database/sql/driver" 10 | ) 11 | 12 | type ERole string 13 | 14 | const ( 15 | ERole_ADMIN ERole = "ADMIN" 16 | ERole_USER ERole = "USER" 17 | ) 18 | 19 | var ERoles = [...]ERole{ERole_ADMIN, ERole_USER} 20 | 21 | func (this ERole) Parse(value string) (ERole, bool) { 22 | for _, element := range ERoles { 23 | v, _ := element.Value() 24 | if v == value { 25 | return element, true 26 | } 27 | } 28 | 29 | return ERole(""), false 30 | } 31 | 32 | func (this *ERole) Scan(value interface{}) error { 33 | var x sql.NullString 34 | err := x.Scan(value) 35 | if !x.Valid || err != nil { 36 | return err 37 | } 38 | 39 | *this = ERole(x.String) 40 | return nil 41 | } 42 | 43 | // Value implements the driver Valuer interface. 44 | func (this ERole) Value() (driver.Value, error) { 45 | return string(this), nil 46 | } 47 | -------------------------------------------------------------------------------- /go/service/0_ITaskBoardService.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code! do not change! 3 | * Generated by: go/IService.ftl 4 | */ 5 | package service 6 | 7 | import ( 8 | "github.com/quintans/maze" 9 | "github.com/quintans/taskboard/go/dto" 10 | "github.com/quintans/taskboard/go/entity" 11 | "github.com/quintans/toolkit/web/app" 12 | ) 13 | 14 | type ITaskBoardService interface { 15 | 16 | // return 17 | WhoAmI(c maze.IContext) (dto.IdentityDTO, error) 18 | 19 | // return only the users that belong to board 20 | // 21 | // param id 22 | // return 23 | FetchBoardUsers(c maze.IContext, id int64) ([]dto.BoardUserDTO, error) 24 | 25 | // return all users and flag them if they belong to board 26 | // 27 | // param criteria 28 | // return 29 | FetchBoardAllUsers(c maze.IContext, criteria dto.BoardUserSearchDTO) (app.Page, error) 30 | 31 | // param criteria 32 | // return 33 | FetchBoards(c maze.IContext, criteria dto.BoardSearchDTO) (app.Page, error) 34 | 35 | // param id 36 | // return 37 | FetchBoardById(c maze.IContext, id int64) (*entity.Board, error) 38 | 39 | // param id 40 | // return 41 | FullyLoadBoardById(c maze.IContext, id int64) (*entity.Board, error) 42 | 43 | // param board 44 | // return 45 | SaveBoard(c maze.IContext, board *entity.Board) (*entity.Board, error) 46 | 47 | // param id 48 | // return 49 | DeleteBoard(c maze.IContext, id int64) error 50 | 51 | // param boardId 52 | // return 53 | AddLane(c maze.IContext, boardId int64) error 54 | 55 | // param lane 56 | // return 57 | SaveLane(c maze.IContext, lane *entity.Lane) (*entity.Lane, error) 58 | 59 | // param boardId 60 | // return 61 | DeleteLastLane(c maze.IContext, boardId int64) error 62 | 63 | // param user 64 | // return 65 | SaveUser(c maze.IContext, user dto.UserDTO) (bool, error) 66 | 67 | // param criteria 68 | // return 69 | FetchUsers(c maze.IContext, criteria dto.UserSearchDTO) (app.Page, error) 70 | 71 | // param user 72 | // return 73 | DeleteUser(c maze.IContext, user dto.IdVersionDTO) error 74 | 75 | // param boardId 76 | // param userId 77 | // return 78 | AddUserToBoard(c maze.IContext, input AddUserToBoardIn) error 79 | 80 | // param boardId 81 | // param userId 82 | // return 83 | RemoveUserFromBoard(c maze.IContext, input RemoveUserFromBoardIn) error 84 | 85 | // param name 86 | // return 87 | SaveUserName(c maze.IContext, name *string) error 88 | 89 | // param oldPwd 90 | // param newPwd 91 | // return 92 | ChangeUserPassword(c maze.IContext, input ChangeUserPasswordIn) (string, error) 93 | 94 | // param task 95 | // return 96 | SaveTask(c maze.IContext, task *entity.Task) (*entity.Task, error) 97 | 98 | // param taskId 99 | // param laneId 100 | // param position position = -1 is to eliminate 101 | // return 102 | MoveTask(c maze.IContext, input MoveTaskIn) error 103 | 104 | // param criteria 105 | // return 106 | FetchNotifications(c maze.IContext, criteria dto.NotificationSearchDTO) (app.Page, error) 107 | 108 | // if it returns null, it means the notification was not saved due a matching one 109 | // 110 | // param notification 111 | // return 112 | SaveNotification(c maze.IContext, notification *entity.Notification) (*entity.Notification, error) 113 | 114 | // param id 115 | // return 116 | DeleteNotification(c maze.IContext, id int64) error 117 | } 118 | -------------------------------------------------------------------------------- /go/service/0_TaskBoardServiceIn.go: -------------------------------------------------------------------------------- 1 | /** 2 | * WARNING: Generated code. Changes will be overwritten. 3 | * Generated by: go/ServiceOperationIn.ftl 4 | */ 5 | package service 6 | 7 | 8 | type AddUserToBoardIn struct { 9 | BoardId int64 `json:"boardId"` 10 | UserId int64 `json:"userId"` 11 | } 12 | 13 | type RemoveUserFromBoardIn struct { 14 | BoardId int64 `json:"boardId"` 15 | UserId int64 `json:"userId"` 16 | } 17 | 18 | type ChangeUserPasswordIn struct { 19 | OldPwd string `json:"oldPwd"` 20 | NewPwd string `json:"newPwd"` 21 | } 22 | 23 | type MoveTaskIn struct { 24 | TaskId int64 `json:"taskId"` 25 | LaneId int64 `json:"laneId"` 26 | // position = -1 is to eliminate 27 | Position int64 `json:"position"` 28 | } 29 | -------------------------------------------------------------------------------- /go/tables/0_TBoard.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Warning: Generated code! do not change! 3 | * Generated by: go/EntityDB.ftl 4 | */ 5 | package tables 6 | import ( 7 | "github.com/quintans/goSQL/db" 8 | "github.com/quintans/taskboard/go/entity" 9 | . "github.com/quintans/toolkit/ext" 10 | ) 11 | 12 | var ( 13 | BOARD = db.TABLE("BOARD") 14 | BOARD_C_ID = BOARD.KEY("ID") 15 | BOARD_C_VERSION = BOARD.VERSION("VERSION") 16 | // Audit 17 | BOARD_C_CREATION = BOARD.COLUMN("CREATION") 18 | BOARD_C_MODIFICATION = BOARD.COLUMN("MODIFICATION") 19 | BOARD_C_USER_CREATION = BOARD.COLUMN("USER_CREATION").As("UserCreationId") 20 | BOARD_C_USER_MODIFICATION = BOARD.COLUMN("USER_MODIFICATION").As("UserModificationId") 21 | // Atributos 22 | BOARD_C_NAME = BOARD.COLUMN("NAME") 23 | BOARD_C_DESCRIPTION = BOARD.COLUMN("DESCRIPTION") 24 | //FK's 25 | BOARD_A_LANES = BOARD. 26 | ASSOCIATE(BOARD_C_ID).TO(LANE_C_BOARD_ID). 27 | As("Lanes") 28 | 29 | // intermediary table 30 | BOARD_USER = db.TABLE("USERS").As("Users") 31 | BOARD_USER_C_USERS_ID = BOARD_USER.KEY("USER").As("Users") 32 | BOARD_USER_C_BOARDS_ID = BOARD_USER.KEY("BOARD").As("Boards") 33 | 34 | // many to many 35 | BOARD_A_USERS = db.NewM2MAssociation( 36 | "users", 37 | db.ASSOCIATE(BOARD_C_ID).WITH(BOARD_USER_C_BOARDS_ID), 38 | db.ASSOCIATE(BOARD_USER_C_USERS_ID).WITH(USER_C_ID), 39 | ) 40 | 41 | BOARD_A_USER_CREATION = BOARD.ASSOCIATE(BOARD_C_USER_CREATION).TO(USER_C_ID).As("UserCreation") 42 | BOARD_A_USER_MODIFICATION = BOARD.ASSOCIATE(BOARD_C_USER_MODIFICATION).TO(USER_C_ID).As("UserModification") 43 | ) 44 | 45 | func init() { 46 | BOARD.PreInsertTrigger = func(ins *db.Insert) { 47 | ins.Set(BOARD_C_VERSION, 1) 48 | ins.Set(BOARD_C_CREATION, NOW()) 49 | uid, ok := ins.GetDb().GetAttribute(entity.ATTR_USERID) 50 | if ok { 51 | ins.Set(BOARD_C_USER_CREATION, uid.(int64)) 52 | } 53 | } 54 | BOARD.PreUpdateTrigger = func(upd *db.Update) { 55 | upd.Set(BOARD_C_MODIFICATION, NOW()) 56 | uid, ok := upd.GetDb().GetAttribute(entity.ATTR_USERID) 57 | if ok { 58 | upd.Set(BOARD_C_USER_MODIFICATION, uid.(int64)) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /go/tables/0_TLane.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Warning: Generated code! do not change! 3 | * Generated by: go/EntityDB.ftl 4 | */ 5 | package tables 6 | import ( 7 | "github.com/quintans/goSQL/db" 8 | "github.com/quintans/taskboard/go/entity" 9 | . "github.com/quintans/toolkit/ext" 10 | ) 11 | 12 | var ( 13 | LANE = db.TABLE("LANE") 14 | LANE_C_ID = LANE.KEY("ID") 15 | LANE_C_VERSION = LANE.VERSION("VERSION") 16 | // Audit 17 | LANE_C_CREATION = LANE.COLUMN("CREATION") 18 | LANE_C_MODIFICATION = LANE.COLUMN("MODIFICATION") 19 | LANE_C_USER_CREATION = LANE.COLUMN("USER_CREATION").As("UserCreationId") 20 | LANE_C_USER_MODIFICATION = LANE.COLUMN("USER_MODIFICATION").As("UserModificationId") 21 | // Atributos 22 | LANE_C_NAME = LANE.COLUMN("NAME") 23 | LANE_C_POSITION = LANE.COLUMN("POSITION") 24 | LANE_C_BOARD_ID = LANE.COLUMN("BOARD").As("BoardId") 25 | //FK's 26 | LANE_A_BOARD = LANE. 27 | ASSOCIATE(LANE_C_BOARD_ID).TO(BOARD_C_ID). 28 | As("Board") 29 | LANE_A_TASKS = LANE. 30 | ASSOCIATE(LANE_C_ID).TO(TASK_C_LANE_ID). 31 | As("Tasks") 32 | LANE_A_NOTIFICATIONS = LANE. 33 | ASSOCIATE(LANE_C_ID).TO(NOTIFICATION_C_LANE_ID). 34 | As("Notifications") 35 | LANE_A_USER_CREATION = LANE.ASSOCIATE(LANE_C_USER_CREATION).TO(USER_C_ID).As("UserCreation") 36 | LANE_A_USER_MODIFICATION = LANE.ASSOCIATE(LANE_C_USER_MODIFICATION).TO(USER_C_ID).As("UserModification") 37 | ) 38 | 39 | func init() { 40 | LANE.PreInsertTrigger = func(ins *db.Insert) { 41 | ins.Set(LANE_C_VERSION, 1) 42 | ins.Set(LANE_C_CREATION, NOW()) 43 | uid, ok := ins.GetDb().GetAttribute(entity.ATTR_USERID) 44 | if ok { 45 | ins.Set(LANE_C_USER_CREATION, uid.(int64)) 46 | } 47 | } 48 | LANE.PreUpdateTrigger = func(upd *db.Update) { 49 | upd.Set(LANE_C_MODIFICATION, NOW()) 50 | uid, ok := upd.GetDb().GetAttribute(entity.ATTR_USERID) 51 | if ok { 52 | upd.Set(LANE_C_USER_MODIFICATION, uid.(int64)) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /go/tables/0_TNotification.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Warning: Generated code! do not change! 3 | * Generated by: go/EntityDB.ftl 4 | */ 5 | package tables 6 | import ( 7 | "github.com/quintans/goSQL/db" 8 | "github.com/quintans/taskboard/go/entity" 9 | . "github.com/quintans/toolkit/ext" 10 | ) 11 | 12 | var ( 13 | NOTIFICATION = db.TABLE("NOTIFICATION") 14 | NOTIFICATION_C_ID = NOTIFICATION.KEY("ID") 15 | NOTIFICATION_C_VERSION = NOTIFICATION.VERSION("VERSION") 16 | // Audit 17 | NOTIFICATION_C_CREATION = NOTIFICATION.COLUMN("CREATION") 18 | NOTIFICATION_C_MODIFICATION = NOTIFICATION.COLUMN("MODIFICATION") 19 | NOTIFICATION_C_USER_CREATION = NOTIFICATION.COLUMN("USER_CREATION").As("UserCreationId") 20 | NOTIFICATION_C_USER_MODIFICATION = NOTIFICATION.COLUMN("USER_MODIFICATION").As("UserModificationId") 21 | // Atributos 22 | NOTIFICATION_C_EMAIL = NOTIFICATION.COLUMN("EMAIL") 23 | NOTIFICATION_C_TASK_ID = NOTIFICATION.COLUMN("TASK").As("TaskId") 24 | NOTIFICATION_C_LANE_ID = NOTIFICATION.COLUMN("LANE").As("LaneId") 25 | //FK's 26 | NOTIFICATION_A_TASK = NOTIFICATION. 27 | ASSOCIATE(NOTIFICATION_C_TASK_ID).TO(TASK_C_ID). 28 | As("Task") 29 | NOTIFICATION_A_LANE = NOTIFICATION. 30 | ASSOCIATE(NOTIFICATION_C_LANE_ID).TO(LANE_C_ID). 31 | As("Lane") 32 | NOTIFICATION_A_USER_CREATION = NOTIFICATION.ASSOCIATE(NOTIFICATION_C_USER_CREATION).TO(USER_C_ID).As("UserCreation") 33 | NOTIFICATION_A_USER_MODIFICATION = NOTIFICATION.ASSOCIATE(NOTIFICATION_C_USER_MODIFICATION).TO(USER_C_ID).As("UserModification") 34 | ) 35 | 36 | func init() { 37 | NOTIFICATION.PreInsertTrigger = func(ins *db.Insert) { 38 | ins.Set(NOTIFICATION_C_VERSION, 1) 39 | ins.Set(NOTIFICATION_C_CREATION, NOW()) 40 | uid, ok := ins.GetDb().GetAttribute(entity.ATTR_USERID) 41 | if ok { 42 | ins.Set(NOTIFICATION_C_USER_CREATION, uid.(int64)) 43 | } 44 | } 45 | NOTIFICATION.PreUpdateTrigger = func(upd *db.Update) { 46 | upd.Set(NOTIFICATION_C_MODIFICATION, NOW()) 47 | uid, ok := upd.GetDb().GetAttribute(entity.ATTR_USERID) 48 | if ok { 49 | upd.Set(NOTIFICATION_C_USER_MODIFICATION, uid.(int64)) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /go/tables/0_TRole.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Warning: Generated code! do not change! 3 | * Generated by: go/EntityDB.ftl 4 | */ 5 | package tables 6 | import ( 7 | "github.com/quintans/goSQL/db" 8 | "github.com/quintans/taskboard/go/entity" 9 | . "github.com/quintans/toolkit/ext" 10 | ) 11 | 12 | var ( 13 | ROLE = db.TABLE("ROLE") 14 | ROLE_C_ID = ROLE.KEY("ID") 15 | ROLE_C_VERSION = ROLE.VERSION("VERSION") 16 | // Audit 17 | ROLE_C_CREATION = ROLE.COLUMN("CREATION") 18 | ROLE_C_MODIFICATION = ROLE.COLUMN("MODIFICATION") 19 | ROLE_C_USER_CREATION = ROLE.COLUMN("USER_CREATION").As("UserCreationId") 20 | ROLE_C_USER_MODIFICATION = ROLE.COLUMN("USER_MODIFICATION").As("UserModificationId") 21 | // Atributos 22 | ROLE_C_KIND = ROLE.COLUMN("KIND") 23 | ROLE_C_USER_ID = ROLE.COLUMN("USER").As("UserId") 24 | //FK's 25 | ROLE_A_USER = ROLE. 26 | ASSOCIATE(ROLE_C_USER_ID).TO(USER_C_ID). 27 | As("User") 28 | ROLE_A_USER_CREATION = ROLE.ASSOCIATE(ROLE_C_USER_CREATION).TO(USER_C_ID).As("UserCreation") 29 | ROLE_A_USER_MODIFICATION = ROLE.ASSOCIATE(ROLE_C_USER_MODIFICATION).TO(USER_C_ID).As("UserModification") 30 | ) 31 | 32 | func init() { 33 | ROLE.PreInsertTrigger = func(ins *db.Insert) { 34 | ins.Set(ROLE_C_VERSION, 1) 35 | ins.Set(ROLE_C_CREATION, NOW()) 36 | uid, ok := ins.GetDb().GetAttribute(entity.ATTR_USERID) 37 | if ok { 38 | ins.Set(ROLE_C_USER_CREATION, uid.(int64)) 39 | } 40 | } 41 | ROLE.PreUpdateTrigger = func(upd *db.Update) { 42 | upd.Set(ROLE_C_MODIFICATION, NOW()) 43 | uid, ok := upd.GetDb().GetAttribute(entity.ATTR_USERID) 44 | if ok { 45 | upd.Set(ROLE_C_USER_MODIFICATION, uid.(int64)) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /go/tables/0_TTask.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Warning: Generated code! do not change! 3 | * Generated by: go/EntityDB.ftl 4 | */ 5 | package tables 6 | import ( 7 | "github.com/quintans/goSQL/db" 8 | "github.com/quintans/taskboard/go/entity" 9 | . "github.com/quintans/toolkit/ext" 10 | ) 11 | 12 | var ( 13 | TASK = db.TABLE("TASK") 14 | TASK_C_ID = TASK.KEY("ID") 15 | TASK_C_VERSION = TASK.VERSION("VERSION") 16 | // Audit 17 | TASK_C_CREATION = TASK.COLUMN("CREATION") 18 | TASK_C_MODIFICATION = TASK.COLUMN("MODIFICATION") 19 | TASK_C_USER_CREATION = TASK.COLUMN("USER_CREATION").As("UserCreationId") 20 | TASK_C_USER_MODIFICATION = TASK.COLUMN("USER_MODIFICATION").As("UserModificationId") 21 | // Atributos 22 | TASK_C_TITLE = TASK.COLUMN("TITLE") 23 | TASK_C_DETAIL = TASK.COLUMN("DETAIL") 24 | TASK_C_HEAD_COLOR = TASK.COLUMN("HEAD_COLOR") 25 | TASK_C_BODY_COLOR = TASK.COLUMN("BODY_COLOR") 26 | TASK_C_POSITION = TASK.COLUMN("POSITION") 27 | TASK_C_REFERENCE = TASK.COLUMN("REFERENCE") 28 | TASK_C_SPENT = TASK.COLUMN("SPENT") 29 | TASK_C_REMAINING = TASK.COLUMN("REMAINING") 30 | // who is with this task 31 | TASK_C_USER_ID = TASK.COLUMN("USER").As("UserId") 32 | TASK_C_LANE_ID = TASK.COLUMN("LANE").As("LaneId") 33 | //FK's 34 | // who is with this task 35 | TASK_A_USER = TASK. 36 | ASSOCIATE(TASK_C_USER_ID).TO(USER_C_ID). 37 | As("User") 38 | TASK_A_LANE = TASK. 39 | ASSOCIATE(TASK_C_LANE_ID).TO(LANE_C_ID). 40 | As("Lane") 41 | TASK_A_NOTIFICATIONS = TASK. 42 | ASSOCIATE(TASK_C_ID).TO(NOTIFICATION_C_TASK_ID). 43 | As("Notifications") 44 | TASK_A_USER_CREATION = TASK.ASSOCIATE(TASK_C_USER_CREATION).TO(USER_C_ID).As("UserCreation") 45 | TASK_A_USER_MODIFICATION = TASK.ASSOCIATE(TASK_C_USER_MODIFICATION).TO(USER_C_ID).As("UserModification") 46 | ) 47 | 48 | func init() { 49 | TASK.PreInsertTrigger = func(ins *db.Insert) { 50 | ins.Set(TASK_C_VERSION, 1) 51 | ins.Set(TASK_C_CREATION, NOW()) 52 | uid, ok := ins.GetDb().GetAttribute(entity.ATTR_USERID) 53 | if ok { 54 | ins.Set(TASK_C_USER_CREATION, uid.(int64)) 55 | } 56 | } 57 | TASK.PreUpdateTrigger = func(upd *db.Update) { 58 | upd.Set(TASK_C_MODIFICATION, NOW()) 59 | uid, ok := upd.GetDb().GetAttribute(entity.ATTR_USERID) 60 | if ok { 61 | upd.Set(TASK_C_USER_MODIFICATION, uid.(int64)) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /go/tables/0_TUser.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Warning: Generated code! do not change! 3 | * Generated by: go/EntityDB.ftl 4 | */ 5 | package tables 6 | import ( 7 | "github.com/quintans/goSQL/db" 8 | "github.com/quintans/taskboard/go/entity" 9 | . "github.com/quintans/toolkit/ext" 10 | ) 11 | 12 | var ( 13 | USER = db.TABLE("USER") 14 | USER_C_ID = USER.KEY("ID") 15 | USER_C_VERSION = USER.VERSION("VERSION") 16 | // Audit 17 | USER_C_CREATION = USER.COLUMN("CREATION") 18 | USER_C_MODIFICATION = USER.COLUMN("MODIFICATION") 19 | USER_C_USER_CREATION = USER.COLUMN("USER_CREATION").As("UserCreationId") 20 | USER_C_USER_MODIFICATION = USER.COLUMN("USER_MODIFICATION").As("UserModificationId") 21 | // Atributos 22 | USER_C_NAME = USER.COLUMN("NAME") 23 | USER_C_USERNAME = USER.COLUMN("USERNAME") 24 | USER_C_PASSWORD = USER.COLUMN("PASSWORD") 25 | //FK's 26 | // many to many 27 | USER_A_BOARDS = db.NewM2MAssociation( 28 | "boards", 29 | db.ASSOCIATE(USER_C_ID).WITH(BOARD_USER_C_USERS_ID), 30 | db.ASSOCIATE(BOARD_USER_C_BOARDS_ID).WITH(BOARD_C_ID), 31 | ) 32 | 33 | USER_A_ROLES = USER. 34 | ASSOCIATE(USER_C_ID).TO(ROLE_C_USER_ID). 35 | As("Roles") 36 | USER_A_TASKS = USER. 37 | ASSOCIATE(USER_C_ID).TO(TASK_C_USER_ID). 38 | As("Tasks") 39 | USER_A_USER_CREATION = USER.ASSOCIATE(USER_C_USER_CREATION).TO(USER_C_ID).As("UserCreation") 40 | USER_A_USER_MODIFICATION = USER.ASSOCIATE(USER_C_USER_MODIFICATION).TO(USER_C_ID).As("UserModification") 41 | ) 42 | 43 | func init() { 44 | USER.PreInsertTrigger = func(ins *db.Insert) { 45 | ins.Set(USER_C_VERSION, 1) 46 | ins.Set(USER_C_CREATION, NOW()) 47 | uid, ok := ins.GetDb().GetAttribute(entity.ATTR_USERID) 48 | if ok { 49 | ins.Set(USER_C_USER_CREATION, uid.(int64)) 50 | } 51 | } 52 | USER.PreUpdateTrigger = func(upd *db.Update) { 53 | upd.Set(USER_C_MODIFICATION, NOW()) 54 | uid, ok := upd.GetDb().GetAttribute(entity.ATTR_USERID) 55 | if ok { 56 | upd.Set(USER_C_USER_MODIFICATION, uid.(int64)) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quintans/maze" 5 | "github.com/quintans/taskboard/go/impl" 6 | "github.com/quintans/toolkit/log" 7 | 8 | "runtime" 9 | "runtime/debug" 10 | "time" 11 | ) 12 | 13 | func main() { 14 | runtime.GOMAXPROCS(runtime.NumCPU()) 15 | 16 | logger := log.LoggerFor("taskboad") 17 | 18 | defer func() { 19 | err := recover() 20 | if err != nil { 21 | logger.Errorf("%s\n%s\n", err, debug.Stack()) 22 | } 23 | // give time for the loggers to write 24 | time.Sleep(100 * time.Millisecond) 25 | }() 26 | 27 | var app = App{} 28 | app.ContextFactory = impl.ContextFactory 29 | app.Limit = impl.Limit 30 | app.AuthenticationFilter = impl.AuthenticationFilter 31 | app.ResponseBuffer = maze.ResponseBuffer 32 | app.TransactionFilter = impl.TransactionFilter 33 | app.LoginFilter = impl.LoginFilter 34 | app.PingFilter = impl.PingFilter 35 | app.Poll = impl.Poll 36 | app.IpPort = impl.IpPort 37 | 38 | appCtx := impl.NewAppCtx(nil, nil, nil, impl.TaskBoardService) 39 | // service factory 40 | app.JsonRpc = appCtx.BuildJsonRpcTaskBoardService(app.TransactionFilter) 41 | 42 | app.ContentDir = impl.ContentDir 43 | 44 | app.Start() 45 | } 46 | -------------------------------------------------------------------------------- /sql/01_create.sql: -------------------------------------------------------------------------------- 1 | DROP DATABASE IF EXISTS taskboard; 2 | CREATE DATABASE taskboard; 3 | CREATE USER IF NOT EXISTS 'tb' IDENTIFIED BY 'tb'; 4 | GRANT ALL PRIVILEGES ON taskboard.* TO 'tb'; 5 | 6 | -------------------------------------------------------------------------------- /sql/02_taskboard.sql: -------------------------------------------------------------------------------- 1 | -- table for the entity - "Board[board]" 2 | CREATE TABLE `BOARD` ( 3 | ID BIGINT NOT NULL AUTO_INCREMENT, 4 | PRIMARY KEY(ID), 5 | `NAME` VARCHAR(255), 6 | `DESCRIPTION` VARCHAR(255), 7 | `CREATION` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | `MODIFICATION` TIMESTAMP NULL, 9 | USER_CREATION BIGINT NOT NULL, 10 | USER_MODIFICATION BIGINT, 11 | `VERSION` INTEGER NOT NULL 12 | ) 13 | ENGINE=InnoDB 14 | DEFAULT CHARSET=utf8; 15 | 16 | 17 | -- table between Board and User 18 | CREATE TABLE `USERS` ( 19 | `BOARD` BIGINT NOT NULL 20 | ,`USER` BIGINT NOT NULL 21 | ) 22 | ENGINE=InnoDB 23 | DEFAULT CHARSET=utf8; 24 | 25 | -- table for the entity - "Lane[lane]" 26 | CREATE TABLE `LANE` ( 27 | ID BIGINT NOT NULL AUTO_INCREMENT, 28 | PRIMARY KEY(ID), 29 | `NAME` VARCHAR(255), 30 | `POSITION` BIGINT, 31 | `BOARD` BIGINT, 32 | `CREATION` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 33 | `MODIFICATION` TIMESTAMP NULL, 34 | USER_CREATION BIGINT NOT NULL, 35 | USER_MODIFICATION BIGINT, 36 | `VERSION` INTEGER NOT NULL 37 | ) 38 | ENGINE=InnoDB 39 | DEFAULT CHARSET=utf8; 40 | 41 | 42 | -- table for the entity - "User[user]" 43 | CREATE TABLE `USER` ( 44 | ID BIGINT NOT NULL AUTO_INCREMENT, 45 | PRIMARY KEY(ID), 46 | `NAME` VARCHAR(255), 47 | `USERNAME` VARCHAR(255), 48 | `PASSWORD` VARCHAR(255), 49 | `CREATION` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 50 | `MODIFICATION` TIMESTAMP NULL, 51 | USER_CREATION BIGINT NOT NULL, 52 | USER_MODIFICATION BIGINT, 53 | `VERSION` INTEGER NOT NULL 54 | ) 55 | ENGINE=InnoDB 56 | DEFAULT CHARSET=utf8; 57 | 58 | 59 | -- table for the entity - "Task[task]" 60 | CREATE TABLE `TASK` ( 61 | ID BIGINT NOT NULL AUTO_INCREMENT, 62 | PRIMARY KEY(ID), 63 | `TITLE` VARCHAR(255), 64 | `DETAIL` VARCHAR(10000), 65 | `HEAD_COLOR` VARCHAR(255), 66 | `BODY_COLOR` VARCHAR(255), 67 | `POSITION` BIGINT, 68 | `REFERENCE` VARCHAR(255), 69 | `SPENT` BIGINT, 70 | `REMAINING` BIGINT, 71 | `USER` BIGINT, 72 | `LANE` BIGINT, 73 | `CREATION` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 74 | `MODIFICATION` TIMESTAMP NULL, 75 | USER_CREATION BIGINT NOT NULL, 76 | USER_MODIFICATION BIGINT, 77 | `VERSION` INTEGER NOT NULL 78 | ) 79 | ENGINE=InnoDB 80 | DEFAULT CHARSET=utf8; 81 | 82 | 83 | -- table for the entity - "Notification[notification]" 84 | CREATE TABLE `NOTIFICATION` ( 85 | ID BIGINT NOT NULL AUTO_INCREMENT, 86 | PRIMARY KEY(ID), 87 | `EMAIL` VARCHAR(255) NOT NULL, 88 | `TASK` BIGINT NOT NULL, 89 | `LANE` BIGINT NOT NULL, 90 | `CREATION` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 91 | `MODIFICATION` TIMESTAMP NULL, 92 | USER_CREATION BIGINT NOT NULL, 93 | USER_MODIFICATION BIGINT, 94 | `VERSION` INTEGER NOT NULL 95 | ) 96 | ENGINE=InnoDB 97 | DEFAULT CHARSET=utf8; 98 | 99 | 100 | -- table for the entity - "Role[role]" 101 | CREATE TABLE `ROLE` ( 102 | ID BIGINT NOT NULL AUTO_INCREMENT, 103 | PRIMARY KEY(ID), 104 | `KIND` VARCHAR(50) NOT NULL, 105 | `USER` BIGINT, 106 | `CREATION` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 107 | `MODIFICATION` TIMESTAMP NULL, 108 | USER_CREATION BIGINT NOT NULL, 109 | USER_MODIFICATION BIGINT, 110 | `VERSION` INTEGER NOT NULL 111 | ) 112 | ENGINE=InnoDB 113 | DEFAULT CHARSET=utf8; 114 | 115 | 116 | -- CREATING ENTITY CONSTRAINTS "Board" 117 | ALTER TABLE `BOARD` ADD CONSTRAINT UC_BOARD FOREIGN KEY (USER_CREATION) REFERENCES `USER` (ID); 118 | ALTER TABLE `BOARD` ADD CONSTRAINT UM_BOARD FOREIGN KEY (USER_MODIFICATION) REFERENCES `USER` (ID); 119 | ALTER TABLE `BOARD` ADD CONSTRAINT UK_BOARD1 UNIQUE (`NAME`); 120 | ALTER TABLE `BOARD` ADD CONSTRAINT UK_BOARD2 UNIQUE (`DESCRIPTION`); 121 | -- FKs da tabela intermedia BOARD + USER 122 | -- lado A (OWNER) 123 | ALTER TABLE `USERS` ADD CONSTRAINT FK_USERS1 FOREIGN KEY (`BOARD`) REFERENCES `BOARD` (ID); 124 | -- lado B 125 | ALTER TABLE `USERS` ADD CONSTRAINT FK_USERS2 FOREIGN KEY (`USER`) REFERENCES `USER` (ID); 126 | -- a combinacao lado A lado B eh unica 127 | ALTER TABLE `USERS` ADD CONSTRAINT UK_USERS UNIQUE (`BOARD`, `USER`); 128 | -- CREATING ENTITY CONSTRAINTS "Lane" 129 | ALTER TABLE `LANE` ADD CONSTRAINT UC_LANE FOREIGN KEY (USER_CREATION) REFERENCES `USER` (ID); 130 | ALTER TABLE `LANE` ADD CONSTRAINT UM_LANE FOREIGN KEY (USER_MODIFICATION) REFERENCES `USER` (ID); 131 | ALTER TABLE `LANE` ADD CONSTRAINT UK_POS UNIQUE (`BOARD`, `POSITION`); 132 | ALTER TABLE `LANE` ADD CONSTRAINT FK_LANE1 FOREIGN KEY (`BOARD`) REFERENCES `BOARD` (ID); 133 | -- CREATING ENTITY CONSTRAINTS "User" 134 | ALTER TABLE `USER` ADD CONSTRAINT UC_USER FOREIGN KEY (USER_CREATION) REFERENCES `USER` (ID); 135 | ALTER TABLE `USER` ADD CONSTRAINT UM_USER FOREIGN KEY (USER_MODIFICATION) REFERENCES `USER` (ID); 136 | ALTER TABLE `USER` ADD CONSTRAINT UK_USER1 UNIQUE (`USERNAME`); 137 | -- CREATING ENTITY CONSTRAINTS "Task" 138 | ALTER TABLE `TASK` ADD CONSTRAINT UC_TASK FOREIGN KEY (USER_CREATION) REFERENCES `USER` (ID); 139 | ALTER TABLE `TASK` ADD CONSTRAINT UM_TASK FOREIGN KEY (USER_MODIFICATION) REFERENCES `USER` (ID); 140 | ALTER TABLE `TASK` ADD CONSTRAINT UK_POS UNIQUE (`LANE`, `POSITION`); 141 | ALTER TABLE `TASK` ADD CONSTRAINT FK_TASK1 FOREIGN KEY (`USER`) REFERENCES `USER` (ID); 142 | ALTER TABLE `TASK` ADD CONSTRAINT FK_TASK2 FOREIGN KEY (`LANE`) REFERENCES `LANE` (ID); 143 | -- CREATING ENTITY CONSTRAINTS "Notification" 144 | ALTER TABLE `NOTIFICATION` ADD CONSTRAINT UC_NOTIFICATION FOREIGN KEY (USER_CREATION) REFERENCES `USER` (ID); 145 | ALTER TABLE `NOTIFICATION` ADD CONSTRAINT UM_NOTIFICATION FOREIGN KEY (USER_MODIFICATION) REFERENCES `USER` (ID); 146 | ALTER TABLE `NOTIFICATION` ADD CONSTRAINT FK_NOTIFICATION1 FOREIGN KEY (`TASK`) REFERENCES `TASK` (ID); 147 | ALTER TABLE `NOTIFICATION` ADD CONSTRAINT FK_NOTIFICATION2 FOREIGN KEY (`LANE`) REFERENCES `LANE` (ID); 148 | -- CREATING ENTITY CONSTRAINTS "Role" 149 | ALTER TABLE `ROLE` ADD CONSTRAINT UC_ROLE FOREIGN KEY (USER_CREATION) REFERENCES `USER` (ID); 150 | ALTER TABLE `ROLE` ADD CONSTRAINT UM_ROLE FOREIGN KEY (USER_MODIFICATION) REFERENCES `USER` (ID); 151 | ALTER TABLE `ROLE` ADD CONSTRAINT UK_USER_KIND UNIQUE (`USER`, `KIND`); 152 | ALTER TABLE `ROLE` ADD CONSTRAINT FK_ROLE1 FOREIGN KEY (`USER`) REFERENCES `USER` (ID); 153 | -------------------------------------------------------------------------------- /sql/03_populate.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO taskboard.USER (ID,NAME,USERNAME,PASSWORD,VERSION,CREATION,MODIFICATION,USER_CREATION,USER_MODIFICATION) VALUES 2 | (1,'Admin','admin','admin',2,STR_TO_DATE('2015-03-15 19:33:40','%Y-%m-%d %H:%i:%s'),STR_TO_DATE('2015-03-15 19:33:40','%Y-%m-%d %H:%i:%s'),1,1) 3 | ; 4 | 5 | INSERT INTO taskboard.ROLE (ID,KIND,USER,VERSION,CREATION,MODIFICATION,USER_CREATION,USER_MODIFICATION) VALUES 6 | (1,'USER',1,1,STR_TO_DATE('2015-02-02 22:37:21','%Y-%m-%d %H:%i:%s'),STR_TO_DATE('2015-02-02 22:37:29','%Y-%m-%d %H:%i:%s'),1,NULL) 7 | ,(2,'ADMIN',1,1,STR_TO_DATE('2015-02-02 22:37:21','%Y-%m-%d %H:%i:%s'),STR_TO_DATE('2015-02-02 22:37:29','%Y-%m-%d %H:%i:%s'),1,NULL) 8 | ; 9 | 10 | -------------------------------------------------------------------------------- /taskboard.ini: -------------------------------------------------------------------------------- 1 | [web] 2 | dir=./www 3 | ip_port=:9090 4 | https_only=false 5 | # 10MB 6 | post_limit=10485760 7 | 8 | [log] 9 | # DEBUG, INFO, WARN, ERROR, FATAL, NONE 10 | level=DEBUG 11 | file=taskboard.log 12 | count=5 13 | # 10MB 14 | size=10485760 15 | console=true 16 | 17 | [database] 18 | driver_name=mysql 19 | data_source_name=tb:tb@/taskboard?parseTime=true 20 | statement_cache=1000 21 | idle_connections=2 22 | 23 | [smtp] 24 | #host=smtp.gmail.com 25 | #port=587 26 | #user= 27 | #pass= 28 | #host=172.18.8.4 29 | #port=25 30 | #user= 31 | #pass= 32 | #from=as-idscm@efacec.com 33 | -------------------------------------------------------------------------------- /taskboard.ini.template: -------------------------------------------------------------------------------- 1 | [web] 2 | dir=./www 3 | ip_port=:9090 4 | https_only=false 5 | # 10MB 6 | post_limit=10485760 7 | 8 | [log] 9 | # DEBUG, INFO, WARN, ERROR, FATAL, NONE 10 | level=DEBUG 11 | file=taskboard.log 12 | count=5 13 | # 10MB 14 | size=10485760 15 | console=true 16 | 17 | [database] 18 | driver_name=mysql 19 | data_source_name=:@/taskboard?parseTime=true 20 | statement_cache=1000 21 | idle_connections=2 22 | 23 | [smtp] 24 | #from=noreply-taskboard@gmail.com 25 | #host=smtp.gmail.com 26 | #port=587 27 | #user= 28 | #pass= 29 | -------------------------------------------------------------------------------- /taskboard_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/quintans/goSQL/db" 9 | "github.com/quintans/goSQL/test/common" 10 | "github.com/quintans/goSQL/translators" 11 | T "github.com/quintans/taskboard/biz/tables" 12 | "github.com/quintans/taskboard/common/dto" 13 | ) 14 | 15 | func initDb() (db.ITransactionManager, *sql.DB) { 16 | translator := translators.NewMySQL5Translator() 17 | return common.InitDB("mysql", "tb:tb@/taskboard?parseTime=true", translator) 18 | } 19 | 20 | /* 21 | func TestUserRoles(t *testing.T) { 22 | tm, theDB := initDb() 23 | store := tm.Store() 24 | users, err := store.Query(T.USER). 25 | Column(T.USER_C_ID). 26 | Column(T.USER_C_VERSION). 27 | Column(T.USER_C_NAME). 28 | Column(T.USER_C_USERNAME). 29 | Outer(T.USER_A_ROLES).On(T.ROLE_C_KIND.Matches(lov.ERole_ADMIN)).Fetch(). 30 | Where(T.USER_C_ID.Matches(2)). 31 | ListFlatTreeOf((*entity.User)(nil)) 32 | 33 | if err == nil { 34 | for e := users.Enumerator(); e.HasNext(); { 35 | user := e.Next().(*entity.User) 36 | fmt.Printf("User \"%v\" has %v roles\n", *user.Name, len(user.Roles)) 37 | } 38 | } else { 39 | t.Error("There was an error: ", err) 40 | } 41 | 42 | theDB.Close() 43 | } 44 | */ 45 | 46 | func TestListFunction(t *testing.T) { 47 | tm, theDB := initDb() 48 | store := tm.Store() 49 | 50 | _, err := store.Query(T.USER). 51 | Column(T.USER_C_ID). 52 | Column(T.USER_C_NAME). 53 | Column(db.Null()). 54 | ListInto(func(id *int64, name string, dummy string) { 55 | fmt.Printf("=====> Id: %v, Name: %v -- %v\n", *id, name, dummy) 56 | }) 57 | 58 | if err != nil { 59 | t.Error("There was an error: ", err) 60 | } 61 | 62 | theDB.Close() 63 | } 64 | 65 | func TestListFunctionStruct(t *testing.T) { 66 | tm, theDB := initDb() 67 | store := tm.Store() 68 | 69 | //var users []dto.BoardUserDTO 70 | _, err := store.Query(T.USER). 71 | Column(T.USER_C_ID). 72 | Column(T.USER_C_NAME). 73 | //List(&users) 74 | ListInto(func(u dto.BoardUserDTO) { 75 | fmt.Println("X====> ", *u.Name) 76 | }) 77 | 78 | if err != nil { 79 | t.Error("There was an error: ", err) 80 | } 81 | 82 | //for _, v := range users { 83 | // fmt.Println("X====> ", *v.Name) 84 | //} 85 | 86 | theDB.Close() 87 | } 88 | 89 | /* 90 | func TestInclude(t *testing.T) { 91 | tm, theDB := initDb() 92 | store := tm.Store() 93 | 94 | var board = new(entity.Board) 95 | _, err := store.Query(T.BOARD).All(). 96 | Outer(T.BOARD_A_LANES).OrderBy(T.LANE_C_POSITION). 97 | Outer(T.LANE_A_TASKS).OrderBy(T.TASK_C_POSITION). 98 | Outer(T.TASK_A_USER). 99 | Include(T.USER_C_ID, T.USER_C_NAME). 100 | Fetch(). 101 | Where(T.BOARD_C_ID.Matches(7)). 102 | SelectTree(board) 103 | 104 | if err != nil { 105 | t.Error("######### There was an error: ", err) 106 | } else { 107 | fmt.Printf("X====> %+v ", *board) 108 | } 109 | 110 | theDB.Close() 111 | } 112 | */ 113 | -------------------------------------------------------------------------------- /ts/toolkit.ts: -------------------------------------------------------------------------------- 1 | interface IToastrOptions { 2 | closeButton?: boolean; 3 | debug?: boolean; 4 | positionClass?: string; 5 | onclick?: () => void; 6 | showDuration?: string; 7 | hideDuration?: string; 8 | timeOut?: string; 9 | extendedTimeOut?: string; 10 | showEasing?: string; 11 | hideEasing?: string; 12 | showMethod?: string; 13 | hideMethod?: string; 14 | } 15 | 16 | interface IToastr { 17 | info: (message: string, title: string) => void; 18 | success: (message: string, title: string) => void; 19 | warning: (message: string, title: string) => void; 20 | error: (message: string, title: string) => void; 21 | clear: () => void; 22 | options: IToastrOptions; 23 | } 24 | 25 | declare var toastr: IToastr; 26 | 27 | interface ConfirmOptions { 28 | heading?: string; 29 | question?: string; 30 | cancelButtonTxt?: string; 31 | okButtonTxt?: string; 32 | callback?: () => void; 33 | } 34 | 35 | interface DragData { 36 | startLane: number; 37 | endLane: number; 38 | startPosition: number; 39 | endPosition: number; 40 | } 41 | 42 | module toolkit { 43 | export function LightenDarkenColor(col: string, amt: number) { 44 | var usePound = false; 45 | 46 | if (col[0] == "#") { 47 | col = col.slice(1); 48 | usePound = true; 49 | } 50 | var num = parseInt(col, 16); 51 | var r = (num >> 16) + amt; 52 | 53 | if (r > 255) r = 255; 54 | else if (r < 0) r = 0; 55 | 56 | var b = ((num >> 8) & 0x00FF) + amt; 57 | 58 | if (b > 255) b = 255; 59 | else if (b < 0) b = 0; 60 | 61 | var g = (num & 0x0000FF) + amt; 62 | 63 | if (g > 255) g = 255; 64 | else if (g < 0) g = 0; 65 | 66 | return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16); 67 | } 68 | 69 | export function stickyError(aTitle: string, message: string) { 70 | toastr.options.closeButton = true; 71 | toastr.options.timeOut = "5000"; 72 | toastr.error(message, aTitle); 73 | }; 74 | 75 | export function success(message: string) { 76 | toastr.options.closeButton = true; 77 | toastr.options.timeOut = "3000"; 78 | toastr.success(message, "Success"); 79 | }; 80 | 81 | export function notice(aTitle: string, message: string) { 82 | toastr.options.closeButton = true; 83 | toastr.options.timeOut = "5000"; 84 | toastr.info(message, aTitle); 85 | }; 86 | 87 | var successfulOperation = function() { 88 | success("Successful Operation"); 89 | }; 90 | 91 | 92 | export function confirm(opts: ConfirmOptions) { 93 | var defaults = { 94 | heading: "Confirmation", 95 | question: "Are you sure?", 96 | cancelButtonTxt: "Cancel", 97 | okButtonTxt: "Ok" 98 | } 99 | 100 | var options = $.extend(defaults, opts); 101 | 102 | var dialog = ''; 121 | 122 | var confirmModal = $(dialog).prependTo('body'); 123 | 124 | confirmModal.find('#okButton').click(function(event) { 125 | if (options.callback) 126 | options.callback(); 127 | confirmModal.modal('hide'); 128 | }); 129 | 130 | confirmModal.on('hidden', function() { 131 | $(this).remove(); 132 | }); 133 | 134 | confirmModal.modal('show'); 135 | }; 136 | 137 | export class Fail { 138 | error: string; 139 | message: string; 140 | } 141 | 142 | export class Criteria { 143 | page: number; 144 | pageSize: number = 10; 145 | countRecords: boolean = false; 146 | 147 | orderBy: string; 148 | ascending: boolean = true; 149 | 150 | clone(): Criteria { 151 | var o = new Criteria(); 152 | o.copy(this); 153 | return o; 154 | } 155 | 156 | copy(c: Criteria) { 157 | this.countRecords = c.countRecords; 158 | this.page = c.page; 159 | this.pageSize = c.pageSize; 160 | this.orderBy = c.orderBy; 161 | this.ascending = c.ascending; 162 | } 163 | } 164 | 165 | export class Page { 166 | // max number of records returned by the datasource 167 | count: number; 168 | // if this is the last page 169 | last: boolean; 170 | results: Array; 171 | } 172 | 173 | 174 | export interface IDataSource { 175 | fetch(criteria: Criteria, successCallback: (p: Page) => void); 176 | } 177 | 178 | 179 | export interface IProvider { 180 | saveConfiguration(name: string, value: string); 181 | findConfiguration(name: string, callback: (result: string) => void); 182 | criteria: Criteria; 183 | refresh(); 184 | reset(); 185 | maxPages: number; 186 | currentPage: number; 187 | pageFirst: number; 188 | pageLast: number; 189 | maxRecords: number; 190 | onFetch(callback: () => void); 191 | fetchPage(page: number); 192 | fetchNextPage(); 193 | fetchPreviousPage(); 194 | results: Array; // last retrived page results 195 | } 196 | 197 | export class Provider implements IProvider { 198 | dataSource: IDataSource; 199 | criteria: Criteria; 200 | maxPages = 0; 201 | currentPage = 1; 202 | maxRecords = 0; 203 | results: Array; 204 | pageFirst = 0; 205 | pageLast = 0; 206 | 207 | fetchCallbacks: Array<() => void>; 208 | 209 | constructor(dataSource: IDataSource, criteria: Criteria) { 210 | this.dataSource = dataSource; 211 | if (criteria.page == null) { 212 | criteria.page = 1; 213 | } 214 | this.criteria = criteria; 215 | this.results = []; 216 | } 217 | 218 | onFetch(c: () => void) { 219 | if (this.fetchCallbacks == null) 220 | this.fetchCallbacks = []; 221 | 222 | this.fetchCallbacks.push(c); 223 | } 224 | 225 | fetchPreviousPage(): boolean { 226 | if (this.currentPage > 1) { 227 | this.fetchPage(this.currentPage - 1); 228 | return true; 229 | } else { 230 | return false; 231 | } 232 | } 233 | 234 | fetchNextPage(): boolean { 235 | if (this.currentPage < this.maxPages) { 236 | this.fetchPage(this.currentPage + 1); 237 | return true; 238 | } else { 239 | return false; 240 | } 241 | } 242 | 243 | fetchPage(pageNumber: number) { 244 | this.criteria.page = pageNumber; 245 | this.refresh(); 246 | } 247 | 248 | getCriteria(): Criteria { 249 | var crt: Criteria = this.criteria.clone(); 250 | crt.countRecords = (this.criteria.page == 1); 251 | return crt; 252 | } 253 | 254 | refresh(callback?: () => void) { 255 | var self = this; 256 | this.dataSource.fetch(this.getCriteria(), function(result: Page) { 257 | self.currentPage = self.criteria.page; 258 | 259 | self.pageFirst = (self.criteria.page - 1) * self.criteria.pageSize + 1; 260 | self.pageLast = self.pageFirst + result.results.length - 1; 261 | self.results = result.results; 262 | if (result.count != null) { 263 | self.maxRecords = result.count; 264 | self.maxPages = Math.floor(self.maxRecords / self.criteria.pageSize) 265 | + ((self.maxRecords % self.criteria.pageSize) == 0 ? 0.0 : 1.0); 266 | } 267 | 268 | // invoke callbacks 269 | var callbacks = self.fetchCallbacks; 270 | if (callbacks != null) { 271 | var i: number; 272 | for (i = 0; i < callbacks.length; i++) { 273 | callbacks[i](); 274 | } 275 | } 276 | if(callback) { 277 | callback(); 278 | } 279 | }); 280 | } 281 | 282 | reset() { 283 | this.criteria.page = 1; 284 | this.maxPages = 0; 285 | this.currentPage = 1; 286 | this.maxRecords = 0; 287 | this.results = []; 288 | this.pageFirst = 0; 289 | this.pageLast = 0; 290 | } 291 | 292 | saveConfiguration(name: string, value: string) { 293 | // TODO: implement 294 | } 295 | 296 | findConfiguration(name: string, callback: (result: string) => void) { 297 | // TODO: implement 298 | } 299 | 300 | } 301 | 302 | // initialize form 303 | export function showModal(id: string, show: boolean, onClose?: (e: Event) => void) { 304 | var modal = $(id); 305 | if(show) { 306 | modal.modal("show").on('hidden.bs.modal', onClose); 307 | } else { 308 | modal.modal("hide"); 309 | } 310 | }; 311 | 312 | export function isEmpty(str: string): boolean { 313 | return str === undefined || str === null || str === ''; 314 | } 315 | } -------------------------------------------------------------------------------- /ts/typings/angularjs/angular-cookies.d.ts: -------------------------------------------------------------------------------- 1 | /// Type definitions for Angular JS 1.2 (ngCookies module) 2 | // Project: http://angularjs.org 3 | // Definitions by: Diego Vilar 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | 7 | /// 8 | 9 | /////////////////////////////////////////////////////////////////////////////// 10 | // ngCookies module (angular-cookies.js) 11 | /////////////////////////////////////////////////////////////////////////////// 12 | declare module ng.cookies { 13 | 14 | /////////////////////////////////////////////////////////////////////////// 15 | // CookieService 16 | // see http://docs.angularjs.org/api/ngCookies.$cookies 17 | /////////////////////////////////////////////////////////////////////////// 18 | interface ICookiesService {} 19 | 20 | /////////////////////////////////////////////////////////////////////////// 21 | // CookieStoreService 22 | // see http://docs.angularjs.org/api/ngCookies.$cookieStore 23 | /////////////////////////////////////////////////////////////////////////// 24 | interface ICookieStoreService { 25 | get(key: string): any; 26 | put(key: string, value: any): void; 27 | remove(key: string): void; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ts/typings/angularjs/angular-route.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Angular JS 1.2 (ngRoute module) 2 | // Project: http://angularjs.org 3 | // Definitions by: Jonathan Park 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | /// 7 | 8 | 9 | /////////////////////////////////////////////////////////////////////////////// 10 | // ngRoute module (angular-route.js) 11 | /////////////////////////////////////////////////////////////////////////////// 12 | declare module ng.route { 13 | 14 | /////////////////////////////////////////////////////////////////////////// 15 | // RouteParamsService 16 | // see http://docs.angularjs.org/api/ngRoute.$routeParams 17 | /////////////////////////////////////////////////////////////////////////// 18 | interface IRouteParamsService {} 19 | 20 | /////////////////////////////////////////////////////////////////////////// 21 | // RouteService 22 | // see http://docs.angularjs.org/api/ngRoute.$route 23 | // see http://docs.angularjs.org/api/ngRoute.$routeProvider 24 | /////////////////////////////////////////////////////////////////////////// 25 | interface IRouteService { 26 | reload(): void; 27 | routes: any; 28 | 29 | // May not always be available. For instance, current will not be available 30 | // to a controller that was not initialized as a result of a route maching. 31 | current?: ICurrentRoute; 32 | } 33 | 34 | // see http://docs.angularjs.org/api/ngRoute.$routeProvider#when for options explanations 35 | interface IRoute { 36 | controller?: any; 37 | controllerAs?: any; 38 | name?: string; 39 | template?: string; 40 | templateUrl?: any; 41 | resolve?: any; 42 | redirectTo?: any; 43 | reloadOnSearch?: boolean; 44 | } 45 | 46 | // see http://docs.angularjs.org/api/ng.$route#current 47 | interface ICurrentRoute extends IRoute { 48 | locals: { 49 | $scope: IScope; 50 | $template: string; 51 | }; 52 | 53 | params: any; 54 | } 55 | 56 | interface IRouteProvider extends IServiceProvider { 57 | otherwise(params: any): IRouteProvider; 58 | /** 59 | * This is a description 60 | * 61 | */ 62 | when(path: string, route: IRoute): IRouteProvider; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ts/typings/bootstrap/bootstrap.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for Bootstrap 2.2 2 | // Project: http://twitter.github.com/bootstrap/ 3 | // Definitions by: Boris Yankov 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | 7 | /// 8 | 9 | interface ModalOptions { 10 | backdrop?: boolean; 11 | keyboard?: boolean; 12 | show?: boolean; 13 | remote?: string; 14 | } 15 | 16 | interface ModalOptionsBackdropString { 17 | backdrop?: string; // for "static" 18 | keyboard?: boolean; 19 | show?: boolean; 20 | remote?: string; 21 | } 22 | 23 | interface ScrollSpyOptions { 24 | offset?: number; 25 | } 26 | 27 | interface TooltipOptions { 28 | animation?: boolean; 29 | html?: boolean; 30 | placement?: any; 31 | selector?: string; 32 | title?: any; 33 | trigger?: string; 34 | delay?: any; 35 | container?: any; 36 | } 37 | 38 | interface PopoverOptions { 39 | animation?: boolean; 40 | html?: boolean; 41 | placement?: any; 42 | selector?: string; 43 | trigger?: string; 44 | title?: any; 45 | content?: any; 46 | delay?: any; 47 | container?: any; 48 | } 49 | 50 | interface CollapseOptions { 51 | parent?: any; 52 | toggle?: boolean; 53 | } 54 | 55 | interface CarouselOptions { 56 | interval?: number; 57 | pause?: string; 58 | } 59 | 60 | interface TypeaheadOptions { 61 | source?: any; 62 | items?: number; 63 | minLength?: number; 64 | matcher?: (item: any) => boolean; 65 | sorter?: (items: any[]) => any[]; 66 | updater?: (item: any) => any; 67 | highlighter?: (item: any) => string; 68 | } 69 | 70 | interface AffixOptions { 71 | offset?: any; 72 | } 73 | 74 | interface JQuery { 75 | modal(options?: ModalOptions): JQuery; 76 | modal(options?: ModalOptionsBackdropString): JQuery; 77 | modal(command: string): JQuery; 78 | 79 | dropdown(): JQuery; 80 | dropdown(command: string): JQuery; 81 | 82 | scrollspy(command: string): JQuery; 83 | scrollspy(options?: ScrollSpyOptions): JQuery; 84 | 85 | tab(): JQuery; 86 | tab(command: string): JQuery; 87 | 88 | tooltip(options?: TooltipOptions): JQuery; 89 | //tooltip(command: string): JQuery; 90 | 91 | popover(options?: PopoverOptions): JQuery; 92 | popover(command: string): JQuery; 93 | 94 | alert(): JQuery; 95 | alert(command: string): JQuery; 96 | 97 | //button(): JQuery; 98 | //button(command: string): JQuery; 99 | 100 | collapse(options?: CollapseOptions): JQuery; 101 | collapse(command: string): JQuery; 102 | 103 | carousel(options?: CarouselOptions): JQuery; 104 | carousel(command: string): JQuery; 105 | 106 | typeahead(options?: TypeaheadOptions): JQuery; 107 | 108 | affix(options?: AffixOptions): JQuery; 109 | } 110 | -------------------------------------------------------------------------------- /www/css/angular-markdown-editor.css: -------------------------------------------------------------------------------- 1 | .outline { 2 | border: 1px solid #ddd; 3 | border-radius: 3px; 4 | } 5 | textarea.processed.md-input { 6 | padding: 10px; 7 | } 8 | textarea { 9 | -webkit-box-sizing: border-box; 10 | -moz-box-sizing: border-box; 11 | box-sizing: border-box; 12 | 13 | width: 100%; 14 | } 15 | 16 | /* http://johnsardine.com/freebies/dl-html-css/simple-little-tab/ */ 17 | .marked > table a:link, 18 | .markdown > table a:link { 19 | color: #666; 20 | font-weight: bold; 21 | text-decoration:none; 22 | } 23 | .marked > table a:visited, 24 | .markdown > table a:visited { 25 | color: #999999; 26 | font-weight:bold; 27 | text-decoration:none; 28 | } 29 | .marked > table a:active, 30 | .marked > table a:hover, 31 | .markdown > table a:active, 32 | .markdown > table a:hover { 33 | color: #bd5a35; 34 | text-decoration:underline; 35 | } 36 | .marked > table, 37 | .markdown > table { 38 | font-family:Arial, Helvetica, sans-serif; 39 | color:#606060; 40 | font-size:15px; 41 | background:#eaeaea; 42 | margin:10px; 43 | border:#ccc 1px solid; 44 | 45 | -moz-border-radius:3px; 46 | -webkit-border-radius:3px; 47 | border-radius:3px; 48 | } 49 | .marked > table th, 50 | .markdown > table th { 51 | padding:8px 15px; 52 | border-bottom:1px solid #e0e0e0; 53 | background: #f6f6f6; 54 | color:#333; 55 | } 56 | .marked > table th:first-child, 57 | .markdown > table th:first-child { 58 | text-align: left; 59 | padding-left:10px; 60 | } 61 | .marked > table tr:first-child th:first-child, 62 | .markdown > table tr:first-child th:first-child { 63 | -moz-border-radius-topleft:3px; 64 | -webkit-border-top-left-radius:3px; 65 | border-top-left-radius:3px; 66 | } 67 | .marked > table tr:first-child th:last-child, 68 | .markdown > table tr:first-child th:last-child { 69 | -moz-border-radius-topright:3px; 70 | -webkit-border-top-right-radius:3px; 71 | border-top-right-radius:3px; 72 | } 73 | .marked > table tr, 74 | .markdown > table tr { 75 | text-align: center; 76 | padding-left:10px; 77 | } 78 | .marked > table td:first-child, 79 | .markdown > table td:first-child { 80 | text-align: left; 81 | padding-left:10px; 82 | border-left: 0; 83 | } 84 | .marked > table td, 85 | .markdown > table td { 86 | padding:8px; 87 | border-top: 1px solid #ffffff; 88 | border-bottom:1px solid #e0e0e0; 89 | border-left: 1px solid #e0e0e0; 90 | 91 | background: #fff; 92 | } 93 | 94 | .marked > table tr:nth-child(2n) td, 95 | .markdown > table tr:nth-child(2n) td { 96 | background: #f8f8f8; 97 | } 98 | .marked > table tr:last-child td, 99 | .markdown > table tr:last-child td { 100 | border-bottom:0; 101 | } 102 | .marked > table tr:last-child td:first-child, 103 | .markdown > table tr:last-child td:first-child { 104 | -moz-border-radius-bottomleft:3px; 105 | -webkit-border-bottom-left-radius:3px; 106 | border-bottom-left-radius:3px; 107 | } 108 | .marked > table tr:last-child td:last-child, 109 | .markdown > table tr:last-child td:last-child { 110 | -moz-border-radius-bottomright:3px; 111 | -webkit-border-bottom-right-radius:3px; 112 | border-bottom-right-radius:3px; 113 | } 114 | .marked > table tr:hover td, 115 | .markdown > table tr:hover td { 116 | background: #f4f4f4; 117 | background: -webkit-gradient(linear, left top, left bottom, from(#f4f4f4), to(#f2f2f2)); 118 | background: -moz-linear-gradient(top, #f4f4f4, #f2f2f2); 119 | } 120 | -------------------------------------------------------------------------------- /www/css/app.css: -------------------------------------------------------------------------------- 1 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { 2 | display: none !important; 3 | } 4 | 5 | body { 6 | padding-top: 50px; 7 | } 8 | 9 | .board { 10 | /*border-collapse:collapse;*/ 11 | border-bottom: 1px solid #cccccc; 12 | width: 100%; 13 | } 14 | 15 | .board th { 16 | border: 1px solid #cccccc; 17 | padding: 4px; 18 | font-size: 14px; 19 | } 20 | 21 | .board td, .board th { 22 | border-left: 1px solid #cccccc; 23 | border-right: 1px solid #cccccc; 24 | height: 44px; 25 | } 26 | 27 | .board td { 28 | vertical-align: top; 29 | height: 500px; 30 | width: 33%; 31 | } 32 | 33 | .lane { 34 | padding: 1px; 35 | } 36 | 37 | .dragging { 38 | -moz-box-shadow: 10px 10px 10px #999; 39 | -webkit-box-shadow: 10px 10px 10px #999; 40 | box-shadow: 10px 10px 10px #999; 41 | } 42 | 43 | .spacebar { 44 | padding-top: 10px; 45 | } 46 | 47 | .spacer { 48 | margin-top: 10px; 49 | } 50 | 51 | .header { 52 | margin-top: 5px; 53 | } 54 | 55 | .footer { 56 | margin-bottom: 5px; 57 | } 58 | 59 | span.glyphicon { 60 | margin-right: 2px; 61 | } 62 | 63 | .draggablePanel { 64 | cursor: move; 65 | } 66 | 67 | .portlet-header .subject { 68 | float: left; 69 | } 70 | 71 | .portlet-header .login { 72 | clear: both; 73 | font-weight: normal; 74 | } 75 | 76 | .portlet-header .deadline { 77 | float: right; 78 | font-weight: normal; 79 | } 80 | 81 | .card-line { 82 | height: 15px; 83 | font-weight: normal; 84 | } 85 | 86 | .clearfix { 87 | clear: both; 88 | } 89 | 90 | .portlet { 91 | margin: 0 2px 2px 0; 92 | font-family: Verdana, Arial, sans-serif; 93 | font-size: 0.9em; 94 | } 95 | 96 | .portlet-header { 97 | border: 0px; 98 | padding-top: 2px; 99 | padding-bottom: 4px; 100 | padding-left: 0.2em; 101 | padding-right: 0.2em; 102 | border-bottom: 1px solid #aaaaaa; 103 | color: #222222; 104 | font-weight: bold; 105 | } 106 | 107 | .portlet-header .ui-icon { 108 | float: right; 109 | } 110 | 111 | .portlet-content { 112 | padding: 0.4em; 113 | display: none; 114 | white-space: pre-wrap; 115 | } 116 | 117 | .ui-sortable-placeholder { 118 | border: 1px dotted black; 119 | visibility: visible !important; 120 | height: 50px !important; 121 | } 122 | 123 | .ui-sortable-placeholder * { 124 | visibility: hidden; 125 | } 126 | 127 | .cardcolor { 128 | float: left; 129 | height: 16px; 130 | width: 16px; 131 | border: 1px solid black; 132 | margin: 2px; 133 | } 134 | 135 | .ccselected { 136 | height: 18px; 137 | width: 18px; 138 | border: 2px solid red; 139 | margin: 1px; 140 | } 141 | 142 | .no-padding { 143 | padding: 0px; 144 | } 145 | 146 | .around-margin { 147 | margin-right: 0px !important; 148 | margin-left: 0px !important; 149 | } 150 | 151 | .modal-wide { 152 | width:940px; 153 | } 154 | 155 | input.ng-invalid-required { 156 | border-color: orange; 157 | background-color: #FFFFDD; 158 | } 159 | input.ng-valid-required { 160 | background-color: #FFFFDD; 161 | } 162 | 163 | .datatable tr th { 164 | background-color: #dddddd; 165 | background-position: right; 166 | background-repeat:no-repeat; 167 | text-align: left; 168 | text-shadow: 1px 1px 1px white; 169 | color: black; 170 | } 171 | 172 | .th-no-order { 173 | } 174 | 175 | .glyphicon { 176 | cursor: default; 177 | } 178 | 179 | .datatable thead tr th.th-sort-none { 180 | background-image: url('images/sort-none.png'); 181 | cursor: pointer; 182 | } 183 | 184 | .datatable thead tr th.th-sort-asc { 185 | background-image: url('images/sort-asc.png'); 186 | } 187 | 188 | .datatable thead tr th.th-sort-desc { 189 | background-image: url('images/sort-desc.png'); 190 | } 191 | 192 | .datatable>thead>tr>th, .table>tbody>tr>td { 193 | padding: 2px; 194 | vertical-align: middle; 195 | } 196 | 197 | .datatable .form-control { 198 | height: 26px; 199 | padding: 2px 3px; 200 | } 201 | 202 | .no-head-margin, 203 | .no-foot-margin, 204 | .ui-paginator, 205 | .ui-paginator-msg { 206 | margin-top: 0px; 207 | } 208 | 209 | .form-control-feedback { 210 | pointer-events: auto; 211 | } -------------------------------------------------------------------------------- /www/css/bootstrap-markdown.min.css: -------------------------------------------------------------------------------- 1 | .md-editor{display:block;border:1px solid #ddd}.md-editor .md-footer,.md-editor>.md-header{display:block;padding:6px 4px;background:#f5f5f5}.md-editor>.md-header{margin:0}.md-editor>.md-preview{background:#fff;border-top:1px dashed #ddd;border-bottom:1px dashed #ddd;min-height:10px;overflow:auto}.md-editor>textarea{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;font-size:14px;outline:0;margin:0;display:block;padding:0;width:100%;border:0;border-top:1px dashed #ddd;border-bottom:1px dashed #ddd;border-radius:0;box-shadow:none;background:#eee}.md-editor>textarea:focus{box-shadow:none;background:#fff}.md-editor.active{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.md-editor .md-controls{float:right;padding:3px}.md-editor .md-controls .md-control{right:5px;color:#bebebe;padding:3px 3px 3px 10px}.md-editor .md-controls .md-control:hover{color:#333}.md-editor.md-fullscreen-mode{width:100%;height:100%;position:fixed;top:0;left:0;z-index:99999;padding:60px 30px 15px;background:#fff!important;border:0!important}.md-editor.md-fullscreen-mode .md-footer{display:none}.md-editor.md-fullscreen-mode .md-input,.md-editor.md-fullscreen-mode .md-preview{margin:0 auto!important;height:100%!important;font-size:20px!important;padding:20px!important;color:#999;line-height:1.6em!important;resize:none!important;box-shadow:none!important;background:#fff!important;border:0!important}.md-editor.md-fullscreen-mode .md-preview{color:#333;overflow:auto}.md-editor.md-fullscreen-mode .md-input:focus,.md-editor.md-fullscreen-mode .md-input:hover{color:#333;background:#fff!important}.md-editor.md-fullscreen-mode .md-header{background:0 0;text-align:center;position:fixed;width:100%;top:20px}.md-editor.md-fullscreen-mode .btn-group{float:none}.md-editor.md-fullscreen-mode .btn{border:0;background:0 0;color:#b3b3b3}.md-editor.md-fullscreen-mode .btn.active,.md-editor.md-fullscreen-mode .btn:active,.md-editor.md-fullscreen-mode .btn:focus,.md-editor.md-fullscreen-mode .btn:hover{box-shadow:none;color:#333}.md-editor.md-fullscreen-mode .md-fullscreen-controls{position:absolute;top:20px;right:20px;text-align:right;z-index:1002;display:block}.md-editor.md-fullscreen-mode .md-fullscreen-controls a{color:#b3b3b3;clear:right;margin:10px;width:30px;height:30px;text-align:center}.md-editor.md-fullscreen-mode .md-fullscreen-controls a:hover{color:#333;text-decoration:none}.md-editor.md-fullscreen-mode .md-editor{height:100%!important;position:relative}.md-editor .md-fullscreen-controls{display:none}.md-nooverflow{overflow:hidden;position:fixed;width:100%} -------------------------------------------------------------------------------- /www/css/highlight-github.min.css: -------------------------------------------------------------------------------- 1 | .hljs{display:block;overflow-x:auto;padding:0.5em;color:#333;background:#f8f8f8}.hljs-comment,.hljs-quote{color:#998;font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:bold}.hljs-number,.hljs-literal,.hljs-variable,.hljs-template-variable,.hljs-tag .hljs-attr{color:#008080}.hljs-string,.hljs-doctag{color:#d14}.hljs-title,.hljs-section,.hljs-selector-id{color:#900;font-weight:bold}.hljs-subst{font-weight:normal}.hljs-type,.hljs-class .hljs-title{color:#458;font-weight:bold}.hljs-tag,.hljs-name,.hljs-attribute{color:#000080;font-weight:normal}.hljs-regexp,.hljs-link{color:#009926}.hljs-symbol,.hljs-bullet{color:#990073}.hljs-built_in,.hljs-builtin-name{color:#0086b3}.hljs-meta{color:#999;font-weight:bold}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:bold} -------------------------------------------------------------------------------- /www/css/images/sort-asc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/css/images/sort-asc.png -------------------------------------------------------------------------------- /www/css/images/sort-desc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/css/images/sort-desc.png -------------------------------------------------------------------------------- /www/css/images/sort-none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/css/images/sort-none.png -------------------------------------------------------------------------------- /www/css/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/css/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /www/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/css/images/ui-bg_highlight-soft_75_cccccc_1x100.png -------------------------------------------------------------------------------- /www/css/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/css/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /www/css/toastr.min.css: -------------------------------------------------------------------------------- 1 | .toast-title{font-weight:bold}.toast-message{-ms-word-wrap:break-word;word-wrap:break-word}.toast-message a,.toast-message label{color:#fff}.toast-message a:hover{color:#ccc;text-decoration:none}.toast-close-button{position:relative;right:-.3em;top:-.3em;float:right;font-size:20px;font-weight:bold;color:#fff;-webkit-text-shadow:0 1px 0 #fff;text-shadow:0 1px 0 #fff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}.toast-close-button:hover,.toast-close-button:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.4;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=40);filter:alpha(opacity=40)}button.toast-close-button{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}#toast-container{position:fixed;z-index:999999}#toast-container *{-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}#toast-container>div{margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;-moz-border-radius:3px 3px 3px 3px;-webkit-border-radius:3px 3px 3px 3px;border-radius:3px 3px 3px 3px;background-position:15px center;background-repeat:no-repeat;-moz-box-shadow:0 0 12px #999;-webkit-box-shadow:0 0 12px #999;box-shadow:0 0 12px #999;color:#fff;opacity:.8;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=80);filter:alpha(opacity=80)}#toast-container>:hover{-moz-box-shadow:0 0 12px #000;-webkit-box-shadow:0 0 12px #000;box-shadow:0 0 12px #000;opacity:1;-ms-filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);filter:alpha(opacity=100);cursor:pointer}#toast-container>.toast-info{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII=")!important}#toast-container>.toast-error{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII=")!important}#toast-container>.toast-success{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg==")!important}#toast-container>.toast-warning{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII=")!important}#toast-container.toast-top-full-width>div,#toast-container.toast-bottom-full-width>div{width:96%;margin:auto}.toast{background-color:#030303}.toast-success{background-color:#51a351}.toast-error{background-color:#bd362f}.toast-info{background-color:#2f96b4}.toast-warning{background-color:#f89406}@media all and (max-width:240px){#toast-container>div{padding:8px 8px 8px 50px;width:11em}#toast-container .toast-close-button{right:-.2em;top:-.2em}}@media all and (min-width:241px)and (max-width:480px){#toast-container>div{padding:8px 8px 8px 50px;width:18em}#toast-container .toast-close-button{right:-.2em;top:-.2em}}@media all and (min-width:481px)and (max-width:768px){#toast-container>div{padding:15px 15px 15px 50px;width:25em}} -------------------------------------------------------------------------------- /www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/favicon.ico -------------------------------------------------------------------------------- /www/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /www/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /www/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /www/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /www/img/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/img/check.png -------------------------------------------------------------------------------- /www/img/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/img/delete.png -------------------------------------------------------------------------------- /www/img/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/img/edit.png -------------------------------------------------------------------------------- /www/img/remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quintans/taskboard/4751eea275f1088df7c06cc25e56f058c9f37c8e/www/img/remove.png -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Task Board 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 |
Loading...
26 |
27 | 28 | 53 | 54 |
55 | 98 |
99 |
100 | 101 | 137 | 138 | 192 | 193 | 194 | 240 | 241 | 242 |
243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | -------------------------------------------------------------------------------- /www/js/.gitignore: -------------------------------------------------------------------------------- 1 | /gen/ 2 | -------------------------------------------------------------------------------- /www/js/gen/toolkit.js: -------------------------------------------------------------------------------- 1 | var toolkit; 2 | (function (toolkit) { 3 | function LightenDarkenColor(col, amt) { 4 | var usePound = false; 5 | if (col[0] == "#") { 6 | col = col.slice(1); 7 | usePound = true; 8 | } 9 | var num = parseInt(col, 16); 10 | var r = (num >> 16) + amt; 11 | if (r > 255) 12 | r = 255; 13 | else if (r < 0) 14 | r = 0; 15 | var b = ((num >> 8) & 0x00FF) + amt; 16 | if (b > 255) 17 | b = 255; 18 | else if (b < 0) 19 | b = 0; 20 | var g = (num & 0x0000FF) + amt; 21 | if (g > 255) 22 | g = 255; 23 | else if (g < 0) 24 | g = 0; 25 | return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16); 26 | } 27 | toolkit.LightenDarkenColor = LightenDarkenColor; 28 | function stickyError(aTitle, message) { 29 | toastr.options.closeButton = true; 30 | toastr.options.timeOut = "5000"; 31 | toastr.error(message, aTitle); 32 | } 33 | toolkit.stickyError = stickyError; 34 | ; 35 | function success(message) { 36 | toastr.options.closeButton = true; 37 | toastr.options.timeOut = "3000"; 38 | toastr.success(message, "Success"); 39 | } 40 | toolkit.success = success; 41 | ; 42 | function notice(aTitle, message) { 43 | toastr.options.closeButton = true; 44 | toastr.options.timeOut = "5000"; 45 | toastr.info(message, aTitle); 46 | } 47 | toolkit.notice = notice; 48 | ; 49 | var successfulOperation = function () { 50 | success("Successful Operation"); 51 | }; 52 | function confirm(opts) { 53 | var defaults = { 54 | heading: "Confirmation", 55 | question: "Are you sure?", 56 | cancelButtonTxt: "Cancel", 57 | okButtonTxt: "Ok" 58 | }; 59 | var options = $.extend(defaults, opts); 60 | var dialog = ''; 65 | var confirmModal = $(dialog).prependTo('body'); 66 | confirmModal.find('#okButton').click(function (event) { 67 | if (options.callback) 68 | options.callback(); 69 | confirmModal.modal('hide'); 70 | }); 71 | confirmModal.on('hidden', function () { 72 | $(this).remove(); 73 | }); 74 | confirmModal.modal('show'); 75 | } 76 | toolkit.confirm = confirm; 77 | ; 78 | var Fail = (function () { 79 | function Fail() { 80 | } 81 | return Fail; 82 | })(); 83 | toolkit.Fail = Fail; 84 | var Criteria = (function () { 85 | function Criteria() { 86 | this.pageSize = 10; 87 | this.countRecords = false; 88 | this.ascending = true; 89 | } 90 | Criteria.prototype.clone = function () { 91 | var o = new Criteria(); 92 | o.copy(this); 93 | return o; 94 | }; 95 | Criteria.prototype.copy = function (c) { 96 | this.countRecords = c.countRecords; 97 | this.page = c.page; 98 | this.pageSize = c.pageSize; 99 | this.orderBy = c.orderBy; 100 | this.ascending = c.ascending; 101 | }; 102 | return Criteria; 103 | })(); 104 | toolkit.Criteria = Criteria; 105 | var Page = (function () { 106 | function Page() { 107 | } 108 | return Page; 109 | })(); 110 | toolkit.Page = Page; 111 | var Provider = (function () { 112 | function Provider(dataSource, criteria) { 113 | this.maxPages = 0; 114 | this.currentPage = 1; 115 | this.maxRecords = 0; 116 | this.pageFirst = 0; 117 | this.pageLast = 0; 118 | this.dataSource = dataSource; 119 | if (criteria.page == null) { 120 | criteria.page = 1; 121 | } 122 | this.criteria = criteria; 123 | this.results = []; 124 | } 125 | Provider.prototype.onFetch = function (c) { 126 | if (this.fetchCallbacks == null) 127 | this.fetchCallbacks = []; 128 | this.fetchCallbacks.push(c); 129 | }; 130 | Provider.prototype.fetchPreviousPage = function () { 131 | if (this.currentPage > 1) { 132 | this.fetchPage(this.currentPage - 1); 133 | return true; 134 | } 135 | else { 136 | return false; 137 | } 138 | }; 139 | Provider.prototype.fetchNextPage = function () { 140 | if (this.currentPage < this.maxPages) { 141 | this.fetchPage(this.currentPage + 1); 142 | return true; 143 | } 144 | else { 145 | return false; 146 | } 147 | }; 148 | Provider.prototype.fetchPage = function (pageNumber) { 149 | this.criteria.page = pageNumber; 150 | this.refresh(); 151 | }; 152 | Provider.prototype.getCriteria = function () { 153 | var crt = this.criteria.clone(); 154 | crt.countRecords = (this.criteria.page == 1); 155 | return crt; 156 | }; 157 | Provider.prototype.refresh = function (callback) { 158 | var self = this; 159 | this.dataSource.fetch(this.getCriteria(), function (result) { 160 | self.currentPage = self.criteria.page; 161 | self.pageFirst = (self.criteria.page - 1) * self.criteria.pageSize + 1; 162 | self.pageLast = self.pageFirst + result.results.length - 1; 163 | self.results = result.results; 164 | if (result.count != null) { 165 | self.maxRecords = result.count; 166 | self.maxPages = Math.floor(self.maxRecords / self.criteria.pageSize) + ((self.maxRecords % self.criteria.pageSize) == 0 ? 0.0 : 1.0); 167 | } 168 | // invoke callbacks 169 | var callbacks = self.fetchCallbacks; 170 | if (callbacks != null) { 171 | var i; 172 | for (i = 0; i < callbacks.length; i++) { 173 | callbacks[i](); 174 | } 175 | } 176 | if (callback) { 177 | callback(); 178 | } 179 | }); 180 | }; 181 | Provider.prototype.reset = function () { 182 | this.criteria.page = 1; 183 | this.maxPages = 0; 184 | this.currentPage = 1; 185 | this.maxRecords = 0; 186 | this.results = []; 187 | this.pageFirst = 0; 188 | this.pageLast = 0; 189 | }; 190 | Provider.prototype.saveConfiguration = function (name, value) { 191 | // TODO: implement 192 | }; 193 | Provider.prototype.findConfiguration = function (name, callback) { 194 | // TODO: implement 195 | }; 196 | return Provider; 197 | })(); 198 | toolkit.Provider = Provider; 199 | // initialize form 200 | function showModal(id, show, onClose) { 201 | var modal = $(id); 202 | if (show) { 203 | modal.modal("show").on('hidden.bs.modal', onClose); 204 | } 205 | else { 206 | modal.modal("hide"); 207 | } 208 | } 209 | toolkit.showModal = showModal; 210 | ; 211 | function isEmpty(str) { 212 | return str === undefined || str === null || str === ''; 213 | } 214 | toolkit.isEmpty = isEmpty; 215 | })(toolkit || (toolkit = {})); 216 | -------------------------------------------------------------------------------- /www/js/lib/angular-highlightjs.min.js: -------------------------------------------------------------------------------- 1 | /*! angular-highlightjs 2 | version: 0.6.2 3 | build date: 2016-08-19 4 | author: Chih-Hsuan Fan 5 | https://github.com/pc035860/angular-highlightjs.git */ 6 | !function(a,b){"object"==typeof exports||"object"==typeof module&&module.exports?module.exports=b(require("angular"),require("highlight.js")):"function"==typeof define&&define.amd?define(["angular","hljs"],b):a.returnExports=b(a.angular,a.hljs)}(this,function(a,b){function c(b){return function(c){switch(c){case"escape":return a.isDefined(b.hljsEscape)?b.hljsEscape:b.escape;case"no-escape":return a.isDefined(b.hljsNoEscape)?b.hljsNoEscape:b.noEscape;case"onhighlight":return a.isDefined(b.hljsOnhighlight)?b.hljsOnhighlight:b.onhighlight}}}function d(b){var c=!0;return a.forEach(["source","include"],function(a){b[a]&&(c=!1)}),c}var e=a.module("hljs",[]);e.provider("hljsService",function(){var c={};return{setOptions:function(b){a.extend(c,b)},getOptions:function(){return a.copy(c)},$get:function(){return(b.configure||a.noop)(c),b}}}),e.factory("hljsCache",["$cacheFactory",function(a){return a("hljsCache")}]),e.controller("HljsCtrl",["hljsCache","hljsService","$interpolate","$window",function(b,c,d,e){function f(a,b,c){var d;return function(){var f=this,g=arguments,h=function(){d=null,c||a.apply(f,g)},i=c&&!d;e.clearTimeout(d),d=e.setTimeout(h,b),i&&a.apply(f,g)}}function g(a,b){var c=b?"\\\\$&":"\\$&";return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,c)}function h(a){for(var b,c=[],d=new RegExp(q,"g"),e="",f=0;null!==(b=d.exec(a));)e+=a.substring(f,b.index)+r,f=b.index+b[0].length,c.push(b[0]);return e+=a.substr(f),{code:e,tokens:c}}function i(a,b){for(var c,d=new RegExp(r,"g"),e="",f=0;null!==(c=d.exec(a));)e+=a.substring(f,c.index)+b.shift(),f=c.index+c[0].length;return e+=a.substr(f)}var j=this,k=null,l=null,m=null,n=!1,o=null,p=null,q=g(d.startSymbol())+"((.|\\s)+?)"+g(d.endSymbol()),r="∫";j.init=function(a){k=a},j.setInterpolateScope=function(a){n=a,m&&j.highlight(m)},j.setLanguage=function(a){l=a,m&&j.highlight(m)},j.highlightCallback=function(a){p=a},j._highlight=function(e){if(k){var f,g,q;if(m=e,n&&(q=h(e),e=q.code),l?(g=j._cacheKey(l,!!n,e),f=b.get(g),f||(f=c.highlight(l,c.fixMarkup(e),!0),b.put(g,f))):(g=j._cacheKey(!!n,e),f=b.get(g),f||(f=c.highlightAuto(c.fixMarkup(e)),b.put(g,f))),e=f.value,n){(o||a.noop)(),q&&(e=i(e,q.tokens));var r=d(e);o=n.$watch(r,function(a,b){a!==b&&k.html(a)}),n.$apply(),k.html(r(n))}else k.html(e);k.addClass(f.language),null!==p&&a.isFunction(p)&&p()}},j.highlight=f(j._highlight,17),j.clear=function(){k&&(m=null,k.text(""))},j.release=function(){k=null,n=null,(o||a.noop)(),o=null},j._cacheKey=function(){var a=Array.prototype.slice.call(arguments),b="!angular-highlightjs!";return a.join(b)}}]);var f,g,h,i,j;return f=["$parse",function(b){return{restrict:"EA",controller:"HljsCtrl",compile:function(e){var f=e[0].innerHTML.replace(/^(\r\n|\r|\n)/m,""),g=e[0].textContent.replace(/^(\r\n|\r|\n)/m,"");return e.html('
'),function(e,h,i,j){var k,l=c(i);if(a.isDefined(l("escape"))?k=b(l("escape")):a.isDefined(l("no-escape"))&&(k=b("false")),j.init(h.find("code")),l("onhighlight")&&j.highlightCallback(function(){e.$eval(l("onhighlight"))}),(f||g)&&d(i)){var m;m=k&&!k(e)?g:f,j.highlight(m)}e.$on("$destroy",function(){j.release()})}}}}],h=function(b){return function(){return{require:"?hljs",restrict:"A",link:function(c,d,e,f){f&&e.$observe(b,function(b){a.isDefined(b)&&f.setLanguage(b)})}}}},g=function(a){return function(){return{require:"?hljs",restrict:"A",link:function(b,c,d,e){e&&b.$watch(d[a],function(a,c){(a||a!==c)&&e.setInterpolateScope(a?b:null)})}}}},i=function(a){return function(){return{require:"?hljs",restrict:"A",link:function(b,c,d,e){e&&b.$watch(d[a],function(a){a?e.highlight(a):e.clear()})}}}},j=function(b){return["$http","$templateCache","$q",function(c,d,e){return{require:"?hljs",restrict:"A",compile:function(f,g){var h=g[b];return function(b,f,g,i){var j=0;i&&b.$watch(h,function(b){var f=++j;if(b&&a.isString(b)){var g,h;g=d.get(b),g||(h=e.defer(),c.get(b,{cache:d,transformResponse:function(a){return a}}).success(function(a){f===j&&h.resolve(a)}).error(function(){f===j&&i.clear(),h.resolve()}),g=h.promise),e.when(g).then(function(b){b&&(a.isArray(b)?b=b[1]:a.isObject(b)&&(b=b.data),b=b.replace(/^(\r\n|\r|\n)/m,""),i.highlight(b))})}else i.clear()})}}}}]},function(b){b.directive("hljs",f),a.forEach(["interpolate","hljsInterpolate","compile","hljsCompile"],function(a){b.directive(a,g(a))}),a.forEach(["language","hljsLanguage"],function(a){b.directive(a,h(a))}),a.forEach(["source","hljsSource"],function(a){b.directive(a,i(a))}),a.forEach(["include","hljsInclude"],function(a){b.directive(a,j(a))})}(e),"hljs"}); -------------------------------------------------------------------------------- /www/js/lib/angular-markdown-editor.js: -------------------------------------------------------------------------------- 1 | angular 2 | .module('angular-markdown-editor', []) 3 | .directive('markdownEditor', ['$parse', function(parse) { 4 | return { 5 | restrict: 'A', 6 | require: 'ngModel', 7 | link: function(scope, element, attrs, ngModel) { 8 | var options = scope.$eval(attrs.markdownEditor); 9 | 10 | // Only initialize the $.markdown plugin once. 11 | if (! element.hasClass('processed')) { 12 | element.addClass('processed'); 13 | 14 | // Setup the markdown WYSIWYG. 15 | element.markdown({ 16 | autofocus: options.autofocus || false, 17 | saveable: options.saveable || false, 18 | iconlibrary: options.iconlibrary || 'glyph', 19 | hideable: options.hideable || false, 20 | width: options.width || 'inherit', 21 | height: options.height || 'inherit', 22 | resize: options.resize || 'none', 23 | language: options.language || 'en', 24 | footer: options.footer || '', 25 | fullscreen: options.fullscreen || { enable: true, icons: {}}, 26 | hiddenButtons: options.hiddenButtons || null, 27 | disabledButtons: options.disabledButtons || null, 28 | initialstate: options.initialstate || 'editor', 29 | parser: options.parser || null, 30 | dropZoneOptions: options.dropZoneOptions || null, 31 | enableDropDataUri: options.enableDropDataUri || false, 32 | showButtons: options.showButtons || null, 33 | additionalButtons: options.additionalButtons || (options.addExtraButtons ? addNewButtons() : []), 34 | onChange: function(event) { 35 | // When a change occurs, we need to update scope in case the user clicked one of the plugin buttons 36 | // (which isn't the same as a keydown event that angular would listen for). 37 | ngModel.$setViewValue(event.getContent()); 38 | } 39 | }); 40 | } 41 | } 42 | }; 43 | }]); 44 | 45 | /** 46 | * Add new extra buttons: Strikethrough & Table 47 | * @return mixed additionButtons 48 | */ 49 | function addNewButtons() { 50 | return [[{ 51 | name: "groupFont", 52 | data: [{ 53 | name: "cmdStrikethrough", 54 | toggle: false, 55 | title: "Strikethrough", 56 | icon: { 57 | fa: "fa fa-strikethrough", 58 | glyph: "glyphicon glyphicon-minus" 59 | }, 60 | callback: function(e) { 61 | // Give/remove ~~ surround the selection 62 | var chunk, cursor, selected = e.getSelection(), 63 | content = e.getContent(); 64 | 65 | if (selected.length === 0) { 66 | // Give extra word 67 | chunk = e.__localize('strikethrough'); 68 | } else { 69 | chunk = selected.text; 70 | } 71 | 72 | // transform selection and set the cursor into chunked text 73 | if (content.substr(selected.start - 2, 2) === '~~' && 74 | content.substr(selected.end, 2) === '~~') { 75 | e.setSelection(selected.start - 2, selected.end + 2); 76 | e.replaceSelection(chunk); 77 | cursor = selected.start - 2; 78 | } else { 79 | e.replaceSelection('~~' + chunk + '~~'); 80 | cursor = selected.start + 2; 81 | } 82 | 83 | // Set the cursor 84 | e.setSelection(cursor, cursor + chunk.length); 85 | } 86 | }] 87 | }, 88 | { 89 | name: "groupMisc", 90 | data: [{ 91 | name: "cmdTable", 92 | toggle: false, 93 | title: "Table", 94 | icon: { 95 | fa: "fa fa-table", 96 | glyph: "glyphicon glyphicon-th" 97 | }, 98 | callback: function(e) { 99 | // Replace selection with some drinks 100 | var chunk, cursor, 101 | selected = e.getSelection(), content = e.getContent(), 102 | chunk = "\n| Tables | Are | Cool | \n" 103 | + "| ------------- |:-------------:| -----:| \n" 104 | + "| col 3 is | right-aligned | $1600 | \n" 105 | + "| col 2 is | centered | $12 | \n" 106 | + "| zebra stripes | are neat | $1 |" 107 | 108 | // transform selection and set the cursor into chunked text 109 | e.replaceSelection(chunk) 110 | cursor = selected.start 111 | 112 | // Set the cursor 113 | e.setSelection(cursor,cursor+chunk.length); 114 | } 115 | }] 116 | }]]; 117 | } 118 | -------------------------------------------------------------------------------- /www/js/lib/angular-marked.min.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.angularMarked=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o"+string+""}var renderCode=r.code.bind(r);r.code=function(code,lang,escaped){return wrapNonBindable(renderCode(code,lang,escaped))};var renderCodespan=r.codespan.bind(r);r.codespan=function(code){return wrapNonBindable(renderCodespan(code))};self.defaults=self.defaults||{};self.defaults.renderer=r;m.setOptions(self.defaults);return m}]}markedDirective.$inject=["marked","$templateRequest","$compile"];function markedDirective(marked,$templateRequest,$compile){return{restrict:"AE",replace:true,scope:{opts:"=",marked:"=",compile:"@",src:"="},link:function(scope,element,attrs){if(attrs.marked){set(scope.marked);scope.$watch("marked",set)}else if(attrs.src){scope.$watch("src",function(src){$templateRequest(src,true).then(function(response){set(response)},function(){set("");scope.$emit("$markedIncludeError",attrs.src)})})}else{set(element.text())}function set(text){text=unindent(String(text||""));element.html(marked(text,scope.opts||null));if(scope.$eval(attrs.compile)){$compile(element.contents())(scope.$parent)}}}}}module.exports=angular.module("hc.marked",[]).directive("marked",markedDirective).provider("marked",markedProvider).name},{"./strip-indent":2,marked:"marked"}],2:[function(require,module,exports){module.exports=function unindent(text){if(!text){return text}var lines=text.replace(/\t/g," ").split(/\r?\n/);var min=null;var len=lines.length;var i;for(i=0;i0){for(i=0;i/g,">")}function u(a){if(a.nodeType===s.Node.ELEMENT_NODE)for(var e=a.attributes,b=0,c=e.length;b"))},end:function(a){a= 10 | q(a);b||!0!==w[a]||!0===y[a]||(c(""));a==b&&(b=!1)},chars:function(a){b||c(G(a))}}};var J=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,K=/([^\#-~ |!])/g,y=k("area,br,col,hr,img,wbr"),d=k("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),r=k("rp,rt"),p=l({},r,d),d=l({},d,k("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul")),r=l({},r,k("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")), 11 | x=k("circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,stop,svg,switch,text,title,tspan"),z=k("script,style"),w=l({},y,d,r,p),n=k("background,cite,href,longdesc,src,xlink:href"),p=k("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,valign,value,vspace,width"), 12 | r=k("accent-height,accumulate,additive,alphabetic,arabic-form,ascent,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan", 13 | !0),m=l({},n,r,p),f;(function(a){if(a.document&&a.document.implementation)a=a.document.implementation.createHTMLDocument("inert");else throw B("noinert");var e=(a.documentElement||a.getDocumentElement()).getElementsByTagName("body");1===e.length?f=e[0]:(e=a.createElement("html"),f=a.createElement("body"),e.appendChild(f),a.appendChild(e))})(s)});g.module("ngSanitize").filter("linky",["$sanitize",function(k){var l=/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i, 14 | q=/^mailto:/i,u=g.$$minErr("linky"),v=g.isDefined,s=g.isFunction,t=g.isObject,y=g.isString;return function(d,g,p){function x(a){a&&m.push(H(a))}function z(a,b){var c,d=w(a);m.push("');x(b);m.push("")}if(null==d||""===d)return d;if(!y(d))throw u("notstring",d);for(var w=s(p)?p:t(p)?function(){return p}:function(){return{}},n=d,m=[],f,a;d=n.match(l);)f=d[0],d[2]|| 15 | d[4]||(f=(d[3]?"http://":"mailto:")+f),a=d.index,x(n.substr(0,a)),z(f,d[0].replace(q,"")),n=n.substring(a+d[0].length);x(n);return k(m.join(""))}}])})(window,window.angular); 16 | //# sourceMappingURL=angular-sanitize.min.js.map 17 | -------------------------------------------------------------------------------- /www/js/lib/angular-sanitize.min.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version":3, 3 | "file":"angular-sanitize.min.js", 4 | "lineCount":15, 5 | "mappings":"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", 6 | "sources":["angular-sanitize.js"], 7 | "names":["window","angular","sanitizeText","chars","buf","htmlSanitizeWriter","writer","noop","join","$sanitizeMinErr","$$minErr","bind","extend","forEach","isDefined","lowercase","htmlParser","module","provider","$SanitizeProvider","toMap","str","lowercaseKeys","obj","items","split","i","length","attrToMap","attrs","map","ii","attr","name","value","encodeEntities","replace","SURROGATE_PAIR_REGEXP","hi","charCodeAt","low","NON_ALPHANUMERIC_REGEXP","stripCustomNsAttrs","node","nodeType","Node","ELEMENT_NODE","attributes","l","attrNode","attrName","toLowerCase","lastIndexOf","removeAttributeNode","nextNode","firstChild","nextSibling","svgEnabled","$get","$$sanitizeUri","validElements","svgElements","html","uri","isImage","test","enableSvg","this.enableSvg","htmlParserImpl","handler","undefined","inertBodyElement","innerHTML","mXSSAttempts","document","documentMode","start","nodeName","textContent","end","parentNode","removeChild","htmlSanitizeWriterImpl","uriValidator","ignoreCurrentElement","out","push","tag","blockedElements","key","lkey","validAttrs","uriAttrs","voidElements","optionalEndTagBlockElements","optionalEndTagInlineElements","optionalEndTagElements","blockElements","inlineElements","htmlAttrs","svgAttrs","implementation","doc","createHTMLDocument","bodyElements","getElementsByTagName","documentElement","getDocumentElement","createElement","appendChild","filter","$sanitize","LINKY_URL_REGEXP","MAILTO_REGEXP","linkyMinErr","isFunction","isObject","isString","text","target","addText","addLink","url","linkAttributes","attributesFn","getAttributesObject","getEmptyAttributesObject","raw","match","index","substr","substring"] 8 | } 9 | -------------------------------------------------------------------------------- /www/js/lib/jquery.bootstrap.confirm.popover.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2011 Damien Antipa, http://www.nethead.at/, http://damien.antipa.at 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | /* 24 | * jQuery Plugin: Confirmation Dialog 25 | * 26 | * Requirements: jQuery 1.6.4, Bootstrap 1.3.0 27 | * http://jquery.com/ 28 | * http://twitter.github.com/bootstrap/ 29 | * 30 | * This Plugin can be used for anchors, to show a confirmation popup before redirecting to the link. 31 | * Original code by Damian Antipa 32 | * 33 | */ 34 | (function($){ 35 | $.fn.extend({ 36 | confirmDialog: function(options) { 37 | var defaults = { 38 | //title: 'Confirmação', 39 | message: 'Tem certeza que deseja realizar esta operação?', 40 | dialog: '
' + 41 | '
' + 42 | '
' + 43 | //'

' + 44 | '
' + 45 | '

' + 46 | '

 

' + 47 | '
' + 48 | '
' + 49 | '
', 50 | okButton: 'Sim', 51 | cancelButton: 'Não', 52 | callback: null 53 | }; 54 | var options = $.extend(defaults, options); 55 | 56 | return this.each(function() { 57 | var o = options; 58 | var $elem = $(this) 59 | 60 | $elem.bind('click', function(e) { 61 | e.preventDefault(); 62 | if(!$('#confirm-dialog').length) { 63 | 64 | var offset = $elem.offset(); 65 | var $dialog = $(o.dialog).appendTo('body'); 66 | 67 | //$dialog.find('h3.popover-title').html(o.title); 68 | 69 | $dialog.find('p.message').html('' + o.message + ''); 70 | 71 | $dialog.find('button.btn:eq(0)').text(o.okButton).bind('click', function(e) { 72 | $dialog.remove(); 73 | if(o.callback) 74 | o.callback(); 75 | }); 76 | 77 | $dialog.find('button.btn:eq(1)').text(o.cancelButton).bind('click', function(e) { 78 | $dialog.remove(); 79 | }); 80 | 81 | $dialog.bind('mouseleave', function() { 82 | $dialog.fadeOut('slow', function() { 83 | $dialog.remove(); 84 | }); 85 | }); 86 | 87 | var x; 88 | if(offset.left > $dialog.width()) { 89 | //dialog can be left 90 | x = e.pageX - $dialog.width() - 20; 91 | $dialog.addClass('left'); 92 | } else { 93 | x = e.pageX + 10; 94 | $dialog.addClass('right'); 95 | } 96 | var y = e.pageY - $dialog.height() / 2 - $elem.innerHeight() / 2; 97 | 98 | $dialog.css({ 99 | display: 'block', 100 | position: 'absolute', 101 | top: y, 102 | left: x 103 | }); 104 | 105 | $dialog.find('p.button-group').css({ 106 | marginTop: '5px', 107 | textAlign: 'right' 108 | }); 109 | 110 | $dialog.find('a.btn').css({ 111 | marginLeft: '3px' 112 | }); 113 | 114 | } 115 | }); 116 | }); 117 | } 118 | }); 119 | })(jQuery); -------------------------------------------------------------------------------- /www/js/lib/jquery.ui.touch-punch.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery UI Touch Punch 0.2.3 3 | * 4 | * Copyright 2011–2014, Dave Furfero 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | * 7 | * Depends: 8 | * jquery.ui.widget.js 9 | * jquery.ui.mouse.js 10 | */ 11 | !function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery); -------------------------------------------------------------------------------- /www/js/lib/ngStorage.min.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | 5 | /** 6 | * @ngdoc overview 7 | * @name ngStorage 8 | */ 9 | 10 | angular.module('ngStorage', []). 11 | 12 | /** 13 | * @ngdoc object 14 | * @name ngStorage.$localStorage 15 | * @requires $rootScope 16 | * @requires $window 17 | */ 18 | 19 | factory('$localStorage', _storageFactory('localStorage')). 20 | 21 | /** 22 | * @ngdoc object 23 | * @name ngStorage.$sessionStorage 24 | * @requires $rootScope 25 | * @requires $window 26 | */ 27 | 28 | factory('$sessionStorage', _storageFactory('sessionStorage')); 29 | 30 | function _storageFactory(storageType) { 31 | return [ 32 | '$rootScope', 33 | '$window', 34 | '$log', 35 | 36 | function( 37 | $rootScope, 38 | $window, 39 | $log 40 | ){ 41 | // #9: Assign a placeholder object if Web Storage is unavailable to prevent breaking the entire AngularJS app 42 | var webStorage = $window[storageType] || ($log.warn('This browser does not support Web Storage!'), {}), 43 | $storage = { 44 | $default: function(items) { 45 | for (var k in items) { 46 | angular.isDefined($storage[k]) || ($storage[k] = items[k]); 47 | } 48 | 49 | return $storage; 50 | }, 51 | $reset: function(items) { 52 | for (var k in $storage) { 53 | '$' === k[0] || delete $storage[k]; 54 | } 55 | 56 | return $storage.$default(items); 57 | } 58 | }, 59 | _last$storage, 60 | _debounce; 61 | 62 | for (var i = 0, k; i < webStorage.length; i++) { 63 | // #8, #10: `webStorage.key(i)` may be an empty string (or throw an exception in IE9 if `webStorage` is empty) 64 | (k = webStorage.key(i)) && 'ngStorage-' === k.slice(0, 10) && ($storage[k.slice(10)] = angular.fromJson(webStorage.getItem(k))); 65 | } 66 | 67 | _last$storage = angular.copy($storage); 68 | 69 | $rootScope.$watch(function() { 70 | _debounce || (_debounce = setTimeout(function() { 71 | _debounce = null; 72 | 73 | if (!angular.equals($storage, _last$storage)) { 74 | angular.forEach($storage, function(v, k) { 75 | angular.isDefined(v) && '$' !== k[0] && webStorage.setItem('ngStorage-' + k, angular.toJson(v)); 76 | 77 | delete _last$storage[k]; 78 | }); 79 | 80 | for (var k in _last$storage) { 81 | webStorage.removeItem('ngStorage-' + k); 82 | } 83 | 84 | _last$storage = angular.copy($storage); 85 | } 86 | }, 100)); 87 | }); 88 | 89 | // #6: Use `$window.addEventListener` instead of `angular.element` to avoid the jQuery-specific `event.originalEvent` 90 | 'localStorage' === storageType && $window.addEventListener && $window.addEventListener('storage', function(event) { 91 | if ('ngStorage-' === event.key.slice(0, 10)) { 92 | event.newValue ? $storage[event.key.slice(10)] = angular.fromJson(event.newValue) : delete $storage[event.key.slice(10)]; 93 | 94 | _last$storage = angular.copy($storage); 95 | 96 | $rootScope.$apply(); 97 | } 98 | }); 99 | 100 | return $storage; 101 | } 102 | ]; 103 | } 104 | 105 | })(); -------------------------------------------------------------------------------- /www/js/lib/poller.js: -------------------------------------------------------------------------------- 1 | /// 2 | var Poller = (function () { 3 | function Poller(poll_url, options) { 4 | this.poll_url = poll_url; 5 | this.connected = false; 6 | this.run = false; 7 | this.running = false; 8 | this.versions = {}; 9 | // map declaration 10 | this.callbacks = {}; 11 | var self = this; 12 | 13 | options = options || {}; 14 | this.config = { 15 | timeout: options.timeout || 60000 16 | }; 17 | 18 | this.onMessage = function (eventName, callback) { 19 | var list = self.callbacks[eventName]; 20 | if (list == null) { 21 | self.versions[eventName] = 0; 22 | list = new Array(); 23 | self.callbacks[eventName] = list; 24 | } 25 | list.push(callback); 26 | return self; 27 | }; 28 | 29 | this.removeListener = function (eventName, callback) { 30 | var list = self.callbacks[eventName]; 31 | if (list != null) { 32 | for (var i = 0; i < list.length; i++) { 33 | if (list[i] === callback) { 34 | self.callbacks[eventName] = list.splice(i, 1); 35 | return self; 36 | } 37 | } 38 | } 39 | return self; 40 | }; 41 | 42 | function poll() { 43 | var poll_interval = 0; 44 | if (self.running) { 45 | return; 46 | } else { 47 | self.running = true; 48 | } 49 | 50 | $.ajax(self.poll_url, { 51 | type: 'GET', 52 | dataType: 'json', 53 | cache: false, 54 | data: self.versions, 55 | timeout: self.config.timeout 56 | }).done(function (messages) { 57 | for (var i = 0; i < messages.length; i++) { 58 | var message = messages[i]; 59 | if (message.version != 0) { 60 | self.versions[message.name] = message.version; 61 | var list = self.callbacks[message.name]; 62 | if (list != null) { 63 | for (var i = 0; i < list.length; i++) { 64 | var callback = list[i]; 65 | callback(message.data); 66 | } 67 | } 68 | } 69 | } 70 | if (!self.connected && self.onConnect != null) { 71 | self.onConnect(); 72 | } 73 | self.connected = true; 74 | poll_interval = 0; 75 | }).fail(function () { 76 | if (self.connected && self.onDisconnect != null) { 77 | self.onConnect(); 78 | } 79 | self.connected = false; 80 | poll_interval = 1000; 81 | }).always(function () { 82 | if (self.run) { 83 | setTimeout(poll, poll_interval); 84 | } 85 | self.running = false; 86 | }); 87 | } 88 | ; 89 | 90 | this.connect = function () { 91 | self.run = true; 92 | poll(); 93 | }; 94 | this.disconnect = function () { 95 | self.run = false; 96 | }; 97 | } 98 | return Poller; 99 | })(); 100 | -------------------------------------------------------------------------------- /www/js/lib/resizable-tables.js: -------------------------------------------------------------------------------- 1 | // Original from: http://bz.var.ru/comp/web/resizable-tables.js 2 | // 3 | // Resizable Table Columns. 4 | // version: 1.0 5 | // 6 | // (c) 2006, bz 7 | // 8 | // 25.12.2006: first working prototype 9 | // 26.12.2006: now works in IE as well but not in Opera (Opera is @#$%!) 10 | // 27.12.2006: changed initialization, now just make class='resizable' in table and load script 11 | // 12 | 13 | 14 | (function($){ 15 | $.fn.resizableColumns = function() { 16 | // get argument list passed and make into an array to use later 17 | var args = Array.prototype.slice.apply(arguments); 18 | 19 | return this.each(function() { 20 | var tElement = $(this); 21 | 22 | var table = tElement.get(0); 23 | if (table.tagName != 'TABLE') return; 24 | 25 | // ============================================================ 26 | 27 | if (table.rows.length == 0) return; 28 | var dragColumns = table.rows[0].cells; // first row columns, used for changing of width 29 | if (!dragColumns) return; // return if no table exists or no one row exists 30 | 31 | var dragColumnNo; // current dragging column 32 | var dragX; // last event X mouse coordinate 33 | 34 | var settings = { 35 | onStop: null, 36 | widths : null 37 | }, 38 | // setup default configuration value 39 | instanceMethods = { 40 | widths : function(values) { 41 | // apply widths 42 | for(var i=0; i< dragColumns.length && i < values.length; i++) { 43 | dragColumns[i].style.width = values[i]; 44 | } 45 | }, 46 | onStop : function(handler) { 47 | settings.onStop = handler; 48 | } 49 | }; 50 | 51 | // the "data" in jQuery is used to keep track of instance state 52 | if (tElement.data('resizableColumns-defined')) { 53 | settings = tElement.data('resizableColumns-config'); 54 | if (args.length > 0) { 55 | instanceMethods[args[0]].apply(this, args.slice(1,args.length)); 56 | } 57 | } else { 58 | tElement.data('resizableColumns-defined', true); 59 | if (args.length > 0 && $.isPlainObject(args[0])) { 60 | settings = $.extend(settings, args[0]); 61 | } 62 | tElement.data('resizableColumns-config', settings); 63 | } 64 | 65 | 66 | // ============================================================ 67 | // methods 68 | 69 | // ============================================================ 70 | // do changes columns widths 71 | // returns true if success and false otherwise 72 | var changeColumnWidth = function(no, w) { 73 | if (!dragColumns) return false; 74 | 75 | if (no < 0) return false; 76 | if (dragColumns.length < no) return false; 77 | 78 | if (parseInt(dragColumns[no].style.width) <= -w) return false; 79 | if (dragColumns[no+1] && parseInt(dragColumns[no+1].style.width) <= w) return false; 80 | 81 | dragColumns[no].style.width = parseInt(dragColumns[no].style.width) + w +'px'; 82 | if (dragColumns[no+1]) 83 | dragColumns[no+1].style.width = parseInt(dragColumns[no+1].style.width) - w + 'px'; 84 | 85 | return true; 86 | } 87 | 88 | // ============================================================ 89 | // do drag column width 90 | var columnDrag = function(e) { 91 | var X = e.pageX; 92 | if (!changeColumnWidth(dragColumnNo, X - dragX)) { 93 | // stop drag! 94 | stopColumnDrag(e); 95 | } 96 | 97 | dragX = X; 98 | // prevent other event handling 99 | e.stopPropagation(); 100 | return false; 101 | } 102 | 103 | // ============================================================ 104 | // stops column dragging 105 | var stopColumnDrag = function(e) { 106 | if (!dragColumns) return; 107 | 108 | if(settings.onStop != null){ 109 | var widths = []; 110 | for(var i=0; i< dragColumns.length; i++) 111 | widths.push(dragColumns[i].style.width); 112 | 113 | settings.onStop(widths); 114 | } 115 | 116 | // restore handlers & cursor 117 | $(document).unbind(e); 118 | $(document).unbind("mousemove", columnDrag); 119 | 120 | e.stopPropagation(); 121 | } 122 | 123 | // ============================================================ 124 | 125 | // prepare table header to be draggable 126 | // it runs during class creation 127 | tElement.find("th").each(function(index){ 128 | var TH = $(this); 129 | var padd = TH.css("padding-right"); 130 | // div around the title, to avoid unwanted click events. Dragging over this div won't fire click events. 131 | TH.wrapInner("
"); 132 | // the drag slider 133 | $("
").appendTo(TH); 135 | // content div 136 | TH.wrapInner("
"); 137 | // init data and start dragging 138 | TH.find(".slider") 139 | .css("margin-right", "-=" + padd) 140 | .bind("mousedown", function(e) { 141 | // remember dragging object 142 | dragColumnNo = index; 143 | dragX = e.pageX; 144 | 145 | // set up current columns widths in their particular attributes 146 | // do it in two steps to avoid jumps on page! 147 | var colWidth = new Array(); 148 | for (var i=0; i×<\/button>",newestOnTop:!0}}function h(n){e&&e(n)}function u(r){function l(t){if(!n(":focus",e).length||t)return e[u.hideMethod]({duration:u.hideDuration,easing:u.hideEasing,complete:function(){c(e),u.onHidden&&u.onHidden(),s.state="hidden",s.endTime=new Date,h(s)}})}function b(){(u.timeOut>0||u.extendedTimeOut>0)&&(y=setTimeout(l,u.extendedTimeOut))}function k(){clearTimeout(y),e.stop(!0,!0)[u.showMethod]({duration:u.showDuration,easing:u.showEasing})}var u=i(),v=r.iconClass||u.iconClass;typeof r.optionsOverride!="undefined"&&(u=n.extend(u,r.optionsOverride),v=r.optionsOverride.iconClass||v),o++,t=f(u);var y=null,e=n("
"),p=n("
"),w=n("
"),a=n(u.closeHtml),s={toastId:o,state:"visible",startTime:new Date,options:u,map:r};return r.iconClass&&e.addClass(u.toastClass).addClass(v),r.title&&(p.append(r.title).addClass(u.titleClass),e.append(p)),r.message&&(w.append(r.message).addClass(u.messageClass),e.append(w)),u.closeButton&&(a.addClass("toast-close-button"),e.prepend(a)),e.hide(),u.newestOnTop?t.prepend(e):t.append(e),e[u.showMethod]({duration:u.showDuration,easing:u.showEasing,complete:u.onShown}),u.timeOut>0&&(y=setTimeout(l,u.timeOut)),e.hover(k,b),!u.onclick&&u.tapToDismiss&&e.click(l),u.closeButton&&a&&a.click(function(n){n.stopPropagation(),l(!0)}),u.onclick&&e.click(function(){u.onclick(),l()}),h(s),u.debug&&console&&console.log(s),e}function f(r){return(r||(r=i()),t=n("#"+r.containerId),t.length)?t:(t=n("
").attr("id",r.containerId).addClass(r.positionClass),t.appendTo(n(r.target)),t)}function i(){return n.extend({},b(),s.options)}function c(n){(t||(t=f()),n.is(":visible"))||(n.remove(),n=null,t.children().length===0&&t.remove())}var t,e,o=0,r={error:"error",info:"info",success:"success",warning:"warning"},s={clear:w,error:l,getContainer:f,info:a,options:{},subscribe:v,success:y,version:"2.0.1",warning:p};return s}()})})(typeof define=="function"&&define.amd?define:function(n,t){typeof module!="undefined"&&module.exports?module.exports=t(require(n[0])):window.toastr=t(window.jQuery)}); 2 | //@ sourceMappingURL=toastr.min.js.map -------------------------------------------------------------------------------- /www/partials/about.html: -------------------------------------------------------------------------------- 1 |
2 |
About
3 | 4 |

Task Board

5 |

paulo.quintans@gmail.com

6 |
-------------------------------------------------------------------------------- /www/partials/board.html: -------------------------------------------------------------------------------- 1 |
2 |
Add columns to start using the board
3 | 4 | 5 | 16 | 17 | 18 | 43 | 44 |
6 | {{lane.name}} 7 | 8 | 15 |
19 |
20 |
21 | 23 | 31 | {{task.title}} 32 | {{task.title}} 33 | 34 |
35 | spent: {{task.spent || 0}} h 36 | remaining: {{task.remaining || 0}} h 37 |
38 |
39 |
41 |
42 |
45 |
46 | 47 | 118 | 119 | 211 | -------------------------------------------------------------------------------- /www/partials/user.html: -------------------------------------------------------------------------------- 1 |
2 |

{{identity.name}}

3 | 4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 | 19 |
20 |
21 | 22 | 24 |
25 |
26 | 27 | 29 |
30 |
31 | 32 |
33 |
34 | 37 | 38 |
-------------------------------------------------------------------------------- /www/partials/users.html: -------------------------------------------------------------------------------- 1 |
2 |

Users

3 | 4 |
5 |
6 | 7 |
8 | 9 | 10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 43 | 44 | 45 |
NameLoginAdminActions
{{user.name}}{{user.username}}admin 37 | delete 42 |
46 |
47 |
48 | 51 | 52 | 107 | 108 |
-------------------------------------------------------------------------------- /www/partials/welcome.html: -------------------------------------------------------------------------------- 1 |
2 |
Welcome
3 | 4 |

Task Board

5 |

Choose an existing task board or create a new one.

6 |
--------------------------------------------------------------------------------