1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-19 05:09:42 +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 `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
DROP TABLE IF EXISTS `audit`;
DROP TABLE IF EXISTS `search_old`;
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.
type Logger interface {
Info(message string)
Trace(message string)
Error(message string, err error)
// SetDB(l Logger, db *sqlx.DB) Logger
}

View file

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

View file

@ -90,9 +90,9 @@ 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, 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) {
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 {
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)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, "select documents by space")
}
@ -277,6 +280,11 @@ func (s Scope) Delete(ctx domain.RequestContext, documentID string) (rows int64,
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)
}

View file

@ -31,8 +31,9 @@ import (
"github.com/documize/community/domain/section/provider"
"github.com/documize/community/model/activity"
"github.com/documize/community/model/audit"
"github.com/documize/community/model/doc"
"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"
)
@ -53,17 +54,14 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
return
}
// check param
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
}
// read payload
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
@ -90,6 +88,38 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
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()
model.Page.RefID = 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.Page.SetDefaults()
model.Meta.SetDefaults()
// page.Title = template.HTMLEscapeString(page.Title)
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
@ -229,16 +258,11 @@ func (h *Handler) GetPages(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, pages)
}
// Delete deletes a page.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
method := "page.delete"
// 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)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
@ -251,156 +275,31 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
if !permission.CanViewDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
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 {
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)
if meta.DocumentID != documentID {
response.WriteBadRequestError(w, method, "documentID mismatch")
h.Runtime.Log.Error(method, err)
return
}
page, 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(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)
response.WriteJSON(w, meta)
}
// Update will persist changed page and note the fact
@ -415,11 +314,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
// if !ctx.Editor {
// response.WriteForbiddenError(w)
// return
// }
// Check params
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
@ -432,11 +327,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
// Read payload
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
@ -458,13 +349,26 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
// Check protection and approval process
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
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()
if err != nil {
response.WriteServerError(w, method, err)
@ -492,6 +396,11 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
skipRevision := false
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)
if err != nil {
response.WriteServerError(w, method, err)
@ -562,6 +471,258 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
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.
func (h *Handler) ChangePageSequence(w http.ResponseWriter, r *http.Request) {
method := "page.sequence"
@ -684,75 +845,10 @@ func (h *Handler) ChangePageLevel(w http.ResponseWriter, r *http.Request) {
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
//**************************************************
// 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.
func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
method := "page.targets"
@ -790,6 +886,12 @@ func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
return
}
// workflow check
if doc.Protection == workflow.ProtectionLock || doc.Protection == workflow.ProtectionReview {
response.WriteForbiddenError(w)
return
}
p, err := h.Store.Page.Get(ctx, pageID)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, documentID)
@ -1101,3 +1203,176 @@ func (h *Handler) Rollback(w http.ResponseWriter, r *http.Request) {
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
}
//**************************************************
// 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.
func (s Scope) Add(ctx domain.RequestContext, model page.NewPage) (err error) {
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
}
_, err = ctx.Transaction.Exec("INSERT INTO page (refid, orgid, documentid, userid, contenttype, pagetype, level, title, body, revisions, sequence, blockid, 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)
_, 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.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 (?, ?, ?, ?, ?, ?, ?, ?, ?)",
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.
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)
if err != nil {
@ -79,9 +83,9 @@ func (s Scope) Get(ctx domain.RequestContext, pageID string) (p page.Page, err e
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) {
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 {
err = errors.Wrap(err, "execute get pages")
@ -90,10 +94,21 @@ func (s Scope) GetPages(ctx domain.RequestContext, documentID string) (p []page.
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,
// but without the body field (which holds the HTML content).
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 {
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
_, 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)
if err != nil {
@ -139,6 +154,27 @@ func (s Scope) Update(ctx domain.RequestContext, page page.Page, refID, userID s
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.
func (s Scope) UpdateMeta(ctx domain.RequestContext, meta page.Meta, updateUserID bool) (err error) {
meta.Revised = time.Now().UTC()
@ -157,43 +193,6 @@ func (s Scope) UpdateMeta(ctx domain.RequestContext, meta page.Meta, updateUserI
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.
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=?",
@ -222,9 +221,51 @@ func (s Scope) GetDocumentPageMeta(ctx domain.RequestContext, documentID string,
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.
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
}
// 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)
}
// 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) {
method := "space.GetUserDocumentPermissions"
ctx := domain.GetRequestContext(r)

View file

@ -16,6 +16,7 @@ import (
"github.com/documize/community/domain"
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.
@ -153,7 +154,6 @@ func CanViewSpace(ctx domain.RequestContext, s domain.Store, spaceID string) boo
if err != nil {
return false
}
for _, role := range roles {
if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" &&
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
}
// 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/page"
"github.com/documize/community/model/search"
"github.com/documize/community/model/workflow"
"github.com/jmoiron/sqlx"
"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.
// Any existing document entries are removed.
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
_, err = ctx.Transaction.Exec("DELETE FROM search WHERE orgid=? AND documentid=? AND itemid=? AND itemtype='page'",
ctx.OrgID, p.DocumentID, p.RefID)

View file

@ -248,17 +248,18 @@ type PageStorer interface {
Add(ctx RequestContext, model page.NewPage) (err error)
Get(ctx RequestContext, pageID 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)
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)
UpdateSequence(ctx RequestContext, documentID, pageID string, sequence float64) (err error)
UpdateLevel(ctx RequestContext, documentID, pageID string, level int) (err error)
Delete(ctx RequestContext, documentID, pageID string) (rows int64, err error)
GetPageMeta(ctx RequestContext, pageID string) (meta page.Meta, err error)
GetNextPageSequence(ctx RequestContext, documentID string) (maxSeq float64, err error)
GetPageRevision(ctx RequestContext, revisionID string) (revision page.Revision, err error)
GetPageRevisions(ctx RequestContext, pageID 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)
GetNextPageSequence(ctx RequestContext, documentID string) (maxSeq float64, err error)
}

View file

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

View file

@ -25,8 +25,9 @@ import (
// Logger is how we log.
type Logger struct {
db *sqlx.DB
log *log.Logger
db *sqlx.DB
log *log.Logger
trace bool // shows Info() entries
}
// Info logs message.
@ -34,6 +35,13 @@ func (l Logger) Info(message string) {
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.
func (l Logger) Error(message string, err error) {
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.
func NewLogger() env.Logger {
func NewLogger(trace bool) env.Logger {
l := log.New(os.Stdout, "", 0)
l.SetOutput(os.Stdout)
// log.SetOutput(os.Stdout)
var logger Logger
logger.log = l
logger.trace = trace
return logger
}

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,6 @@
// https://documize.com
import { set } from '@ember/object';
import { A } from '@ember/array';
import ArrayProxy from '@ember/array/proxy';
import Service, { inject as service } from '@ember/service';
@ -21,6 +20,10 @@ export default Service.extend({
ajax: service(),
store: service(),
//**************************************************
// Document
//**************************************************
// Returns document model for specified document id.
getDocument(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) {
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) {
var revision = skipRevision ? "?r=true" : "?r=false";
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
getPages(documentId) {
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
getAttachments(documentId) {

View file

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

View file

@ -38,6 +38,9 @@ const (
// SourceTypeDocument indicates activity against a document.
SourceTypeDocument SourceType = 2
// SourceTypePage indicates activity against a document page.
SourceTypePage SourceType = 3
)
const (
@ -70,6 +73,9 @@ const (
// TypeFeedback records user providing document feedback
TypeFeedback Type = 10
// TypeRejected records user rejecting document
TypeRejected Type = 11
)
// 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.
type DocumentMeta struct {
Viewers []DocumentMetaViewer `json:"viewers"`

View file

@ -72,3 +72,71 @@ func Numberize(pages []Page) {
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"
"github.com/documize/community/model"
"github.com/documize/community/model/workflow"
)
// Page represents a section within a document.
type Page struct {
model.BaseEntity
OrgID string `json:"orgId"`
DocumentID string `json:"documentId"`
UserID string `json:"userId"`
ContentType string `json:"contentType"`
PageType string `json:"pageType"`
BlockID string `json:"blockId"`
Level uint64 `json:"level"`
Sequence float64 `json:"sequence"`
Numbering string `json:"numbering"`
Title string `json:"title"`
Body string `json:"body"`
Revisions uint64 `json:"revisions"`
OrgID string `json:"orgId"`
DocumentID string `json:"documentId"`
UserID string `json:"userId"`
ContentType string `json:"contentType"`
PageType string `json:"pageType"`
BlockID string `json:"blockId"`
Level uint64 `json:"level"`
Sequence float64 `json:"sequence"`
Numbering string `json:"numbering"`
Title string `json:"title"`
Body string `json:"body"`
Revisions uint64 `json:"revisions"`
Status workflow.ChangeStatus `json:"status"`
RelativeID string `json:"relativeId"` // links page to pending page edits
}
// SetDefaults ensures no blank values.
@ -41,6 +44,10 @@ func (p *Page) SetDefaults() {
p.ContentType = "wysiwyg"
}
if p.Level == 0 {
p.Level = 1
}
p.Title = strings.TrimSpace(p.Title)
}
@ -114,3 +121,18 @@ type LevelRequest struct {
PageID string `json:"pageId"`
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
const (
// NoProtection means no protection so data item changes are permitted
NoProtection Protection = 0
// ProtectionNone means no protection so data item changes are permitted
ProtectionNone Protection = 0
// Lock means no data itme changes
Lock Protection = 1
// ProtectionLock means no data itme changes
ProtectionLock Protection = 1
// Review means changes must be reviewed and approved
Review Protection = 2
// ProtectionReview means changes must be reviewed and approved
ProtectionReview Protection = 2
)
// Approval tells us how some data item change is to be approved
type Approval int
const (
// NoApproval means no approval necessary
NoApproval Approval = 0
// ApprovalNone means no approval necessary
ApprovalNone Approval = 0
// Anybody can approve data item change
Anybody Approval = 1
// ApprovalAnybody can approve data item change
ApprovalAnybody Approval = 1
// Majority must approve data item change
Majority Approval = 2
// ApprovalMajority must approve data item change
ApprovalMajority Approval = 2
// Unanimous approval must be given for data item change
Unanimous Approval = 3
// ApprovalUnanimous approval must be given for data item change
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{"DELETE", "OPTIONS"}, nil, block.Delete)
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", []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
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)
Add(rt, RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, meta.RobotsTxt)
Add(rt, RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, meta.Sitemap)