1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-22 06:39:43 +02:00

improve level code

This commit is contained in:
Harvey Kandola 2018-01-10 16:07:17 +00:00
parent 049b83e0b9
commit 5f59e95495
25 changed files with 1104 additions and 461 deletions

View file

@ -4,7 +4,18 @@
ALTER TABLE document ADD COLUMN `protection` INT NOT NULL DEFAULT 0 AFTER `template`; ALTER TABLE document ADD COLUMN `protection` INT NOT NULL DEFAULT 0 AFTER `template`;
ALTER TABLE document ADD COLUMN `approval` INT NOT NULL DEFAULT 0 AFTER `protection`; ALTER TABLE document ADD COLUMN `approval` INT NOT NULL DEFAULT 0 AFTER `protection`;
-- page workflow status
ALTER TABLE page ADD COLUMN `status` INT NOT NULL DEFAULT 0 AFTER `revisions`;
-- links pending changes to another page
ALTER TABLE page ADD COLUMN `relativeid` CHAR(16) DEFAULT '' NOT NULL COLLATE utf8_bin AFTER `approval`;
-- useraction captures what is being actioned
ALTER TABLE useraction ADD COLUMN `reftype` CHAR(1) DEFAULT 'D' NOT NULL COLLATE utf8_bin AFTER `iscomplete`;
ALTER TABLE useraction ADD COLUMN `reftypeid` CHAR(16) NOT NULL COLLATE utf8_bin AFTER `reftype`;
-- data migration clean up from previous releases -- data migration clean up from previous releases
DROP TABLE IF EXISTS `audit`; DROP TABLE IF EXISTS `audit`;
DROP TABLE IF EXISTS `search_old`; DROP TABLE IF EXISTS `search_old`;
ALTER TABLE document DROP COLUMN `layout`; ALTER TABLE document DROP COLUMN `layout`;
DELETE FROM categorymember WHERE documentid NOT IN (SELECT refid FROM document);
UPDATE page SET level=1 WHERE level=0;

1
core/env/logger.go vendored
View file

@ -15,6 +15,7 @@ package env
// Logger provides the interface for Documize compatible loggers. // Logger provides the interface for Documize compatible loggers.
type Logger interface { type Logger interface {
Info(message string) Info(message string)
Trace(message string)
Error(message string, err error) Error(message string, err error)
// SetDB(l Logger, db *sqlx.DB) Logger // SetDB(l Logger, db *sqlx.DB) Logger
} }

View file

@ -16,6 +16,7 @@ import (
"encoding/json" "encoding/json"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"sort"
"github.com/documize/community/core/env" "github.com/documize/community/core/env"
"github.com/documize/community/core/request" "github.com/documize/community/core/request"
@ -155,7 +156,7 @@ func (h *Handler) BySpace(w http.ResponseWriter, r *http.Request) {
// get complete list of documents // get complete list of documents
documents, err := h.Store.Document.GetBySpace(ctx, spaceID) documents, err := h.Store.Document.GetBySpace(ctx, spaceID)
if err != nil && err != sql.ErrNoRows { if err != nil {
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err) h.Runtime.Log.Error(method, err)
return return
@ -164,6 +165,8 @@ func (h *Handler) BySpace(w http.ResponseWriter, r *http.Request) {
documents = []doc.Document{} documents = []doc.Document{}
} }
sort.Sort(doc.ByTitle(documents))
// remove documents that cannot be seen due to lack of // remove documents that cannot be seen due to lack of
// category view/access permission // category view/access permission
filtered := []doc.Document{} filtered := []doc.Document{}
@ -411,7 +414,6 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
if len(perms) == 0 { if len(perms) == 0 {
perms = []pm.Permission{} perms = []pm.Permission{}
} }
record := pm.DecodeUserPermissions(perms) record := pm.DecodeUserPermissions(perms)
roles, err := h.Store.Permission.GetUserDocumentPermissions(ctx, document.RefID) roles, err := h.Store.Permission.GetUserDocumentPermissions(ctx, document.RefID)
@ -422,7 +424,6 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
if len(roles) == 0 { if len(roles) == 0 {
roles = []pm.Permission{} roles = []pm.Permission{}
} }
rolesRecord := pm.DecodeUserDocumentPermissions(roles) rolesRecord := pm.DecodeUserDocumentPermissions(roles)
// links // links

View file

@ -90,9 +90,9 @@ func (s Scope) DocumentMeta(ctx domain.RequestContext, id string) (meta doc.Docu
return return
} }
// GetAll returns a slice containg all of the the documents for the client's organisation, with the most recient first. // 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) { 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 revised DESC", ctx.OrgID) 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 { if err != nil {
err = errors.Wrap(err, "select documents") err = errors.Wrap(err, "select documents")
@ -118,6 +118,9 @@ func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (documents
) )
ORDER BY title`, ctx.OrgID, ctx.OrgID, ctx.OrgID, spaceID, ctx.OrgID, ctx.UserID, ctx.OrgID, spaceID, ctx.UserID) ORDER BY title`, ctx.OrgID, ctx.OrgID, ctx.OrgID, spaceID, ctx.OrgID, ctx.UserID, ctx.OrgID, spaceID, ctx.UserID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil { if err != nil {
err = errors.Wrap(err, "select documents by space") err = errors.Wrap(err, "select documents by space")
} }
@ -277,6 +280,11 @@ func (s Scope) Delete(ctx domain.RequestContext, documentID string) (rows int64,
return return
} }
_, err = b.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE from categorymember WHERE documentid=\"%s\" AND orgid=\"%s\"", documentID, ctx.OrgID))
if err != nil {
return
}
return b.DeleteConstrained(ctx.Transaction, "document", ctx.OrgID, documentID) return b.DeleteConstrained(ctx.Transaction, "document", ctx.OrgID, documentID)
} }

View file

@ -31,8 +31,9 @@ import (
"github.com/documize/community/domain/section/provider" "github.com/documize/community/domain/section/provider"
"github.com/documize/community/model/activity" "github.com/documize/community/model/activity"
"github.com/documize/community/model/audit" "github.com/documize/community/model/audit"
"github.com/documize/community/model/doc"
"github.com/documize/community/model/page" "github.com/documize/community/model/page"
pm "github.com/documize/community/model/permission"
"github.com/documize/community/model/workflow"
htmldiff "github.com/documize/html-diff" htmldiff "github.com/documize/html-diff"
) )
@ -53,17 +54,14 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
return return
} }
// check param
documentID := request.Param(r, "documentID") documentID := request.Param(r, "documentID")
if len(documentID) == 0 { if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID") response.WriteMissingDataError(w, method, "documentID")
return return
} }
if !permission.CanChangeDocument(ctx, *h.Store, documentID) { // read payload
response.WriteForbiddenError(w)
return
}
defer streamutil.Close(r.Body) defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body) body, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
@ -90,6 +88,38 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
return return
} }
// Check protection and approval process
document, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
// Protect locked
if document.Protection == workflow.ProtectionLock {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted write to locked document")
return
}
// Check edit permission
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
// if document review process then we must mark page as pending
if document.Protection == workflow.ProtectionReview {
if model.Page.RelativeID == "" {
model.Page.Status = workflow.ChangePendingNew
} else {
model.Page.Status = workflow.ChangePending
}
} else {
model.Page.Status = workflow.ChangePublished
}
pageID := uniqueid.Generate() pageID := uniqueid.Generate()
model.Page.RefID = pageID model.Page.RefID = pageID
model.Meta.PageID = pageID model.Meta.PageID = pageID
@ -97,7 +127,6 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
model.Meta.UserID = ctx.UserID // required for Render call below model.Meta.UserID = ctx.UserID // required for Render call below
model.Page.SetDefaults() model.Page.SetDefaults()
model.Meta.SetDefaults() model.Meta.SetDefaults()
// page.Title = template.HTMLEscapeString(page.Title)
doc, err := h.Store.Document.Get(ctx, documentID) doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil { if err != nil {
@ -229,16 +258,11 @@ func (h *Handler) GetPages(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, pages) response.WriteJSON(w, pages)
} }
// Delete deletes a page. // GetMeta gets page meta data for specified document page.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { func (h *Handler) GetMeta(w http.ResponseWriter, r *http.Request) {
method := "page.delete" method := "page.meta"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID") documentID := request.Param(r, "documentID")
if len(documentID) == 0 { if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID") response.WriteMissingDataError(w, method, "documentID")
@ -251,156 +275,31 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
return return
} }
if !permission.CanChangeDocument(ctx, *h.Store, documentID) { if !permission.CanViewDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w) response.WriteForbiddenError(w)
return return
} }
doc, err := h.Store.Document.Get(ctx, documentID) meta, err := h.Store.Page.GetPageMeta(ctx, pageID)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, pageID)
h.Runtime.Log.Info(method + " no record")
meta = page.Meta{}
response.WriteJSON(w, meta)
return
}
if err != nil { if err != nil {
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err) h.Runtime.Log.Error(method, err)
return return
} }
if meta.DocumentID != documentID {
ctx.Transaction, err = h.Runtime.Db.Beginx() response.WriteBadRequestError(w, method, "documentID mismatch")
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err) h.Runtime.Log.Error(method, err)
return return
} }
page, err := h.Store.Page.Get(ctx, pageID) response.WriteJSON(w, meta)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(page.BlockID) > 0 {
h.Store.Block.DecrementUsage(ctx, page.BlockID)
}
_, err = h.Store.Page.Delete(ctx, documentID, pageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
SourceID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDeleted})
h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)
go h.Indexer.DeleteContent(ctx, pageID)
h.Store.Link.DeleteSourcePageLinks(ctx, pageID)
h.Store.Link.MarkOrphanPageLink(ctx, pageID)
h.Store.Page.DeletePageRevisions(ctx, pageID)
ctx.Transaction.Commit()
response.WriteEmpty(w)
}
// DeletePages batch deletes pages.
func (h *Handler) DeletePages(w http.ResponseWriter, r *http.Request) {
method := "page.delete.pages"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, "Bad body")
return
}
model := new([]page.LevelRequest)
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteBadRequestError(w, method, "JSON marshal")
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
for _, page := range *model {
pageData, err := h.Store.Page.Get(ctx, page.PageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(pageData.BlockID) > 0 {
h.Store.Block.DecrementUsage(ctx, pageData.BlockID)
}
_, err = h.Store.Page.Delete(ctx, documentID, page.PageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
go h.Indexer.DeleteContent(ctx, page.PageID)
h.Store.Link.DeleteSourcePageLinks(ctx, page.PageID)
h.Store.Link.MarkOrphanPageLink(ctx, page.PageID)
h.Store.Page.DeletePageRevisions(ctx, page.PageID)
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
SourceID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDeleted})
h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)
ctx.Transaction.Commit()
response.WriteEmpty(w)
} }
// Update will persist changed page and note the fact // Update will persist changed page and note the fact
@ -415,11 +314,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
return return
} }
// if !ctx.Editor { // Check params
// response.WriteForbiddenError(w)
// return
// }
documentID := request.Param(r, "documentID") documentID := request.Param(r, "documentID")
if len(documentID) == 0 { if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID") response.WriteMissingDataError(w, method, "documentID")
@ -432,11 +327,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
return return
} }
if !permission.CanChangeDocument(ctx, *h.Store, documentID) { // Read payload
response.WriteForbiddenError(w)
return
}
defer streamutil.Close(r.Body) defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body) body, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
@ -458,13 +349,26 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
return return
} }
// Check protection and approval process
doc, err := h.Store.Document.Get(ctx, documentID) doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil { if err != nil {
response.WriteServerError(w, method, err) response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err) h.Runtime.Log.Error(method, err)
return return
} }
if doc.Protection == workflow.ProtectionLock {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted write to locked document")
return
}
// Check edit permission
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx() ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil { if err != nil {
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
@ -492,6 +396,11 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
skipRevision := false skipRevision := false
skipRevision, err = strconv.ParseBool(request.Query(r, "r")) skipRevision, err = strconv.ParseBool(request.Query(r, "r"))
// We don't track revisions for non-published pages
if model.Page.Status != workflow.ChangePublished {
skipRevision = true
}
err = h.Store.Page.Update(ctx, model.Page, refID, ctx.UserID, skipRevision) err = h.Store.Page.Update(ctx, model.Page, refID, ctx.UserID, skipRevision)
if err != nil { if err != nil {
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
@ -562,6 +471,258 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, updatedPage) response.WriteJSON(w, updatedPage)
} }
// Delete deletes a page.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
method := "page.delete"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
pageID := request.Param(r, "pageID")
if len(pageID) == 0 {
response.WriteMissingDataError(w, method, "pageID")
return
}
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// Protect locked
if doc.Protection == workflow.ProtectionLock {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted delete section on locked document")
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
p, err := h.Store.Page.Get(ctx, pageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(p.BlockID) > 0 {
h.Store.Block.DecrementUsage(ctx, p.BlockID)
}
_, err = h.Store.Page.Delete(ctx, documentID, pageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
SourceID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDeleted})
h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)
go h.Indexer.DeleteContent(ctx, pageID)
h.Store.Link.DeleteSourcePageLinks(ctx, pageID)
h.Store.Link.MarkOrphanPageLink(ctx, pageID)
h.Store.Page.DeletePageRevisions(ctx, pageID)
ctx.Transaction.Commit()
// Re-level all pages in document
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
p2, err := h.Store.Page.GetPages(ctx, documentID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
page.Levelize(p2)
for _, i := range p2 {
err = h.Store.Page.UpdateLevel(ctx, documentID, i.RefID, int(i.Level))
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
}
ctx.Transaction.Commit()
response.WriteEmpty(w)
}
// DeletePages batch deletes pages.
func (h *Handler) DeletePages(w http.ResponseWriter, r *http.Request) {
method := "page.delete.pages"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, "Bad body")
return
}
model := new([]page.LevelRequest)
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteBadRequestError(w, method, "JSON marshal")
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// Protect locked
if doc.Protection == workflow.ProtectionLock {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted delete sections on locked document")
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
for _, page := range *model {
pageData, err := h.Store.Page.Get(ctx, page.PageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(pageData.BlockID) > 0 {
h.Store.Block.DecrementUsage(ctx, pageData.BlockID)
}
_, err = h.Store.Page.Delete(ctx, documentID, page.PageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
go h.Indexer.DeleteContent(ctx, page.PageID)
h.Store.Link.DeleteSourcePageLinks(ctx, page.PageID)
h.Store.Link.MarkOrphanPageLink(ctx, page.PageID)
h.Store.Page.DeletePageRevisions(ctx, page.PageID)
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
SourceID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDeleted})
h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)
ctx.Transaction.Commit()
// Re-level all pages in document
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
p2, err := h.Store.Page.GetPages(ctx, documentID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
page.Levelize(p2)
for _, i := range p2 {
err = h.Store.Page.UpdateLevel(ctx, documentID, i.RefID, int(i.Level))
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
}
ctx.Transaction.Commit()
response.WriteEmpty(w)
}
//**************************************************
// Table of Contents
//**************************************************
// ChangePageSequence will swap page sequence for a given number of pages. // ChangePageSequence will swap page sequence for a given number of pages.
func (h *Handler) ChangePageSequence(w http.ResponseWriter, r *http.Request) { func (h *Handler) ChangePageSequence(w http.ResponseWriter, r *http.Request) {
method := "page.sequence" method := "page.sequence"
@ -684,75 +845,10 @@ func (h *Handler) ChangePageLevel(w http.ResponseWriter, r *http.Request) {
response.WriteEmpty(w) response.WriteEmpty(w)
} }
// GetMeta gets page meta data for specified document page.
func (h *Handler) GetMeta(w http.ResponseWriter, r *http.Request) {
method := "page.meta"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
pageID := request.Param(r, "pageID")
if len(pageID) == 0 {
response.WriteMissingDataError(w, method, "pageID")
return
}
if !permission.CanViewDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
meta, err := h.Store.Page.GetPageMeta(ctx, pageID)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, pageID)
h.Runtime.Log.Info(method + " no record")
meta = page.Meta{}
response.WriteJSON(w, meta)
return
}
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if meta.DocumentID != documentID {
response.WriteBadRequestError(w, method, "documentID mismatch")
h.Runtime.Log.Error(method, err)
return
}
response.WriteJSON(w, meta)
}
//************************************************** //**************************************************
// Copy Move Page // Copy Move Page
//************************************************** //**************************************************
// GetMoveCopyTargets returns available documents for page copy/move axction.
func (h *Handler) GetMoveCopyTargets(w http.ResponseWriter, r *http.Request) {
method := "page.targets"
ctx := domain.GetRequestContext(r)
var d []doc.Document
var err error
d, err = h.Store.Document.DocumentList(ctx)
if len(d) == 0 {
d = []doc.Document{}
}
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
response.WriteJSON(w, d)
}
// Copy copies page to either same or different document. // Copy copies page to either same or different document.
func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) { func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
method := "page.targets" method := "page.targets"
@ -790,6 +886,12 @@ func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
return return
} }
// workflow check
if doc.Protection == workflow.ProtectionLock || doc.Protection == workflow.ProtectionReview {
response.WriteForbiddenError(w)
return
}
p, err := h.Store.Page.Get(ctx, pageID) p, err := h.Store.Page.Get(ctx, pageID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, documentID) response.WriteNotFoundError(w, method, documentID)
@ -1101,3 +1203,176 @@ func (h *Handler) Rollback(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, p) response.WriteJSON(w, p)
} }
//**************************************************
// Bulk data fetching (reduce network traffic)
//**************************************************
// FetchPages returns all page data for given document: page, meta data, pending changes.
func (h *Handler) FetchPages(w http.ResponseWriter, r *http.Request) {
method := "page.FetchPages"
ctx := domain.GetRequestContext(r)
model := []page.BulkRequest{}
// check params
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// published pages and new pages awaiting approval
pages, err := h.Store.Page.GetPages(ctx, documentID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(pages) == 0 {
pages = []page.Page{}
}
// unpublished pages
unpublished, err := h.Store.Page.GetUnpublishedPages(ctx, documentID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(unpublished) == 0 {
unpublished = []page.Page{}
}
// meta for all pages
meta, err := h.Store.Page.GetDocumentPageMeta(ctx, documentID, false)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(meta) == 0 {
meta = []page.Meta{}
}
// permissions
perms, err := h.Store.Permission.GetUserSpacePermissions(ctx, doc.LabelID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(perms) == 0 {
perms = []pm.Permission{}
}
permissions := pm.DecodeUserPermissions(perms)
roles, err := h.Store.Permission.GetUserDocumentPermissions(ctx, doc.RefID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(roles) == 0 {
roles = []pm.Permission{}
}
docRoles := pm.DecodeUserDocumentPermissions(roles)
// check document view permissions
if !permissions.SpaceView && !permissions.SpaceManage && !permissions.SpaceOwner {
response.WriteForbiddenError(w)
return
}
// process published pages
for _, p := range pages {
// only send back pages that user can see
process := false
forcePending := false
if process == false && p.Status == workflow.ChangePublished {
process = true
}
if process == false && p.Status == workflow.ChangePendingNew && p.RelativeID == "" && p.UserID == ctx.UserID {
process = true
forcePending = true // user has newly created page which should be treated as pending
}
if process == false && p.Status == workflow.ChangeUnderReview && p.RelativeID == "" && p.UserID == ctx.UserID {
process = true
forcePending = true // user has newly created page which should be treated as pending
}
if process == false && p.Status == workflow.ChangeUnderReview && p.RelativeID == "" && (permissions.DocumentApprove || docRoles.DocumentRoleApprove) {
process = true
forcePending = true // user has newly created page which should be treated as pending
}
if process {
d := page.BulkRequest{}
d.Page = p
for _, m := range meta {
if p.RefID == m.PageID {
d.Meta = m
break
}
}
d.Pending = []page.PendingPage{}
// process pending pages
for _, up := range unpublished {
if up.RelativeID == p.RefID {
ud := page.PendingPage{}
ud.Page = up
for _, m := range meta {
if up.RefID == m.PageID {
ud.Meta = m
break
}
}
owner, err := h.Store.User.Get(ctx, up.UserID)
if err == nil {
ud.Owner = owner.Fullname()
}
d.Pending = append(d.Pending, ud)
}
}
// Handle situation where we need approval, and user has created new page
if forcePending && len(d.Pending) == 0 && doc.Protection == workflow.ProtectionReview {
ud := page.PendingPage{}
ud.Page = d.Page
ud.Meta = d.Meta
owner, err := h.Store.User.Get(ctx, d.Page.UserID)
if err == nil {
ud.Owner = owner.Fullname()
}
d.Pending = append(d.Pending, ud)
}
model = append(model, d)
}
}
// Attach numbers to pages, 1.1, 2.1.1 etc.
t := []page.Page{}
for _, i := range model {
t = append(t, i.Page)
}
page.Numberize(t)
for i, j := range t {
model[i].Page = j
}
// deliver payload
response.WriteJSON(w, model)
}

View file

@ -28,6 +28,10 @@ type Scope struct {
Runtime *env.Runtime Runtime *env.Runtime
} }
//**************************************************
// Page Revisions
//**************************************************
// Add inserts the given page into the page table, adds that page to the queue of pages to index and audits that the page has been added. // Add inserts the given page into the page table, adds that page to the queue of pages to index and audits that the page has been added.
func (s Scope) Add(ctx domain.RequestContext, model page.NewPage) (err error) { func (s Scope) Add(ctx domain.RequestContext, model page.NewPage) (err error) {
model.Page.OrgID = ctx.OrgID model.Page.OrgID = ctx.OrgID
@ -54,8 +58,8 @@ func (s Scope) Add(ctx domain.RequestContext, model page.NewPage) (err error) {
model.Page.Sequence = maxSeq * 2 model.Page.Sequence = maxSeq * 2
} }
_, err = ctx.Transaction.Exec("INSERT INTO page (refid, orgid, documentid, userid, contenttype, pagetype, level, title, body, revisions, sequence, blockid, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", _, err = ctx.Transaction.Exec("INSERT INTO page (refid, orgid, documentid, userid, contenttype, pagetype, level, title, body, revisions, sequence, blockid, status, relativeid, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
model.Page.RefID, model.Page.OrgID, model.Page.DocumentID, model.Page.UserID, model.Page.ContentType, model.Page.PageType, model.Page.Level, model.Page.Title, model.Page.Body, model.Page.Revisions, model.Page.Sequence, model.Page.BlockID, model.Page.Created, model.Page.Revised) model.Page.RefID, model.Page.OrgID, model.Page.DocumentID, model.Page.UserID, model.Page.ContentType, model.Page.PageType, model.Page.Level, model.Page.Title, model.Page.Body, model.Page.Revisions, model.Page.Sequence, model.Page.BlockID, model.Page.Status, model.Page.RelativeID, model.Page.Created, model.Page.Revised)
_, err = ctx.Transaction.Exec("INSERT INTO pagemeta (pageid, orgid, userid, documentid, rawbody, config, externalsource, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", _, err = ctx.Transaction.Exec("INSERT INTO pagemeta (pageid, orgid, userid, documentid, rawbody, config, externalsource, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
model.Meta.PageID, model.Meta.OrgID, model.Meta.UserID, model.Meta.DocumentID, model.Meta.RawBody, model.Meta.Config, model.Meta.ExternalSource, model.Meta.Created, model.Meta.Revised) model.Meta.PageID, model.Meta.OrgID, model.Meta.UserID, model.Meta.DocumentID, model.Meta.RawBody, model.Meta.Config, model.Meta.ExternalSource, model.Meta.Created, model.Meta.Revised)
@ -69,7 +73,7 @@ func (s Scope) Add(ctx domain.RequestContext, model page.NewPage) (err error) {
// Get returns the pageID page record from the page table. // Get returns the pageID page record from the page table.
func (s Scope) Get(ctx domain.RequestContext, pageID string) (p page.Page, err error) { func (s Scope) Get(ctx domain.RequestContext, pageID string) (p page.Page, err error) {
err = s.Runtime.Db.Get(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.refid=?", err = s.Runtime.Db.Get(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.status, a.relativeid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.refid=?",
ctx.OrgID, pageID) ctx.OrgID, pageID)
if err != nil { if err != nil {
@ -79,9 +83,9 @@ func (s Scope) Get(ctx domain.RequestContext, pageID string) (p page.Page, err e
return return
} }
// GetPages returns a slice containing all the page records for a given documentID, in presentation sequence. // GetPages returns a slice containing all published page records for a given documentID, in presentation sequence.
func (s Scope) GetPages(ctx domain.RequestContext, documentID string) (p []page.Page, err error) { func (s Scope) GetPages(ctx domain.RequestContext, documentID string) (p []page.Page, err error) {
err = s.Runtime.Db.Select(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? ORDER BY a.sequence", ctx.OrgID, documentID) err = s.Runtime.Db.Select(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.status, a.relativeid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? AND (a.status=0 OR ((a.status=4 OR a.status=2) AND a.relativeid='')) ORDER BY a.sequence", ctx.OrgID, documentID)
if err != nil { if err != nil {
err = errors.Wrap(err, "execute get pages") err = errors.Wrap(err, "execute get pages")
@ -90,10 +94,21 @@ func (s Scope) GetPages(ctx domain.RequestContext, documentID string) (p []page.
return return
} }
// GetUnpublishedPages returns a slice containing all published page records for a given documentID, in presentation sequence.
func (s Scope) GetUnpublishedPages(ctx domain.RequestContext, documentID string) (p []page.Page, err error) {
err = s.Runtime.Db.Select(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.status, a.relativeid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? AND a.status!=0 AND a.relativeid!='' ORDER BY a.sequence", ctx.OrgID, documentID)
if err != nil {
err = errors.Wrap(err, "execute get unpublished pages")
}
return
}
// GetPagesWithoutContent returns a slice containing all the page records for a given documentID, in presentation sequence, // GetPagesWithoutContent returns a slice containing all the page records for a given documentID, in presentation sequence,
// but without the body field (which holds the HTML content). // but without the body field (which holds the HTML content).
func (s Scope) GetPagesWithoutContent(ctx domain.RequestContext, documentID string) (pages []page.Page, err error) { func (s Scope) GetPagesWithoutContent(ctx domain.RequestContext, documentID string) (pages []page.Page, err error) {
err = s.Runtime.Db.Select(&pages, "SELECT id, refid, orgid, documentid, userid, contenttype, pagetype, sequence, level, title, revisions, blockid, created, revised FROM page WHERE orgid=? AND documentid=? ORDER BY sequence", ctx.OrgID, documentID) err = s.Runtime.Db.Select(&pages, "SELECT id, refid, orgid, documentid, userid, contenttype, pagetype, sequence, level, title, revisions, blockid, status, relativeid, created, revised FROM page WHERE orgid=? AND documentid=? AND status=0 ORDER BY sequence", ctx.OrgID, documentID)
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("Unable to execute select pages for org %s and document %s", ctx.OrgID, documentID)) err = errors.Wrap(err, fmt.Sprintf("Unable to execute select pages for org %s and document %s", ctx.OrgID, documentID))
@ -119,7 +134,7 @@ func (s Scope) Update(ctx domain.RequestContext, page page.Page, refID, userID s
} }
// Update page // Update page
_, err = ctx.Transaction.NamedExec("UPDATE page SET documentid=:documentid, level=:level, title=:title, body=:body, revisions=:revisions, sequence=:sequence, revised=:revised WHERE orgid=:orgid AND refid=:refid", _, err = ctx.Transaction.NamedExec("UPDATE page SET documentid=:documentid, level=:level, title=:title, body=:body, revisions=:revisions, sequence=:sequence, status=:status, relativeid=:relativeid, revised=:revised WHERE orgid=:orgid AND refid=:refid",
&page) &page)
if err != nil { if err != nil {
@ -139,6 +154,27 @@ func (s Scope) Update(ctx domain.RequestContext, page page.Page, refID, userID s
return return
} }
// Delete deletes the pageID page in the document.
// It then propagates that change into the search table, adds a delete the page revisions history, and audits that the page has been removed.
func (s Scope) Delete(ctx domain.RequestContext, documentID, pageID string) (rows int64, err error) {
b := mysql.BaseQuery{}
rows, err = b.DeleteConstrained(ctx.Transaction, "page", ctx.OrgID, pageID)
if err == nil {
_, _ = b.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM pagemeta WHERE orgid='%s' AND pageid='%s'", ctx.OrgID, pageID))
}
if err == nil {
_, _ = b.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM useraction WHERE orgid='%s' AND reftypeid='%s' AND reftype='P'", ctx.OrgID, pageID))
}
return
}
//**************************************************
// Page Meta
//**************************************************
// UpdateMeta persists meta information associated with a document page. // UpdateMeta persists meta information associated with a document page.
func (s Scope) UpdateMeta(ctx domain.RequestContext, meta page.Meta, updateUserID bool) (err error) { func (s Scope) UpdateMeta(ctx domain.RequestContext, meta page.Meta, updateUserID bool) (err error) {
meta.Revised = time.Now().UTC() meta.Revised = time.Now().UTC()
@ -157,43 +193,6 @@ func (s Scope) UpdateMeta(ctx domain.RequestContext, meta page.Meta, updateUserI
return return
} }
// UpdateSequence changes the presentation sequence of the pageID page in the document.
// It then propagates that change into the search table and audits that it has occurred.
func (s Scope) UpdateSequence(ctx domain.RequestContext, documentID, pageID string, sequence float64) (err error) {
_, err = ctx.Transaction.Exec("UPDATE page SET sequence=? WHERE orgid=? AND refid=?", sequence, ctx.OrgID, pageID)
if err != nil {
err = errors.Wrap(err, "execute page sequence update")
}
return
}
// UpdateLevel changes the heading level of the pageID page in the document.
// It then propagates that change into the search table and audits that it has occurred.
func (s Scope) UpdateLevel(ctx domain.RequestContext, documentID, pageID string, level int) (err error) {
_, err = ctx.Transaction.Exec("UPDATE page SET level=? WHERE orgid=? AND refid=?", level, ctx.OrgID, pageID)
if err != nil {
err = errors.Wrap(err, "execute page level update")
}
return
}
// Delete deletes the pageID page in the document.
// It then propagates that change into the search table, adds a delete the page revisions history, and audits that the page has been removed.
func (s Scope) Delete(ctx domain.RequestContext, documentID, pageID string) (rows int64, err error) {
b := mysql.BaseQuery{}
rows, err = b.DeleteConstrained(ctx.Transaction, "page", ctx.OrgID, pageID)
if err == nil {
_, _ = b.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM pagemeta WHERE orgid='%s' AND pageid='%s'", ctx.OrgID, pageID))
}
return
}
// GetPageMeta returns the meta information associated with the page. // GetPageMeta returns the meta information associated with the page.
func (s Scope) GetPageMeta(ctx domain.RequestContext, pageID string) (meta page.Meta, err error) { func (s Scope) GetPageMeta(ctx domain.RequestContext, pageID string) (meta page.Meta, err error) {
err = s.Runtime.Db.Get(&meta, "SELECT id, pageid, orgid, userid, documentid, rawbody, coalesce(config,JSON_UNQUOTE('{}')) as config, externalsource, created, revised FROM pagemeta WHERE orgid=? AND pageid=?", err = s.Runtime.Db.Get(&meta, "SELECT id, pageid, orgid, userid, documentid, rawbody, coalesce(config,JSON_UNQUOTE('{}')) as config, externalsource, created, revised FROM pagemeta WHERE orgid=? AND pageid=?",
@ -222,9 +221,51 @@ func (s Scope) GetDocumentPageMeta(ctx domain.RequestContext, documentID string,
return return
} }
/******************** //**************************************************
* Page Revisions // Table of contents
********************/ //**************************************************
// UpdateSequence changes the presentation sequence of the pageID page in the document.
// It then propagates that change into the search table and audits that it has occurred.
func (s Scope) UpdateSequence(ctx domain.RequestContext, documentID, pageID string, sequence float64) (err error) {
_, err = ctx.Transaction.Exec("UPDATE page SET sequence=? WHERE orgid=? AND refid=?", sequence, ctx.OrgID, pageID)
if err != nil {
err = errors.Wrap(err, "execute page sequence update")
}
return
}
// UpdateLevel changes the heading level of the pageID page in the document.
// It then propagates that change into the search table and audits that it has occurred.
func (s Scope) UpdateLevel(ctx domain.RequestContext, documentID, pageID string, level int) (err error) {
_, err = ctx.Transaction.Exec("UPDATE page SET level=? WHERE orgid=? AND refid=?", level, ctx.OrgID, pageID)
if err != nil {
err = errors.Wrap(err, "execute page level update")
}
return
}
// GetNextPageSequence returns the next sequence numbner to use for a page in given document.
func (s Scope) GetNextPageSequence(ctx domain.RequestContext, documentID string) (maxSeq float64, err error) {
row := s.Runtime.Db.QueryRow("SELECT max(sequence) FROM page WHERE orgid=? AND documentid=?", ctx.OrgID, documentID)
err = row.Scan(&maxSeq)
if err != nil {
maxSeq = 2048
}
maxSeq = maxSeq * 2
return
}
//**************************************************
// Page Revisions
//**************************************************
// GetPageRevision returns the revisionID page revision record. // GetPageRevision returns the revisionID page revision record.
func (s Scope) GetPageRevision(ctx domain.RequestContext, revisionID string) (revision page.Revision, err error) { func (s Scope) GetPageRevision(ctx domain.RequestContext, revisionID string) (revision page.Revision, err error) {
@ -273,17 +314,3 @@ func (s Scope) DeletePageRevisions(ctx domain.RequestContext, pageID string) (ro
return return
} }
// GetNextPageSequence returns the next sequence numbner to use for a page in given document.
func (s Scope) GetNextPageSequence(ctx domain.RequestContext, documentID string) (maxSeq float64, err error) {
row := s.Runtime.Db.QueryRow("SELECT max(sequence) FROM page WHERE orgid=? AND documentid=?", ctx.OrgID, documentID)
err = row.Scan(&maxSeq)
if err != nil {
maxSeq = 2048
}
maxSeq = maxSeq * 2
return
}

View file

@ -432,7 +432,7 @@ func (h *Handler) GetDocumentPermissions(w http.ResponseWriter, r *http.Request)
response.WriteJSON(w, records) response.WriteJSON(w, records)
} }
// GetUserDocumentPermissions returns permissions for the requested space, for current user. // GetUserDocumentPermissions returns permissions for the requested document, for current user.
func (h *Handler) GetUserDocumentPermissions(w http.ResponseWriter, r *http.Request) { func (h *Handler) GetUserDocumentPermissions(w http.ResponseWriter, r *http.Request) {
method := "space.GetUserDocumentPermissions" method := "space.GetUserDocumentPermissions"
ctx := domain.GetRequestContext(r) ctx := domain.GetRequestContext(r)

View file

@ -16,6 +16,7 @@ import (
"github.com/documize/community/domain" "github.com/documize/community/domain"
pm "github.com/documize/community/model/permission" pm "github.com/documize/community/model/permission"
u "github.com/documize/community/model/user"
) )
// CanViewSpaceDocument returns if the user has permission to view a document within the specified folder. // CanViewSpaceDocument returns if the user has permission to view a document within the specified folder.
@ -153,7 +154,6 @@ func CanViewSpace(ctx domain.RequestContext, s domain.Store, spaceID string) boo
if err != nil { if err != nil {
return false return false
} }
for _, role := range roles { for _, role := range roles {
if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" && if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" &&
pm.ContainsPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) { pm.ContainsPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) {
@ -187,3 +187,40 @@ func HasPermission(ctx domain.RequestContext, s domain.Store, spaceID string, ac
return false return false
} }
// GetDocumentApprovers returns list of users who can approve given document in given space
func GetDocumentApprovers(ctx domain.RequestContext, s domain.Store, spaceID, documentID string) (users []u.User, err error) {
users = []u.User{}
prev := make(map[string]bool) // used to ensure we only process user once
// check space permissions
sp, err := s.Permission.GetSpacePermissions(ctx, spaceID)
for _, p := range sp {
if p.Action == pm.DocumentApprove {
user, err := s.User.Get(ctx, p.WhoID)
if err == nil {
prev[user.RefID] = true
users = append(users, user)
} else {
return users, err
}
}
}
// check document permissions
dp, err := s.Permission.GetDocumentPermissions(ctx, documentID)
for _, p := range dp {
if p.Action == pm.DocumentApprove {
user, err := s.User.Get(ctx, p.WhoID)
if err == nil {
if _, isExisting := prev[user.RefID]; !isExisting {
users = append(users, user)
}
} else {
return users, err
}
}
}
return
}

View file

@ -24,6 +24,7 @@ import (
"github.com/documize/community/model/doc" "github.com/documize/community/model/doc"
"github.com/documize/community/model/page" "github.com/documize/community/model/page"
"github.com/documize/community/model/search" "github.com/documize/community/model/search"
"github.com/documize/community/model/workflow"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -93,6 +94,11 @@ func (s Scope) DeleteDocument(ctx domain.RequestContext, ID string) (err error)
// IndexContent adds search index entry for document context. // IndexContent adds search index entry for document context.
// Any existing document entries are removed. // Any existing document entries are removed.
func (s Scope) IndexContent(ctx domain.RequestContext, p page.Page) (err error) { func (s Scope) IndexContent(ctx domain.RequestContext, p page.Page) (err error) {
// we do not index pending pages
if p.Status == workflow.ChangePending || p.Status == workflow.ChangePendingNew {
return
}
// remove previous search entries // remove previous search entries
_, err = ctx.Transaction.Exec("DELETE FROM search WHERE orgid=? AND documentid=? AND itemid=? AND itemtype='page'", _, err = ctx.Transaction.Exec("DELETE FROM search WHERE orgid=? AND documentid=? AND itemid=? AND itemtype='page'",
ctx.OrgID, p.DocumentID, p.RefID) ctx.OrgID, p.DocumentID, p.RefID)

View file

@ -248,17 +248,18 @@ type PageStorer interface {
Add(ctx RequestContext, model page.NewPage) (err error) Add(ctx RequestContext, model page.NewPage) (err error)
Get(ctx RequestContext, pageID string) (p page.Page, err error) Get(ctx RequestContext, pageID string) (p page.Page, err error)
GetPages(ctx RequestContext, documentID string) (p []page.Page, err error) GetPages(ctx RequestContext, documentID string) (p []page.Page, err error)
GetUnpublishedPages(ctx RequestContext, documentID string) (p []page.Page, err error)
GetPagesWithoutContent(ctx RequestContext, documentID string) (pages []page.Page, err error) GetPagesWithoutContent(ctx RequestContext, documentID string) (pages []page.Page, err error)
Update(ctx RequestContext, page page.Page, refID, userID string, skipRevision bool) (err error) Update(ctx RequestContext, page page.Page, refID, userID string, skipRevision bool) (err error)
Delete(ctx RequestContext, documentID, pageID string) (rows int64, err error)
GetPageMeta(ctx RequestContext, pageID string) (meta page.Meta, err error)
GetDocumentPageMeta(ctx RequestContext, documentID string, externalSourceOnly bool) (meta []page.Meta, err error)
UpdateMeta(ctx RequestContext, meta page.Meta, updateUserID bool) (err error) UpdateMeta(ctx RequestContext, meta page.Meta, updateUserID bool) (err error)
UpdateSequence(ctx RequestContext, documentID, pageID string, sequence float64) (err error) UpdateSequence(ctx RequestContext, documentID, pageID string, sequence float64) (err error)
UpdateLevel(ctx RequestContext, documentID, pageID string, level int) (err error) UpdateLevel(ctx RequestContext, documentID, pageID string, level int) (err error)
Delete(ctx RequestContext, documentID, pageID string) (rows int64, err error) GetNextPageSequence(ctx RequestContext, documentID string) (maxSeq float64, err error)
GetPageMeta(ctx RequestContext, pageID string) (meta page.Meta, err error)
GetPageRevision(ctx RequestContext, revisionID string) (revision page.Revision, err error) GetPageRevision(ctx RequestContext, revisionID string) (revision page.Revision, err error)
GetPageRevisions(ctx RequestContext, pageID string) (revisions []page.Revision, err error) GetPageRevisions(ctx RequestContext, pageID string) (revisions []page.Revision, err error)
GetDocumentRevisions(ctx RequestContext, documentID string) (revisions []page.Revision, err error) GetDocumentRevisions(ctx RequestContext, documentID string) (revisions []page.Revision, err error)
GetDocumentPageMeta(ctx RequestContext, documentID string, externalSourceOnly bool) (meta []page.Meta, err error)
DeletePageRevisions(ctx RequestContext, pageID string) (rows int64, err error) DeletePageRevisions(ctx RequestContext, pageID string) (rows int64, err error)
GetNextPageSequence(ctx RequestContext, documentID string) (maxSeq float64, err error)
} }

View file

@ -33,7 +33,7 @@ func main() {
rt := env.Runtime{} rt := env.Runtime{}
// wire up logging implementation // wire up logging implementation
rt.Log = logging.NewLogger() rt.Log = logging.NewLogger(false)
// wire up embedded web assets handler // wire up embedded web assets handler
web.Embed = embed.NewEmbedder() web.Embed = embed.NewEmbedder()

View file

@ -25,8 +25,9 @@ import (
// Logger is how we log. // Logger is how we log.
type Logger struct { type Logger struct {
db *sqlx.DB db *sqlx.DB
log *log.Logger log *log.Logger
trace bool // shows Info() entries
} }
// Info logs message. // Info logs message.
@ -34,6 +35,13 @@ func (l Logger) Info(message string) {
l.log.Println(message) l.log.Println(message)
} }
// Trace logs message if tracing enabled.
func (l Logger) Trace(message string) {
if l.trace {
l.log.Println(message)
}
}
// Error logs error with message. // Error logs error with message.
func (l Logger) Error(message string, err error) { func (l Logger) Error(message string, err error) {
l.log.Println(message) l.log.Println(message)
@ -56,13 +64,14 @@ func (l Logger) SetDB(logger env.Logger, db *sqlx.DB) env.Logger {
} }
// NewLogger returns initialized logging instance. // NewLogger returns initialized logging instance.
func NewLogger() env.Logger { func NewLogger(trace bool) env.Logger {
l := log.New(os.Stdout, "", 0) l := log.New(os.Stdout, "", 0)
l.SetOutput(os.Stdout) l.SetOutput(os.Stdout)
// log.SetOutput(os.Stdout) // log.SetOutput(os.Stdout)
var logger Logger var logger Logger
logger.log = l logger.log = l
logger.trace = trace
return logger return logger
} }

View file

@ -10,12 +10,10 @@
// https://documize.com // https://documize.com
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Component from '@ember/component'; import Component from '@ember/component';
import NotifierMixin from '../../mixins/notifier';
import TooltipMixin from '../../mixins/tooltip'; import TooltipMixin from '../../mixins/tooltip';
export default Component.extend(NotifierMixin, TooltipMixin, { export default Component.extend(TooltipMixin, {
documentService: service('document'), documentService: service('document'),
sectionService: service('section'), sectionService: service('section'),
editMode: false, editMode: false,

View file

@ -10,7 +10,7 @@
// https://documize.com // https://documize.com
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import { schedule } from '@ember/runloop' import { schedule } from '@ember/runloop';
import { inject as service } from '@ember/service'; import { inject as service } from '@ember/service';
import Component from '@ember/component'; import Component from '@ember/component';
import tocUtil from '../../utils/toc'; import tocUtil from '../../utils/toc';

View file

@ -57,7 +57,6 @@ export default Component.extend(ModalMixin, {
this.attrs.onCancel(); this.attrs.onCancel();
}, },
onAction() { onAction() {
if (this.get('busy')) { if (this.get('busy')) {
return; return;

View file

@ -12,7 +12,6 @@
import { computed } from '@ember/object'; import { computed } from '@ember/object';
import Model from 'ember-data/model'; import Model from 'ember-data/model';
import attr from 'ember-data/attr'; import attr from 'ember-data/attr';
// import { hasMany } from 'ember-data/relationships';
export default Model.extend({ export default Model.extend({
documentId: attr('string'), documentId: attr('string'),
@ -28,10 +27,11 @@ export default Model.extend({
body: attr('string'), body: attr('string'),
rawBody: attr('string'), rawBody: attr('string'),
meta: attr(), meta: attr(),
approval: attr('number', { defaultValue: 0 }),
relativeId: attr('string'),
tagName: computed('level', function () { tagName: computed('level', function () {
return "h2"; return "h2";
// return "h" + (this.get('level') + 1);
}), }),
tocIndent: computed('level', function () { tocIndent: computed('level', function () {

View file

@ -10,7 +10,6 @@
// https://documize.com // https://documize.com
import { set } from '@ember/object'; import { set } from '@ember/object';
import { A } from '@ember/array'; import { A } from '@ember/array';
import ArrayProxy from '@ember/array/proxy'; import ArrayProxy from '@ember/array/proxy';
import Service, { inject as service } from '@ember/service'; import Service, { inject as service } from '@ember/service';
@ -21,6 +20,10 @@ export default Service.extend({
ajax: service(), ajax: service(),
store: service(), store: service(),
//**************************************************
// Document
//**************************************************
// Returns document model for specified document id. // Returns document model for specified document id.
getDocument(documentId) { getDocument(documentId) {
return this.get('ajax').request(`documents/${documentId}`, { return this.get('ajax').request(`documents/${documentId}`, {
@ -62,24 +65,6 @@ export default Service.extend({
}); });
}, },
changePageSequence(documentId, payload) {
let url = `documents/${documentId}/pages/sequence`;
return this.get('ajax').post(url, {
data: JSON.stringify(payload),
contentType: 'json'
});
},
changePageLevel(documentId, payload) {
let url = `documents/${documentId}/pages/level`;
return this.get('ajax').post(url, {
data: JSON.stringify(payload),
contentType: 'json'
});
},
deleteDocument(documentId) { deleteDocument(documentId) {
let url = `documents/${documentId}`; let url = `documents/${documentId}`;
@ -88,6 +73,20 @@ export default Service.extend({
}); });
}, },
//**************************************************
// Page
//**************************************************
// addPage inserts new page to an existing document.
addPage(documentId, payload) {
let url = `documents/${documentId}/pages`;
return this.get('ajax').post(url, {
data: JSON.stringify(payload),
contentType: 'json'
});
},
updatePage(documentId, pageId, payload, skipRevision) { updatePage(documentId, pageId, payload, skipRevision) {
var revision = skipRevision ? "?r=true" : "?r=false"; var revision = skipRevision ? "?r=true" : "?r=false";
let url = `documents/${documentId}/pages/${pageId}${revision}`; let url = `documents/${documentId}/pages/${pageId}${revision}`;
@ -104,106 +103,6 @@ export default Service.extend({
}); });
}, },
// addPage inserts new page to an existing document.
addPage(documentId, payload) {
let url = `documents/${documentId}/pages`;
return this.get('ajax').post(url, {
data: JSON.stringify(payload),
contentType: 'json'
});
},
// Nukes multiple pages from the document.
deletePages(documentId, pageId, payload) {
let url = `documents/${documentId}/pages`;
return this.get('ajax').request(url, {
data: JSON.stringify(payload),
contentType: 'json',
method: 'DELETE'
});
},
// Nukes a single page from the document.
deletePage(documentId, pageId) {
let url = `documents/${documentId}/pages/${pageId}`;
return this.get('ajax').request(url, {
method: 'DELETE'
});
},
getDocumentRevisions(documentId) {
let url = `documents/${documentId}/revisions`;
return this.get('ajax').request(url, {
method: "GET"
});
},
getPageRevisions(documentId, pageId) {
let url = `documents/${documentId}/pages/${pageId}/revisions`;
return this.get('ajax').request(url, {
method: "GET"
});
},
getPageRevisionDiff(documentId, pageId, revisionId) {
let url = `documents/${documentId}/pages/${pageId}/revisions/${revisionId}`;
return this.get('ajax').request(url, {
method: "GET",
dataType: 'text'
}).then((response) => {
return response;
}).catch(() => {
return "";
});
},
rollbackPage(documentId, pageId, revisionId) {
let url = `documents/${documentId}/pages/${pageId}/revisions/${revisionId}`;
return this.get('ajax').request(url, {
method: "POST"
});
},
// document meta referes to number of views, edits, approvals, etc.
getActivity(documentId) {
return this.get('ajax').request(`documents/${documentId}/activity`, {
method: "GET"
}).then((response) => {
let data = [];
data = response.map((obj) => {
let data = this.get('store').normalize('documentActivity', obj);
return this.get('store').push(data);
});
return data;
}).catch(() => {
return [];
});
},
// Returns all pages without the content
getTableOfContents(documentId) {
return this.get('ajax').request(`documents/${documentId}/pages?content=0`, {
method: 'GET'
}).then((response) => {
let data = [];
data = response.map((obj) => {
let data = this.get('store').normalize('page', obj);
return this.get('store').push(data);
});
return data;
});
},
// Returns all document pages with content // Returns all document pages with content
getPages(documentId) { getPages(documentId) {
return this.get('ajax').request(`documents/${documentId}/pages`, { return this.get('ajax').request(`documents/${documentId}/pages`, {
@ -243,6 +142,130 @@ export default Service.extend({
}); });
}, },
// Nukes multiple pages from the document.
deletePages(documentId, pageId, payload) {
let url = `documents/${documentId}/pages`;
return this.get('ajax').request(url, {
data: JSON.stringify(payload),
contentType: 'json',
method: 'DELETE'
});
},
// Nukes a single page from the document.
deletePage(documentId, pageId) {
let url = `documents/${documentId}/pages/${pageId}`;
return this.get('ajax').request(url, {
method: 'DELETE'
});
},
//**************************************************
// Page Revisions
//**************************************************
getDocumentRevisions(documentId) {
let url = `documents/${documentId}/revisions`;
return this.get('ajax').request(url, {
method: "GET"
});
},
getPageRevisions(documentId, pageId) {
let url = `documents/${documentId}/pages/${pageId}/revisions`;
return this.get('ajax').request(url, {
method: "GET"
});
},
getPageRevisionDiff(documentId, pageId, revisionId) {
let url = `documents/${documentId}/pages/${pageId}/revisions/${revisionId}`;
return this.get('ajax').request(url, {
method: "GET",
dataType: 'text'
}).then((response) => {
return response;
}).catch(() => {
return "";
});
},
rollbackPage(documentId, pageId, revisionId) {
let url = `documents/${documentId}/pages/${pageId}/revisions/${revisionId}`;
return this.get('ajax').request(url, {
method: "POST"
});
},
//**************************************************
// Activity
//**************************************************
// document meta referes to number of views, edits, approvals, etc.
getActivity(documentId) {
return this.get('ajax').request(`documents/${documentId}/activity`, {
method: "GET"
}).then((response) => {
let data = [];
data = response.map((obj) => {
let data = this.get('store').normalize('documentActivity', obj);
return this.get('store').push(data);
});
return data;
}).catch(() => {
return [];
});
},
//**************************************************
// Table of contents
//**************************************************
// Returns all pages without the content
getTableOfContents(documentId) {
return this.get('ajax').request(`documents/${documentId}/pages?content=0`, {
method: 'GET'
}).then((response) => {
let data = [];
data = response.map((obj) => {
let data = this.get('store').normalize('page', obj);
return this.get('store').push(data);
});
return data;
});
},
changePageSequence(documentId, payload) {
let url = `documents/${documentId}/pages/sequence`;
return this.get('ajax').post(url, {
data: JSON.stringify(payload),
contentType: 'json'
});
},
changePageLevel(documentId, payload) {
let url = `documents/${documentId}/pages/level`;
return this.get('ajax').post(url, {
data: JSON.stringify(payload),
contentType: 'json'
});
},
//**************************************************
// Attachments
//**************************************************
// document attachments without the actual content // document attachments without the actual content
getAttachments(documentId) { getAttachments(documentId) {

View file

@ -10,18 +10,20 @@
// https://documize.com // https://documize.com
html { html {
overflow-y: scroll;
font-family: $font-regular;
font-size: 0.875rem;
height: 100%; height: 100%;
width: 100%;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-ms-overflow-style: -ms-autohiding-scrollbar; -ms-overflow-style: -ms-autohiding-scrollbar;
overflow-y: scroll;
font-size: 0.875rem;
} }
body { body {
height: 100%; height: 100%;
min-height: 100%; min-height: 100%;
width: 100%;
overflow-y: scroll;
} }
a { a {

View file

@ -38,6 +38,9 @@ const (
// SourceTypeDocument indicates activity against a document. // SourceTypeDocument indicates activity against a document.
SourceTypeDocument SourceType = 2 SourceTypeDocument SourceType = 2
// SourceTypePage indicates activity against a document page.
SourceTypePage SourceType = 3
) )
const ( const (
@ -70,6 +73,9 @@ const (
// TypeFeedback records user providing document feedback // TypeFeedback records user providing document feedback
TypeFeedback Type = 10 TypeFeedback Type = 10
// TypeRejected records user rejecting document
TypeRejected Type = 11
) )
// DocumentActivity represents an activity taken against a document. // DocumentActivity represents an activity taken against a document.

View file

@ -45,6 +45,13 @@ func (d *Document) SetDefaults() {
} }
} }
// ByTitle sorts a collection of documents by document title.
type ByTitle []Document
func (a ByTitle) Len() int { return len(a) }
func (a ByTitle) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByTitle) Less(i, j int) bool { return strings.ToLower(a[i].Title) < strings.ToLower(a[j].Title) }
// DocumentMeta details who viewed the document. // DocumentMeta details who viewed the document.
type DocumentMeta struct { type DocumentMeta struct {
Viewers []DocumentMetaViewer `json:"viewers"` Viewers []DocumentMetaViewer `json:"viewers"`

View file

@ -72,3 +72,71 @@ func Numberize(pages []Page) {
prevPageLevel = p.Level prevPageLevel = p.Level
} }
} }
// Levelize ensure page level increments are consistent
// after a page is inserted or removed.
//
// Valid: 1, 2, 3, 4, 4, 4, 2, 1
// Invalid: 1, 2, 4, 4, 2, 1 (note the jump from 2 --> 4)
//
// Rules:
// 1. levels can increase by 1 only (e.g. from 1 to 2 to 3 to 4)
// 2. levels can decrease by any amount (e.g. drop from 4 to 1)
func Levelize(pages []Page) {
var prevLevel uint64
prevLevel = 1
for i := 0; i < len(pages); i++ {
currLevel := pages[i].Level
// handle deprecated level value of 0
if pages[i].Level == 0 {
pages[i].Level = 1
}
// first time thru
if i == 0 {
// first time thru
pages[i].Level = 1
prevLevel = 1
continue
}
if currLevel == prevLevel {
// nothing doing
continue
}
if currLevel > prevLevel+1 {
// bad data detected e.g. prevLevel=1 and pages[i].Level=3
// so we re-level to pages[i].Level=2 and all child pages
// and then increment i to correct point
prevLevel++
pages[i].Level = prevLevel
// safety check before entering loop and renumbering child pages
if i+1 <= len(pages) {
for j := i + 1; j < len(pages); j++ {
if pages[j].Level < prevLevel {
i = j
break
}
if pages[j].Level == currLevel {
pages[j].Level = prevLevel
} else if (pages[j].Level - prevLevel) > 1 {
currLevel = pages[j].Level
prevLevel++
pages[j].Level = prevLevel
}
i = j
}
}
continue
}
prevLevel = currLevel
}
}

View file

@ -111,4 +111,124 @@ func TestNumberize3(t *testing.T) {
} }
} }
// go test github.com/documize/community/core/model -run TestNumberize // Tests that numbering does not crash because of bad data
func TestNumberize4(t *testing.T) {
pages := []Page{}
pages = append(pages, Page{Level: 0, Sequence: 1000})
pages = append(pages, Page{Level: 1, Sequence: 2000})
pages = append(pages, Page{Level: 1, Sequence: 3000})
// corruption starts here with Level=3 instead of Level=2
pages = append(pages, Page{Level: 3, Sequence: 4000})
pages = append(pages, Page{Level: 4, Sequence: 4000})
pages = append(pages, Page{Level: 1, Sequence: 5000})
pages = append(pages, Page{Level: 2, Sequence: 6000})
Numberize(pages)
expecting := []string{
"1",
"2",
"3",
"3.1",
"3.1.1",
// data below cannot be processed due to corruption
"", // should be 4
"1", // should be 5
}
for i, p := range pages {
if p.Numbering != expecting[i] {
t.Errorf("(Test 4) Position %d: expecting %s got %s\n", i, expecting[i], p.Numbering)
}
}
}
// Tests that good level data is not messed with
func TestLevelize1(t *testing.T) {
pages := []Page{}
pages = append(pages, Page{Level: 1, Sequence: 1000})
pages = append(pages, Page{Level: 1, Sequence: 2000})
pages = append(pages, Page{Level: 2, Sequence: 3000})
pages = append(pages, Page{Level: 3, Sequence: 4000})
pages = append(pages, Page{Level: 4, Sequence: 5000})
pages = append(pages, Page{Level: 1, Sequence: 6000})
pages = append(pages, Page{Level: 2, Sequence: 7000})
Levelize(pages)
expecting := []uint64{1, 1, 2, 3, 4, 1, 2}
for i, p := range pages {
if p.Level != expecting[i] {
t.Errorf("(TestLevelize1) Position %d: expecting %d got %d (sequence: %f)\n", i+1, expecting[i], p.Level, p.Sequence)
}
}
}
// Tests that bad level data
func TestLevelize2(t *testing.T) {
pages := []Page{}
pages = append(pages, Page{Level: 1, Sequence: 1000})
pages = append(pages, Page{Level: 1, Sequence: 2000})
pages = append(pages, Page{Level: 3, Sequence: 3000})
pages = append(pages, Page{Level: 3, Sequence: 4000})
pages = append(pages, Page{Level: 4, Sequence: 5000})
pages = append(pages, Page{Level: 1, Sequence: 6000})
pages = append(pages, Page{Level: 2, Sequence: 7000})
Levelize(pages)
expecting := []uint64{1, 1, 2, 2, 3, 1, 2}
for i, p := range pages {
if p.Level != expecting[i] {
t.Errorf("(TestLevelize2) Position %d: expecting %d got %d (sequence: %f)\n", i+1, expecting[i], p.Level, p.Sequence)
}
}
}
func TestLevelize3(t *testing.T) {
pages := []Page{}
pages = append(pages, Page{Level: 1, Sequence: 1000})
pages = append(pages, Page{Level: 4, Sequence: 2000})
pages = append(pages, Page{Level: 5, Sequence: 3000})
Levelize(pages)
expecting := []uint64{1, 2, 3}
for i, p := range pages {
if p.Level != expecting[i] {
t.Errorf("(TestLevelize3) Position %d: expecting %d got %d (sequence: %f)\n", i+1, expecting[i], p.Level, p.Sequence)
}
}
}
func TestLevelize4(t *testing.T) {
pages := []Page{}
pages = append(pages, Page{Level: 1, Sequence: 1000})
pages = append(pages, Page{Level: 4, Sequence: 2000})
pages = append(pages, Page{Level: 5, Sequence: 3000})
pages = append(pages, Page{Level: 5, Sequence: 4000})
pages = append(pages, Page{Level: 6, Sequence: 5000})
pages = append(pages, Page{Level: 6, Sequence: 6000})
pages = append(pages, Page{Level: 2, Sequence: 7000})
Levelize(pages)
expecting := []uint64{1, 2, 3, 3, 4, 4, 2}
for i, p := range pages {
if p.Level != expecting[i] {
t.Errorf("(TestLevelize4) Position %d: expecting %d got %d (sequence: %f)\n", i+1, expecting[i], p.Level, p.Sequence)
}
}
}
// go test github.com/documize/community/core/model -run TestNumberiz, 3, 4, 4, 2e

View file

@ -16,23 +16,26 @@ import (
"time" "time"
"github.com/documize/community/model" "github.com/documize/community/model"
"github.com/documize/community/model/workflow"
) )
// Page represents a section within a document. // Page represents a section within a document.
type Page struct { type Page struct {
model.BaseEntity model.BaseEntity
OrgID string `json:"orgId"` OrgID string `json:"orgId"`
DocumentID string `json:"documentId"` DocumentID string `json:"documentId"`
UserID string `json:"userId"` UserID string `json:"userId"`
ContentType string `json:"contentType"` ContentType string `json:"contentType"`
PageType string `json:"pageType"` PageType string `json:"pageType"`
BlockID string `json:"blockId"` BlockID string `json:"blockId"`
Level uint64 `json:"level"` Level uint64 `json:"level"`
Sequence float64 `json:"sequence"` Sequence float64 `json:"sequence"`
Numbering string `json:"numbering"` Numbering string `json:"numbering"`
Title string `json:"title"` Title string `json:"title"`
Body string `json:"body"` Body string `json:"body"`
Revisions uint64 `json:"revisions"` Revisions uint64 `json:"revisions"`
Status workflow.ChangeStatus `json:"status"`
RelativeID string `json:"relativeId"` // links page to pending page edits
} }
// SetDefaults ensures no blank values. // SetDefaults ensures no blank values.
@ -41,6 +44,10 @@ func (p *Page) SetDefaults() {
p.ContentType = "wysiwyg" p.ContentType = "wysiwyg"
} }
if p.Level == 0 {
p.Level = 1
}
p.Title = strings.TrimSpace(p.Title) p.Title = strings.TrimSpace(p.Title)
} }
@ -114,3 +121,18 @@ type LevelRequest struct {
PageID string `json:"pageId"` PageID string `json:"pageId"`
Level int `json:"level"` Level int `json:"level"`
} }
// BulkRequest details page, it's meta, pending page changes.
// Used to bulk load data by GUI so as to reduce network requests.
type BulkRequest struct {
Page Page `json:"page"`
Meta Meta `json:"meta"`
Pending []PendingPage `json:"pending"`
}
// PendingPage details page that is yet to be published
type PendingPage struct {
Page Page `json:"page"`
Meta Meta `json:"meta"`
Owner string `json:"owner"`
}

View file

@ -15,29 +15,51 @@ package workflow
type Protection int type Protection int
const ( const (
// NoProtection means no protection so data item changes are permitted // ProtectionNone means no protection so data item changes are permitted
NoProtection Protection = 0 ProtectionNone Protection = 0
// Lock means no data itme changes // ProtectionLock means no data itme changes
Lock Protection = 1 ProtectionLock Protection = 1
// Review means changes must be reviewed and approved // ProtectionReview means changes must be reviewed and approved
Review Protection = 2 ProtectionReview Protection = 2
) )
// Approval tells us how some data item change is to be approved // Approval tells us how some data item change is to be approved
type Approval int type Approval int
const ( const (
// NoApproval means no approval necessary // ApprovalNone means no approval necessary
NoApproval Approval = 0 ApprovalNone Approval = 0
// Anybody can approve data item change // ApprovalAnybody can approve data item change
Anybody Approval = 1 ApprovalAnybody Approval = 1
// Majority must approve data item change // ApprovalMajority must approve data item change
Majority Approval = 2 ApprovalMajority Approval = 2
// Unanimous approval must be given for data item change // ApprovalUnanimous approval must be given for data item change
Unanimous Approval = 3 ApprovalUnanimous Approval = 3
)
// ChangeStatus tells us the state of a data item
type ChangeStatus int
const (
// ChangePublished means data item is visible all
ChangePublished ChangeStatus = 0
// ChangePending means data item is still being edited and not yet requesting review
ChangePending ChangeStatus = 1
// ChangeUnderReview means data item is being reviewed
// Next step would be to mark data item as either
// Published or Rejected
ChangeUnderReview ChangeStatus = 2
// ChangeRejected means data item was not approved for publication
ChangeRejected ChangeStatus = 3
// ChangePendingNew means a new section to a document is pending review
ChangePendingNew ChangeStatus = 4
) )

View file

@ -164,7 +164,6 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
Add(rt, RoutePrefixPrivate, "sections/blocks/{blockID}", []string{"PUT", "OPTIONS"}, nil, block.Update) 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/{blockID}", []string{"DELETE", "OPTIONS"}, nil, block.Delete)
Add(rt, RoutePrefixPrivate, "sections/blocks", []string{"POST", "OPTIONS"}, nil, block.Add) Add(rt, RoutePrefixPrivate, "sections/blocks", []string{"POST", "OPTIONS"}, nil, block.Add)
Add(rt, RoutePrefixPrivate, "sections/targets", []string{"GET", "OPTIONS"}, nil, page.GetMoveCopyTargets)
Add(rt, RoutePrefixPrivate, "links/{folderID}/{documentID}/{pageID}", []string{"GET", "OPTIONS"}, nil, link.GetLinkCandidates) 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, "links", []string{"GET", "OPTIONS"}, nil, link.SearchLinkCandidates)
@ -185,6 +184,7 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
// fetch methods exist to speed up UI rendering by returning data in bulk // 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/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/document/{documentID}", []string{"GET", "OPTIONS"}, nil, document.FetchDocumentData)
Add(rt, RoutePrefixPrivate, "fetch/page/{documentID}", []string{"GET", "OPTIONS"}, nil, page.FetchPages)
Add(rt, RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, meta.RobotsTxt) Add(rt, RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, meta.RobotsTxt)
Add(rt, RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, meta.Sitemap) Add(rt, RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, meta.Sitemap)