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

WIP document versioning

This commit is contained in:
Harvey Kandola 2018-03-16 18:27:33 +00:00
parent ba52dfa11d
commit 089457f16e
6 changed files with 222 additions and 67 deletions

147
domain/document/document.go Normal file
View 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
}

View file

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

View file

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

View file

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

View file

@ -109,7 +109,7 @@
scrollOffsetX, scrollOffsetX,
scrollOffsetY scrollOffsetY
; ;
// Delect scrollEl // Delect scrollEl
if (scrollParentEl !== rootEl) { if (scrollParentEl !== rootEl) {
@ -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;
@ -212,7 +212,7 @@
options.group = group; options.group = group;
} }
; ;
/** /**
@ -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 }
}; };
@ -555,7 +555,7 @@
_onTouchMove: function (/**TouchEvent*/evt) { _onTouchMove: function (/**TouchEvent*/evt) {
if (tapEvt) { if (tapEvt) {
var options = this.options, var options = this.options,
fallbackTolerance = options.fallbackTolerance, fallbackTolerance = options.fallbackTolerance,
fallbackOffset = options.fallbackOffset, fallbackOffset = options.fallbackOffset,
touch = evt.touches ? evt.touches[0] : evt, touch = evt.touches ? evt.touches[0] : evt,
@ -781,7 +781,7 @@
halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5, halfway = (floating ? (evt.clientX - targetRect.left) / width : (evt.clientY - targetRect.top) / height) > 0.5,
nextSibling = target.nextElementSibling, nextSibling = target.nextElementSibling,
after = false after = false
; ;
if (floating) { if (floating) {
var elTop = dragEl.offsetTop, var elTop = dragEl.offsetTop,
@ -795,7 +795,7 @@
} else { } else {
after = tgTop > elTop; after = tgTop > elTop;
} }
} else if (!isMovingBetweenSortable) { } else if (!isMovingBetweenSortable) {
after = (nextSibling !== dragEl) && !isLong || halfway && isLong; after = (nextSibling !== dragEl) && !isLong || halfway && isLong;
} }
@ -963,30 +963,30 @@
this._nulling(); this._nulling();
}, },
_nulling: function() { _nulling: function () {
rootEl = rootEl =
dragEl = dragEl =
parentEl = parentEl =
ghostEl = ghostEl =
nextEl = nextEl =
cloneEl = cloneEl =
lastDownEl = lastDownEl =
scrollEl = scrollEl =
scrollParentEl = scrollParentEl =
tapEvt = tapEvt =
touchEvt = touchEvt =
moved = moved =
newIndex = newIndex =
lastEl = lastEl =
lastCSS = lastCSS =
putSortable = putSortable =
activeGroup = activeGroup =
Sortable.active = null; Sortable.active = null;
savedInputChecked.forEach(function (el) { savedInputChecked.forEach(function (el) {
el.checked = true; el.checked = true;
@ -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/

View file

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