diff --git a/domain/document/document.go b/domain/document/document.go index beafd0c4..4431f828 100644 --- a/domain/document/document.go +++ b/domain/document/document.go @@ -65,7 +65,6 @@ func FilterCategoryProtected(docs []doc.Document, cats []category.Category, memb // CopyDocument clones an existing document func CopyDocument(ctx domain.RequestContext, s domain.Store, documentID string) (newDocumentID string, err error) { - doc, err := s.Document.Get(ctx, documentID) if err != nil { err = errors.Wrap(err, "unable to fetch existing document") @@ -145,3 +144,32 @@ func CopyDocument(ctx domain.RequestContext, s domain.Store, documentID string) return } + +// FilterLastVersion returns the latest version of each document +// by removing all previous versions. +// If a document is not versioned, it is returned as-is. +func FilterLastVersion(docs []doc.Document) (filtered []doc.Document) { + filtered = []doc.Document{} + prev := make(map[string]bool) + + for _, doc := range docs { + add := false + + if doc.GroupID == "" { + add = true + } else { + if _, isExisting := prev[doc.GroupID]; !isExisting { + add = true + prev[doc.GroupID] = true + } else { + add = false + } + } + + if add { + filtered = append(filtered, doc) + } + } + + return +} diff --git a/domain/document/endpoint.go b/domain/document/endpoint.go index 2c01baba..041628e5 100644 --- a/domain/document/endpoint.go +++ b/domain/document/endpoint.go @@ -44,9 +44,10 @@ type Handler struct { Indexer indexer.Indexer } -// Get is an endpoint that returns the document-level information for a given documentID. +// Get is an endpoint that returns the document-level information for a +// given documentID. func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { - method := "document.get" + method := "document.Get" ctx := domain.GetRequestContext(r) id := request.Param(r, "documentID") @@ -101,7 +102,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) { // DocumentLinks is an endpoint returning the links for a document. func (h *Handler) DocumentLinks(w http.ResponseWriter, r *http.Request) { - method := "document.links" + method := "document.DocumentLinks" ctx := domain.GetRequestContext(r) id := request.Param(r, "documentID") @@ -142,7 +143,8 @@ func (h *Handler) BySpace(w http.ResponseWriter, r *http.Request) { // get user permissions viewDrafts := permission.CanViewDrafts(ctx, *h.Store, spaceID) - // get complete list of documents + // Get complete list of documents regardless of category permission + // and versioning. documents, err := h.Store.Document.GetBySpace(ctx, spaceID) if err != nil { response.WriteServerError(w, method, err) @@ -150,21 +152,25 @@ func (h *Handler) BySpace(w http.ResponseWriter, r *http.Request) { return } - // sort by title + // Sort by title. sort.Sort(doc.ByTitle(documents)) - // remove documents that cannot be seen due to lack of category view/access permission + // Remove documents that cannot be seen due to lack of + // category view/access permission. cats, err := h.Store.Category.GetBySpace(ctx, spaceID) members, err := h.Store.Category.GetSpaceCategoryMembership(ctx, spaceID) filtered := FilterCategoryProtected(documents, cats, members, viewDrafts) + // Keep the latest version when faced with multiple versions. + filtered = FilterLastVersion(filtered) + response.WriteJSON(w, filtered) } -// Update updates an existing document using the -// format described in NewDocumentModel() encoded as JSON in the request. +// Update updates an existing document using the format described +// in NewDocumentModel() encoded as JSON in the request. func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { - method := "document.space" + method := "document.Update" ctx := domain.GetRequestContext(r) documentID := request.Param(r, "documentID") @@ -203,7 +209,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { return } - // if space changed for document, remove document categories + // If space changed for document, remove document categories. oldDoc, err := h.Store.Document.Get(ctx, documentID) if err != nil { ctx.Transaction.Rollback() @@ -224,7 +230,20 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { return } - // Record document being marked as archived + // If document part of versioned document group + // then document name must be applied to all documents + // in the group. + if len(d.GroupID) > 0 { + err = h.Store.Document.UpdateGroup(ctx, d) + if err != nil { + ctx.Transaction.Rollback() + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + } + + // Record document being marked as archived. if d.Lifecycle != oldDoc.Lifecycle && d.Lifecycle == workflow.LifecycleArchived { h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{ LabelID: d.LabelID, @@ -233,7 +252,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { ActivityType: activity.TypeArchived}) } - // Record document being marked as draft + // Record document being marked as draft. if d.Lifecycle != oldDoc.Lifecycle && d.Lifecycle == workflow.LifecycleDraft { h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{ LabelID: d.LabelID, @@ -246,7 +265,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { h.Store.Audit.Record(ctx, audit.EventTypeDocumentUpdate) - // Live document indexed for search + // Live document indexed for search. if d.Lifecycle == workflow.LifecycleLive { a, _ := h.Store.Attachment.GetAttachments(ctx, documentID) go h.Indexer.IndexDocument(ctx, d, a) @@ -259,7 +278,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { // Delete is an endpoint that deletes a document specified by documentID. func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { - method := "document.delete" + method := "document.Delete" ctx := domain.GetRequestContext(r) documentID := request.Param(r, "documentID") @@ -348,9 +367,10 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { response.WriteEmpty(w) } -// SearchDocuments endpoint takes a list of keywords and returns a list of document references matching those keywords. +// SearchDocuments endpoint takes a list of keywords and returns a list of +// document references matching those keywords. func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) { - method := "document.search" + method := "document.SearchDocuments" ctx := domain.GetRequestContext(r) defer streamutil.Close(r.Body) @@ -467,12 +487,22 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) { sp = []space.Space{} } + // Get version information for this document. + v, err := h.Store.Document.GetVersions(ctx, document.GroupID) + if err != nil && err != sql.ErrNoRows { + response.WriteServerError(w, method, err) + h.Runtime.Log.Error(method, err) + return + } + + // Prepare response. data := BulkDocumentData{} data.Document = document data.Permissions = record data.Roles = rolesRecord data.Links = l data.Spaces = sp + data.Versions = v ctx.Transaction, err = h.Runtime.Db.Beginx() if err != nil { @@ -509,4 +539,5 @@ type BulkDocumentData struct { Roles pm.DocumentRecord `json:"roles"` Spaces []space.Space `json:"folders"` Links []link.Link `json:"links"` + Versions []doc.Version `json:"versions"` } diff --git a/domain/document/mysql/store.go b/domain/document/mysql/store.go index 28b2fad6..428a5d1d 100644 --- a/domain/document/mysql/store.go +++ b/domain/document/mysql/store.go @@ -97,20 +97,13 @@ func (s Scope) DocumentMeta(ctx domain.RequestContext, id string) (meta doc.Docu return } -// GetAll returns a slice containg all of the the documents for the client's organisation. -func (s Scope) GetAll() (ctx domain.RequestContext, documents []doc.Document, err error) { - err = s.Runtime.Db.Select(&documents, "SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised FROM document WHERE orgid=? AND template=0 ORDER BY title", ctx.OrgID) - - if err != nil { - err = errors.Wrap(err, "select documents") - } - - return -} - // GetBySpace returns a slice containing the documents for a given space. -// No attempt is made to hide documents that are protected -// by category permissions -- caller must filter as required. +// +// No attempt is made to hide documents that are protected by category +// permissions hence caller must filter as required. +// +// All versions of a document are returned, hence caller must +// decide what to do with them. func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (documents []doc.Document, err error) { err = s.Runtime.Db.Select(&documents, ` SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, @@ -125,7 +118,7 @@ func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (documents AND p.who='role' AND p.location='space' AND p.refid=? AND p.action='view' AND (r.userid=? OR r.userid='0') )) ) - ORDER BY title`, ctx.OrgID, ctx.OrgID, ctx.OrgID, spaceID, ctx.OrgID, ctx.UserID, ctx.OrgID, spaceID, ctx.UserID) + ORDER BY title, versionorder`, ctx.OrgID, ctx.OrgID, ctx.OrgID, spaceID, ctx.OrgID, ctx.UserID, ctx.OrgID, spaceID, ctx.UserID) if err == sql.ErrNoRows || len(documents) == 0 { err = nil @@ -138,35 +131,6 @@ func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (documents return } -// Templates returns a slice containing the documents available as templates to the client's organisation, in title order. -func (s Scope) Templates(ctx domain.RequestContext) (documents []doc.Document, err error) { - err = s.Runtime.Db.Select(&documents, - `SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, - protection, approval, lifecycle, versioned, versionid, versionorder, groupid, created, revised - FROM document - WHERE orgid=? AND template=1 AND lifecycle=1 - AND labelid IN - ( - SELECT refid FROM label WHERE orgid=? - AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN ( - SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view' - UNION ALL - SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role' AND p.location='space' AND p.action='view' AND (r.userid=? OR r.userid='0') - )) - ) - ORDER BY title`, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID) - - if err == sql.ErrNoRows || len(documents) == 0 { - err = nil - documents = []doc.Document{} - } - if err != nil { - err = errors.Wrap(err, "select document templates") - } - - return -} - // TemplatesBySpace returns a slice containing the documents available as templates for given space. func (s Scope) TemplatesBySpace(ctx domain.RequestContext, spaceID string) (documents []doc.Document, err error) { err = s.Runtime.Db.Select(&documents, @@ -196,7 +160,9 @@ func (s Scope) TemplatesBySpace(ctx domain.RequestContext, spaceID string) (docu return } -// PublicDocuments returns a slice of SitemapDocument records, holding documents in folders of type 1 (entity.TemplateTypePublic). +// PublicDocuments returns a slice of SitemapDocument records +// linking to documents in public spaces. +// These documents can then be seen by search crawlers. func (s Scope) PublicDocuments(ctx domain.RequestContext, orgID string) (documents []doc.SitemapDocument, err error) { err = s.Runtime.Db.Select(&documents, `SELECT d.refid as documentid, d.title as document, d.revised as revised, l.refid as folderid, l.label as folder @@ -217,35 +183,6 @@ func (s Scope) PublicDocuments(ctx domain.RequestContext, orgID string) (documen return } -// DocumentList returns a slice containing the documents available as templates to the client's organisation, in title order. -func (s Scope) DocumentList(ctx domain.RequestContext) (documents []doc.Document, err error) { - err = s.Runtime.Db.Select(&documents, - `SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, - protection, approval, lifecycle, versioned, versionid, versionorder, groupid, created, revised - FROM document - WHERE orgid=? AND template=0 AND lifecycle=1 - AND labelid IN - ( - SELECT refid FROM label WHERE orgid=? - AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN ( - SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view' - UNION ALL - SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role' AND p.location='space' AND p.action='view' AND (r.userid=? OR r.userid='0') - )) - ) - ORDER BY title`, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID) - - if err == sql.ErrNoRows { - err = nil - documents = []doc.Document{} - } - if err != nil { - err = errors.Wrap(err, "select documents list") - } - - return -} - // Update changes the given document record to the new values, updates search information and audits the action. func (s Scope) Update(ctx domain.RequestContext, document doc.Document) (err error) { document.Revised = time.Now().UTC() @@ -255,11 +192,27 @@ func (s Scope) Update(ctx domain.RequestContext, document doc.Document) (err err SET labelid=:labelid, userid=:userid, job=:job, location=:location, title=:title, excerpt=:excerpt, slug=:slug, tags=:tags, template=:template, protection=:protection, approval=:approval, lifecycle=:lifecycle, versioned=:versioned, versionid=:versionid, versionorder=:versionorder, groupid=:groupid, revised=:revised - WHERE orgid=:orgid AND refid=:refid`, + WHERE orgid=:orgid AND refid=:refid`, &document) if err != nil { - err = errors.Wrap(err, "execute update document") + err = errors.Wrap(err, "document.store.Update") + } + + return +} + +// UpdateGroup applies same values to all documents +// with the same group ID. +func (s Scope) UpdateGroup(ctx domain.RequestContext, d doc.Document) (err error) { + _, err = ctx.Transaction.Exec(`UPDATE document SET title=?, excerpt=? WHERE orgid=? AND groupid=?`, + d.Title, d.Excerpt, ctx.OrgID, d.GroupID) + + if err == sql.ErrNoRows { + err = nil + } + if err != nil { + err = errors.Wrap(err, "document.store.UpdateTitle") } return @@ -341,3 +294,28 @@ func (s Scope) DeleteBySpace(ctx domain.RequestContext, spaceID string) (rows in return b.DeleteConstrained(ctx.Transaction, "document", ctx.OrgID, spaceID) } + +// GetVersions returns a slice containing the documents for a given space. +// +// No attempt is made to hide documents that are protected by category +// permissions hence caller must filter as required. +// +// All versions of a document are returned, hence caller must +// decide what to do with them. +func (s Scope) GetVersions(ctx domain.RequestContext, groupID string) (v []doc.Version, err error) { + err = s.Runtime.Db.Select(&v, ` + SELECT versionid, refid as documentid + FROM document + WHERE orgid=? AND groupid=? + ORDER BY versionorder`, ctx.OrgID, groupID) + + if err == sql.ErrNoRows || len(v) == 0 { + err = nil + v = []doc.Version{} + } + if err != nil { + err = errors.Wrap(err, "document.store.GetVersions") + } + + return +} diff --git a/domain/storer.go b/domain/storer.go index 83b5727c..ed1f8405 100644 --- a/domain/storer.go +++ b/domain/storer.go @@ -168,18 +168,17 @@ type AuditStorer interface { type DocumentStorer interface { Add(ctx RequestContext, document doc.Document) (err error) Get(ctx RequestContext, id string) (document doc.Document, err error) - GetAll() (ctx RequestContext, documents []doc.Document, err error) GetBySpace(ctx RequestContext, spaceID string) (documents []doc.Document, err error) - DocumentList(ctx RequestContext) (documents []doc.Document, err error) - Templates(ctx RequestContext) (documents []doc.Document, err error) TemplatesBySpace(ctx RequestContext, spaceID string) (documents []doc.Document, err error) DocumentMeta(ctx RequestContext, id string) (meta doc.DocumentMeta, err error) PublicDocuments(ctx RequestContext, orgID string) (documents []doc.SitemapDocument, err error) Update(ctx RequestContext, document doc.Document) (err error) + UpdateGroup(ctx RequestContext, document doc.Document) (err error) ChangeDocumentSpace(ctx RequestContext, document, space string) (err error) MoveDocumentSpace(ctx RequestContext, id, move string) (err error) Delete(ctx RequestContext, documentID string) (rows int64, err error) DeleteBySpace(ctx RequestContext, spaceID string) (rows int64, err error) + GetVersions(ctx RequestContext, groupID string) (v []doc.Version, err error) } // SettingStorer defines required methods for persisting global and user level settings diff --git a/gui/app/pods/document/index/route.js b/gui/app/pods/document/index/route.js index a913eb99..ff117463 100644 --- a/gui/app/pods/document/index/route.js +++ b/gui/app/pods/document/index/route.js @@ -43,7 +43,8 @@ export default Route.extend(AuthenticatedRouteMixin, { sections: this.modelFor('document').sections, permissions: this.modelFor('document').permissions, roles: this.modelFor('document').roles, - blocks: this.modelFor('document').blocks + blocks: this.modelFor('document').blocks, + versions: this.modelFor('document').versions }); }, @@ -57,10 +58,11 @@ export default Route.extend(AuthenticatedRouteMixin, { controller.set('permissions', model.permissions); controller.set('roles', model.roles); controller.set('blocks', model.blocks); + controller.set('versions', model.versions); }, - activate: function() { + activate: function () { this._super(...arguments); - window.scrollTo(0,0); + window.scrollTo(0, 0); } }); diff --git a/gui/app/pods/document/index/template.hbs b/gui/app/pods/document/index/template.hbs index 74912bc3..3107d076 100644 --- a/gui/app/pods/document/index/template.hbs +++ b/gui/app/pods/document/index/template.hbs @@ -1,6 +1,5 @@ -{{toolbar/nav-bar}} - -{{toolbar/for-document document=document spaces=folders space=folder permissions=permissions roles=roles tab=tab +{{toolbar/nav-bar}} {{toolbar/for-document document=document spaces=folders space=folder + permissions=permissions roles=roles tab=tab versions=versions onDocumentDelete=(action 'onDocumentDelete') onSaveTemplate=(action 'onSaveTemplate') onSaveDocument=(action 'onSaveDocument') @@ -10,9 +9,10 @@