mirror of
https://github.com/documize/community.git
synced 2025-07-21 22:29:41 +02:00
WIP document versioning
This commit is contained in:
parent
ba52dfa11d
commit
089457f16e
6 changed files with 222 additions and 67 deletions
147
domain/document/document.go
Normal file
147
domain/document/document.go
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
|
||||||
|
//
|
||||||
|
// This software (Documize Community Edition) is licensed under
|
||||||
|
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
|
||||||
|
//
|
||||||
|
// You can operate outside the AGPL restrictions by purchasing
|
||||||
|
// Documize Enterprise Edition and obtaining a commercial license
|
||||||
|
// by contacting <sales@documize.com>.
|
||||||
|
//
|
||||||
|
// https://documize.com
|
||||||
|
|
||||||
|
package document
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/documize/community/core/uniqueid"
|
||||||
|
"github.com/documize/community/domain"
|
||||||
|
"github.com/documize/community/model/category"
|
||||||
|
"github.com/documize/community/model/doc"
|
||||||
|
"github.com/documize/community/model/page"
|
||||||
|
"github.com/documize/community/model/workflow"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FilterCategoryProtected removes documents that cannot be seen by user due to
|
||||||
|
// document cateogory viewing permissions.
|
||||||
|
func FilterCategoryProtected(docs []doc.Document, cats []category.Category, members []category.Member, viewDrafts bool) (filtered []doc.Document) {
|
||||||
|
filtered = []doc.Document{}
|
||||||
|
|
||||||
|
for _, doc := range docs {
|
||||||
|
hasCategory := false
|
||||||
|
canSeeCategory := false
|
||||||
|
skip := false
|
||||||
|
|
||||||
|
// drafts included if user can see them
|
||||||
|
if doc.Lifecycle == workflow.LifecycleDraft && !viewDrafts {
|
||||||
|
skip = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// archived never included
|
||||||
|
if doc.Lifecycle == workflow.LifecycleArchived {
|
||||||
|
skip = true
|
||||||
|
}
|
||||||
|
|
||||||
|
OUTER:
|
||||||
|
|
||||||
|
for _, m := range members {
|
||||||
|
if m.DocumentID == doc.RefID {
|
||||||
|
hasCategory = true
|
||||||
|
for _, cat := range cats {
|
||||||
|
if cat.RefID == m.CategoryID {
|
||||||
|
canSeeCategory = true
|
||||||
|
continue OUTER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !skip && (!hasCategory || canSeeCategory) {
|
||||||
|
filtered = append(filtered, doc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newDocumentID = uniqueid.Generate()
|
||||||
|
doc.RefID = newDocumentID
|
||||||
|
doc.ID = 0
|
||||||
|
doc.Versioned = false
|
||||||
|
doc.VersionID = ""
|
||||||
|
doc.GroupID = ""
|
||||||
|
doc.Template = false
|
||||||
|
|
||||||
|
// Duplicate pages and associated meta
|
||||||
|
pages, err := s.Page.GetPages(ctx, documentID)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "unable to get existing pages")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var pageModel []page.NewPage
|
||||||
|
|
||||||
|
for _, p := range pages {
|
||||||
|
p.DocumentID = newDocumentID
|
||||||
|
p.ID = 0
|
||||||
|
|
||||||
|
meta, err2 := s.Page.GetPageMeta(ctx, p.RefID)
|
||||||
|
if err2 != nil {
|
||||||
|
err = errors.Wrap(err, "unable to get existing pages meta")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pageID := uniqueid.Generate()
|
||||||
|
p.RefID = pageID
|
||||||
|
meta.PageID = pageID
|
||||||
|
meta.DocumentID = newDocumentID
|
||||||
|
|
||||||
|
m := page.NewPage{}
|
||||||
|
m.Page = p
|
||||||
|
m.Meta = meta
|
||||||
|
|
||||||
|
pageModel = append(pageModel, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate attachments
|
||||||
|
attachments, _ := s.Attachment.GetAttachments(ctx, documentID)
|
||||||
|
for i, a := range attachments {
|
||||||
|
a.DocumentID = newDocumentID
|
||||||
|
a.RefID = uniqueid.Generate()
|
||||||
|
a.ID = 0
|
||||||
|
attachments[i] = a
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now create the template: document, attachments, pages and their meta
|
||||||
|
err = s.Document.Add(ctx, doc)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "unable to add copied document")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range attachments {
|
||||||
|
err = s.Attachment.Add(ctx, a)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "unable to add copied attachment")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range pageModel {
|
||||||
|
err = s.Page.Add(ctx, m)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.Wrap(err, "unable to add copied page")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -153,45 +153,10 @@ func (h *Handler) BySpace(w http.ResponseWriter, r *http.Request) {
|
||||||
// sort by title
|
// sort by title
|
||||||
sort.Sort(doc.ByTitle(documents))
|
sort.Sort(doc.ByTitle(documents))
|
||||||
|
|
||||||
// remove documents that cannot be seen due to lack of
|
// remove documents that cannot be seen due to lack of category view/access permission
|
||||||
// category view/access permission
|
|
||||||
filtered := []doc.Document{}
|
|
||||||
cats, err := h.Store.Category.GetBySpace(ctx, spaceID)
|
cats, err := h.Store.Category.GetBySpace(ctx, spaceID)
|
||||||
members, err := h.Store.Category.GetSpaceCategoryMembership(ctx, spaceID)
|
members, err := h.Store.Category.GetSpaceCategoryMembership(ctx, spaceID)
|
||||||
|
filtered := FilterCategoryProtected(documents, cats, members, viewDrafts)
|
||||||
for _, doc := range documents {
|
|
||||||
hasCategory := false
|
|
||||||
canSeeCategory := false
|
|
||||||
skip := false
|
|
||||||
|
|
||||||
// drafts included if user can see them
|
|
||||||
if doc.Lifecycle == workflow.LifecycleDraft && !viewDrafts {
|
|
||||||
skip = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// archived never included
|
|
||||||
if doc.Lifecycle == workflow.LifecycleArchived {
|
|
||||||
skip = true
|
|
||||||
}
|
|
||||||
|
|
||||||
OUTER:
|
|
||||||
|
|
||||||
for _, m := range members {
|
|
||||||
if m.DocumentID == doc.RefID {
|
|
||||||
hasCategory = true
|
|
||||||
for _, cat := range cats {
|
|
||||||
if cat.RefID == m.CategoryID {
|
|
||||||
canSeeCategory = true
|
|
||||||
continue OUTER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !skip && (!hasCategory || canSeeCategory) {
|
|
||||||
filtered = append(filtered, doc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
response.WriteJSON(w, filtered)
|
response.WriteJSON(w, filtered)
|
||||||
}
|
}
|
||||||
|
@ -259,6 +224,24 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
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,
|
||||||
|
DocumentID: documentID,
|
||||||
|
SourceType: activity.SourceTypeDocument,
|
||||||
|
ActivityType: activity.TypeArchived})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
DocumentID: documentID,
|
||||||
|
SourceType: activity.SourceTypeDocument,
|
||||||
|
ActivityType: activity.TypeDraft})
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Transaction.Commit()
|
ctx.Transaction.Commit()
|
||||||
|
|
||||||
h.Store.Audit.Record(ctx, audit.EventTypeDocumentUpdate)
|
h.Store.Audit.Record(ctx, audit.EventTypeDocumentUpdate)
|
||||||
|
|
|
@ -175,7 +175,7 @@ func CanViewDrafts(ctx domain.RequestContext, s domain.Store, spaceID string) bo
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, role := range roles {
|
for _, role := range roles {
|
||||||
if role.RefID == spaceID && role.Location == pm.LocationSpace && role.Scope == pm.ScopeRow &&
|
if role.OrgID == ctx.OrgID && role.RefID == spaceID && role.Location == pm.LocationSpace && role.Scope == pm.ScopeRow &&
|
||||||
pm.ContainsPermission(role.Action, pm.DocumentLifecycle) {
|
pm.ContainsPermission(role.Action, pm.DocumentLifecycle) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -184,6 +184,25 @@ func CanViewDrafts(ctx domain.RequestContext, s domain.Store, spaceID string) bo
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanManageVersion returns if the user has permission to manage versions in space.
|
||||||
|
func CanManageVersion(ctx domain.RequestContext, s domain.Store, spaceID string) bool {
|
||||||
|
roles, err := s.Permission.GetUserSpacePermissions(ctx, spaceID)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, role := range roles {
|
||||||
|
if role.OrgID == ctx.OrgID && role.RefID == spaceID && role.Location == pm.LocationSpace && role.Scope == pm.ScopeRow &&
|
||||||
|
pm.ContainsPermission(role.Action, pm.DocumentVersion) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// HasPermission returns if user can perform specified actions.
|
// HasPermission returns if user can perform specified actions.
|
||||||
func HasPermission(ctx domain.RequestContext, s domain.Store, spaceID string, actions ...pm.Action) bool {
|
func HasPermission(ctx domain.RequestContext, s domain.Store, spaceID string, actions ...pm.Action) bool {
|
||||||
roles, err := s.Permission.GetUserSpacePermissions(ctx, spaceID)
|
roles, err := s.Permission.GetUserSpacePermissions(ctx, spaceID)
|
||||||
|
|
|
@ -28,9 +28,9 @@ export default Model.extend({
|
||||||
approval: attr('number', { defaultValue: 0 }),
|
approval: attr('number', { defaultValue: 0 }),
|
||||||
lifecycle: attr('number', { defaultValue: 1 }),
|
lifecycle: attr('number', { defaultValue: 1 }),
|
||||||
versioned: attr('boolean'),
|
versioned: attr('boolean'),
|
||||||
versionID: attr('string'),
|
versionId: attr('string'),
|
||||||
versionOrder: attr('number', { defaultValue: 0 }),
|
versionOrder: attr('number', { defaultValue: 0 }),
|
||||||
groupID: attr('string'),
|
groupId: attr('string'),
|
||||||
|
|
||||||
// client-side property
|
// client-side property
|
||||||
selected: attr('boolean', { defaultValue: false }),
|
selected: attr('boolean', { defaultValue: false }),
|
||||||
|
|
13
gui/vendor/sortable.js
vendored
13
gui/vendor/sortable.js
vendored
|
@ -160,7 +160,7 @@
|
||||||
scrollOffsetY = vy ? vy * speed : 0;
|
scrollOffsetY = vy ? vy * speed : 0;
|
||||||
scrollOffsetX = vx ? vx * speed : 0;
|
scrollOffsetX = vx ? vx * speed : 0;
|
||||||
|
|
||||||
if ('function' === typeof(scrollCustomFn)) {
|
if ('function' === typeof (scrollCustomFn)) {
|
||||||
return scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt);
|
return scrollCustomFn.call(_this, scrollOffsetX, scrollOffsetY, evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +202,7 @@
|
||||||
var originalGroup = options.group;
|
var originalGroup = options.group;
|
||||||
|
|
||||||
if (!originalGroup || typeof originalGroup != 'object') {
|
if (!originalGroup || typeof originalGroup != 'object') {
|
||||||
originalGroup = {name: originalGroup};
|
originalGroup = { name: originalGroup };
|
||||||
}
|
}
|
||||||
|
|
||||||
group.name = originalGroup.name;
|
group.name = originalGroup.name;
|
||||||
|
@ -261,7 +261,7 @@
|
||||||
fallbackClass: 'sortable-fallback',
|
fallbackClass: 'sortable-fallback',
|
||||||
fallbackOnBody: false,
|
fallbackOnBody: false,
|
||||||
fallbackTolerance: 0,
|
fallbackTolerance: 0,
|
||||||
fallbackOffset: {x: 0, y: 0}
|
fallbackOffset: { x: 0, y: 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -963,7 +963,7 @@
|
||||||
this._nulling();
|
this._nulling();
|
||||||
},
|
},
|
||||||
|
|
||||||
_nulling: function() {
|
_nulling: function () {
|
||||||
rootEl =
|
rootEl =
|
||||||
dragEl =
|
dragEl =
|
||||||
parentEl =
|
parentEl =
|
||||||
|
@ -1455,7 +1455,7 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} catch (err) {}
|
} catch (err) { }
|
||||||
|
|
||||||
// Export utils
|
// Export utils
|
||||||
Sortable.utils = {
|
Sortable.utils = {
|
||||||
|
@ -1489,3 +1489,6 @@
|
||||||
Sortable.version = '1.6.0';
|
Sortable.version = '1.6.0';
|
||||||
return Sortable;
|
return Sortable;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// http://rubaxa.github.io/Sortable/
|
||||||
|
|
|
@ -95,6 +95,9 @@ const (
|
||||||
|
|
||||||
// TypeSentSecureLink records user sending secure document link to email address(es)
|
// TypeSentSecureLink records user sending secure document link to email address(es)
|
||||||
TypeSentSecureLink Type = 12
|
TypeSentSecureLink Type = 12
|
||||||
|
|
||||||
|
// TypeDraft records user marking space/document as draft
|
||||||
|
TypeDraft Type = 13
|
||||||
)
|
)
|
||||||
|
|
||||||
// TypeName returns one-work descriptor for activity type
|
// TypeName returns one-work descriptor for activity type
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue