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:
parent
049b83e0b9
commit
5f59e95495
25 changed files with 1104 additions and 461 deletions
|
@ -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
1
core/env/logger.go
vendored
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -57,7 +57,6 @@ export default Component.extend(ModalMixin, {
|
|||
this.attrs.onCancel();
|
||||
},
|
||||
|
||||
|
||||
onAction() {
|
||||
if (this.get('busy')) {
|
||||
return;
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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) {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue