From 99843e7868ea216bda40b41b99c415f01f9bb834 Mon Sep 17 00:00:00 2001 From: Lee Brown Date: Mon, 5 Aug 2019 03:25:24 -0800 Subject: [PATCH] completed project section --- cmd/web-app/handlers/projects.go | 381 +++++++++++++++++- cmd/web-app/handlers/routes.go | 9 +- .../templates/content/projects-create.gohtml | 27 ++ .../templates/content/projects-index.gohtml | 30 ++ .../templates/content/projects-update.gohtml | 37 ++ .../templates/content/projects-view.gohtml | 57 +++ .../templates/content/users-index.gohtml | 4 +- .../templates/content/users-update.gohtml | 2 +- .../templates/content/users-view.gohtml | 16 +- .../templates/partials/app-sidebar.tmpl | 3 +- .../templates/partials/app-topbar.tmpl | 22 +- internal/account/account_preference/models.go | 11 +- internal/account/models.go | 11 +- internal/platform/web/models.go | 15 +- internal/project/models.go | 11 +- internal/user_account/models.go | 20 +- 16 files changed, 618 insertions(+), 38 deletions(-) create mode 100644 cmd/web-app/templates/content/projects-create.gohtml create mode 100644 cmd/web-app/templates/content/projects-index.gohtml create mode 100644 cmd/web-app/templates/content/projects-update.gohtml create mode 100644 cmd/web-app/templates/content/projects-view.gohtml diff --git a/cmd/web-app/handlers/projects.go b/cmd/web-app/handlers/projects.go index 2843e3c..c05202c 100644 --- a/cmd/web-app/handlers/projects.go +++ b/cmd/web-app/handlers/projects.go @@ -2,20 +2,391 @@ package handlers import ( "context" + "fmt" "net/http" + "strings" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/auth" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/datatable" "geeks-accelerator/oss/saas-starter-kit/internal/platform/web" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext" + "geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror" + "geeks-accelerator/oss/saas-starter-kit/internal/project" + "github.com/gorilla/schema" "github.com/jmoiron/sqlx" + "github.com/pkg/errors" + "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis" ) -// User represents the User API method handler set. +// Projects represents the Projects API method handler set. type Projects struct { MasterDB *sqlx.DB + Redis *redis.Client Renderer web.Renderer - // ADD OTHER STATE LIKE THE LOGGER AND CONFIG HERE. } -// List returns all the existing users in the system. -func (p *Projects) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { - return p.Renderer.Render(ctx, w, r, TmplLayoutBase, "projects-index.tmpl", web.MIMETextHTMLCharsetUTF8, http.StatusOK, nil) +func urlProjectsIndex() string { + return fmt.Sprintf("/projects") +} + +func urlProjectsCreate() string { + return fmt.Sprintf("/projects/create") +} + +func urlProjectsView(projectID string) string { + return fmt.Sprintf("/projects/%s", projectID) +} + +func urlProjectsUpdate(projectID string) string { + return fmt.Sprintf("/projects/%s/update", projectID) +} + +// Index handles listing all the projects for the current account. +func (h *Projects) Index(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + + claims, err := auth.ClaimsFromContext(ctx) + if err != nil { + return err + } + + var statusValues []interface{} + for _, v := range project.ProjectStatus_Values { + statusValues = append(statusValues, string(v)) + } + + statusOpts := web.NewEnumResponse(ctx, nil, statusValues...) + + statusFilterItems := []datatable.FilterOptionItem{} + for _, opt := range statusOpts.Options { + statusFilterItems = append(statusFilterItems, datatable.FilterOptionItem{ + Display: opt.Title, + Value: opt.Value, + }) + } + + fields := []datatable.DisplayField{ + datatable.DisplayField{Field: "id", Title: "ID", Visible: false, Searchable: true, Orderable: true, Filterable: false}, + datatable.DisplayField{Field: "name", Title: "Project", Visible: true, Searchable: true, Orderable: true, Filterable: true, FilterPlaceholder: "filter Name"}, + datatable.DisplayField{Field: "status", Title: "Status", Visible: true, Searchable: true, Orderable: true, Filterable: true, FilterPlaceholder: "All Statuses", FilterItems: statusFilterItems}, + datatable.DisplayField{Field: "updated_at", Title: "Last Updated", Visible: true, Searchable: true, Orderable: true, Filterable: false}, + datatable.DisplayField{Field: "created_at", Title: "Created", Visible: true, Searchable: true, Orderable: true, Filterable: false}, + } + + mapFunc := func(q *project.Project, cols []datatable.DisplayField) (resp []datatable.ColumnValue, err error) { + for i := 0; i < len(cols); i++ { + col := cols[i] + var v datatable.ColumnValue + switch col.Field { + case "id": + v.Value = fmt.Sprintf("%d", q.ID) + case "name": + v.Value = q.Name + v.Formatted = fmt.Sprintf("%s", urlProjectsView(q.ID), v.Value) + case "status": + v.Value = q.Status.String() + + var subStatusClass string + var subStatusIcon string + switch q.Status { + case project.ProjectStatus_Active: + subStatusClass = "text-green" + subStatusIcon = "far fa-dot-circle" + case project.ProjectStatus_Disabled: + subStatusClass = "text-orange" + subStatusIcon = "far fa-circle" + } + + v.Formatted = fmt.Sprintf("%s", subStatusClass, subStatusIcon, web.EnumValueTitle(v.Value)) + case "created_at": + dt := web.NewTimeResponse(ctx, q.CreatedAt) + v.Value = dt.Local + v.Formatted = fmt.Sprintf("%s", v.Value) + case "updated_at": + dt := web.NewTimeResponse(ctx, q.UpdatedAt) + v.Value = dt.Local + v.Formatted = fmt.Sprintf("%s", v.Value) + default: + return resp, errors.Errorf("Failed to map value for %s.", col.Field) + } + resp = append(resp, v) + } + + return resp, nil + } + + loadFunc := func(ctx context.Context, sorting string, fields []datatable.DisplayField) (resp [][]datatable.ColumnValue, err error) { + whereFilter := "account_id = ?" + res, err := project.Find(ctx, claims, h.MasterDB, project.ProjectFindRequest{ + Where: &whereFilter, + Args: []interface{}{claims.Audience}, + Order: strings.Split(sorting, ","), + }) + if err != nil { + return resp, err + } + + for _, a := range res { + l, err := mapFunc(a, fields) + if err != nil { + return resp, errors.Wrapf(err, "Failed to map project for display.") + } + + resp = append(resp, l) + } + + return resp, nil + } + + dt, err := datatable.New(ctx, w, r, h.Redis, fields, loadFunc) + if err != nil { + return err + } + + if dt.HasCache() { + return nil + } + + if ok, err := dt.Render(); ok { + if err != nil { + return err + } + return nil + } + + data := map[string]interface{}{ + "datatable": dt.Response(), + "urlProjectsCreate": urlProjectsCreate(), + } + + return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "projects-index.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) +} + +// Create handles creating a new project for the account. +func (h *Projects) Create(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + + ctxValues, err := webcontext.ContextValues(ctx) + if err != nil { + return err + } + + claims, err := auth.ClaimsFromContext(ctx) + if err != nil { + return err + } + + // + req := new(project.ProjectCreateRequest) + data := make(map[string]interface{}) + f := func() (bool, error) { + if r.Method == http.MethodPost { + err := r.ParseForm() + if err != nil { + return false, err + } + + decoder := schema.NewDecoder() + decoder.IgnoreUnknownKeys(true) + + if err := decoder.Decode(req, r.PostForm); err != nil { + return false, err + } + req.AccountID = claims.Audience + + usr, err := project.Create(ctx, claims, h.MasterDB, *req, ctxValues.Now) + if err != nil { + switch errors.Cause(err) { + default: + if verr, ok := weberror.NewValidationError(ctx, err); ok { + data["validationErrors"] = verr.(*weberror.Error) + return false, nil + } else { + return false, err + } + } + } + + // Display a success message to the project. + webcontext.SessionFlashSuccess(ctx, + "Project Created", + "Project successfully created.") + err = webcontext.ContextSession(ctx).Save(r, w) + if err != nil { + return false, err + } + + http.Redirect(w, r, urlProjectsView(usr.ID), http.StatusFound) + return true, nil + } + + return false, nil + } + + end, err := f() + if err != nil { + return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8) + } else if end { + return nil + } + + data["form"] = req + + if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(project.ProjectCreateRequest{})); ok { + data["validationDefaults"] = verr.(*weberror.Error) + } + + return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "projects-create.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) +} + +// View handles displaying a project. +func (h *Projects) View(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + + projectID := params["project_id"] + + ctxValues, err := webcontext.ContextValues(ctx) + if err != nil { + return err + } + + claims, err := auth.ClaimsFromContext(ctx) + if err != nil { + return err + } + + data := make(map[string]interface{}) + f := func() (bool, error) { + if r.Method == http.MethodPost { + err := r.ParseForm() + if err != nil { + return false, err + } + + switch r.PostForm.Get("action") { + case "archive": + err = project.Archive(ctx, claims, h.MasterDB, project.ProjectArchiveRequest{ + ID: projectID, + }, ctxValues.Now) + if err != nil { + return false, err + } + + webcontext.SessionFlashSuccess(ctx, + "Project Archive", + "Project successfully archive.") + err = webcontext.ContextSession(ctx).Save(r, w) + if err != nil { + return false, err + } + + http.Redirect(w, r, urlProjectsIndex(), http.StatusFound) + return true, nil + } + } + + return false, nil + } + + end, err := f() + if err != nil { + return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8) + } else if end { + return nil + } + + prj, err := project.ReadByID(ctx, claims, h.MasterDB, projectID) + if err != nil { + return err + } + data["project"] = prj.Response(ctx) + + data["urlProjectsUpdate"] = urlProjectsUpdate(projectID) + + return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "projects-view.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) +} + +// Update handles updating a project for the account. +func (h *Projects) Update(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error { + + projectID := params["project_id"] + + ctxValues, err := webcontext.ContextValues(ctx) + if err != nil { + return err + } + + claims, err := auth.ClaimsFromContext(ctx) + if err != nil { + return err + } + + // + req := new(project.ProjectUpdateRequest) + data := make(map[string]interface{}) + f := func() (bool, error) { + if r.Method == http.MethodPost { + err := r.ParseForm() + if err != nil { + return false, err + } + + decoder := schema.NewDecoder() + decoder.IgnoreUnknownKeys(true) + + if err := decoder.Decode(req, r.PostForm); err != nil { + return false, err + } + req.ID = projectID + + err = project.Update(ctx, claims, h.MasterDB, *req, ctxValues.Now) + if err != nil { + switch errors.Cause(err) { + default: + if verr, ok := weberror.NewValidationError(ctx, err); ok { + data["validationErrors"] = verr.(*weberror.Error) + return false, nil + } else { + return false, err + } + } + } + + // Display a success message to the project. + webcontext.SessionFlashSuccess(ctx, + "Project Updated", + "Project successfully updated.") + err = webcontext.ContextSession(ctx).Save(r, w) + if err != nil { + return false, err + } + + http.Redirect(w, r, urlProjectsView(req.ID), http.StatusFound) + return true, nil + } + + return false, nil + } + + end, err := f() + if err != nil { + return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8) + } else if end { + return nil + } + + prj, err := project.ReadByID(ctx, claims, h.MasterDB, projectID) + if err != nil { + return err + } + data["project"] = prj.Response(ctx) + + if req.ID == "" { + req.Name = &prj.Name + req.Status = &prj.Status + } + data["form"] = req + + if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(project.ProjectUpdateRequest{})); ok { + data["validationDefaults"] = verr.(*weberror.Error) + } + + return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "projects-update.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data) } diff --git a/cmd/web-app/handlers/routes.go b/cmd/web-app/handlers/routes.go index 672c202..ed82119 100644 --- a/cmd/web-app/handlers/routes.go +++ b/cmd/web-app/handlers/routes.go @@ -43,8 +43,15 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir // Register project management pages. p := Projects{ MasterDB: masterDB, + Redis: redis, Renderer: renderer, } + app.Handle("POST", "/projects/:project_id/update", p.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("GET", "/projects/:project_id/update", p.Update, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("POST", "/projects/:project_id", p.View, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("GET", "/projects/:project_id", p.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth()) + app.Handle("POST", "/projects/create", p.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("GET", "/projects/create", p.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin)) app.Handle("GET", "/projects", p.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth()) // Register user management pages. @@ -63,7 +70,7 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir app.Handle("GET", "/users/:user_id", us.View, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth()) app.Handle("POST", "/users/create", us.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin)) app.Handle("GET", "/users/create", us.Create, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin)) - app.Handle("GET", "/users", us.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasRole(auth.RoleAdmin)) + app.Handle("GET", "/users", us.Index, mid.AuthenticateSessionRequired(authenticator), mid.HasAuth()) // Register user management and authentication endpoints. u := User{ diff --git a/cmd/web-app/templates/content/projects-create.gohtml b/cmd/web-app/templates/content/projects-create.gohtml new file mode 100644 index 0000000..f089585 --- /dev/null +++ b/cmd/web-app/templates/content/projects-create.gohtml @@ -0,0 +1,27 @@ +{{define "title"}}Create Project{{end}} +{{define "style"}} + +{{end}} +{{define "content"}} +
+
+
+
+ + + {{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Name" }} +
+
+
+
+
+
+ +
+
+
+{{end}} +{{define "js"}} + +{{end}} diff --git a/cmd/web-app/templates/content/projects-index.gohtml b/cmd/web-app/templates/content/projects-index.gohtml new file mode 100644 index 0000000..899bd4b --- /dev/null +++ b/cmd/web-app/templates/content/projects-index.gohtml @@ -0,0 +1,30 @@ +{{define "title"}}Projects{{end}} +{{define "content"}} + {{ if HasRole $._Ctx "admin" }} + Create Project + {{ end }} +
+
+
+
+
+ {{ template "partials/datatable/html" . }} +
+
+
+
+
+{{end}} +{{define "style"}} + {{ template "partials/datatable/style" . }} +{{ end }} +{{define "js"}} + {{ template "partials/datatable/js" . }} + + + +{{end}} diff --git a/cmd/web-app/templates/content/projects-update.gohtml b/cmd/web-app/templates/content/projects-update.gohtml new file mode 100644 index 0000000..4649bc5 --- /dev/null +++ b/cmd/web-app/templates/content/projects-update.gohtml @@ -0,0 +1,37 @@ +{{define "title"}}Update Project - {{ .project.Name }}{{end}} +{{define "style"}} + +{{end}} +{{define "content"}} +
+
+
+
+ + + {{template "invalid-feedback" dict "validationDefaults" $.userValidationDefaults "validationErrors" $.validationErrors "fieldName" "Name" }} +
+
+ + + {{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Status" }} +
+
+
+
+
+
+ +
+
+
+{{end}} +{{define "js"}} + +{{end}} diff --git a/cmd/web-app/templates/content/projects-view.gohtml b/cmd/web-app/templates/content/projects-view.gohtml new file mode 100644 index 0000000..d24be75 --- /dev/null +++ b/cmd/web-app/templates/content/projects-view.gohtml @@ -0,0 +1,57 @@ +{{define "title"}}Project - {{ .project.Name }}{{end}} +{{define "style"}} + +{{end}} +{{define "content"}} +
+
+
+
+

Name

+

+ {{ .project.Name }} +

+
+
+
+
+ Edit Details + {{ if HasRole $._Ctx "admin" }} +
+ {{ end }} +
+
+ +
+ +
+
+

+ Name
+ {{ .project.Name }} +

+
+
+
+

+ Status
+ {{ if .project }} + + {{ if eq .project.Status.Value "active" }} + {{ .project.Status.Title }} + {{else}} + {{.project.Status.Title }} + {{end}} + + {{ end }} +

+

+ ID
+ {{ .project.ID }} +

+
+
+{{end}} +{{define "js"}} + +{{end}} diff --git a/cmd/web-app/templates/content/users-index.gohtml b/cmd/web-app/templates/content/users-index.gohtml index fb3ad91..7401506 100644 --- a/cmd/web-app/templates/content/users-index.gohtml +++ b/cmd/web-app/templates/content/users-index.gohtml @@ -1,6 +1,8 @@ {{define "title"}}Users{{end}} {{define "content"}} - Create User + {{ if HasRole $._Ctx "admin" }} + Create User + {{ end }}
diff --git a/cmd/web-app/templates/content/users-update.gohtml b/cmd/web-app/templates/content/users-update.gohtml index 75a9eda..fdec1a3 100644 --- a/cmd/web-app/templates/content/users-update.gohtml +++ b/cmd/web-app/templates/content/users-update.gohtml @@ -1,4 +1,4 @@ -{{define "title"}}Update Profile{{end}} +{{define "title"}}Update User - {{ .user.Name }}{{end}} {{define "style"}} {{end}} diff --git a/cmd/web-app/templates/content/users-view.gohtml b/cmd/web-app/templates/content/users-view.gohtml index c6521e7..6da88cb 100644 --- a/cmd/web-app/templates/content/users-view.gohtml +++ b/cmd/web-app/templates/content/users-view.gohtml @@ -1,4 +1,4 @@ -{{define "title"}}Profile{{end}} +{{define "title"}}User - {{ .user.Name }}{{end}} {{define "style"}} {{end}} @@ -16,15 +16,15 @@

-
-

Update Avatar

- Edit Details - {{ $ctxUser := ContextUser $._Ctx }} - {{ if $ctxUser }} - {{ if ne .user.ID $ctxUser.ID }} - + {{ if HasRole $._Ctx "admin" }} + Edit Details + {{ $ctxUser := ContextUser $._Ctx }} + {{ if $ctxUser }} + {{ if ne .user.ID $ctxUser.ID }} +
+ {{ end }} {{ end }} {{ end }}
diff --git a/cmd/web-app/templates/partials/app-sidebar.tmpl b/cmd/web-app/templates/partials/app-sidebar.tmpl index 0d0a295..6b8914e 100644 --- a/cmd/web-app/templates/partials/app-sidebar.tmpl +++ b/cmd/web-app/templates/partials/app-sidebar.tmpl @@ -38,8 +38,7 @@ diff --git a/cmd/web-app/templates/partials/app-topbar.tmpl b/cmd/web-app/templates/partials/app-topbar.tmpl index ad62963..8798175 100644 --- a/cmd/web-app/templates/partials/app-topbar.tmpl +++ b/cmd/web-app/templates/partials/app-topbar.tmpl @@ -25,11 +25,11 @@