1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 21:29:42 +02:00

space categorty management

This commit is contained in:
Harvey Kandola 2017-09-19 17:58:33 +01:00
parent a86d52388e
commit 4874d23f15
19 changed files with 915 additions and 40 deletions

View file

@ -34,6 +34,7 @@ CREATE TABLE IF NOT EXISTS `category` (
`labelid` CHAR(16) NOT NULL COLLATE utf8_bin, `labelid` CHAR(16) NOT NULL COLLATE utf8_bin,
`category` VARCHAR(30) NOT NULL, `category` VARCHAR(30) NOT NULL,
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX `idx_category_id` (`id` ASC), UNIQUE INDEX `idx_category_id` (`id` ASC),
INDEX `idx_category_refid` (`refid` ASC), INDEX `idx_category_refid` (`refid` ASC),
INDEX `idx_category_orgid` (`orgid` ASC)) INDEX `idx_category_orgid` (`orgid` ASC))
@ -47,8 +48,11 @@ CREATE TABLE IF NOT EXISTS `categorymember` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`refid` CHAR(16) NOT NULL COLLATE utf8_bin, `refid` CHAR(16) NOT NULL COLLATE utf8_bin,
`orgid` CHAR(16) NOT NULL COLLATE utf8_bin, `orgid` CHAR(16) NOT NULL COLLATE utf8_bin,
`labelid` CHAR(16) NOT NULL COLLATE utf8_bin,
`categoryid` CHAR(16) NOT NULL COLLATE utf8_bin, `categoryid` CHAR(16) NOT NULL COLLATE utf8_bin,
`documentid` CHAR(16) NOT NULL COLLATE utf8_bin, `documentid` CHAR(16) NOT NULL COLLATE utf8_bin,
`created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE INDEX `idx_categorymember_id` (`id` ASC), UNIQUE INDEX `idx_categorymember_id` (`id` ASC),
INDEX `idx_category_documentid` (`documentid`)) INDEX `idx_category_documentid` (`documentid`))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci

285
domain/category/endpoint.go Normal file
View file

@ -0,0 +1,285 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
// Package category handles API calls and persistence for categories.
// Categories sub-divide spaces.
package category
import (
"database/sql"
"encoding/json"
"io/ioutil"
"net/http"
"github.com/documize/community/core/env"
"github.com/documize/community/core/request"
"github.com/documize/community/core/response"
"github.com/documize/community/core/uniqueid"
"github.com/documize/community/domain"
"github.com/documize/community/domain/permission"
"github.com/documize/community/model/audit"
"github.com/documize/community/model/category"
pm "github.com/documize/community/model/permission"
)
// Handler contains the runtime information such as logging and database.
type Handler struct {
Runtime *env.Runtime
Store *domain.Store
}
// Add saves space category.
func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
method := "category.add"
ctx := domain.GetRequestContext(r)
if !ctx.Authenticated {
response.WriteForbiddenError(w)
return
}
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, "body")
h.Runtime.Log.Error(method, err)
return
}
var cat category.Category
err = json.Unmarshal(body, &cat)
if err != nil {
response.WriteBadRequestError(w, method, "category")
h.Runtime.Log.Error(method, err)
return
}
cat.RefID = uniqueid.Generate()
cat.OrgID = ctx.OrgID
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
err = h.Store.Category.Add(ctx, cat)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Audit.Record(ctx, audit.EventTypeCategoryAdd)
ctx.Transaction.Commit()
cat, err = h.Store.Category.Get(ctx, cat.RefID)
if err != nil {
response.WriteServerError(w, method, err)
return
}
response.WriteJSON(w, cat)
}
// Get returns categories visible to user within a space.
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
method := "category.get"
ctx := domain.GetRequestContext(r)
spaceID := request.Param(r, "spaceID")
if len(spaceID) == 0 {
response.WriteMissingDataError(w, method, "spaceID")
return
}
ok := permission.HasPermission(ctx, *h.Store, spaceID,
pm.SpaceManage, pm.SpaceOwner, pm.SpaceView)
if !ok {
response.WriteForbiddenError(w)
return
}
cat, err := h.Store.Category.GetBySpace(ctx, spaceID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(cat) == 0 {
cat = []category.Category{}
}
response.WriteJSON(w, cat)
}
// GetAll returns categories within a space, disregarding permissions.
// Used in admin screens, lists, functions.
func (h *Handler) GetAll(w http.ResponseWriter, r *http.Request) {
method := "category.getAll"
ctx := domain.GetRequestContext(r)
spaceID := request.Param(r, "spaceID")
if len(spaceID) == 0 {
response.WriteMissingDataError(w, method, "spaceID")
return
}
cat, err := h.Store.Category.GetAllBySpace(ctx, spaceID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(cat) == 0 {
cat = []category.Category{}
}
response.WriteJSON(w, cat)
}
// Update saves existing space category.
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
method := "category.update"
ctx := domain.GetRequestContext(r)
categoryID := request.Param(r, "categoryID")
if len(categoryID) == 0 {
response.WriteMissingDataError(w, method, "categoryID")
return
}
defer r.Body.Close()
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, "body")
h.Runtime.Log.Error(method, err)
return
}
var cat category.Category
err = json.Unmarshal(body, &cat)
if err != nil {
response.WriteBadRequestError(w, method, "category")
h.Runtime.Log.Error(method, err)
return
}
cat.OrgID = ctx.OrgID
cat.RefID = categoryID
ok := permission.HasPermission(ctx, *h.Store, cat.LabelID, pm.SpaceManage, pm.SpaceOwner)
if !ok || !ctx.Authenticated {
response.WriteForbiddenError(w)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
err = h.Store.Category.Update(ctx, cat)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Audit.Record(ctx, audit.EventTypeCategoryUpdate)
ctx.Transaction.Commit()
cat, err = h.Store.Category.Get(ctx, cat.RefID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
response.WriteJSON(w, cat)
}
// Delete removes category and associated member records.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
method := "category.delete"
ctx := domain.GetRequestContext(r)
catID := request.Param(r, "categoryID")
if len(catID) == 0 {
response.WriteMissingDataError(w, method, "categoryID")
return
}
cat, err := h.Store.Category.Get(ctx, catID)
if err != nil {
response.WriteServerError(w, method, err)
return
}
ok := permission.HasPermission(ctx, *h.Store, cat.LabelID, pm.SpaceManage, pm.SpaceOwner)
if !ok || !ctx.Authenticated {
response.WriteForbiddenError(w)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// remove category members
_, err = h.Store.Category.RemoveCategoryMembership(ctx, cat.RefID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// remove category permissions
_, err = h.Store.Permission.DeleteCategoryPermissions(ctx, cat.RefID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// remove category
_, err = h.Store.Category.Delete(ctx, cat.RefID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Audit.Record(ctx, audit.EventTypeCategoryDelete)
ctx.Transaction.Commit()
response.WriteEmpty(w)
}
/*
6. add category view permission !!!
7. link/unlink document to category
8. filter space documents by category -- URL param? nested route?
*/

View file

@ -0,0 +1,201 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
// Package mysql handles data persistence for both category definition
// and and document/category association.
package mysql
import (
"database/sql"
"fmt"
"time"
"github.com/documize/community/core/env"
"github.com/documize/community/core/streamutil"
"github.com/documize/community/domain"
"github.com/documize/community/domain/store/mysql"
"github.com/documize/community/model/category"
"github.com/pkg/errors"
)
// Scope provides data access to MySQL.
type Scope struct {
Runtime *env.Runtime
}
// Add inserts the given record into the category table.
func (s Scope) Add(ctx domain.RequestContext, c category.Category) (err error) {
c.Created = time.Now().UTC()
c.Revised = time.Now().UTC()
stmt, err := ctx.Transaction.Preparex("INSERT INTO category (refid, orgid, labelid, category, created, revised) VALUES (?, ?, ?, ?, ?, ?)")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, "unable to prepare insert category")
return
}
_, err = stmt.Exec(c.RefID, c.OrgID, c.LabelID, c.Category, c.Created, c.Revised)
if err != nil {
err = errors.Wrap(err, "unable to execute insert category")
return
}
return
}
// GetBySpace returns space categories for a user.
// Context is used to for user ID.
func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (c []category.Category, err error) {
err = s.Runtime.Db.Select(&c, `
SELECT id, refid, orgid, labelid, category, created, revised FROM category
WHERE orgid=? AND labelid=?
AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='category' AND refid IN (
SELECT refid from permission WHERE orgid=? AND who='user' AND whoid=? AND location='category' UNION ALL
SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role' AND p.location='category'
AND p.action='view' AND r.userid=?
))
ORDER BY category`, ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select categories for space %s", spaceID))
return
}
return
}
// GetAllBySpace returns all space categories.
func (s Scope) GetAllBySpace(ctx domain.RequestContext, spaceID string) (c []category.Category, err error) {
err = s.Runtime.Db.Select(&c, `
SELECT id, refid, orgid, labelid, category, created, revised FROM category
WHERE orgid=? AND labelid=?
AND labelid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN (
SELECT refid from permission WHERE orgid=? AND who='user' AND whoid=? AND location='space' UNION ALL
SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role' AND p.location='space'
AND p.action='view' AND r.userid=?
))
ORDER BY category`, ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select all categories for space %s", spaceID))
return
}
return
}
// Update saves category name change.
func (s Scope) Update(ctx domain.RequestContext, c category.Category) (err error) {
c.Revised = time.Now().UTC()
stmt, err := ctx.Transaction.PrepareNamed("UPDATE category SET category=:category, revised=:revised WHERE orgid=:orgid AND refid=:refid")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare update for category %s", c.RefID))
return
}
_, err = stmt.Exec(&c)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute update for category %s", c.RefID))
return
}
return
}
// Get returns specified category
func (s Scope) Get(ctx domain.RequestContext, id string) (c category.Category, err error) {
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, orgid, labelid, category, created, revised FROM category WHERE orgid=? AND refid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare select for category %s", id))
return
}
err = stmt.Get(&c, ctx.OrgID, id)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to get category %s", id))
return
}
return
}
// Delete removes category from the store.
func (s Scope) Delete(ctx domain.RequestContext, id string) (rows int64, err error) {
b := mysql.BaseQuery{}
return b.DeleteConstrained(ctx.Transaction, "category", ctx.OrgID, id)
}
// AssociateDocument inserts category membership record into the category member table.
func (s Scope) AssociateDocument(ctx domain.RequestContext, m category.Member) (err error) {
m.Created = time.Now().UTC()
m.Revised = time.Now().UTC()
stmt, err := ctx.Transaction.Preparex("INSERT INTO categorymember (refid, orgid, categoryid, labelid, documentid, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?)")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, "unable to prepare insert categorymember")
return
}
_, err = stmt.Exec(m.RefID, m.OrgID, m.CategoryID, m.LabelID, m.DocumentID, m.Created, m.Revised)
if err != nil {
err = errors.Wrap(err, "unable to execute insert categorymember")
return
}
return
}
// DisassociateDocument removes document associatation from category.
func (s Scope) DisassociateDocument(ctx domain.RequestContext, categoryID, documentID string) (rows int64, err error) {
b := mysql.BaseQuery{}
sql := fmt.Sprintf("DELETE FROM categorymember WHERE orgid='%s' AND categoryid='%s' AND documentid='%s'",
ctx.OrgID, categoryID, documentID)
return b.DeleteWhere(ctx.Transaction, sql)
}
// RemoveCategoryMembership removes all category associations from the store.
func (s Scope) RemoveCategoryMembership(ctx domain.RequestContext, categoryID string) (rows int64, err error) {
b := mysql.BaseQuery{}
sql := fmt.Sprintf("DELETE FROM categorymember WHERE orgid='%s' AND categoryid='%s'",
ctx.OrgID, categoryID)
return b.DeleteWhere(ctx.Transaction, sql)
}
// DeleteBySpace removes all category and category associations for given space.
func (s Scope) DeleteBySpace(ctx domain.RequestContext, spaceID string) (rows int64, err error) {
b := mysql.BaseQuery{}
s1 := fmt.Sprintf("DELETE FROM categorymember WHERE orgid='%s' AND labelid='%s'", ctx.OrgID, spaceID)
b.DeleteWhere(ctx.Transaction, s1)
s2 := fmt.Sprintf("DELETE FROM category WHERE orgid='%s' AND labelid='%s'", ctx.OrgID, spaceID)
return b.DeleteWhere(ctx.Transaction, s2)
}

View file

@ -134,3 +134,24 @@ func (s Scope) DeleteUserPermissions(ctx domain.RequestContext, userID string) (
return b.DeleteWhere(ctx.Transaction, sql) return b.DeleteWhere(ctx.Transaction, sql)
} }
// DeleteCategoryPermissions removes records from permissions table for given category ID.
func (s Scope) DeleteCategoryPermissions(ctx domain.RequestContext, categoryID string) (rows int64, err error) {
b := mysql.BaseQuery{}
sql := fmt.Sprintf("DELETE FROM permission WHERE orgid='%s' AND location='category' AND refid='%s'", ctx.OrgID, categoryID)
return b.DeleteWhere(ctx.Transaction, sql)
}
// DeleteSpaceCategoryPermissions removes all category permission for for given space.
func (s Scope) DeleteSpaceCategoryPermissions(ctx domain.RequestContext, spaceID string) (rows int64, err error) {
b := mysql.BaseQuery{}
sql := fmt.Sprintf(`
DELETE FROM permission WHERE orgid='%s' AND location='category'
AND refid IN (SELECT refid FROM category WHERE orgid='%s' AND labelid='%s')`,
ctx.OrgID, ctx.OrgID, spaceID)
return b.DeleteWhere(ctx.Transaction, sql)
}

View file

@ -30,7 +30,7 @@ func CanViewSpaceDocument(ctx domain.RequestContext, s domain.Store, labelID str
for _, role := range roles { for _, role := range roles {
if role.RefID == labelID && role.Location == "space" && role.Scope == "object" && if role.RefID == labelID && role.Location == "space" && role.Scope == "object" &&
pm.HasPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) { pm.ContainsPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) {
return true return true
} }
} }
@ -58,7 +58,7 @@ func CanViewDocument(ctx domain.RequestContext, s domain.Store, documentID strin
for _, role := range roles { for _, role := range roles {
if role.RefID == document.LabelID && role.Location == "space" && role.Scope == "object" && if role.RefID == document.LabelID && role.Location == "space" && role.Scope == "object" &&
pm.HasPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) { pm.ContainsPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) {
return true return true
} }
} }
@ -136,7 +136,7 @@ func CanUploadDocument(ctx domain.RequestContext, s domain.Store, spaceID string
for _, role := range roles { for _, role := range roles {
if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" && if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" &&
pm.HasPermission(role.Action, pm.DocumentAdd) { pm.ContainsPermission(role.Action, pm.DocumentAdd) {
return true return true
} }
} }
@ -156,7 +156,7 @@ func CanViewSpace(ctx domain.RequestContext, s domain.Store, spaceID string) boo
for _, role := range roles { for _, role := range roles {
if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" && if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" &&
pm.HasPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) { pm.ContainsPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) {
return true return true
} }
} }
@ -164,18 +164,9 @@ func CanViewSpace(ctx domain.RequestContext, s domain.Store, spaceID string) boo
return false return false
} }
// HasDocumentAction returns if user can perform specified action. // HasPermission returns if user can perform specified actions.
func HasDocumentAction(ctx domain.RequestContext, s domain.Store, documentID string, a pm.Action) bool { func HasPermission(ctx domain.RequestContext, s domain.Store, spaceID string, actions ...pm.Action) bool {
document, err := s.Document.Get(ctx, documentID) roles, err := s.Permission.GetUserSpacePermissions(ctx, spaceID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
return false
}
roles, err := s.Permission.GetUserSpacePermissions(ctx, document.LabelID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
err = nil err = nil
@ -185,8 +176,12 @@ func HasDocumentAction(ctx domain.RequestContext, s domain.Store, documentID str
} }
for _, role := range roles { for _, role := range roles {
if role.RefID == document.LabelID && role.Location == "space" && role.Scope == "object" && role.Action == a { if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" {
return true for _, a := range actions {
if role.Action == a {
return true
}
}
} }
} }

View file

@ -48,7 +48,7 @@ type Handler struct {
// Add creates a new space. // Add creates a new space.
func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
method := "space.Add" method := "space.add"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() { if !h.Runtime.Product.License.IsValid() {
@ -276,7 +276,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
// Get returns the requested space. // Get returns the requested space.
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
method := "Get" method := "space.get"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
id := request.Param(r, "spaceID") id := request.Param(r, "spaceID")
@ -302,7 +302,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
// GetAll returns spaces the user can see. // GetAll returns spaces the user can see.
func (h *Handler) GetAll(w http.ResponseWriter, r *http.Request) { func (h *Handler) GetAll(w http.ResponseWriter, r *http.Request) {
method := "GetAll" method := "space.getAll"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
sp, err := h.Store.Space.GetAll(ctx) sp, err := h.Store.Space.GetAll(ctx)
@ -322,7 +322,7 @@ func (h *Handler) GetAll(w http.ResponseWriter, r *http.Request) {
// GetSpaceViewers returns the users that can see the shared spaces. // GetSpaceViewers returns the users that can see the shared spaces.
func (h *Handler) GetSpaceViewers(w http.ResponseWriter, r *http.Request) { func (h *Handler) GetSpaceViewers(w http.ResponseWriter, r *http.Request) {
method := "space.Viewers" method := "space.viewers"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
v, err := h.Store.Space.Viewers(ctx) v, err := h.Store.Space.Viewers(ctx)
@ -341,7 +341,7 @@ func (h *Handler) GetSpaceViewers(w http.ResponseWriter, r *http.Request) {
// Update processes request to save space object to the database // Update processes request to save space object to the database
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
method := "space.Update" method := "space.update"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
if !ctx.Editor { if !ctx.Editor {
@ -403,7 +403,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
// Remove moves documents to another space before deleting it // Remove moves documents to another space before deleting it
func (h *Handler) Remove(w http.ResponseWriter, r *http.Request) { func (h *Handler) Remove(w http.ResponseWriter, r *http.Request) {
method := "space.Remove" method := "space.remove"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() { if !h.Runtime.Product.License.IsValid() {
@ -477,7 +477,7 @@ func (h *Handler) Remove(w http.ResponseWriter, r *http.Request) {
// Delete removes space. // Delete removes space.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
method := "space.Delete" method := "space.delete"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() { if !h.Runtime.Product.License.IsValid() {
@ -512,7 +512,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
return return
} }
_, err = h.Store.Space.Delete(ctx, id) _, err = h.Store.Permission.DeleteSpacePermissions(ctx, id)
if err != nil { if err != nil {
ctx.Transaction.Rollback() ctx.Transaction.Rollback()
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
@ -520,7 +520,8 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
return return
} }
_, err = h.Store.Permission.DeleteSpacePermissions(ctx, id) // remove category permissions
_, err = h.Store.Permission.DeleteSpaceCategoryPermissions(ctx, id)
if err != nil { if err != nil {
ctx.Transaction.Rollback() ctx.Transaction.Rollback()
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
@ -536,6 +537,23 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
return return
} }
// remove category and members for space
_, err = h.Store.Category.DeleteBySpace(ctx, id)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
_, err = h.Store.Space.Delete(ctx, id)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Audit.Record(ctx, audit.EventTypeSpaceDelete) h.Store.Audit.Record(ctx, audit.EventTypeSpaceDelete)
ctx.Transaction.Commit() ctx.Transaction.Commit()

View file

@ -18,6 +18,7 @@ import (
"github.com/documize/community/model/attachment" "github.com/documize/community/model/attachment"
"github.com/documize/community/model/audit" "github.com/documize/community/model/audit"
"github.com/documize/community/model/block" "github.com/documize/community/model/block"
"github.com/documize/community/model/category"
"github.com/documize/community/model/doc" "github.com/documize/community/model/doc"
"github.com/documize/community/model/link" "github.com/documize/community/model/link"
"github.com/documize/community/model/org" "github.com/documize/community/model/org"
@ -36,6 +37,7 @@ type Store struct {
Attachment AttachmentStorer Attachment AttachmentStorer
Audit AuditStorer Audit AuditStorer
Block BlockStorer Block BlockStorer
Category CategoryStorer
Document DocumentStorer Document DocumentStorer
Link LinkStorer Link LinkStorer
Organization OrganizationStorer Organization OrganizationStorer
@ -59,6 +61,20 @@ type SpaceStorer interface {
Delete(ctx RequestContext, id string) (rows int64, err error) Delete(ctx RequestContext, id string) (rows int64, err error)
} }
// CategoryStorer defines required methods for category and category membership management
type CategoryStorer interface {
Add(ctx RequestContext, c category.Category) (err error)
Update(ctx RequestContext, c category.Category) (err error)
Get(ctx RequestContext, id string) (c category.Category, err error)
GetBySpace(ctx RequestContext, spaceID string) (c []category.Category, err error)
GetAllBySpace(ctx RequestContext, spaceID string) (c []category.Category, err error)
Delete(ctx RequestContext, id string) (rows int64, err error)
AssociateDocument(ctx RequestContext, m category.Member) (err error)
DisassociateDocument(ctx RequestContext, categoryID, documentID string) (rows int64, err error)
RemoveCategoryMembership(ctx RequestContext, categoryID string) (rows int64, err error)
DeleteBySpace(ctx RequestContext, spaceID string) (rows int64, err error)
}
// PermissionStorer defines required methods for space/document permission management // PermissionStorer defines required methods for space/document permission management
type PermissionStorer interface { type PermissionStorer interface {
AddPermission(ctx RequestContext, r permission.Permission) (err error) AddPermission(ctx RequestContext, r permission.Permission) (err error)
@ -68,6 +84,8 @@ type PermissionStorer interface {
DeleteSpacePermissions(ctx RequestContext, spaceID string) (rows int64, err error) DeleteSpacePermissions(ctx RequestContext, spaceID string) (rows int64, err error)
DeleteUserSpacePermissions(ctx RequestContext, spaceID, userID string) (rows int64, err error) DeleteUserSpacePermissions(ctx RequestContext, spaceID, userID string) (rows int64, err error)
DeleteUserPermissions(ctx RequestContext, userID string) (rows int64, err error) DeleteUserPermissions(ctx RequestContext, userID string) (rows int64, err error)
DeleteCategoryPermissions(ctx RequestContext, categoryID string) (rows int64, err error)
DeleteSpaceCategoryPermissions(ctx RequestContext, spaceID string) (rows int64, err error)
} }
// UserStorer defines required methods for user management // UserStorer defines required methods for user management

View file

@ -113,21 +113,21 @@ func (h *Handler) SaveAs(w http.ResponseWriter, r *http.Request) {
return return
} }
if !permission.HasDocumentAction(ctx, *h.Store, model.DocumentID, pm.DocumentTemplate) { // Duplicate document
response.WriteForbiddenError(w) doc, err := h.Store.Document.Get(ctx, model.DocumentID)
return
}
// DB transaction
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil { if err != nil {
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err) h.Runtime.Log.Error(method, err)
return return
} }
// Duplicate document if !permission.HasPermission(ctx, *h.Store, doc.LabelID, pm.DocumentTemplate) {
doc, err := h.Store.Document.Get(ctx, model.DocumentID) response.WriteForbiddenError(w)
return
}
// DB transaction
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil { if err != nil {
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err) h.Runtime.Log.Error(method, err)

View file

@ -20,6 +20,7 @@ import (
attachment "github.com/documize/community/domain/attachment/mysql" attachment "github.com/documize/community/domain/attachment/mysql"
audit "github.com/documize/community/domain/audit/mysql" audit "github.com/documize/community/domain/audit/mysql"
block "github.com/documize/community/domain/block/mysql" block "github.com/documize/community/domain/block/mysql"
category "github.com/documize/community/domain/category/mysql"
doc "github.com/documize/community/domain/document/mysql" doc "github.com/documize/community/domain/document/mysql"
link "github.com/documize/community/domain/link/mysql" link "github.com/documize/community/domain/link/mysql"
org "github.com/documize/community/domain/organization/mysql" org "github.com/documize/community/domain/organization/mysql"
@ -39,6 +40,7 @@ func StoreMySQL(r *env.Runtime, s *domain.Store) {
s.Attachment = attachment.Scope{Runtime: r} s.Attachment = attachment.Scope{Runtime: r}
s.Audit = audit.Scope{Runtime: r} s.Audit = audit.Scope{Runtime: r}
s.Block = block.Scope{Runtime: r} s.Block = block.Scope{Runtime: r}
s.Category = category.Scope{Runtime: r}
s.Document = doc.Scope{Runtime: r} s.Document = doc.Scope{Runtime: r}
s.Link = link.Scope{Runtime: r} s.Link = link.Scope{Runtime: r}
s.Organization = org.Scope{Runtime: r} s.Organization = org.Scope{Runtime: r}

View file

@ -0,0 +1,91 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
import NotifierMixin from '../../mixins/notifier';
const {
inject: { service }
} = Ember;
export default Ember.Component.extend(NotifierMixin, {
folderService: service('folder'),
categoryService: service('category'),
appMeta: service(),
store: service(),
newCategory: '',
didReceiveAttrs() {
this.load();
},
load() {
this.get('categoryService').getAll(this.get('folder.id')).then((c) => {
this.set('category', c);
});
},
setEdit(id, val) {
let cats = this.get('category');
let cat = cats.findBy('id', id);
if (is.not.undefined(cat)) {
cat.set('editMode', val);
}
return cat;
},
actions: {
onAdd() {
let cat = this.get('newCategory');
if (cat === '') {
$('#new-category-name').addClass('error').focus();
return;
}
$('#new-category-name').removeClass('error').focus();
this.set('newCategory', '');
let c = {
category: cat,
folderId: this.get('folder.id')
};
this.get('categoryService').add(c).then(() => {
this.load();
});
},
onDelete(id) {
this.get('categoryService').delete(id).then(() => {
this.load();
});
},
onEdit(id) {
this.setEdit(id, true);
},
onCancel(id) {
this.setEdit(id, false);
},
onSave(id) {
let cat = this.setEdit(id, false);
this.get('categoryService').save(cat).then(() => {
this.load();
});
}
}
});

View file

@ -0,0 +1,21 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
orgId: attr('string'),
folderId: attr('string'),
category: attr('string'),
created: attr(),
revised: attr()
});

View file

@ -1 +1 @@
{{folder/invite-user folders=model.folders folder=model.folder}} {{folder/category-admin folders=model.folders folder=model.folder}}

View file

@ -0,0 +1,87 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
import Ember from 'ember';
import BaseService from '../services/base';
const {
inject: { service }
} = Ember;
export default BaseService.extend({
sessionService: service('session'),
ajax: service(),
localStorage: service(),
store: service(),
// Add category to space
add(payload) {
return this.get('ajax').post(`category`, {
contentType: 'json',
data: JSON.stringify(payload)
}).then((category) => {
let data = this.get('store').normalize('category', category);
return this.get('store').push(data);
});
},
// Returns space categories viewable by user.
getUserVisible(spaceId) {
return this.get('ajax').request(`category/space/${spaceId}`, {
method: 'GET'
}).then((response) => {
let data = [];
data = response.map((obj) => {
let data = this.get('store').normalize('category', obj);
return this.get('store').push(data);
});
return data;
});
},
// Returns all space categories for admin user.
getAll(spaceId) {
return this.get('ajax').request(`category/space/${spaceId}?filter=all`, {
method: 'GET'
}).then((response) => {
let data = [];
data = response.map((obj) => {
let data = this.get('store').normalize('category', obj);
return this.get('store').push(data);
});
return data;
});
},
// Updates an existing category.
save(category) {
let id = category.get('id');
return this.get('ajax').request(`category/${id}`, {
method: 'PUT',
contentType: 'json',
data: JSON.stringify(category)
}).then((category) => {
let data = this.get('store').normalize('category', category);
return this.get('store').push(data);
});
},
delete(categoryId) {
return this.get('ajax').request(`category/${categoryId}`, {
method: 'DELETE'
});
}
});

View file

@ -34,6 +34,41 @@
} }
} }
} }
} }
.category-table {
padding: 0;
margin: 0 0 0 20px;
width: 60%;
> .row {
margin: 15px 0;
padding: 8px 10px;
background-color: $color-off-white;
@include border-radius(2px);
> .category {
font-size: 1.2rem;
vertical-align: bottom;
display: inline-block;
margin-top: 8px;
}
> .action {
display: inline-block;
}
> .input-control {
margin: 0;
display: inline-block;
> input {
margin: 0;
padding: 0;
font-size: 1.2rem;
}
}
}
}
} }

View file

@ -0,0 +1,53 @@
<div class="space-settings">
<div class="panel">
<div class="form-header">
<div class="title">Categories</div>
<div class="tip">Organize and secure document access with optional categories</div>
</div>
<form id="category-form" {{action 'onAdd' on='submit'}}>
<div class="input-control">
<div class="category-table">
{{#each category as |cat|}}
<div class="row">
{{#if cat.editMode}}
<div class="input-control input-transparent width-60">
{{focus-input id=(concat 'edit-category-' cat.id) type="text" value=cat.category class="input-inline"}}
</div>
{{else}}
<div class="category">{{cat.category}}</div>
{{/if}}
<div class="pull-right">
{{#if cat.editMode}}
<button type="submit" class="round-button-mono" {{action 'onSave' cat.id}}>
<i class="material-icons color-green">check</i>
</button>
<div class="round-button-mono" {{action 'onCancel' cat.id}}>
<i class="material-icons color-gray">close</i>
</div>
{{else}}
<div {{action 'onEdit' cat.id}} class="action round-button-mono button-white">
<i class="material-icons">edit</i>
</div>
<div id="{{concat 'delete-category-' cat.id}}" class="action round-button-mono button-white">
<i class="material-icons">delete</i>
</div>
{{#dropdown-dialog target=(concat 'delete-category-' cat.id) position="bottom right" button="Delete" color="flat-red" onAction=(action 'onDelete' cat.id)}}
<p>Are you sure you want to delete category <b>{{cat.category}}?</b></p>
{{/dropdown-dialog}}
{{/if}}
</div>
</div>
{{else}}
<div class="margin-top-30"><i>No categories defined yet</i></div>
{{/each}}
</div>
<div class="input-control margin-top-50">
<label>Add Category</label>
<div class="tip">Provide a short name</div>
{{focus-input id="new-category-name" type="text" value=newCategory}}
</div>
</div>
<div class="regular-button button-blue" {{action 'onAdd'}}>add</div>
</form>
</div>
</div>

View file

@ -67,4 +67,9 @@ const (
EventTypeSystemSMTP EventType = "changed-system-smtp" EventTypeSystemSMTP EventType = "changed-system-smtp"
EventTypeSessionStart EventType = "started-session" EventTypeSessionStart EventType = "started-session"
EventTypeSearch EventType = "searched" EventTypeSearch EventType = "searched"
EventTypeCategoryAdd EventType = "added-category"
EventTypeCategoryDelete EventType = "removed-category"
EventTypeCategoryUpdate EventType = "updated-category"
EventTypeCategoryLink EventType = "linked-category"
EventTypeCategoryUnlink EventType = "unlinked-category"
) )

View file

@ -0,0 +1,31 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package category
import "github.com/documize/community/model"
// Category represents a category within a space that is persisted to the database.
type Category struct {
model.BaseEntity
OrgID string `json:"orgId"`
LabelID string `json:"folderId"`
Category string `json:"category"`
}
// Member represents 0:M association between a document and category, persisted to the database.
type Member struct {
model.BaseEntity
OrgID string `json:"orgId"`
CategoryID string `json:"categoryId"`
LabelID string `json:"folderId"`
DocumentID string `json:"documentId"`
}

View file

@ -113,8 +113,8 @@ type PermissionsModel struct {
Permissions []Record Permissions []Record
} }
// HasPermission checks if action matches one of the required actions? // ContainsPermission checks if action matches one of the required actions?
func HasPermission(action Action, actions ...Action) bool { func ContainsPermission(action Action, actions ...Action) bool {
for _, a := range actions { for _, a := range actions {
if action == a { if action == a {
return true return true

View file

@ -20,6 +20,7 @@ import (
"github.com/documize/community/domain/auth" "github.com/documize/community/domain/auth"
"github.com/documize/community/domain/auth/keycloak" "github.com/documize/community/domain/auth/keycloak"
"github.com/documize/community/domain/block" "github.com/documize/community/domain/block"
"github.com/documize/community/domain/category"
"github.com/documize/community/domain/conversion" "github.com/documize/community/domain/conversion"
"github.com/documize/community/domain/document" "github.com/documize/community/domain/document"
"github.com/documize/community/domain/link" "github.com/documize/community/domain/link"
@ -54,6 +55,7 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
block := block.Handler{Runtime: rt, Store: s} block := block.Handler{Runtime: rt, Store: s}
section := section.Handler{Runtime: rt, Store: s} section := section.Handler{Runtime: rt, Store: s}
setting := setting.Handler{Runtime: rt, Store: s} setting := setting.Handler{Runtime: rt, Store: s}
category := category.Handler{Runtime: rt, Store: s}
keycloak := keycloak.Handler{Runtime: rt, Store: s} keycloak := keycloak.Handler{Runtime: rt, Store: s}
template := template.Handler{Runtime: rt, Store: s, Indexer: indexer} template := template.Handler{Runtime: rt, Store: s, Indexer: indexer}
document := document.Handler{Runtime: rt, Store: s, Indexer: indexer} document := document.Handler{Runtime: rt, Store: s, Indexer: indexer}
@ -125,6 +127,12 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"GET", "OPTIONS"}, nil, space.Get) Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"GET", "OPTIONS"}, nil, space.Get)
Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"PUT", "OPTIONS"}, nil, space.Update) Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"PUT", "OPTIONS"}, nil, space.Update)
Add(rt, RoutePrefixPrivate, "category/space/{spaceID}", []string{"GET", "OPTIONS"}, []string{"filter", "all"}, category.GetAll)
Add(rt, RoutePrefixPrivate, "category/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.Get)
Add(rt, RoutePrefixPrivate, "category", []string{"POST", "OPTIONS"}, nil, category.Add)
Add(rt, RoutePrefixPrivate, "category/{categoryID}", []string{"PUT", "OPTIONS"}, nil, category.Update)
Add(rt, RoutePrefixPrivate, "category/{categoryID}", []string{"DELETE", "OPTIONS"}, nil, category.Delete)
Add(rt, RoutePrefixPrivate, "users/{userID}/password", []string{"POST", "OPTIONS"}, nil, user.ChangePassword) Add(rt, RoutePrefixPrivate, "users/{userID}/password", []string{"POST", "OPTIONS"}, nil, user.ChangePassword)
Add(rt, RoutePrefixPrivate, "users", []string{"POST", "OPTIONS"}, nil, user.Add) Add(rt, RoutePrefixPrivate, "users", []string{"POST", "OPTIONS"}, nil, user.Add)
Add(rt, RoutePrefixPrivate, "users/folder/{folderID}", []string{"GET", "OPTIONS"}, nil, user.GetSpaceUsers) Add(rt, RoutePrefixPrivate, "users/folder/{folderID}", []string{"GET", "OPTIONS"}, nil, user.GetSpaceUsers)