Commit 99843e78 authored by Lee Brown's avatar Lee Brown
Browse files

completed project section

parent d574f8e2
......@@ -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("<a href='%s'>%s</a>", 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("<span class='cell-font-status %s'><i class='%s'></i>%s</span>", subStatusClass, subStatusIcon, web.EnumValueTitle(v.Value))
case "created_at":
dt := web.NewTimeResponse(ctx, q.CreatedAt)
v.Value = dt.Local
v.Formatted = fmt.Sprintf("<span class='cell-font-date'>%s</span>", v.Value)
case "updated_at":
dt := web.NewTimeResponse(ctx, q.UpdatedAt)
v.Value = dt.Local
v.Formatted = fmt.Sprintf("<span class='cell-font-date'>%s</span>", 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)
}
......@@ -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{
......
{{define "title"}}Create Project{{end}}
{{define "style"}}
{{end}}
{{define "content"}}
<form class="user" method="post" novalidate>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="inputName">Name</label>
<input type="text" class="form-control {{ ValidationFieldClass $.validationErrors "Name" }}"
placeholder="enter name" name="Name" id="inputName" value="{{ .form.Name }}" required>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Name" }}
</div>
</div>
</div>
<div class="spacer-30"></div>
<div class="row">
<div class="col">
<input id="btnSubmit" type="submit" name="action" value="Save" class="btn btn-primary"/>
</div>
</div>
</form>
{{end}}
{{define "js"}}
{{end}}
{{define "title"}}Projects{{end}}
{{define "content"}}
{{ if HasRole $._Ctx "admin" }}
<a href="{{ .urlProjectsCreate }}">Create Project</a>
{{ end }}
<div class="row">
<div class="col">
<form method="post">
<div class="card">
<div class="table-responsive dataTable_card">
{{ template "partials/datatable/html" . }}
</div>
</div>
</form>
</div>
</div>
{{end}}
{{define "style"}}
{{ template "partials/datatable/style" . }}
{{ end }}
{{define "js"}}
{{ template "partials/datatable/js" . }}
<script>
$(document).ready(function(){
//$("#dataTable_filter").hide();
});
</script>
{{end}}
{{define "title"}}Update Project - {{ .project.Name }}{{end}}
{{define "style"}}
{{end}}
{{define "content"}}
<form class="user" method="post" novalidate>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="inputName">Name</label>
<input type="text" class="form-control {{ ValidationFieldClass $.validationErrors "Name" }}"
placeholder="enter name" name="Name" id="inputName" value="{{ .form.Name }}" required>
{{template "invalid-feedback" dict "validationDefaults" $.userValidationDefaults "validationErrors" $.validationErrors "fieldName" "Name" }}
</div>
<div class="form-group">
<label for="selectStatus">Status</label>
<select class="form-control {{ ValidationFieldClass $.validationErrors "Status" }}"
id="selectStatus" name="Status">
{{ range $idx, $t := .project.Status.Options }}
<option value="{{ $t.Value }}" {{ if $t.Selected }}selected="selected"{{ end }}>{{ $t.Title }}</option>
{{ end }}
</select>
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Status" }}
</div>
</div>
</div>
<div class="spacer-30"></div>
<div class="row">
<div class="col">
<input id="btnSubmit" type="submit" name="action" value="Save" class="btn btn-primary"/>
</div>
</div>
</form>
{{end}}
{{define "js"}}
{{end}}
{{define "title"}}Project - {{ .project.Name }}{{end}}
{{define "style"}}
{{end}}
{{define "content"}}
<div class="row">
<div class="col">
<div class="row">
<div class="col">
<h4>Name</h4>
<p class="font-14">
{{ .project.Name }}
</p>
</div>
</div>
</div>
<div class="col-auto">
<a href="{{ .urlProjectsUpdate }}" class="btn btn-outline-success"><i class="fal fa-edit"></i>Edit Details</a>
{{ if HasRole $._Ctx "admin" }}
<form method="post"><input type="hidden" name="action" value="archive" /><input type="submit" value="Archive"></form>
{{ end }}
</div>
</div>
<div class="spacer-30"></div>
<div class="row">
<div class="col-md-6">
<p>
<small>Name</small><br/>
<b>{{ .project.Name }}</b>
</p>
<div class="spacer-15"></div>
</div>
<div class="col-md-6">
<p>
<small>Status</small><br/>
{{ if .project }}
<b>
{{ if eq .project.Status.Value "active" }}
<span class="text-green"><i class="fas fa-circle"></i>{{ .project.Status.Title }}</span>
{{else}}
<span class="text-orange"><i class="far fa-circle"></i>{{.project.Status.Title }}</span>
{{end}}
</b>
{{ end }}
</p>
<p>
<small>ID</small><br/>
<b>{{ .project.ID }}</b>
</p>
</div>
</div>
{{end}}
{{define "js"}}
{{end}}
{{define "title"}}Users{{end}}
{{define "content"}}
<a href="{{ .urlUsersCreate }}">Create User</a>
{{ if HasRole $._Ctx "admin" }}
<a href="{{ .urlUsersCreate }}">Create User</a>
{{ end }}
<div class="row">
<div class="col">
<form method="post">
......
{{define "title"}}Update Profile{{end}}
{{define "title"}}Update User - {{ .user.Name }}{{end}}