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

Support for document versioning

This commit is contained in:
sauls8t 2018-03-19 12:24:58 +00:00
parent bc2cab5721
commit a7a82d9fe3
9 changed files with 158 additions and 110 deletions

View file

@ -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
}

View file

@ -44,9 +44,10 @@ type Handler struct {
Indexer indexer.Indexer
}
// Get is an endpoint that returns the document-level information for a given documentID.
// Get is an endpoint that returns the document-level information for a
// given documentID.
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
method := "document.get"
method := "document.Get"
ctx := domain.GetRequestContext(r)
id := request.Param(r, "documentID")
@ -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"`
}

View file

@ -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
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -94,3 +94,9 @@ type SitemapDocument struct {
Folder string
Revised time.Time
}
// Version points to a version of a document.
type Version struct {
VersionID string `json:"versionId"`
DocumentID string `json:"documentId"`
}