1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-25 08:09:43 +02:00

Merge pull request #137 from documize/version-drafts-history-archiving

Document lifecycle and versioning
This commit is contained in:
Saul S 2018-03-19 16:20:40 +00:00 committed by GitHub
commit 7eb99c52f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 7393 additions and 1278 deletions

View file

@ -25,7 +25,7 @@ Anyone who wants a single place for any kind of document.
Anyone who wants to loop in external participants complete security.
Anyone who wishes documentation and knowledge capture worked like agile software development.
## What's different about Documize?
Sane organization through personal, team and public spaces.
@ -52,7 +52,7 @@ Space view.
## Latest version
Community edition: v1.58.0
Community edition: v1.59.0
## OS support

View file

@ -170,7 +170,7 @@ func setupAccount(rt *env.Runtime, completion onboardRequest, serial string) (er
}
// assign permissions to space
perms := []string{"view", "manage", "own", "doc-add", "doc-edit", "doc-delete", "doc-move", "doc-copy", "doc-template", "doc-approve"}
perms := []string{"view", "manage", "own", "doc-add", "doc-edit", "doc-delete", "doc-move", "doc-copy", "doc-template", "doc-approve", "doc-version", "doc-lifecycle"}
for _, p := range perms {
sql = fmt.Sprintf("insert into permission (orgid, who, whoid, action, scope, location, refid) values (\"%s\", 'user', \"%s\", \"%s\", 'object', 'space', \"%s\")", orgID, userID, p, labelID)
_, err = runSQL(rt, sql)

View file

@ -0,0 +1,27 @@
/* enterprise edition */
-- document lifecycle and versioning
ALTER TABLE document ADD COLUMN `lifecycle` INT NOT NULL DEFAULT 1 AFTER `approval`;
ALTER TABLE document ADD COLUMN `versioned` INT NOT NULL DEFAULT 0 AFTER `lifecycle`;
ALTER TABLE document ADD COLUMN `versionid` VARCHAR(100) DEFAULT '' NOT NULL AFTER `versioned`;
ALTER TABLE document ADD COLUMN `versionorder` INT NOT NULL DEFAULT 0 AFTER `versionid`;
ALTER TABLE document ADD COLUMN `groupid` CHAR(16) NOT NULL COLLATE utf8_bin AFTER `versionorder`;
-- grant doc-lifecycle permission
INSERT INTO permission(orgid, who, whoid, action, scope, location, refid, created)
SELECT orgid, who, whoid, 'doc-lifecycle' AS action, scope, location, refid, created
FROM permission
WHERE action = 'doc-edit' OR action = 'doc-approve';
-- grant doc-versions permission
INSERT INTO permission(orgid, who, whoid, action, scope, location, refid, created)
SELECT orgid, who, whoid, 'doc-version' AS action, scope, location, refid, created
FROM permission
WHERE action = 'doc-edit' OR action = 'doc-approve';
-- implement document section name search indexing
INSERT INTO search (orgid, documentid, itemid, itemtype, content)
SELECT orgid, documentid, refid as itemid, "page" as itemtype, title as content
FROM page WHERE status=0
-- deprecations

View file

@ -13,10 +13,12 @@ package mysql
import (
"database/sql"
"fmt"
"time"
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
"github.com/documize/community/domain/store/mysql"
"github.com/documize/community/model/activity"
"github.com/pkg/errors"
)
@ -69,3 +71,12 @@ func (s Scope) GetDocumentActivity(ctx domain.RequestContext, id string) (a []ac
return
}
// DeleteDocumentChangeActivity removes all entries for document changes (add, remove, update).
func (s Scope) DeleteDocumentChangeActivity(ctx domain.RequestContext, documentID string) (rows int64, err error) {
b := mysql.BaseQuery{}
rows, err = b.DeleteWhere(ctx.Transaction,
fmt.Sprintf("DELETE FROM useractivity WHERE orgid='%s' AND documentid='%s' AND (activitytype=1 OR activitytype=2 OR activitytype=3 OR activitytype=4 OR activitytype=7)", ctx.OrgID, documentID))
return
}

View file

@ -30,6 +30,7 @@ import (
indexer "github.com/documize/community/domain/search"
"github.com/documize/community/model/attachment"
"github.com/documize/community/model/audit"
"github.com/documize/community/model/workflow"
uuid "github.com/nu7hatch/gouuid"
)
@ -161,7 +162,12 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
a, _ := h.Store.Attachment.GetAttachments(ctx, documentID)
d, _ := h.Store.Document.Get(ctx, documentID)
go h.Indexer.IndexDocument(ctx, d, a)
if d.Lifecycle == workflow.LifecycleLive {
go h.Indexer.IndexDocument(ctx, d, a)
} else {
go h.Indexer.DeleteDocument(ctx, d.RefID)
}
response.WriteEmpty(w)
}
@ -236,7 +242,12 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
all, _ := h.Store.Attachment.GetAttachments(ctx, documentID)
d, _ := h.Store.Document.Get(ctx, documentID)
go h.Indexer.IndexDocument(ctx, d, all)
if d.Lifecycle == workflow.LifecycleLive {
go h.Indexer.IndexDocument(ctx, d, all)
} else {
go h.Indexer.DeleteDocument(ctx, d.RefID)
}
response.WriteEmpty(w)
}

175
domain/document/document.go Normal file
View file

@ -0,0 +1,175 @@
// 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 document
import (
"github.com/documize/community/core/uniqueid"
"github.com/documize/community/domain"
"github.com/documize/community/model/category"
"github.com/documize/community/model/doc"
"github.com/documize/community/model/page"
"github.com/documize/community/model/workflow"
"github.com/pkg/errors"
)
// FilterCategoryProtected removes documents that cannot be seen by user due to
// document cateogory viewing permissions.
func FilterCategoryProtected(docs []doc.Document, cats []category.Category, members []category.Member, viewDrafts bool) (filtered []doc.Document) {
filtered = []doc.Document{}
for _, doc := range docs {
hasCategory := false
canSeeCategory := false
skip := false
// drafts included if user can see them
if doc.Lifecycle == workflow.LifecycleDraft && !viewDrafts {
skip = true
}
// archived never included
if doc.Lifecycle == workflow.LifecycleArchived {
skip = true
}
OUTER:
for _, m := range members {
if m.DocumentID == doc.RefID {
hasCategory = true
for _, cat := range cats {
if cat.RefID == m.CategoryID {
canSeeCategory = true
continue OUTER
}
}
}
}
if !skip && (!hasCategory || canSeeCategory) {
filtered = append(filtered, doc)
}
}
return
}
// CopyDocument clones an existing document
func CopyDocument(ctx domain.RequestContext, s domain.Store, documentID string) (newDocumentID string, err error) {
doc, err := s.Document.Get(ctx, documentID)
if err != nil {
err = errors.Wrap(err, "unable to fetch existing document")
return
}
newDocumentID = uniqueid.Generate()
doc.RefID = newDocumentID
doc.ID = 0
doc.Versioned = false
doc.VersionID = ""
doc.GroupID = ""
doc.Template = false
// Duplicate pages and associated meta
pages, err := s.Page.GetPages(ctx, documentID)
if err != nil {
err = errors.Wrap(err, "unable to get existing pages")
return
}
var pageModel []page.NewPage
for _, p := range pages {
p.DocumentID = newDocumentID
p.ID = 0
meta, err2 := s.Page.GetPageMeta(ctx, p.RefID)
if err2 != nil {
err = errors.Wrap(err, "unable to get existing pages meta")
return
}
pageID := uniqueid.Generate()
p.RefID = pageID
meta.PageID = pageID
meta.DocumentID = newDocumentID
m := page.NewPage{}
m.Page = p
m.Meta = meta
pageModel = append(pageModel, m)
}
// Duplicate attachments
attachments, _ := s.Attachment.GetAttachments(ctx, documentID)
for i, a := range attachments {
a.DocumentID = newDocumentID
a.RefID = uniqueid.Generate()
a.ID = 0
attachments[i] = a
}
// Now create the template: document, attachments, pages and their meta
err = s.Document.Add(ctx, doc)
if err != nil {
err = errors.Wrap(err, "unable to add copied document")
return
}
for _, a := range attachments {
err = s.Attachment.Add(ctx, a)
if err != nil {
err = errors.Wrap(err, "unable to add copied attachment")
return
}
}
for _, m := range pageModel {
err = s.Page.Add(ctx, m)
if err != nil {
err = errors.Wrap(err, "unable to add copied page")
return
}
}
return
}
// FilterLastVersion returns the latest version of each document
// by removing all previous versions.
// If a document is not versioned, it is returned as-is.
func FilterLastVersion(docs []doc.Document) (filtered []doc.Document) {
filtered = []doc.Document{}
prev := make(map[string]bool)
for _, doc := range docs {
add := false
if doc.GroupID == "" {
add = true
} else {
if _, isExisting := prev[doc.GroupID]; !isExisting {
add = true
prev[doc.GroupID] = true
} else {
add = false
}
}
if add {
filtered = append(filtered, doc)
}
}
return
}

View file

@ -44,9 +44,10 @@ type Handler struct {
Indexer indexer.Indexer
}
// Get is an endpoint that returns the document-level information for a given documentID.
// Get is an endpoint that returns the document-level information for a
// given documentID.
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
method := "document.get"
method := "document.Get"
ctx := domain.GetRequestContext(r)
id := request.Param(r, "documentID")
@ -78,15 +79,18 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: document.LabelID,
DocumentID: document.RefID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeRead})
// draft mode does not record document views
if document.Lifecycle == workflow.LifecycleLive {
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: document.LabelID,
DocumentID: document.RefID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeRead})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
}
ctx.Transaction.Commit()
@ -98,7 +102,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
// DocumentLinks is an endpoint returning the links for a document.
func (h *Handler) DocumentLinks(w http.ResponseWriter, r *http.Request) {
method := "document.links"
method := "document.DocumentLinks"
ctx := domain.GetRequestContext(r)
id := request.Param(r, "documentID")
@ -136,55 +140,37 @@ func (h *Handler) BySpace(w http.ResponseWriter, r *http.Request) {
return
}
// get complete list of documents
// get user permissions
viewDrafts := permission.CanViewDrafts(ctx, *h.Store, spaceID)
// Get complete list of documents regardless of category permission
// and versioning.
documents, err := h.Store.Document.GetBySpace(ctx, spaceID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(documents) == 0 {
documents = []doc.Document{}
}
// Sort by title.
sort.Sort(doc.ByTitle(documents))
// remove documents that cannot be seen due to lack of
// category view/access permission
filtered := []doc.Document{}
// Remove documents that cannot be seen due to lack of
// category view/access permission.
cats, err := h.Store.Category.GetBySpace(ctx, spaceID)
members, err := h.Store.Category.GetSpaceCategoryMembership(ctx, spaceID)
filtered := FilterCategoryProtected(documents, cats, members, viewDrafts)
for _, doc := range documents {
hasCategory := false
canSeeCategory := false
OUTER:
for _, m := range members {
if m.DocumentID == doc.RefID {
hasCategory = true
for _, cat := range cats {
if cat.RefID == m.CategoryID {
canSeeCategory = true
continue OUTER
}
}
}
}
if !hasCategory || canSeeCategory {
filtered = append(filtered, doc)
}
}
// Keep the latest version when faced with multiple versions.
filtered = FilterLastVersion(filtered)
response.WriteJSON(w, filtered)
}
// Update updates an existing document using the
// format described in NewDocumentModel() encoded as JSON in the request.
// Update updates an existing document using the format described
// in NewDocumentModel() encoded as JSON in the request.
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
method := "document.space"
method := "document.Update"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
@ -223,7 +209,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
// if space changed for document, remove document categories
// If space changed for document, remove document categories.
oldDoc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
ctx.Transaction.Rollback()
@ -244,19 +230,60 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
// If document part of versioned document group
// then document name must be applied to all documents
// in the group.
if len(d.GroupID) > 0 {
err = h.Store.Document.UpdateGroup(ctx, d)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
}
// Record document being marked as archived.
if d.Lifecycle != oldDoc.Lifecycle && d.Lifecycle == workflow.LifecycleArchived {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: d.LabelID,
DocumentID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeArchived})
}
// Record document being marked as draft.
if d.Lifecycle != oldDoc.Lifecycle && d.Lifecycle == workflow.LifecycleDraft {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: d.LabelID,
DocumentID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDraft})
}
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeDocumentUpdate)
a, _ := h.Store.Attachment.GetAttachments(ctx, documentID)
go h.Indexer.IndexDocument(ctx, d, a)
// Live document indexed for search.
if d.Lifecycle == workflow.LifecycleLive {
a, _ := h.Store.Attachment.GetAttachments(ctx, documentID)
go h.Indexer.IndexDocument(ctx, d, a)
pages, _ := h.Store.Page.GetPages(ctx, d.RefID)
for i := range pages {
go h.Indexer.IndexContent(ctx, pages[i])
}
} else {
go h.Indexer.DeleteDocument(ctx, d.RefID)
}
response.WriteEmpty(w)
}
// Delete is an endpoint that deletes a document specified by documentID.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
method := "document.delete"
method := "document.Delete"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
@ -327,11 +354,14 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
h.Store.Link.MarkOrphanDocumentLink(ctx, documentID)
h.Store.Link.DeleteSourceDocumentLinks(ctx, documentID)
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDeleted})
// Draft actions are not logged
if doc.Lifecycle == workflow.LifecycleLive {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDeleted})
}
ctx.Transaction.Commit()
@ -342,9 +372,10 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
response.WriteEmpty(w)
}
// SearchDocuments endpoint takes a list of keywords and returns a list of document references matching those keywords.
// SearchDocuments endpoint takes a list of keywords and returns a list of
// document references matching those keywords.
func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) {
method := "document.search"
method := "document.SearchDocuments"
ctx := domain.GetRequestContext(r)
defer streamutil.Close(r.Body)
@ -412,6 +443,12 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
return
}
// Don't serve archived document
if document.Lifecycle == workflow.LifecycleArchived {
response.WriteForbiddenError(w)
return
}
// permissions
perms, err := h.Store.Permission.GetUserSpacePermissions(ctx, document.LabelID)
if err != nil && err != sql.ErrNoRows {
@ -455,12 +492,22 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
sp = []space.Space{}
}
// Get version information for this document.
v, err := h.Store.Document.GetVersions(ctx, document.GroupID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// Prepare response.
data := BulkDocumentData{}
data.Document = document
data.Permissions = record
data.Roles = rolesRecord
data.Links = l
data.Spaces = sp
data.Versions = v
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
@ -469,15 +516,17 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: document.LabelID,
DocumentID: document.RefID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeRead})
if document.Lifecycle == workflow.LifecycleLive {
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: document.LabelID,
DocumentID: document.RefID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeRead})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
}
ctx.Transaction.Commit()
@ -495,4 +544,5 @@ type BulkDocumentData struct {
Roles pm.DocumentRecord `json:"roles"`
Spaces []space.Space `json:"folders"`
Links []link.Link `json:"links"`
Versions []doc.Version `json:"versions"`
}

View file

@ -29,13 +29,16 @@ type Scope struct {
}
// Add inserts the given document record into the document table and audits that it has been done.
func (s Scope) Add(ctx domain.RequestContext, document doc.Document) (err error) {
document.OrgID = ctx.OrgID
document.Created = time.Now().UTC()
document.Revised = document.Created // put same time in both fields
func (s Scope) Add(ctx domain.RequestContext, d doc.Document) (err error) {
d.OrgID = ctx.OrgID
d.Created = time.Now().UTC()
d.Revised = d.Created // put same time in both fields
_, err = ctx.Transaction.Exec("INSERT INTO document (refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
document.RefID, document.OrgID, document.LabelID, document.UserID, document.Job, document.Location, document.Title, document.Excerpt, document.Slug, document.Tags, document.Template, document.Protection, document.Approval, document.Created, document.Revised)
_, err = ctx.Transaction.Exec(`
INSERT INTO document (refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, lifecycle, versioned, versionid, versionorder, groupid, created, revised)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
d.RefID, d.OrgID, d.LabelID, d.UserID, d.Job, d.Location, d.Title, d.Excerpt, d.Slug, d.Tags,
d.Template, d.Protection, d.Approval, d.Lifecycle, d.Versioned, d.VersionID, d.VersionOrder, d.GroupID, d.Created, d.Revised)
if err != nil {
err = errors.Wrap(err, "execuet insert document")
@ -46,7 +49,11 @@ func (s Scope) Add(ctx domain.RequestContext, document doc.Document) (err error)
// Get fetches the document record with the given id fromt the document table and audits that it has been got.
func (s Scope) Get(ctx domain.RequestContext, id string) (document doc.Document, err error) {
err = s.Runtime.Db.Get(&document, "SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised FROM document WHERE orgid=? and refid=?",
err = s.Runtime.Db.Get(&document, `
SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template,
protection, approval, lifecycle, versioned, versionid, versionorder, groupid, created, revised
FROM document
WHERE orgid=? and refid=?`,
ctx.OrgID, id)
if err != nil {
@ -90,37 +97,32 @@ func (s Scope) DocumentMeta(ctx domain.RequestContext, id string) (meta doc.Docu
return
}
// GetAll returns a slice containg all of the the documents for the client's organisation.
func (s Scope) GetAll() (ctx domain.RequestContext, documents []doc.Document, err error) {
err = s.Runtime.Db.Select(&documents, "SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised FROM document WHERE orgid=? AND template=0 ORDER BY title", ctx.OrgID)
if err != nil {
err = errors.Wrap(err, "select documents")
}
return
}
// GetBySpace returns a slice containing the documents for a given space.
// No attempt is made to hide documents that are protected
// by category permissions -- caller must filter as required.
//
// No attempt is made to hide documents that are protected by category
// permissions hence caller must filter as required.
//
// All versions of a document are returned, hence caller must
// decide what to do with them.
func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (documents []doc.Document, err error) {
err = s.Runtime.Db.Select(&documents, `
SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised
SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template,
protection, approval, lifecycle, versioned, versionid, versionorder, groupid, created, revised
FROM document
WHERE orgid=? AND template=0 AND labelid IN (
SELECT refid FROM label WHERE orgid=? AND refid IN
SELECT refid FROM label WHERE orgid=? AND refid IN
(SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid=? AND refid IN (
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view'
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view'
UNION ALL
SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=?
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.refid=? AND p.action='view' AND (r.userid=? OR r.userid='0')
))
)
ORDER BY title`, ctx.OrgID, ctx.OrgID, ctx.OrgID, spaceID, ctx.OrgID, ctx.UserID, ctx.OrgID, spaceID, ctx.UserID)
ORDER BY title, versionorder`, ctx.OrgID, ctx.OrgID, ctx.OrgID, spaceID, ctx.OrgID, ctx.UserID, ctx.OrgID, spaceID, ctx.UserID)
if err == sql.ErrNoRows {
if err == sql.ErrNoRows || len(documents) == 0 {
err = nil
documents = []doc.Document{}
}
if err != nil {
err = errors.Wrap(err, "select documents by space")
@ -129,37 +131,18 @@ func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (documents
return
}
// Templates returns a slice containing the documents available as templates to the client's organisation, in title order.
func (s Scope) Templates(ctx domain.RequestContext) (documents []doc.Document, err error) {
err = s.Runtime.Db.Select(&documents,
`SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised FROM document WHERE orgid=? AND template=1
AND labelid IN
(
SELECT refid FROM label WHERE orgid=?
AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN (
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view'
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=? OR r.userid='0')
))
)
ORDER BY title`, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)
if err != nil {
err = errors.Wrap(err, "select document templates")
}
return
}
// TemplatesBySpace returns a slice containing the documents available as templates for given space.
func (s Scope) TemplatesBySpace(ctx domain.RequestContext, spaceID string) (documents []doc.Document, err error) {
err = s.Runtime.Db.Select(&documents,
`SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised FROM document WHERE orgid=? AND labelid=? AND template=1
`SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template,
protection, approval, lifecycle, versioned, versionid, versionorder, groupid, created, revised
FROM document
WHERE orgid=? AND labelid=? AND template=1 ANd lifecycle=1
AND labelid IN
(
SELECT refid FROM label WHERE orgid=?
AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN (
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view'
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view'
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=? OR r.userid='0')
))
@ -170,7 +153,6 @@ func (s Scope) TemplatesBySpace(ctx domain.RequestContext, spaceID string) (docu
err = nil
documents = []doc.Document{}
}
if err != nil {
err = errors.Wrap(err, "select space document templates")
}
@ -178,44 +160,24 @@ func (s Scope) TemplatesBySpace(ctx domain.RequestContext, spaceID string) (docu
return
}
// PublicDocuments returns a slice of SitemapDocument records, holding documents in folders of type 1 (entity.TemplateTypePublic).
// PublicDocuments returns a slice of SitemapDocument records
// linking to documents in public spaces.
// These documents can then be seen by search crawlers.
func (s Scope) PublicDocuments(ctx domain.RequestContext, orgID string) (documents []doc.SitemapDocument, err error) {
err = s.Runtime.Db.Select(&documents,
`SELECT d.refid as documentid, d.title as document, d.revised as revised, l.refid as folderid, l.label as folder
FROM document d LEFT JOIN label l ON l.refid=d.labelid
WHERE d.orgid=?
AND l.type=1
AND d.template=0`, orgID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute GetPublicDocuments for org %s%s", orgID))
}
return
}
// DocumentList returns a slice containing the documents available as templates to the client's organisation, in title order.
func (s Scope) DocumentList(ctx domain.RequestContext) (documents []doc.Document, err error) {
err = s.Runtime.Db.Select(&documents,
`SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised FROM document WHERE orgid=? AND template=0
AND labelid IN
(
SELECT refid FROM label WHERE orgid=?
AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN (
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view'
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=? OR r.userid='0')
))
)
ORDER BY title`, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)
AND l.type=1
AND d.lifecycle=1
AND d.template=0`, orgID)
if err == sql.ErrNoRows {
err = nil
documents = []doc.Document{}
documents = []doc.SitemapDocument{}
}
if err != nil {
err = errors.Wrap(err, "select documents list")
err = errors.Wrap(err, fmt.Sprintf("execute GetPublicDocuments for org %s%s", orgID))
}
return
@ -225,11 +187,32 @@ func (s Scope) DocumentList(ctx domain.RequestContext) (documents []doc.Document
func (s Scope) Update(ctx domain.RequestContext, document doc.Document) (err error) {
document.Revised = time.Now().UTC()
_, err = ctx.Transaction.NamedExec("UPDATE document SET labelid=:labelid, userid=:userid, job=:job, location=:location, title=:title, excerpt=:excerpt, slug=:slug, tags=:tags, template=:template, protection=:protection, approval=:approval, revised=:revised WHERE orgid=:orgid AND refid=:refid",
_, err = ctx.Transaction.NamedExec(`
UPDATE document
SET
labelid=:labelid, userid=:userid, job=:job, location=:location, title=:title, excerpt=:excerpt, slug=:slug, tags=:tags, template=:template,
protection=:protection, approval=:approval, lifecycle=:lifecycle, versioned=:versioned, versionid=:versionid, versionorder=:versionorder, groupid=:groupid, revised=:revised
WHERE orgid=:orgid AND refid=:refid`,
&document)
if err != nil {
err = errors.Wrap(err, "execute update document")
err = errors.Wrap(err, "document.store.Update")
}
return
}
// UpdateGroup applies same values to all documents
// with the same group ID.
func (s Scope) UpdateGroup(ctx domain.RequestContext, d doc.Document) (err error) {
_, err = ctx.Transaction.Exec(`UPDATE document SET title=?, excerpt=? WHERE orgid=? AND groupid=?`,
d.Title, d.Excerpt, ctx.OrgID, d.GroupID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, "document.store.UpdateTitle")
}
return
@ -311,3 +294,28 @@ func (s Scope) DeleteBySpace(ctx domain.RequestContext, spaceID string) (rows in
return b.DeleteConstrained(ctx.Transaction, "document", ctx.OrgID, spaceID)
}
// GetVersions returns a slice containing the documents for a given space.
//
// No attempt is made to hide documents that are protected by category
// permissions hence caller must filter as required.
//
// All versions of a document are returned, hence caller must
// decide what to do with them.
func (s Scope) GetVersions(ctx domain.RequestContext, groupID string) (v []doc.Version, err error) {
err = s.Runtime.Db.Select(&v, `
SELECT versionid, refid as documentid
FROM document
WHERE orgid=? AND groupid=?
ORDER BY versionorder`, ctx.OrgID, groupID)
if err == sql.ErrNoRows || len(v) == 0 {
err = nil
v = []doc.Version{}
}
if err != nil {
err = errors.Wrap(err, "document.store.GetVersions")
}
return
}

View file

@ -164,19 +164,27 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
h.Store.Block.IncrementUsage(ctx, model.Page.BlockID)
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: model.Page.DocumentID,
PageID: model.Page.RefID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeCreated})
// Draft actions are not logged
if doc.Lifecycle == workflow.LifecycleLive {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: model.Page.DocumentID,
PageID: model.Page.RefID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeCreated})
}
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSectionAdd)
np, _ := h.Store.Page.Get(ctx, pageID)
go h.Indexer.IndexContent(ctx, np)
if doc.Lifecycle == workflow.LifecycleLive {
go h.Indexer.IndexContent(ctx, np)
} else {
go h.Indexer.DeleteDocument(ctx, doc.RefID)
}
response.WriteJSON(w, np)
}
@ -309,6 +317,7 @@ func (h *Handler) GetMeta(w http.ResponseWriter, r *http.Request) {
// Update will persist changed page and note the fact
// that this is a new revision. If the page is the first in a document
// then the corresponding document title will also be changed.
// Draft documents do not get revision entry.
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
method := "page.update"
ctx := domain.GetRequestContext(r)
@ -406,6 +415,11 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
skipRevision = true
}
// We only track revisions for live documents
if doc.Lifecycle != workflow.LifecycleLive {
skipRevision = true
}
err = h.Store.Page.Update(ctx, model.Page, refID, ctx.UserID, skipRevision)
if err != nil {
ctx.Transaction.Rollback()
@ -422,12 +436,15 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: model.Page.DocumentID,
PageID: model.Page.RefID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeEdited})
// Draft edits are not logged
if doc.Lifecycle == workflow.LifecycleLive {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: model.Page.DocumentID,
PageID: model.Page.RefID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeEdited})
}
h.Store.Audit.Record(ctx, audit.EventTypeSectionUpdate)
@ -470,7 +487,11 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
ctx.Transaction.Commit()
go h.Indexer.IndexContent(ctx, model.Page)
if doc.Lifecycle == workflow.LifecycleLive {
go h.Indexer.IndexContent(ctx, model.Page)
} else {
go h.Indexer.DeleteDocument(ctx, doc.RefID)
}
updatedPage, err := h.Store.Page.Get(ctx, pageID)
@ -547,12 +568,15 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
return
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: documentID,
PageID: pageID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeDeleted})
// Draft actions are not logged
if doc.Lifecycle == workflow.LifecycleLive {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: documentID,
PageID: pageID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeDeleted})
}
go h.Indexer.DeleteContent(ctx, pageID)
@ -660,12 +684,15 @@ func (h *Handler) DeletePages(w http.ResponseWriter, r *http.Request) {
h.Store.Page.DeletePageRevisions(ctx, page.PageID)
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: documentID,
PageID: page.PageID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeDeleted})
// Draft actions are not logged
if doc.Lifecycle == workflow.LifecycleLive {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: documentID,
PageID: page.PageID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeDeleted})
}
}
ctx.Transaction.Commit()
@ -925,13 +952,15 @@ func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
h.Store.Block.IncrementUsage(ctx, model.Page.BlockID)
}
// Log action against target document
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: targetID,
PageID: newPageID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeCreated})
// Log t actions are not logged
if doc.Lifecycle == workflow.LifecycleLive {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: targetID,
PageID: newPageID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeCreated})
}
ctx.Transaction.Commit()
@ -1174,12 +1203,15 @@ func (h *Handler) Rollback(w http.ResponseWriter, r *http.Request) {
return
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: p.DocumentID,
PageID: p.RefID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeReverted})
// Draft actions are not logged
if doc.Lifecycle == workflow.LifecycleLive {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: p.DocumentID,
PageID: p.RefID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeReverted})
}
ctx.Transaction.Commit()

View file

@ -165,6 +165,44 @@ func CanViewSpace(ctx domain.RequestContext, s domain.Store, spaceID string) boo
return false
}
// CanViewDrafts returns if the user has permission to view drafts in space.
func CanViewDrafts(ctx domain.RequestContext, s domain.Store, spaceID string) bool {
roles, err := s.Permission.GetUserSpacePermissions(ctx, spaceID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
return false
}
for _, role := range roles {
if role.OrgID == ctx.OrgID && role.RefID == spaceID && role.Location == pm.LocationSpace && role.Scope == pm.ScopeRow &&
pm.ContainsPermission(role.Action, pm.DocumentLifecycle) {
return true
}
}
return false
}
// CanManageVersion returns if the user has permission to manage versions in space.
func CanManageVersion(ctx domain.RequestContext, s domain.Store, spaceID string) bool {
roles, err := s.Permission.GetUserSpacePermissions(ctx, spaceID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
return false
}
for _, role := range roles {
if role.OrgID == ctx.OrgID && role.RefID == spaceID && role.Location == pm.LocationSpace && role.Scope == pm.ScopeRow &&
pm.ContainsPermission(role.Action, pm.DocumentVersion) {
return true
}
}
return false
}
// HasPermission returns if user can perform specified actions.
func HasPermission(ctx domain.RequestContext, s domain.Store, spaceID string, actions ...pm.Action) bool {
roles, err := s.Permission.GetUserSpacePermissions(ctx, spaceID)

View file

@ -121,6 +121,12 @@ func (s Scope) IndexContent(ctx domain.RequestContext, p page.Page) (err error)
err = errors.Wrap(err, "execute insert document content entry")
}
_, err = ctx.Transaction.Exec("INSERT INTO search (orgid, documentid, itemid, itemtype, content) VALUES (?, ?, ?, ?, ?)",
ctx.OrgID, p.DocumentID, p.RefID, "page", p.Title)
if err != nil {
err = errors.Wrap(err, "execute insert document page title entry")
}
return nil
}
@ -148,7 +154,6 @@ func (s Scope) DeleteContent(ctx domain.RequestContext, pageID string) (err erro
// Visible documents include both those in the client's own organisation and those that are public, or whose visibility includes the client.
func (s Scope) Documents(ctx domain.RequestContext, q search.QueryOptions) (results []search.QueryResult, err error) {
q.Keywords = strings.TrimSpace(q.Keywords)
if len(q.Keywords) == 0 {
return
}
@ -204,29 +209,29 @@ func (s Scope) Documents(ctx domain.RequestContext, q search.QueryOptions) (resu
func (s Scope) matchFullText(ctx domain.RequestContext, keywords, itemType string) (r []search.QueryResult, err error) {
sql1 := `
SELECT
s.id, s.orgid, s.documentid, s.itemid, s.itemtype,
d.labelid as spaceid, COALESCE(d.title,'Unknown') AS document, d.tags, d.excerpt,
SELECT
s.id, s.orgid, s.documentid, s.itemid, s.itemtype,
d.labelid as spaceid, COALESCE(d.title,'Unknown') AS document, d.tags,
d.excerpt, d.template, d.versionid,
COALESCE(l.label,'Unknown') AS space
FROM
search s,
document d
LEFT JOIN
LEFT JOIN
label l ON l.orgid=d.orgid AND l.refid = d.labelid
WHERE
s.orgid = ?
AND s.itemtype = ?
AND s.documentid = d.refid
-- AND d.template = 0
AND d.labelid IN
AND s.documentid = d.refid
AND d.labelid IN
(
SELECT refid FROM label WHERE orgid=?
AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN (
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view'
SELECT refid FROM label WHERE orgid=? AND refid IN
(
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view'
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=?
))
)
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 r.userid=?
)
)
AND MATCH(s.content) AGAINST(? IN BOOLEAN MODE)`
err = s.Runtime.Db.Select(&r,
@ -235,7 +240,6 @@ func (s Scope) matchFullText(ctx domain.RequestContext, keywords, itemType strin
itemType,
ctx.OrgID,
ctx.OrgID,
ctx.OrgID,
ctx.UserID,
ctx.OrgID,
ctx.UserID,
@ -245,7 +249,6 @@ func (s Scope) matchFullText(ctx domain.RequestContext, keywords, itemType strin
err = nil
r = []search.QueryResult{}
}
if err != nil {
err = errors.Wrap(err, "search document "+itemType)
}
@ -261,25 +264,25 @@ func (s Scope) matchLike(ctx domain.RequestContext, keywords, itemType string) (
keywords = fmt.Sprintf("%%%s%%", keywords)
sql1 := `
SELECT
s.id, s.orgid, s.documentid, s.itemid, s.itemtype,
d.labelid as spaceid, COALESCE(d.title,'Unknown') AS document, d.tags, d.excerpt,
SELECT
s.id, s.orgid, s.documentid, s.itemid, s.itemtype,
d.labelid as spaceid, COALESCE(d.title,'Unknown') AS document, d.tags, d.excerpt,
COALESCE(l.label,'Unknown') AS space
FROM
search s,
document d
LEFT JOIN
LEFT JOIN
label l ON l.orgid=d.orgid AND l.refid = d.labelid
WHERE
s.orgid = ?
AND s.itemtype = ?
AND s.documentid = d.refid
AND s.documentid = d.refid
-- AND d.template = 0
AND d.labelid IN
AND d.labelid IN
(
SELECT refid FROM label WHERE orgid=?
AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN (
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view'
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view'
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=? OR r.userid='0')

View file

@ -168,18 +168,17 @@ type AuditStorer interface {
type DocumentStorer interface {
Add(ctx RequestContext, document doc.Document) (err error)
Get(ctx RequestContext, id string) (document doc.Document, err error)
GetAll() (ctx RequestContext, documents []doc.Document, err error)
GetBySpace(ctx RequestContext, spaceID string) (documents []doc.Document, err error)
DocumentList(ctx RequestContext) (documents []doc.Document, err error)
Templates(ctx RequestContext) (documents []doc.Document, err error)
TemplatesBySpace(ctx RequestContext, spaceID string) (documents []doc.Document, err error)
DocumentMeta(ctx RequestContext, id string) (meta doc.DocumentMeta, err error)
PublicDocuments(ctx RequestContext, orgID string) (documents []doc.SitemapDocument, err error)
Update(ctx RequestContext, document doc.Document) (err error)
UpdateGroup(ctx RequestContext, document doc.Document) (err error)
ChangeDocumentSpace(ctx RequestContext, document, space string) (err error)
MoveDocumentSpace(ctx RequestContext, id, move string) (err error)
Delete(ctx RequestContext, documentID string) (rows int64, err error)
DeleteBySpace(ctx RequestContext, spaceID string) (rows int64, err error)
GetVersions(ctx RequestContext, groupID string) (v []doc.Version, err error)
}
// SettingStorer defines required methods for persisting global and user level settings
@ -217,6 +216,7 @@ type LinkStorer interface {
type ActivityStorer interface {
RecordUserActivity(ctx RequestContext, activity activity.UserActivity) (err error)
GetDocumentActivity(ctx RequestContext, id string) (a []activity.DocumentActivity, err error)
DeleteDocumentChangeActivity(ctx RequestContext, id string) (rows int64, err error)
}
// SearchStorer defines required methods for persisting search queries

View file

@ -35,6 +35,7 @@ import (
"github.com/documize/community/model/page"
pm "github.com/documize/community/model/permission"
"github.com/documize/community/model/template"
"github.com/documize/community/model/workflow"
uuid "github.com/nu7hatch/gouuid"
)
@ -377,7 +378,12 @@ func (h *Handler) Use(w http.ResponseWriter, r *http.Request) {
event.Handler().Publish(string(event.TypeAddDocument), nd.Title)
a, _ := h.Store.Attachment.GetAttachments(ctx, documentID)
go h.Indexer.IndexDocument(ctx, nd, a)
if nd.Lifecycle == workflow.LifecycleLive {
go h.Indexer.IndexDocument(ctx, nd, a)
} else {
go h.Indexer.DeleteDocument(ctx, d.RefID)
}
response.WriteJSON(w, nd)
}

View file

@ -41,7 +41,7 @@ func main() {
// product details
rt.Product = env.ProdInfo{}
rt.Product.Major = "1"
rt.Product.Minor = "58"
rt.Product.Minor = "59"
rt.Product.Patch = "0"
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
rt.Product.Edition = "Community"

File diff suppressed because one or more lines are too long

View file

@ -9,18 +9,16 @@
//
// https://documize.com
import Service, { inject as service } from '@ember/service';
import AuthProvider from '../../mixins/auth';
import ModalMixin from '../../mixins/modal';
import Component from '@ember/component';
export default Service.extend({
ajax: service(),
export default Component.extend(AuthProvider, ModalMixin, {
getDocumentSummary(documentId) {
return this.get('ajax').request(`activity/document/${documentId}`, {
method: "GET"
}).then((response) => {
return response;
}).catch(() => {
return [];
});
init() {
this._super(...arguments);
},
actions: {
}
});

View file

@ -13,11 +13,12 @@ import $ from 'jquery';
import { computed } from '@ember/object';
import { notEmpty } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { A } from "@ember/array"
import { schedule } from '@ember/runloop';
import ModalMixin from '../../mixins/modal';
import Component from '@ember/component';
export default Component.extend({
export default Component.extend(ModalMixin, {
documentService: service('document'),
categoryService: service('category'),
sessionService: service('session'),

View file

@ -19,12 +19,12 @@ export default Component.extend(ModalMixin, {
groupSvc: service('group'),
spaceSvc: service('folder'),
userSvc: service('user'),
appMeta: service(),
appMeta: service(),
store: service(),
spacePermissions: null,
users: null,
searchText: '',
didReceiveAttrs() {
let spacePermissions = A([]);
let constants = this.get('constants');
@ -65,7 +65,7 @@ export default Component.extend(ModalMixin, {
// always show everyone
if (!hasEveryoneId) {
let pr = this.permissionRecord(constants.WhoType.User, constants.EveryoneUserId, ' ' + constants.EveryoneUserName);
spacePermissions.pushObject(pr);
spacePermissions.pushObject(pr);
}
this.set('spacePermissions', spacePermissions.sortBy('who', 'name'));
@ -93,6 +93,8 @@ export default Component.extend(ModalMixin, {
documentCopy: false,
documentTemplate: false,
documentApprove: false,
documentLifecycle: false,
documentVersion: false,
};
let rec = this.get('store').normalize('space-permission', raw);
@ -132,7 +134,8 @@ export default Component.extend(ModalMixin, {
let hasEveryone = _.find(permissions, (permission) => {
return permission.get('whoId') === constants.EveryoneUserId &&
(permission.get('spaceView') || permission.get('documentAdd') || permission.get('documentEdit') || permission.get('documentDelete') ||
permission.get('documentMove') || permission.get('documentCopy') || permission.get('documentTemplate') || permission.get('documentApprove'));
permission.get('documentMove') || permission.get('documentCopy') || permission.get('documentTemplate') ||
permission.get('documentApprove') || permission.get('documentLifecycle') || permission.get('documentVersion'));
});
// see if more than oen user is granted access to space (excluding everyone)
@ -140,8 +143,9 @@ export default Component.extend(ModalMixin, {
permissions.forEach((permission) => {
if (permission.get('whoId') !== constants.EveryoneUserId &&
(permission.get('spaceView') || permission.get('documentAdd') || permission.get('documentEdit') || permission.get('documentDelete') ||
permission.get('documentMove') || permission.get('documentCopy') || permission.get('documentTemplate') || permission.get('documentApprove'))) {
roleCount += 1;
permission.get('documentMove') || permission.get('documentCopy') || permission.get('documentTemplate') ||
permission.get('documentApprove') || permission.get('documentLifecycle') || permission.get('documentVersion'))) {
roleCount += 1;
}
});
@ -161,7 +165,7 @@ export default Component.extend(ModalMixin, {
},
onSearch() {
debounce(this, function() {
debounce(this, function () {
let searchText = this.get('searchText').trim();
if (searchText.length === 0) {
@ -181,7 +185,6 @@ export default Component.extend(ModalMixin, {
if (is.undefined(exists)) {
spacePermissions.pushObject(this.permissionRecord(constants.WhoType.User, user.get('id'), user.get('fullname')));
this.set('spacePermissions', spacePermissions);
// this.set('spacePermissions', spacePermissions.sortBy('who', 'name'));
}
},
}

View file

@ -12,21 +12,18 @@
import Component from '@ember/component';
export default Component.extend({
resultPhrase: "",
init() {
this._super(...arguments);
this.results = [];
},
resultPhrase: '',
didReceiveAttrs() {
this._super(...arguments);
let docs = this.get('results');
let duped = [];
let phrase = 'Nothing found';
if (docs.length > 0) {
duped = _.uniq(docs, function (item) {
return item.documentId;
return item.get('documentId');
});
let references = docs.length === 1 ? "reference" : "references";

View file

@ -11,10 +11,10 @@
import $ from 'jquery';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import AuthMixin from '../../mixins/auth';
import TooltipMixin from '../../mixins/tooltip';
import ModalMixin from '../../mixins/modal';
import Component from '@ember/component';
export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, {
userSvc: service('user'),

View file

@ -1,11 +1,11 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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>.
// by contacting <sales@documize.com>.
//
// https://documize.com
@ -15,52 +15,70 @@ import EmberObject from "@ember/object";
// let constants = this.get('constants');
let constants = EmberObject.extend({
// Document
ProtectionType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
None: 0,
Lock: 1,
Review: 2,
// Document
ProtectionType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
None: 0,
Lock: 1,
Review: 2,
NoneLabel: 'Changes permitted without approval',
LockLabel: 'Locked, changes not permitted',
ReviewLabel: 'Changes require approval before publication'
},
NoneLabel: 'Changes permitted without approval',
LockLabel: 'Locked, changes not permitted',
ReviewLabel: 'Changes require approval before publication'
},
// Document
ApprovalType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
None: 0,
Anybody: 1,
Majority: 2,
Unanimous: 3,
// Document
ApprovalType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
None: 0,
Anybody: 1,
Majority: 2,
Unanimous: 3,
AnybodyLabel: 'Approval required from any approver',
MajorityLabel: 'Majority approval required from approvers',
UnanimousLabel: 'Unanimous approval required from all approvers'
},
AnybodyLabel: 'Approval required from any approver',
MajorityLabel: 'Majority approval required from approvers',
UnanimousLabel: 'Unanimous approval required from all approvers'
},
// Section
ChangeState: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
Published: 0,
Pending: 1,
UnderReview: 2,
Rejected: 3,
PendingNew: 4,
},
// Section
ChangeState: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
Published: 0,
Pending: 1,
UnderReview: 2,
Rejected: 3,
PendingNew: 4,
},
// Section
PageType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
Tab: 'tab',
Section: 'section'
},
// Section
PageType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
Tab: 'tab',
Section: 'section'
},
// Who a permission record relates to
WhoType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
User: 'user',
Group: 'role'
},
// Who a permission record relates to
WhoType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
User: 'user',
Group: 'role'
},
EveryoneUserId: "0",
EveryoneUserName: "Everyone"
EveryoneUserId: '0',
EveryoneUserName: "Everyone",
// Document
Lifecycle: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
Draft: 0,
Live: 1,
Archived: 2,
DraftLabel: 'Draft',
LiveLabel: 'Live',
ArchivedLabel: 'Archived',
},
// Document Version -- document.groupId links different versions of documents together
VersionCreateMode: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
Unversioned: 1, // turn unversioned into versioned document
Cloned: 2, // create versioned document by cloning existing versioned document
Linked: 3 // link existing unversion document into this version group
}
});
export default { constants }
export default { constants }

View file

@ -1,24 +1,25 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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>.
// by contacting <sales@documize.com>.
//
// https://documize.com
export function initialize(application) {
application.inject('route', 'econstants', 'econstants:main');
application.inject('controller', 'econstants', 'econstants:main');
application.inject('component', 'econstants', 'econstants:main');
application.inject('template', 'econstants', 'econstants:main');
application.inject('service', 'econstants', 'econstants:main');
application.inject('route', 'econstants', 'econstants:main');
application.inject('controller', 'econstants', 'econstants:main');
application.inject('component', 'econstants', 'econstants:main');
application.inject('template', 'econstants', 'econstants:main');
application.inject('service', 'econstants', 'econstants:main');
application.inject('model', 'econstants', 'econstants:main');
}
export default {
name: 'econstants',
after: "application",
initialize: initialize
};
name: 'econstants',
after: "application",
initialize: initialize
};

View file

@ -24,5 +24,7 @@ export default Model.extend({
space: attr(),
spaceId: attr(),
spaceSlug: attr(),
template: attr(),
versionId: attr(),
selected: attr()
});

View file

@ -10,10 +10,9 @@
// https://documize.com
import { computed } from '@ember/object';
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import stringUtil from '../utils/string';
// import { belongsTo, hasMany } from 'ember-data/relationships';
import Model from 'ember-data/model';
export default Model.extend({
name: attr('string'),
@ -27,6 +26,11 @@ export default Model.extend({
template: attr('boolean'),
protection: attr('number', { defaultValue: 0 }),
approval: attr('number', { defaultValue: 0 }),
lifecycle: attr('number', { defaultValue: 1 }),
versioned: attr('boolean'),
versionId: attr('string'),
versionOrder: attr('number', { defaultValue: 0 }),
groupId: attr('string'),
// client-side property
selected: attr('boolean', { defaultValue: false }),
@ -34,5 +38,24 @@ export default Model.extend({
return stringUtil.makeSlug(this.get('name'));
}),
created: attr(),
revised: attr()
revised: attr(),
isDraft: computed('lifecycle', function () {
let constants = this.get('constants');
return this.get('lifecycle') == constants.Lifecycle.Draft;
}),
lifecycleLabel: computed('lifecycle', function () {
let constants = this.get('constants');
switch (this.get('lifecycle')) {
case constants.Lifecycle.Draft:
return constants.Lifecycle.DraftLabel;
case constants.Lifecycle.Live:
return constants.Lifecycle.LiveLabel;
case constants.Lifecycle.Archived:
return constants.Lifecycle.ArchivedLabel;
}
return '';
}),
});

View file

@ -10,11 +10,10 @@
// https://documize.com
import { computed } from '@ember/object';
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import constants from '../utils/constants';
import stringUtil from '../utils/string';
// import { belongsTo, hasMany } from 'ember-data/relationships';
import Model from 'ember-data/model';
export default Model.extend({
name: attr('string'),

View file

@ -27,6 +27,8 @@ export default Model.extend({
documentCopy: attr('boolean'),
documentTemplate: attr('boolean'),
documentApprove: attr('boolean'),
documentLifecycle: attr('boolean'),
documentVersion: attr('boolean'),
name: attr('string'), // read-only
members: attr('number') // read-only
});

View file

@ -0,0 +1,17 @@
// 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 Controller from '@ember/controller';
export default Controller.extend({
actions: {
}
});

View file

@ -0,0 +1,25 @@
// 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 Route from '@ember/routing/route';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
export default Route.extend(AuthenticatedRouteMixin, {
beforeModel() {
if (!this.session.isAdmin) {
this.transitionTo('auth.login');
}
},
activate() {
this.get('browser').setTitle('Archive');
}
});

View file

@ -0,0 +1 @@
{{customize/archive-admin}}

View file

@ -2,7 +2,7 @@
{{#toolbar/t-toolbar}}
{{#toolbar/t-links}}
{{#link-to "folders" class="link" tagName="li"}}Spaces{{/link-to}}
{{#link-to "folders" class="link" tagName="li" }}Spaces{{/link-to}}
{{/toolbar/t-links}}
{{#toolbar/t-actions}}
{{/toolbar/t-actions}}
@ -12,19 +12,20 @@
<div class="row">
<div class="col my-5">
<ul class="tabnav-control">
{{#link-to 'customize.general' activeClass='selected' class="tab" tagName="li"}}General{{/link-to}}
{{#link-to 'customize.folders' activeClass='selected' class="tab" tagName="li"}}Spaces{{/link-to}}
{{#link-to 'customize.groups' activeClass='selected' class="tab" tagName="li"}}Groups{{/link-to}}
{{#link-to 'customize.users' activeClass='selected' class="tab" tagName="li"}}Users{{/link-to}}
{{#link-to 'customize.general' activeClass='selected' class="tab" tagName="li" }}General{{/link-to}}
{{#link-to 'customize.folders' activeClass='selected' class="tab" tagName="li" }}Spaces{{/link-to}}
{{#link-to 'customize.groups' activeClass='selected' class="tab" tagName="li" }}Groups{{/link-to}}
{{#link-to 'customize.users' activeClass='selected' class="tab" tagName="li" }}Users{{/link-to}}
{{#if session.isGlobalAdmin}}
{{#link-to 'customize.smtp' activeClass='selected' class="tab" tagName="li"}}SMTP{{/link-to}}
{{#link-to 'customize.license' activeClass='selected' class="tab" tagName="li"}}License{{/link-to}}
{{#link-to 'customize.auth' activeClass='selected' class="tab" tagName="li"}}Authentication{{/link-to}}
{{#link-to 'customize.smtp' activeClass='selected' class="tab" tagName="li" }}SMTP{{/link-to}}
{{#link-to 'customize.license' activeClass='selected' class="tab" tagName="li" }}License{{/link-to}}
{{#link-to 'customize.auth' activeClass='selected' class="tab" tagName="li" }}Authentication{{/link-to}}
{{/if}}
{{#link-to 'customize.archive' activeClass='selected' class="tab" tagName="li" }}Archive{{/link-to}}
</ul>
</div>
</div>
<div class="mt-4 margin-bottom-100">
{{outlet}}
</div>
</div>
</div>

View file

@ -43,7 +43,8 @@ export default Route.extend(AuthenticatedRouteMixin, {
sections: this.modelFor('document').sections,
permissions: this.modelFor('document').permissions,
roles: this.modelFor('document').roles,
blocks: this.modelFor('document').blocks
blocks: this.modelFor('document').blocks,
versions: this.modelFor('document').versions
});
},
@ -57,10 +58,11 @@ export default Route.extend(AuthenticatedRouteMixin, {
controller.set('permissions', model.permissions);
controller.set('roles', model.roles);
controller.set('blocks', model.blocks);
controller.set('versions', model.versions);
},
activate: function() {
activate: function () {
this._super(...arguments);
window.scrollTo(0,0);
window.scrollTo(0, 0);
}
});

View file

@ -1,6 +1,5 @@
{{toolbar/nav-bar}}
{{toolbar/for-document document=document spaces=folders space=folder permissions=permissions roles=roles tab=tab
{{toolbar/nav-bar}} {{toolbar/for-document document=document spaces=folders space=folder
permissions=permissions roles=roles tab=tab versions=versions
onDocumentDelete=(action 'onDocumentDelete')
onSaveTemplate=(action 'onSaveTemplate')
onSaveDocument=(action 'onSaveDocument')
@ -10,9 +9,10 @@
<div class="row">
<div class="col-12">
{{document/document-heading document=document permissions=permissions
versions=versions
onSaveDocument=(action 'onSaveDocument')}}
{{document/document-meta document=document folder=folder folders=folders permissions=permissions pages=pages
{{document/document-meta document=document folder=folder folders=folders
permissions=permissions pages=pages versions=versions
onSaveDocument=(action 'onSaveDocument')}}
</div>
</div>
@ -38,8 +38,8 @@
{{#if (eq tab 'content')}}
{{document/view-content
document=document links=links pages=pages blocks=blocks currentPageId=currentPageId
folder=folder folders=folders sections=sections permissions=permissions roles=roles
document=document links=links pages=pages blocks=blocks currentPageId=currentPageId
folder=folder folders=folders sections=sections permissions=permissions roles=roles
onSavePage=(action 'onSavePage') onInsertSection=(action 'onInsertSection')
onSavePageAsBlock=(action 'onSavePageAsBlock') onDeleteBlock=(action 'onDeleteBlock')
onCopyPage=(action 'onCopyPage') onMovePage=(action 'onMovePage') onDeletePage=(action 'onPageDeleted')
@ -56,4 +56,4 @@
{{/if}}
</div>
</div>
</div>
</div>

View file

@ -32,6 +32,7 @@ export default Route.extend(AuthenticatedRouteMixin, {
this.set('permissions', data.permissions);
this.set('roles', data.roles);
this.set('links', data.links);
this.set('versions', data.versions);
resolve();
});
});
@ -45,13 +46,14 @@ export default Route.extend(AuthenticatedRouteMixin, {
permissions: this.get('permissions'),
roles: this.get('roles'),
links: this.get('links'),
versions: this.get('versions'),
sections: this.get('sectionService').getAll(),
blocks: this.get('sectionService').getSpaceBlocks(this.get('folder.id'))
});
},
actions: {
error(error /*, transition*/ ) {
error(error /*, transition*/) {
if (error) {
this.transitionTo('/not-found');
return false;

View file

@ -16,7 +16,7 @@ var Router = EmberRouter.extend({
location: config.locationType
});
export default Router.map(function() {
export default Router.map(function () {
this.route('folders', {
path: '/'
});
@ -30,7 +30,7 @@ export default Router.map(function() {
{
path: 's/:folder_id/:folder_slug'
},
function() {
function () {
this.route('category', {
path: 'category'
});
@ -42,7 +42,7 @@ export default Router.map(function() {
{
path: 's/:folder_id/:folder_slug/d/:document_id/:document_slug'
},
function() {
function () {
this.route('section', {
path: 'section/:page_id'
});
@ -57,7 +57,7 @@ export default Router.map(function() {
{
path: 'settings'
},
function() {
function () {
this.route('general', {
path: 'general'
});
@ -82,6 +82,9 @@ export default Router.map(function() {
this.route('audit', {
path: 'audit'
});
this.route('archive', {
path: 'archive'
});
}
);
@ -98,7 +101,7 @@ export default Router.map(function() {
{
path: 'auth'
},
function() {
function () {
this.route('sso', {
path: 'sso/:token'
});

View file

@ -334,6 +334,7 @@ export default Service.extend({
folders: [],
folder: {},
links: [],
versions: [],
};
let doc = this.get('store').normalize('document', response.document);
@ -357,6 +358,7 @@ export default Service.extend({
data.folders = folders;
data.folder = folders.findBy('id', doc.get('folderId'));
data.links = response.links;
data.versions = response.versions;
return data;
}).catch((error) => {
@ -366,7 +368,7 @@ export default Service.extend({
// fetchPages returns all pages, page meta and pending changes for document.
// This method bulk fetches data to reduce network chatter.
// We produce a bunch of calculated boolean's for UI display purposes
// We produce a bunch of calculated boolean's for UI display purposes
// that can tell us quickly about pending changes for UI display.
fetchPages(documentId, currentUserId) {
let constants = this.get('constants');
@ -400,7 +402,7 @@ export default Service.extend({
page.pending.forEach((i) => {
let p = this.get('store').normalize('page', i.page);
p = this.get('store').push(p);
let m = this.get('store').normalize('page-meta', i.meta);
m = this.get('store').push(m);
@ -451,7 +453,7 @@ export default Service.extend({
userHasChangeRejected: userHasChangeRejected,
userHasNewPagePending: p.isNewPageUserPending(this.get('sessionService.user.id'))
};
let pim = this.get('store').normalize('page-container', pi);
pim = this.get('store').push(pim);
data.pushObject(pim);

View file

@ -39,4 +39,4 @@ export default Service.extend({
return error;
});
},
});
});

View file

@ -100,6 +100,7 @@ $link-hover-decoration: none;
@import "node_modules/bootstrap/scss/popover";
@import "node_modules/bootstrap/scss/tooltip";
@import "node_modules/bootstrap/scss/tables";
@import "node_modules/bootstrap/scss/badge";
// @import "node_modules/bootstrap/scss/navbar";
// @import "node_modules/bootstrap/scss/images";

View file

@ -23,7 +23,7 @@
margin: 0 0 0 10px;
display: inline-block;
cursor: pointer;
> .email {
font-size: 0.9rem;
color: $color-off-black;
@ -124,7 +124,7 @@
}
}
}
> .smtp-failure {
font-size: 1.2rem;
font-weight: bold;
@ -136,4 +136,16 @@
font-weight: bold;
color: $color-green;
}
> .archive-admin {
> .list {
> .item {
margin: 15px 0;
padding: 15px;
@include ease-in();
font-size: 1.2rem;
color: $color-primary;
}
}
}
}

View file

@ -17,7 +17,6 @@
position: relative;
margin: 0 0 30px 0;
width: 100%;
// height: 150px;
&:hover {
> .checkbox {
@ -33,26 +32,33 @@
> .title {
color: $color-black;
font-size: 1.3rem;
font-weight: bold;
font-size: 1.4rem;
margin-bottom: 5px;
> .version {
font-size: 1.1rem;
font-weight: bold;
color: $color-gray;
}
}
> .space {
color: $color-off-black;
font-size: 1.2rem;
margin-bottom: 5px;
}
> .snippet {
color: $color-gray;
font-size: 1rem;
line-height: 24px;
font-size: 1.1rem;
margin-bottom: 10px;
}
&:hover {
color: $color-gray;
> .title {
color: $color-link;
}
> .snippet {
color: $color-link;
}
}
}
@ -61,7 +67,7 @@
display: inline-block;
margin: 5px 10px 0 5px;
color: $color-gray;
font-size: 0.875rem;
font-size: 1rem;
font-style: italic;
&:hover {

View file

@ -61,6 +61,27 @@
box-shadow: 0 27px 24px 0 rgba(0, 0, 0, 0.2), 0 40px 77px 0 rgba(0, 0, 0, 0.22);
}
.drag-handle {
font-size: 1.5rem;
color: $color-gray-light;
cursor: pointer;
}
.drag-indicator-dropzone {
opacity: 1 !important;
border: 2px dotted $color-border;
}
.drag-indicator-chosen {
opacity: 1 !important;
background: $color-off-white;
}
.drag-indicator-dragged {
opacity: 1 !important;
@include card();
}
@import "widget-avatar";
@import "widget-button";
@import "widget-checkbox";

View file

@ -0,0 +1,25 @@
<div class="row">
<div class="col">
<div class="view-customize">
<h1 class="admin-heading">Archive</h1>
<h2 class="sub-heading">Mark as live documents currently marked as archived</h2>
<div class="archive-admin my-5">
<ul class="list">
{{#each docs as |doc|}}
<li class="item row">
<div class="col-12 col-sm-10">{{doc.name}}</div>
<div class="col-12 col-sm-2 float-right">
<button class="btn btn-success" {{action 'onMarkLive' doc.id}}>Unarchive</button>
</div>
</li>
{{/each}}
</ul>
{{#if (eq docs.length 0)}}
<p>Nothing found</p>
{{/if}}
</div>
</div>
</div>
</div>

View file

@ -7,6 +7,9 @@
<div class="title">{{ document.name }}</div>
<div class="snippet">{{ document.excerpt }}</div>
{{folder/document-tags documentTags=document.tags}}
{{#if (not-eq document.lifecycle constants.Lifecycle.Live)}}
<button type="button" class="mt-3 btn btn-warning text-uppercase font-weight-bold">{{document.lifecycleLabel}}</button>
{{/if}}
{{/link-to}}
{{#if hasDocumentActions}}
@ -39,11 +42,13 @@
</ul>
</div>
{{#ui/ui-dialog title="Delete Documents" confirmCaption="Delete" buttonType="btn-danger" show=showDeleteDialog onAction=(action 'onDeleteDocuments')}}
{{#ui/ui-dialog title="Delete Documents" confirmCaption="Delete" buttonType="btn-danger" show=showDeleteDialog onAction=(action
'onDeleteDocuments')}}
<p>Are you sure you want to delete {{selectedDocuments.length}} {{selectedCaption}}?</p>
{{/ui/ui-dialog}}
{{#ui/ui-dialog title="Move Documents" confirmCaption="Move" buttonType="btn-success" show=showMoveDialog onAction=(action 'onMoveDocuments')}}
{{#ui/ui-dialog title="Move Documents" confirmCaption="Move" buttonType="btn-success" show=showMoveDialog onAction=(action
'onMoveDocuments')}}
<p>Select space for {{selectedDocuments.length}} {{selectedCaption}}</p>
{{ui/ui-list-picker items=moveOptions nameField='name' singleSelect=true}}
{{/ui/ui-dialog}}

View file

@ -4,7 +4,7 @@
<div class="modal-header">Space Permissions</div>
<div class="modal-body" style="overflow-x: auto;">
<div class="space-admin table-responsive">
<table class="table table-hover permission-table mb-3">
<thead>
<tr>
@ -24,6 +24,8 @@
<th class="text-info">Copy</th>
<th class="text-info">Templates</th>
<th class="text-info">Approval</th>
<th class="text-info">Lifecycle</th>
<th class="text-info">Versions</th>
</tr>
</thead>
<tbody>
@ -38,53 +40,35 @@
<small class="form-text text-muted d-inline-block">({{permission.members}})</small>
</span>
{{else}}
{{#if (eq permission.whoId constants.EveryoneUserId)}}
<span class="button-icon-green button-icon-small align-middle">
<i class="material-icons">language</i>
</span>
<span class="color-green">&nbsp;{{permission.name}}</span>
{{else}}
<span class="button-icon-gray button-icon-small align-middle">
<i class="material-icons">person</i>
</span>
<span class="">&nbsp;{{permission.name}}
{{#if (eq permission.whoId session.user.id)}}
<small class="form-text text-muted d-inline-block">(you)</small>
{{/if}}
</span>
{{/if}}
{{#if (eq permission.whoId constants.EveryoneUserId)}}
<span class="button-icon-green button-icon-small align-middle">
<i class="material-icons">language</i>
</span>
<span class="color-green">&nbsp;{{permission.name}}</span>
{{else}}
<span class="button-icon-gray button-icon-small align-middle">
<i class="material-icons">person</i>
</span>
<span class="">&nbsp;{{permission.name}}
{{#if (eq permission.whoId session.user.id)}}
<small class="form-text text-muted d-inline-block">(you)</small>
{{/if}}
</span>
{{/if}}
{{/if}}
</td>
<td>
{{input type="checkbox" id=(concat 'space-role-view-' permission.whoId) checked=permission.spaceView}}
</td>
<td>
{{input type="checkbox" id=(concat 'space-role-manage-' permission.whoId) checked=permission.spaceManage}}
</td>
<td>
{{input type="checkbox" id=(concat 'space-role-owner-' permission.whoId) checked=permission.spaceOwner}}
</td>
<td>
{{input type="checkbox" id=(concat 'doc-role-add-' permission.whoId) checked=permission.documentAdd}}
</td>
<td>
{{input type="checkbox" id=(concat 'doc-role-edit-' permission.whoId) checked=permission.documentEdit}}
</td>
<td>
{{input type="checkbox" id=(concat 'doc-role-delete-' permission.whoId) checked=permission.documentDelete}}
</td>
<td>
{{input type="checkbox" id=(concat 'doc-role-move-' permission.whoId) checked=permission.documentMove}}
</td>
<td>
{{input type="checkbox" id=(concat 'doc-role-copy-' permission.whoId) checked=permission.documentCopy}}
</td>
<td>
{{input type="checkbox" id=(concat 'doc-role-template-' permission.whoId) checked=permission.documentTemplate}}
</td>
<td>
{{input type="checkbox" id=(concat 'doc-role-approve-' permission.whoId) checked=permission.documentApprove}}
</td>
<td>{{input type="checkbox" id=(concat 'space-role-view-' permission.whoId) checked=permission.spaceView}}</td>
<td>{{input type="checkbox" id=(concat 'space-role-manage-' permission.whoId) checked=permission.spaceManage}}</td>
<td>{{input type="checkbox" id=(concat 'space-role-owner-' permission.whoId) checked=permission.spaceOwner}}</td>
<td>{{input type="checkbox" id=(concat 'doc-role-add-' permission.whoId) checked=permission.documentAdd}}</td>
<td>{{input type="checkbox" id=(concat 'doc-role-edit-' permission.whoId) checked=permission.documentEdit}}</td>
<td>{{input type="checkbox" id=(concat 'doc-role-delete-' permission.whoId) checked=permission.documentDelete}}</td>
<td>{{input type="checkbox" id=(concat 'doc-role-move-' permission.whoId) checked=permission.documentMove}}</td>
<td>{{input type="checkbox" id=(concat 'doc-role-copy-' permission.whoId) checked=permission.documentCopy}}</td>
<td>{{input type="checkbox" id=(concat 'doc-role-template-' permission.whoId) checked=permission.documentTemplate}}</td>
<td>{{input type="checkbox" id=(concat 'doc-role-approve-' permission.whoId) checked=permission.documentApprove}}</td>
<td>{{input type="checkbox" id=(concat 'doc-role-lifecycle-' permission.whoId) checked=permission.documentLifecycle}}</td>
<td>{{input type="checkbox" id=(concat 'doc-role-version-' permission.whoId) checked=permission.documentVersion}}</td>
</tr>
{{/each}}
</tbody>
@ -108,13 +92,13 @@
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-success" onclick={{action 'setPermissions'}}>Save</button>
<button type="button" class="btn btn-success" onclick= {{action 'setPermissions'}}>Save</button>
</div>
</div>
</div>
</div>
</div>

View file

@ -1,15 +1,23 @@
<div class="view-search my-5">
<div class="heading">{{resultPhrase}}</div>
<ul class="documents">
{{#each documents key="id" as |result index|}}
<div class="heading">{{resultPhrase}}</div>
<ul class="documents">
{{#each documents key="id" as |result index|}}
<li class="document">
<a class="link" href="s/{{result.spaceId}}/{{result.spaceSlug}}/d/{{ result.documentId }}/{{result.documentSlug}}?page={{ result.itemId }}">
<div class="title">{{result.document}}</div>
<a class="link" href="s/{{result.spaceId}}/{{result.spaceSlug}}/d/{{ result.documentId }}/{{result.documentSlug}}?page={{ result.itemId }}">
<div class="title">
{{result.document}}
{{#if (gt result.versionId.length 0)}}
<span class="version">&nbsp;&nbsp;{{result.versionId}}</span>
{{/if}}
</div>
<div class="space">{{result.space}}</div>
<div class="snippet">{{result.excerpt}}</div>
<div class="snippet">({{result.space}})</div>
{{folder/document-tags documentTags=result.tags}}
</a>
</li>
{{/each}}
</ul>
</div>
{{#if result.template}}
<button type="button" class="mt-3 btn btn-warning text-uppercase font-weight-bold">TEMPLATE</button>
{{/if}}
</a>
</li>
{{/each}}
</ul>
</div>

3933
gui/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "documize",
"version": "1.58.0",
"version": "1.59.0",
"description": "The Document IDE",
"private": true,
"repository": "",
@ -51,6 +51,7 @@
"bootstrap": "^4.0.0",
"mermaid": "^7.1.2",
"node-sass": "^4.7.2",
"npm": "^5.7.1",
"popper.js": "^1.12.9"
}
}

116
gui/vendor/sortable.js vendored
View file

@ -1,3 +1,65 @@
/**
* jQuery plugin for Sortable
* @author RubaXa <trash@rubaxa.org>
* @license MIT
*/
(function (factory) {
"use strict";
if (typeof define === "function" && define.amd) {
define(["jquery"], factory);
}
else {
/* jshint sub:true */
factory(jQuery);
}
})(function ($) {
"use strict";
/* CODE */
/**
* jQuery plugin for Sortable
* @param {Object|String} options
* @param {..*} [args]
* @returns {jQuery|*}
*/
$.fn.sortable = function (options) {
var retVal,
args = arguments;
this.each(function () {
var $el = $(this),
sortable = $el.data('sortable');
if (!sortable && (options instanceof Object || !options)) {
sortable = new Sortable(this, options);
$el.data('sortable', sortable);
}
if (sortable) {
if (options === 'widget') {
retVal = sortable;
}
else if (options === 'destroy') {
sortable.destroy();
$el.removeData('sortable');
}
else if (typeof sortable[options] === 'function') {
retVal = sortable[options].apply(sortable, [].slice.call(args, 1));
}
else if (options in sortable.options) {
retVal = sortable.option.apply(sortable, args);
}
}
});
return (retVal === void 0) ? this : retVal;
};
});
/**!
* Sortable
* @author RubaXa <trash@rubaxa.org>
@ -109,7 +171,7 @@
scrollOffsetX,
scrollOffsetY
;
;
// Delect scrollEl
if (scrollParentEl !== rootEl) {
@ -160,7 +222,7 @@
scrollOffsetY = vy ? vy * speed : 0;
scrollOffsetX = vx ? vx * speed : 0;
if ('function' === typeof(scrollCustomFn)) {
if ('function' === typeof (scrollCustomFn)) {
return scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt);
}
@ -202,7 +264,7 @@
var originalGroup = options.group;
if (!originalGroup || typeof originalGroup != 'object') {
originalGroup = {name: originalGroup};
originalGroup = { name: originalGroup };
}
group.name = originalGroup.name;
@ -212,7 +274,7 @@
options.group = group;
}
;
;
/**
@ -261,7 +323,7 @@
fallbackClass: 'sortable-fallback',
fallbackOnBody: false,
fallbackTolerance: 0,
fallbackOffset: {x: 0, y: 0}
fallbackOffset: { x: 0, y: 0 }
};
@ -555,7 +617,7 @@
_onTouchMove: function (/**TouchEvent*/evt) {
if (tapEvt) {
var options = this.options,
var options = this.options,
fallbackTolerance = options.fallbackTolerance,
fallbackOffset = options.fallbackOffset,
touch = evt.touches ? evt.touches[0] : evt,
@ -781,7 +843,7 @@
halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5,
nextSibling = target.nextElementSibling,
after = false
;
;
if (floating) {
var elTop = dragEl.offsetTop,
@ -795,7 +857,7 @@
} else {
after = tgTop > elTop;
}
} else if (!isMovingBetweenSortable) {
} else if (!isMovingBetweenSortable) {
after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
}
@ -963,30 +1025,30 @@
this._nulling();
},
_nulling: function() {
_nulling: function () {
rootEl =
dragEl =
parentEl =
ghostEl =
nextEl =
cloneEl =
lastDownEl =
dragEl =
parentEl =
ghostEl =
nextEl =
cloneEl =
lastDownEl =
scrollEl =
scrollParentEl =
scrollEl =
scrollParentEl =
tapEvt =
touchEvt =
tapEvt =
touchEvt =
moved =
newIndex =
moved =
newIndex =
lastEl =
lastCSS =
lastEl =
lastCSS =
putSortable =
activeGroup =
Sortable.active = null;
putSortable =
activeGroup =
Sortable.active = null;
savedInputChecked.forEach(function (el) {
el.checked = true;
@ -1455,7 +1517,7 @@
};
}
}));
} catch (err) {}
} catch (err) { }
// Export utils
Sortable.utils = {

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,14 @@
{
"community":
{
"version": "1.58.0",
"community": {
"version": "1.59.0",
"major": 1,
"minor": 58,
"minor": 59,
"patch": 0
},
"enterprise":
{
"version": "1.60.0",
"enterprise": {
"version": "1.61.0",
"major": 1,
"minor": 60,
"minor": 61,
"patch": 0
}
}
}

View file

@ -95,6 +95,12 @@ const (
// TypeSentSecureLink records user sending secure document link to email address(es)
TypeSentSecureLink Type = 12
// TypeDraft records user marking space/document as draft
TypeDraft Type = 13
// TypeVersioned records user creating new document version
TypeVersioned Type = 14
)
// TypeName returns one-work descriptor for activity type

View file

@ -22,18 +22,23 @@ import (
// Document represents the purpose of Documize.
type Document struct {
model.BaseEntity
OrgID string `json:"orgId"`
LabelID string `json:"folderId"`
UserID string `json:"userId"`
Job string `json:"job"`
Location string `json:"location"`
Title string `json:"name"`
Excerpt string `json:"excerpt"`
Slug string `json:"-"`
Tags string `json:"tags"`
Template bool `json:"template"`
Protection workflow.Protection `json:"protection"`
Approval workflow.Approval `json:"approval"`
OrgID string `json:"orgId"`
LabelID string `json:"folderId"`
UserID string `json:"userId"`
Job string `json:"job"`
Location string `json:"location"`
Title string `json:"name"`
Excerpt string `json:"excerpt"`
Slug string `json:"-"`
Tags string `json:"tags"`
Template bool `json:"template"`
Protection workflow.Protection `json:"protection"`
Approval workflow.Approval `json:"approval"`
Lifecycle workflow.Lifecycle `json:"lifecycle"`
Versioned bool `json:"versioned"`
VersionID string `json:"versionId"`
VersionOrder int `json:"versionOrder"`
GroupID string `json:"groupId"`
}
// SetDefaults ensures on blanks and cleans.
@ -89,3 +94,9 @@ type SitemapDocument struct {
Folder string
Revised time.Time
}
// Version points to a version of a document.
type Version struct {
VersionID string `json:"versionId"`
DocumentID string `json:"documentId"`
}

View file

@ -93,6 +93,12 @@ const (
// DocumentApprove means you can approve a change to a document
DocumentApprove Action = "doc-approve"
// DocumentLifecycle means you can move a document between DRAFT/LIVE/ARCHIVE states
DocumentLifecycle Action = "doc-lifecycle"
// DocumentVersion means you can manage document versions
DocumentVersion Action = "doc-version"
// CategoryView action means you can view a category and documents therein
CategoryView Action = "view"
)

View file

@ -15,21 +15,23 @@ package permission
// This data structure is made from database permission records for the space,
// and it is designed to be sent to HTTP clients (web, mobile).
type Record struct {
OrgID string `json:"orgId"`
SpaceID string `json:"folderId"`
WhoID string `json:"whoId"`
Who WhoType `json:"who"`
SpaceView bool `json:"spaceView"`
SpaceManage bool `json:"spaceManage"`
SpaceOwner bool `json:"spaceOwner"`
DocumentAdd bool `json:"documentAdd"`
DocumentEdit bool `json:"documentEdit"`
DocumentDelete bool `json:"documentDelete"`
DocumentMove bool `json:"documentMove"`
DocumentCopy bool `json:"documentCopy"`
DocumentTemplate bool `json:"documentTemplate"`
DocumentApprove bool `json:"documentApprove"`
Name string `json:"name"` // read-only, user or group name
OrgID string `json:"orgId"`
SpaceID string `json:"folderId"`
WhoID string `json:"whoId"`
Who WhoType `json:"who"`
SpaceView bool `json:"spaceView"`
SpaceManage bool `json:"spaceManage"`
SpaceOwner bool `json:"spaceOwner"`
DocumentAdd bool `json:"documentAdd"`
DocumentEdit bool `json:"documentEdit"`
DocumentDelete bool `json:"documentDelete"`
DocumentMove bool `json:"documentMove"`
DocumentCopy bool `json:"documentCopy"`
DocumentTemplate bool `json:"documentTemplate"`
DocumentApprove bool `json:"documentApprove"`
DocumentLifecycle bool `json:"documentLifecycle"`
DocumentVersion bool `json:"documentVersion"`
Name string `json:"name"` // read-only, user or group name
}
// DecodeUserPermissions returns a flat, usable permission summary record
@ -67,6 +69,10 @@ func DecodeUserPermissions(perm []Permission) (r Record) {
r.DocumentTemplate = true
case DocumentApprove:
r.DocumentApprove = true
case DocumentLifecycle:
r.DocumentLifecycle = true
case DocumentVersion:
r.DocumentVersion = true
}
}
@ -107,6 +113,12 @@ func EncodeUserPermissions(r Record) (perm []Permission) {
if r.DocumentApprove {
perm = append(perm, EncodeRecord(r, DocumentApprove))
}
if r.DocumentVersion {
perm = append(perm, EncodeRecord(r, DocumentVersion))
}
if r.DocumentLifecycle {
perm = append(perm, EncodeRecord(r, DocumentLifecycle))
}
return
}
@ -114,7 +126,8 @@ func EncodeUserPermissions(r Record) (perm []Permission) {
// HasAnyPermission returns true if user has at least one permission.
func HasAnyPermission(p Record) bool {
return p.SpaceView || p.SpaceManage || p.SpaceOwner || p.DocumentAdd || p.DocumentEdit ||
p.DocumentDelete || p.DocumentMove || p.DocumentCopy || p.DocumentTemplate || p.DocumentApprove
p.DocumentDelete || p.DocumentMove || p.DocumentCopy || p.DocumentTemplate || p.DocumentApprove ||
p.DocumentLifecycle || p.DocumentVersion
}
// EncodeRecord creates standard permission record representing user permissions for a space.
@ -138,7 +151,6 @@ type CategoryViewRequestModel struct {
CategoryID string `json:"categoryID"`
WhoID string `json:"whoId"`
Who WhoType `json:"who"`
// UserID string `json:"userId"`
}
// SpaceRequestModel details which users have what permissions on a given space.

View file

@ -34,4 +34,6 @@ type QueryResult struct {
SpaceID string `json:"spaceId"`
Space string `json:"space"`
SpaceSlug string `json:"spaceSlug"`
Template bool `json:"template"`
VersionID string `json:"versionId"`
}

View file

@ -63,3 +63,17 @@ const (
// ChangePendingNew means a new section to a document is pending review
ChangePendingNew ChangeStatus = 4
)
// Lifecycle tells us if document is in Draft, Live, Archived
type Lifecycle int
const (
// LifecycleDraft means document is in draft mode with restricted viewing
LifecycleDraft Lifecycle = 0
// LifecycleLive means document can be seen by all
LifecycleLive Lifecycle = 1
// LifecycleArchived means document has been archived
LifecycleArchived Lifecycle = 2
)

View file

@ -13,8 +13,10 @@ package server
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
@ -77,9 +79,10 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http
var org = org.Organization{}
var err = errors.New("")
var dom string
if len(rc.OrgID) == 0 {
dom := organization.GetRequestSubdomain(r)
dom = organization.GetRequestSubdomain(r)
dom = m.Store.Organization.CheckDomain(rc, dom)
org, err = m.Store.Organization.GetOrganizationByDomain(dom)
} else {
@ -88,6 +91,12 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http
// Inability to find org record spells the end of this request.
if err != nil {
if err == sql.ErrNoRows {
response.WriteForbiddenError(w)
m.Runtime.Log.Info(fmt.Sprintf("unable to find org (domain: %s, orgID: %s)", dom, rc.OrgID))
return
}
response.WriteForbiddenError(w)
m.Runtime.Log.Error(method, err)
return

View file

@ -88,6 +88,16 @@ func Add(rt *env.Runtime, prefix, path string, methods, queries []string, endPtF
return nil
}
// AddPrivate endpoint
func AddPrivate(rt *env.Runtime, path string, methods, queries []string, endPtFn RouteFunc) error {
return Add(rt, RoutePrefixPrivate, path, methods, queries, endPtFn)
}
// AddPublic endpoint
func AddPublic(rt *env.Runtime, path string, methods, queries []string, endPtFn RouteFunc) error {
return Add(rt, RoutePrefixPublic, path, methods, queries, endPtFn)
}
// Remove an endpoint.
func Remove(rt *env.Runtime, prefix, path string, methods, queries []string) error {
k, e := routesKey(rt, prefix, path, methods, queries)

View file

@ -70,8 +70,8 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
// Non-secure public info routes
//**************************************************
Add(rt, RoutePrefixPublic, "meta", []string{"GET", "OPTIONS"}, nil, meta.Meta)
Add(rt, RoutePrefixPublic, "version", []string{"GET", "OPTIONS"}, nil, func(w http.ResponseWriter, r *http.Request) {
AddPublic(rt, "meta", []string{"GET", "OPTIONS"}, nil, meta.Meta)
AddPublic(rt, "version", []string{"GET", "OPTIONS"}, nil, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(rt.Product.Version))
})
@ -79,129 +79,129 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
// Non-secure public service routes
//**************************************************
Add(rt, RoutePrefixPublic, "authenticate/keycloak", []string{"POST", "OPTIONS"}, nil, keycloak.Authenticate)
Add(rt, RoutePrefixPublic, "authenticate", []string{"POST", "OPTIONS"}, nil, auth.Login)
Add(rt, RoutePrefixPublic, "validate", []string{"GET", "OPTIONS"}, nil, auth.ValidateToken)
Add(rt, RoutePrefixPublic, "forgot", []string{"POST", "OPTIONS"}, nil, user.ForgotPassword)
Add(rt, RoutePrefixPublic, "reset/{token}", []string{"POST", "OPTIONS"}, nil, user.ResetPassword)
Add(rt, RoutePrefixPublic, "share/{spaceID}", []string{"POST", "OPTIONS"}, nil, space.AcceptInvitation)
Add(rt, RoutePrefixPublic, "attachments/{orgID}/{attachmentID}", []string{"GET", "OPTIONS"}, nil, attachment.Download)
AddPublic(rt, "authenticate/keycloak", []string{"POST", "OPTIONS"}, nil, keycloak.Authenticate)
AddPublic(rt, "authenticate", []string{"POST", "OPTIONS"}, nil, auth.Login)
AddPublic(rt, "validate", []string{"GET", "OPTIONS"}, nil, auth.ValidateToken)
AddPublic(rt, "forgot", []string{"POST", "OPTIONS"}, nil, user.ForgotPassword)
AddPublic(rt, "reset/{token}", []string{"POST", "OPTIONS"}, nil, user.ResetPassword)
AddPublic(rt, "share/{spaceID}", []string{"POST", "OPTIONS"}, nil, space.AcceptInvitation)
AddPublic(rt, "attachments/{orgID}/{attachmentID}", []string{"GET", "OPTIONS"}, nil, attachment.Download)
//**************************************************
// Secured private routes (require authentication)
//**************************************************
Add(rt, RoutePrefixPrivate, "import/folder/{folderID}", []string{"POST", "OPTIONS"}, nil, conversion.UploadConvert)
AddPrivate(rt, "import/folder/{folderID}", []string{"POST", "OPTIONS"}, nil, conversion.UploadConvert)
Add(rt, RoutePrefixPrivate, "documents", []string{"GET", "OPTIONS"}, nil, document.BySpace)
Add(rt, RoutePrefixPrivate, "documents/{documentID}", []string{"GET", "OPTIONS"}, nil, document.Get)
Add(rt, RoutePrefixPrivate, "documents/{documentID}", []string{"PUT", "OPTIONS"}, nil, document.Update)
Add(rt, RoutePrefixPrivate, "documents/{documentID}", []string{"DELETE", "OPTIONS"}, nil, document.Delete)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/level", []string{"POST", "OPTIONS"}, nil, page.ChangePageLevel)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/sequence", []string{"POST", "OPTIONS"}, nil, page.ChangePageSequence)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/revisions", []string{"GET", "OPTIONS"}, nil, page.GetRevisions)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/revisions/{revisionID}", []string{"GET", "OPTIONS"}, nil, page.GetDiff)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/revisions/{revisionID}", []string{"POST", "OPTIONS"}, nil, page.Rollback)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/revisions", []string{"GET", "OPTIONS"}, nil, page.GetDocumentRevisions)
AddPrivate(rt, "documents", []string{"GET", "OPTIONS"}, nil, document.BySpace)
AddPrivate(rt, "documents/{documentID}", []string{"GET", "OPTIONS"}, nil, document.Get)
AddPrivate(rt, "documents/{documentID}", []string{"PUT", "OPTIONS"}, nil, document.Update)
AddPrivate(rt, "documents/{documentID}", []string{"DELETE", "OPTIONS"}, nil, document.Delete)
AddPrivate(rt, "documents/{documentID}/pages/level", []string{"POST", "OPTIONS"}, nil, page.ChangePageLevel)
AddPrivate(rt, "documents/{documentID}/pages/sequence", []string{"POST", "OPTIONS"}, nil, page.ChangePageSequence)
AddPrivate(rt, "documents/{documentID}/pages/{pageID}/revisions", []string{"GET", "OPTIONS"}, nil, page.GetRevisions)
AddPrivate(rt, "documents/{documentID}/pages/{pageID}/revisions/{revisionID}", []string{"GET", "OPTIONS"}, nil, page.GetDiff)
AddPrivate(rt, "documents/{documentID}/pages/{pageID}/revisions/{revisionID}", []string{"POST", "OPTIONS"}, nil, page.Rollback)
AddPrivate(rt, "documents/{documentID}/revisions", []string{"GET", "OPTIONS"}, nil, page.GetDocumentRevisions)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages", []string{"GET", "OPTIONS"}, nil, page.GetPages)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}", []string{"PUT", "OPTIONS"}, nil, page.Update)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}", []string{"DELETE", "OPTIONS"}, nil, page.Delete)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages", []string{"DELETE", "OPTIONS"}, nil, page.DeletePages)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}", []string{"GET", "OPTIONS"}, nil, page.GetPage)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages", []string{"POST", "OPTIONS"}, nil, page.Add)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/attachments", []string{"GET", "OPTIONS"}, nil, attachment.Get)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/attachments/{attachmentID}", []string{"DELETE", "OPTIONS"}, nil, attachment.Delete)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/attachments", []string{"POST", "OPTIONS"}, nil, attachment.Add)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/meta", []string{"GET", "OPTIONS"}, nil, page.GetMeta)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/pages/{pageID}/copy/{targetID}", []string{"POST", "OPTIONS"}, nil, page.Copy)
AddPrivate(rt, "documents/{documentID}/pages", []string{"GET", "OPTIONS"}, nil, page.GetPages)
AddPrivate(rt, "documents/{documentID}/pages/{pageID}", []string{"PUT", "OPTIONS"}, nil, page.Update)
AddPrivate(rt, "documents/{documentID}/pages/{pageID}", []string{"DELETE", "OPTIONS"}, nil, page.Delete)
AddPrivate(rt, "documents/{documentID}/pages", []string{"DELETE", "OPTIONS"}, nil, page.DeletePages)
AddPrivate(rt, "documents/{documentID}/pages/{pageID}", []string{"GET", "OPTIONS"}, nil, page.GetPage)
AddPrivate(rt, "documents/{documentID}/pages", []string{"POST", "OPTIONS"}, nil, page.Add)
AddPrivate(rt, "documents/{documentID}/attachments", []string{"GET", "OPTIONS"}, nil, attachment.Get)
AddPrivate(rt, "documents/{documentID}/attachments/{attachmentID}", []string{"DELETE", "OPTIONS"}, nil, attachment.Delete)
AddPrivate(rt, "documents/{documentID}/attachments", []string{"POST", "OPTIONS"}, nil, attachment.Add)
AddPrivate(rt, "documents/{documentID}/pages/{pageID}/meta", []string{"GET", "OPTIONS"}, nil, page.GetMeta)
AddPrivate(rt, "documents/{documentID}/pages/{pageID}/copy/{targetID}", []string{"POST", "OPTIONS"}, nil, page.Copy)
Add(rt, RoutePrefixPrivate, "organizations/{orgID}", []string{"GET", "OPTIONS"}, nil, organization.Get)
Add(rt, RoutePrefixPrivate, "organizations/{orgID}", []string{"PUT", "OPTIONS"}, nil, organization.Update)
AddPrivate(rt, "organizations/{orgID}", []string{"GET", "OPTIONS"}, nil, organization.Get)
AddPrivate(rt, "organizations/{orgID}", []string{"PUT", "OPTIONS"}, nil, organization.Update)
Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"DELETE", "OPTIONS"}, nil, space.Delete)
Add(rt, RoutePrefixPrivate, "space/{spaceID}/move/{moveToId}", []string{"DELETE", "OPTIONS"}, nil, space.Remove)
Add(rt, RoutePrefixPrivate, "space/{spaceID}/invitation", []string{"POST", "OPTIONS"}, nil, space.Invite)
Add(rt, RoutePrefixPrivate, "space/manage", []string{"GET", "OPTIONS"}, nil, space.GetAll)
Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"GET", "OPTIONS"}, nil, space.Get)
Add(rt, RoutePrefixPrivate, "space", []string{"GET", "OPTIONS"}, nil, space.GetViewable)
Add(rt, RoutePrefixPrivate, "space/{spaceID}", []string{"PUT", "OPTIONS"}, nil, space.Update)
Add(rt, RoutePrefixPrivate, "space", []string{"POST", "OPTIONS"}, nil, space.Add)
AddPrivate(rt, "space/{spaceID}", []string{"DELETE", "OPTIONS"}, nil, space.Delete)
AddPrivate(rt, "space/{spaceID}/move/{moveToId}", []string{"DELETE", "OPTIONS"}, nil, space.Remove)
AddPrivate(rt, "space/{spaceID}/invitation", []string{"POST", "OPTIONS"}, nil, space.Invite)
AddPrivate(rt, "space/manage", []string{"GET", "OPTIONS"}, nil, space.GetAll)
AddPrivate(rt, "space/{spaceID}", []string{"GET", "OPTIONS"}, nil, space.Get)
AddPrivate(rt, "space", []string{"GET", "OPTIONS"}, nil, space.GetViewable)
AddPrivate(rt, "space/{spaceID}", []string{"PUT", "OPTIONS"}, nil, space.Update)
AddPrivate(rt, "space", []string{"POST", "OPTIONS"}, nil, space.Add)
Add(rt, RoutePrefixPrivate, "category/space/{spaceID}/summary", []string{"GET", "OPTIONS"}, nil, category.GetSummary)
Add(rt, RoutePrefixPrivate, "category/document/{documentID}", []string{"GET", "OPTIONS"}, nil, category.GetDocumentCategoryMembership)
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/member/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.GetSpaceCategoryMembers)
Add(rt, RoutePrefixPrivate, "category/member", []string{"POST", "OPTIONS"}, nil, category.SetDocumentCategoryMembership)
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, "category", []string{"POST", "OPTIONS"}, nil, category.Add)
AddPrivate(rt, "category/space/{spaceID}/summary", []string{"GET", "OPTIONS"}, nil, category.GetSummary)
AddPrivate(rt, "category/document/{documentID}", []string{"GET", "OPTIONS"}, nil, category.GetDocumentCategoryMembership)
AddPrivate(rt, "category/space/{spaceID}", []string{"GET", "OPTIONS"}, []string{"filter", "all"}, category.GetAll)
AddPrivate(rt, "category/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.Get)
AddPrivate(rt, "category/member/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.GetSpaceCategoryMembers)
AddPrivate(rt, "category/member", []string{"POST", "OPTIONS"}, nil, category.SetDocumentCategoryMembership)
AddPrivate(rt, "category/{categoryID}", []string{"PUT", "OPTIONS"}, nil, category.Update)
AddPrivate(rt, "category/{categoryID}", []string{"DELETE", "OPTIONS"}, nil, category.Delete)
AddPrivate(rt, "category", []string{"POST", "OPTIONS"}, nil, category.Add)
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/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, user.GetSpaceUsers)
Add(rt, RoutePrefixPrivate, "users", []string{"GET", "OPTIONS"}, nil, user.GetOrganizationUsers)
Add(rt, RoutePrefixPrivate, "users/{userID}", []string{"GET", "OPTIONS"}, nil, user.Get)
Add(rt, RoutePrefixPrivate, "users/{userID}", []string{"PUT", "OPTIONS"}, nil, user.Update)
Add(rt, RoutePrefixPrivate, "users/{userID}", []string{"DELETE", "OPTIONS"}, nil, user.Delete)
Add(rt, RoutePrefixPrivate, "users/sync", []string{"GET", "OPTIONS"}, nil, keycloak.Sync)
Add(rt, RoutePrefixPrivate, "users/match", []string{"POST", "OPTIONS"}, nil, user.MatchUsers)
Add(rt, RoutePrefixPrivate, "users/import", []string{"POST", "OPTIONS"}, nil, user.BulkImport)
AddPrivate(rt, "users/{userID}/password", []string{"POST", "OPTIONS"}, nil, user.ChangePassword)
AddPrivate(rt, "users", []string{"POST", "OPTIONS"}, nil, user.Add)
AddPrivate(rt, "users/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, user.GetSpaceUsers)
AddPrivate(rt, "users", []string{"GET", "OPTIONS"}, nil, user.GetOrganizationUsers)
AddPrivate(rt, "users/{userID}", []string{"GET", "OPTIONS"}, nil, user.Get)
AddPrivate(rt, "users/{userID}", []string{"PUT", "OPTIONS"}, nil, user.Update)
AddPrivate(rt, "users/{userID}", []string{"DELETE", "OPTIONS"}, nil, user.Delete)
AddPrivate(rt, "users/sync", []string{"GET", "OPTIONS"}, nil, keycloak.Sync)
AddPrivate(rt, "users/match", []string{"POST", "OPTIONS"}, nil, user.MatchUsers)
AddPrivate(rt, "users/import", []string{"POST", "OPTIONS"}, nil, user.BulkImport)
Add(rt, RoutePrefixPrivate, "search", []string{"POST", "OPTIONS"}, nil, document.SearchDocuments)
AddPrivate(rt, "search", []string{"POST", "OPTIONS"}, nil, document.SearchDocuments)
Add(rt, RoutePrefixPrivate, "templates", []string{"POST", "OPTIONS"}, nil, template.SaveAs)
Add(rt, RoutePrefixPrivate, "templates/{templateID}/folder/{folderID}", []string{"POST", "OPTIONS"}, []string{"type", "saved"}, template.Use)
Add(rt, RoutePrefixPrivate, "templates/{folderID}", []string{"GET", "OPTIONS"}, nil, template.SavedList)
AddPrivate(rt, "templates", []string{"POST", "OPTIONS"}, nil, template.SaveAs)
AddPrivate(rt, "templates/{templateID}/folder/{folderID}", []string{"POST", "OPTIONS"}, []string{"type", "saved"}, template.Use)
AddPrivate(rt, "templates/{folderID}", []string{"GET", "OPTIONS"}, nil, template.SavedList)
Add(rt, RoutePrefixPrivate, "sections", []string{"GET", "OPTIONS"}, nil, section.GetSections)
Add(rt, RoutePrefixPrivate, "sections", []string{"POST", "OPTIONS"}, nil, section.RunSectionCommand)
Add(rt, RoutePrefixPrivate, "sections/refresh", []string{"GET", "OPTIONS"}, nil, section.RefreshSections)
Add(rt, RoutePrefixPrivate, "sections/blocks/space/{folderID}", []string{"GET", "OPTIONS"}, nil, block.GetBySpace)
Add(rt, RoutePrefixPrivate, "sections/blocks/{blockID}", []string{"GET", "OPTIONS"}, nil, block.Get)
Add(rt, RoutePrefixPrivate, "sections/blocks/{blockID}", []string{"PUT", "OPTIONS"}, nil, block.Update)
Add(rt, RoutePrefixPrivate, "sections/blocks/{blockID}", []string{"DELETE", "OPTIONS"}, nil, block.Delete)
Add(rt, RoutePrefixPrivate, "sections/blocks", []string{"POST", "OPTIONS"}, nil, block.Add)
AddPrivate(rt, "sections", []string{"GET", "OPTIONS"}, nil, section.GetSections)
AddPrivate(rt, "sections", []string{"POST", "OPTIONS"}, nil, section.RunSectionCommand)
AddPrivate(rt, "sections/refresh", []string{"GET", "OPTIONS"}, nil, section.RefreshSections)
AddPrivate(rt, "sections/blocks/space/{folderID}", []string{"GET", "OPTIONS"}, nil, block.GetBySpace)
AddPrivate(rt, "sections/blocks/{blockID}", []string{"GET", "OPTIONS"}, nil, block.Get)
AddPrivate(rt, "sections/blocks/{blockID}", []string{"PUT", "OPTIONS"}, nil, block.Update)
AddPrivate(rt, "sections/blocks/{blockID}", []string{"DELETE", "OPTIONS"}, nil, block.Delete)
AddPrivate(rt, "sections/blocks", []string{"POST", "OPTIONS"}, nil, block.Add)
Add(rt, RoutePrefixPrivate, "links/{folderID}/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, link.GetLinkCandidates)
Add(rt, RoutePrefixPrivate, "links", []string{"GET", "OPTIONS"}, nil, link.SearchLinkCandidates)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/links", []string{"GET", "OPTIONS"}, nil, document.DocumentLinks)
AddPrivate(rt, "links/{folderID}/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, link.GetLinkCandidates)
AddPrivate(rt, "links", []string{"GET", "OPTIONS"}, nil, link.SearchLinkCandidates)
AddPrivate(rt, "documents/{documentID}/links", []string{"GET", "OPTIONS"}, nil, document.DocumentLinks)
Add(rt, RoutePrefixPrivate, "global/smtp", []string{"GET", "OPTIONS"}, nil, setting.SMTP)
Add(rt, RoutePrefixPrivate, "global/smtp", []string{"PUT", "OPTIONS"}, nil, setting.SetSMTP)
Add(rt, RoutePrefixPrivate, "global/license", []string{"GET", "OPTIONS"}, nil, setting.License)
Add(rt, RoutePrefixPrivate, "global/license", []string{"PUT", "OPTIONS"}, nil, setting.SetLicense)
Add(rt, RoutePrefixPrivate, "global/auth", []string{"GET", "OPTIONS"}, nil, setting.AuthConfig)
Add(rt, RoutePrefixPrivate, "global/auth", []string{"PUT", "OPTIONS"}, nil, setting.SetAuthConfig)
AddPrivate(rt, "global/smtp", []string{"GET", "OPTIONS"}, nil, setting.SMTP)
AddPrivate(rt, "global/smtp", []string{"PUT", "OPTIONS"}, nil, setting.SetSMTP)
AddPrivate(rt, "global/license", []string{"GET", "OPTIONS"}, nil, setting.License)
AddPrivate(rt, "global/license", []string{"PUT", "OPTIONS"}, nil, setting.SetLicense)
AddPrivate(rt, "global/auth", []string{"GET", "OPTIONS"}, nil, setting.AuthConfig)
AddPrivate(rt, "global/auth", []string{"PUT", "OPTIONS"}, nil, setting.SetAuthConfig)
Add(rt, RoutePrefixPrivate, "pin/{userID}", []string{"POST", "OPTIONS"}, nil, pin.Add)
Add(rt, RoutePrefixPrivate, "pin/{userID}", []string{"GET", "OPTIONS"}, nil, pin.GetUserPins)
Add(rt, RoutePrefixPrivate, "pin/{userID}/sequence", []string{"POST", "OPTIONS"}, nil, pin.UpdatePinSequence)
Add(rt, RoutePrefixPrivate, "pin/{userID}/{pinID}", []string{"DELETE", "OPTIONS"}, nil, pin.DeleteUserPin)
AddPrivate(rt, "pin/{userID}", []string{"POST", "OPTIONS"}, nil, pin.Add)
AddPrivate(rt, "pin/{userID}", []string{"GET", "OPTIONS"}, nil, pin.GetUserPins)
AddPrivate(rt, "pin/{userID}/sequence", []string{"POST", "OPTIONS"}, nil, pin.UpdatePinSequence)
AddPrivate(rt, "pin/{userID}/{pinID}", []string{"DELETE", "OPTIONS"}, nil, pin.DeleteUserPin)
Add(rt, RoutePrefixPrivate, "group/{groupID}/members", []string{"GET", "OPTIONS"}, nil, group.GetGroupMembers)
Add(rt, RoutePrefixPrivate, "group", []string{"POST", "OPTIONS"}, nil, group.Add)
Add(rt, RoutePrefixPrivate, "group", []string{"GET", "OPTIONS"}, nil, group.Groups)
Add(rt, RoutePrefixPrivate, "group/{groupID}", []string{"PUT", "OPTIONS"}, nil, group.Update)
Add(rt, RoutePrefixPrivate, "group/{groupID}", []string{"DELETE", "OPTIONS"}, nil, group.Delete)
Add(rt, RoutePrefixPrivate, "group/{groupID}/join/{userID}", []string{"POST", "OPTIONS"}, nil, group.JoinGroup)
Add(rt, RoutePrefixPrivate, "group/{groupID}/leave/{userID}", []string{"DELETE", "OPTIONS"}, nil, group.LeaveGroup)
AddPrivate(rt, "group/{groupID}/members", []string{"GET", "OPTIONS"}, nil, group.GetGroupMembers)
AddPrivate(rt, "group", []string{"POST", "OPTIONS"}, nil, group.Add)
AddPrivate(rt, "group", []string{"GET", "OPTIONS"}, nil, group.Groups)
AddPrivate(rt, "group/{groupID}", []string{"PUT", "OPTIONS"}, nil, group.Update)
AddPrivate(rt, "group/{groupID}", []string{"DELETE", "OPTIONS"}, nil, group.Delete)
AddPrivate(rt, "group/{groupID}/join/{userID}", []string{"POST", "OPTIONS"}, nil, group.JoinGroup)
AddPrivate(rt, "group/{groupID}/leave/{userID}", []string{"DELETE", "OPTIONS"}, nil, group.LeaveGroup)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/permissions", []string{"GET", "OPTIONS"}, nil, permission.GetDocumentPermissions)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/permissions", []string{"PUT", "OPTIONS"}, nil, permission.SetDocumentPermissions)
Add(rt, RoutePrefixPrivate, "documents/{documentID}/permissions/user", []string{"GET", "OPTIONS"}, nil, permission.GetUserDocumentPermissions)
Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions", []string{"PUT", "OPTIONS"}, nil, permission.SetSpacePermissions)
Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions/user", []string{"GET", "OPTIONS"}, nil, permission.GetUserSpacePermissions)
Add(rt, RoutePrefixPrivate, "space/{spaceID}/permissions", []string{"GET", "OPTIONS"}, nil, permission.GetSpacePermissions)
Add(rt, RoutePrefixPrivate, "category/{categoryID}/permission", []string{"PUT", "OPTIONS"}, nil, permission.SetCategoryPermissions)
Add(rt, RoutePrefixPrivate, "category/{categoryID}/permission", []string{"GET", "OPTIONS"}, nil, permission.GetCategoryPermissions)
Add(rt, RoutePrefixPrivate, "category/{categoryID}/user", []string{"GET", "OPTIONS"}, nil, permission.GetCategoryViewers)
AddPrivate(rt, "documents/{documentID}/permissions", []string{"GET", "OPTIONS"}, nil, permission.GetDocumentPermissions)
AddPrivate(rt, "documents/{documentID}/permissions", []string{"PUT", "OPTIONS"}, nil, permission.SetDocumentPermissions)
AddPrivate(rt, "documents/{documentID}/permissions/user", []string{"GET", "OPTIONS"}, nil, permission.GetUserDocumentPermissions)
AddPrivate(rt, "space/{spaceID}/permissions", []string{"PUT", "OPTIONS"}, nil, permission.SetSpacePermissions)
AddPrivate(rt, "space/{spaceID}/permissions/user", []string{"GET", "OPTIONS"}, nil, permission.GetUserSpacePermissions)
AddPrivate(rt, "space/{spaceID}/permissions", []string{"GET", "OPTIONS"}, nil, permission.GetSpacePermissions)
AddPrivate(rt, "category/{categoryID}/permission", []string{"PUT", "OPTIONS"}, nil, permission.SetCategoryPermissions)
AddPrivate(rt, "category/{categoryID}/permission", []string{"GET", "OPTIONS"}, nil, permission.GetCategoryPermissions)
AddPrivate(rt, "category/{categoryID}/user", []string{"GET", "OPTIONS"}, nil, permission.GetCategoryViewers)
// fetch methods exist to speed up UI rendering by returning data in bulk
Add(rt, RoutePrefixPrivate, "fetch/category/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.FetchSpaceData)
Add(rt, RoutePrefixPrivate, "fetch/document/{documentID}", []string{"GET", "OPTIONS"}, nil, document.FetchDocumentData)
Add(rt, RoutePrefixPrivate, "fetch/page/{documentID}", []string{"GET", "OPTIONS"}, nil, page.FetchPages)
AddPrivate(rt, "fetch/category/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.FetchSpaceData)
AddPrivate(rt, "fetch/document/{documentID}", []string{"GET", "OPTIONS"}, nil, document.FetchDocumentData)
AddPrivate(rt, "fetch/page/{documentID}", []string{"GET", "OPTIONS"}, nil, page.FetchPages)
Add(rt, RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, meta.RobotsTxt)
Add(rt, RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, meta.Sitemap)