mirror of
https://github.com/documize/community.git
synced 2025-07-20 05:39:42 +02:00
Support for document draft-live publication workflows
This commit is contained in:
parent
bde0091a4a
commit
9235c183c5
14 changed files with 828 additions and 697 deletions
|
@ -52,9 +52,9 @@ Space view.
|
|||
|
||||
## Latest version
|
||||
|
||||
[Community edition: v1.62.0](https://github.com/documize/community/releases)
|
||||
[Community edition: v1.63.0](https://github.com/documize/community/releases)
|
||||
|
||||
[Enterprise edition: v1.64.0](https://documize.com/downloads)
|
||||
[Enterprise edition: v1.65.0](https://documize.com/downloads)
|
||||
|
||||
## OS support
|
||||
|
||||
|
|
6
core/database/scripts/autobuild/db_00022.sql
Normal file
6
core/database/scripts/autobuild/db_00022.sql
Normal file
|
@ -0,0 +1,6 @@
|
|||
/* enterprise edition */
|
||||
|
||||
-- document lifecycle default option
|
||||
ALTER TABLE label ADD COLUMN `lifecycle` INT NOT NULL DEFAULT 1 AFTER `type`;
|
||||
|
||||
-- deprecations
|
|
@ -19,11 +19,8 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/documize/community/model/workflow"
|
||||
|
||||
"github.com/documize/community/core/env"
|
||||
|
||||
api "github.com/documize/community/core/convapi"
|
||||
"github.com/documize/community/core/env"
|
||||
"github.com/documize/community/core/request"
|
||||
"github.com/documize/community/core/response"
|
||||
"github.com/documize/community/core/stringutil"
|
||||
|
@ -37,6 +34,7 @@ import (
|
|||
"github.com/documize/community/model/audit"
|
||||
"github.com/documize/community/model/doc"
|
||||
"github.com/documize/community/model/page"
|
||||
"github.com/documize/community/model/space"
|
||||
uuid "github.com/nu7hatch/gouuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
@ -144,7 +142,16 @@ func (h *Handler) convert(w http.ResponseWriter, r *http.Request, job, folderID
|
|||
return
|
||||
}
|
||||
|
||||
nd, err := processDocument(ctx, h.Runtime, h.Store, h.Indexer, filename, job, folderID, fileResult)
|
||||
// Fetch space where document resides.
|
||||
sp, err := h.Store.Space.Get(ctx, folderID)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
return
|
||||
}
|
||||
|
||||
nd, err := processDocument(ctx, h.Runtime, h.Store, h.Indexer, filename, job, sp, fileResult)
|
||||
if err != nil {
|
||||
ctx.Transaction.Rollback()
|
||||
response.WriteServerError(w, method, err)
|
||||
|
@ -158,16 +165,16 @@ func (h *Handler) convert(w http.ResponseWriter, r *http.Request, job, folderID
|
|||
response.WriteJSON(w, nd)
|
||||
}
|
||||
|
||||
func processDocument(ctx domain.RequestContext, r *env.Runtime, store *domain.Store, indexer indexer.Indexer, filename, job, folderID string, fileResult *api.DocumentConversionResponse) (newDocument doc.Document, err error) {
|
||||
func processDocument(ctx domain.RequestContext, r *env.Runtime, store *domain.Store, indexer indexer.Indexer, filename, job string, sp space.Space, fileResult *api.DocumentConversionResponse) (newDocument doc.Document, err error) {
|
||||
// Convert into database objects
|
||||
document := convertFileResult(filename, fileResult)
|
||||
document.Job = job
|
||||
document.OrgID = ctx.OrgID
|
||||
document.LabelID = folderID
|
||||
document.LabelID = sp.RefID
|
||||
document.UserID = ctx.UserID
|
||||
documentID := uniqueid.Generate()
|
||||
document.RefID = documentID
|
||||
document.Lifecycle = workflow.LifecycleLive
|
||||
document.Lifecycle = sp.Lifecycle
|
||||
|
||||
err = store.Document.Add(ctx, document)
|
||||
if err != nil {
|
||||
|
|
|
@ -253,22 +253,34 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// 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})
|
||||
}
|
||||
// Detect change in document status/lifecycle.
|
||||
if d.Lifecycle != oldDoc.Lifecycle {
|
||||
// Record document being marked as archived.
|
||||
if 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})
|
||||
// Record document being marked as draft.
|
||||
if d.Lifecycle == workflow.LifecycleDraft {
|
||||
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
|
||||
LabelID: d.LabelID,
|
||||
DocumentID: documentID,
|
||||
SourceType: activity.SourceTypeDocument,
|
||||
ActivityType: activity.TypeDraft})
|
||||
}
|
||||
|
||||
// Record document being marked as live.
|
||||
if d.Lifecycle == workflow.LifecycleLive {
|
||||
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
|
||||
LabelID: d.LabelID,
|
||||
DocumentID: documentID,
|
||||
SourceType: activity.SourceTypeDocument,
|
||||
ActivityType: activity.TypePublished})
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Transaction.Commit()
|
||||
|
@ -318,7 +330,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// If approval workflow then only approvers can delete page
|
||||
if doc.Protection == workflow.ProtectionReview {
|
||||
approvers, err := permission.GetDocumentApprovers(ctx, *h.Store, doc.LabelID, doc.RefID)
|
||||
approvers, err := permission.GetUsersWithDocumentPermission(ctx, *h.Store, doc.LabelID, doc.RefID, pm.DocumentApprove)
|
||||
if err != nil {
|
||||
response.WriteForbiddenError(w)
|
||||
h.Runtime.Log.Error(method, err)
|
||||
|
|
|
@ -1440,7 +1440,7 @@ func (h *Handler) workflowPermitsChange(doc dm.Document, ctx domain.RequestConte
|
|||
|
||||
// If approval workflow then only approvers can delete page
|
||||
if doc.Protection == workflow.ProtectionReview {
|
||||
approvers, err := permission.GetDocumentApprovers(ctx, *h.Store, doc.LabelID, doc.RefID)
|
||||
approvers, err := permission.GetUsersWithDocumentPermission(ctx, *h.Store, doc.LabelID, doc.RefID, pm.DocumentApprove)
|
||||
if err != nil {
|
||||
h.Runtime.Log.Error("workflowAllowsChange", err)
|
||||
return false, err
|
||||
|
|
|
@ -227,8 +227,74 @@ 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) {
|
||||
// // 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
|
||||
|
||||
// // Permissions can be assigned to both groups and individual users.
|
||||
// // Pre-fetch users with group membership to help us work out
|
||||
// // if user belongs to a group with permissions.
|
||||
// groupMembers, err := s.Group.GetMembers(ctx)
|
||||
// if err != nil {
|
||||
// return users, err
|
||||
// }
|
||||
|
||||
// // space permissions
|
||||
// sp, err := s.Permission.GetSpacePermissions(ctx, spaceID)
|
||||
// if err != nil {
|
||||
// return users, err
|
||||
// }
|
||||
// // document permissions
|
||||
// dp, err := s.Permission.GetDocumentPermissions(ctx, documentID)
|
||||
// if err != nil {
|
||||
// return users, err
|
||||
// }
|
||||
|
||||
// // all permissions
|
||||
// all := sp
|
||||
// all = append(all, dp...)
|
||||
|
||||
// for _, p := range all {
|
||||
// // only approvers
|
||||
// if p.Action != pm.DocumentApprove {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if p.Who == pm.GroupPermission {
|
||||
// // get group records for just this group
|
||||
// groupRecords := group.FilterGroupRecords(groupMembers, p.WhoID)
|
||||
|
||||
// for i := range groupRecords {
|
||||
// user, err := s.User.Get(ctx, groupRecords[i].UserID)
|
||||
// if err != nil {
|
||||
// return users, err
|
||||
// }
|
||||
// if _, isExisting := prev[user.RefID]; !isExisting {
|
||||
// users = append(users, user)
|
||||
// prev[user.RefID] = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// if p.Who == pm.UserPermission {
|
||||
// user, err := s.User.Get(ctx, p.WhoID)
|
||||
// if err != nil {
|
||||
// return users, err
|
||||
// }
|
||||
|
||||
// if _, isExisting := prev[user.RefID]; !isExisting {
|
||||
// users = append(users, user)
|
||||
// prev[user.RefID] = true
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return users, err
|
||||
// }
|
||||
|
||||
// GetUsersWithDocumentPermission returns list of users who have specified document permission in given space
|
||||
func GetUsersWithDocumentPermission(ctx domain.RequestContext, s domain.Store, spaceID, documentID string, permissionRequired pm.Action) (users []u.User, err error) {
|
||||
users = []u.User{}
|
||||
prev := make(map[string]bool) // used to ensure we only process user once
|
||||
|
||||
|
@ -257,7 +323,7 @@ func GetDocumentApprovers(ctx domain.RequestContext, s domain.Store, spaceID, do
|
|||
|
||||
for _, p := range all {
|
||||
// only approvers
|
||||
if p.Action != pm.DocumentApprove {
|
||||
if p.Action != permissionRequired {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -35,8 +35,8 @@ func (s Scope) Add(ctx domain.RequestContext, sp space.Space) (err error) {
|
|||
sp.Created = time.Now().UTC()
|
||||
sp.Revised = time.Now().UTC()
|
||||
|
||||
_, err = ctx.Transaction.Exec("INSERT INTO label (refid, label, orgid, userid, type, likes, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
sp.RefID, sp.Name, sp.OrgID, sp.UserID, sp.Type, sp.Likes, sp.Created, sp.Revised)
|
||||
_, err = ctx.Transaction.Exec("INSERT INTO label (refid, label, orgid, userid, type, lifecycle, likes, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
sp.RefID, sp.Name, sp.OrgID, sp.UserID, sp.Type, sp.Lifecycle, sp.Likes, sp.Created, sp.Revised)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "unable to execute insert for label")
|
||||
|
@ -47,7 +47,7 @@ func (s Scope) Add(ctx domain.RequestContext, sp space.Space) (err error) {
|
|||
|
||||
// Get returns a space from the store.
|
||||
func (s Scope) Get(ctx domain.RequestContext, id string) (sp space.Space, err error) {
|
||||
err = s.Runtime.Db.Get(&sp, "SELECT id,refid,label as name,orgid,userid,type,likes,created,revised FROM label WHERE orgid=? and refid=?",
|
||||
err = s.Runtime.Db.Get(&sp, "SELECT id,refid,label as name,orgid,userid,type,lifecycle,likes,created,revised FROM label WHERE orgid=? and refid=?",
|
||||
ctx.OrgID, id)
|
||||
|
||||
if err != nil {
|
||||
|
@ -59,7 +59,7 @@ func (s Scope) Get(ctx domain.RequestContext, id string) (sp space.Space, err er
|
|||
|
||||
// PublicSpaces returns spaces that anyone can see.
|
||||
func (s Scope) PublicSpaces(ctx domain.RequestContext, orgID string) (sp []space.Space, err error) {
|
||||
qry := "SELECT id,refid,label as name,orgid,userid,type,likes,created,revised FROM label a where orgid=? AND type=1"
|
||||
qry := "SELECT id,refid,label as name,orgid,userid,type,lifecycle,likes,created,revised FROM label a where orgid=? AND type=1"
|
||||
|
||||
err = s.Runtime.Db.Select(&sp, qry, orgID)
|
||||
|
||||
|
@ -78,7 +78,7 @@ func (s Scope) PublicSpaces(ctx domain.RequestContext, orgID string) (sp []space
|
|||
// Also handles which spaces can be seen by anonymous users.
|
||||
func (s Scope) GetViewable(ctx domain.RequestContext) (sp []space.Space, err error) {
|
||||
q := `
|
||||
SELECT id,refid,label as name,orgid,userid,type,likes,created,revised FROM label
|
||||
SELECT id,refid,label as name,orgid,userid,type,lifecycle,likes,created,revised 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
|
||||
|
@ -109,7 +109,7 @@ func (s Scope) GetViewable(ctx domain.RequestContext) (sp []space.Space, err err
|
|||
// GetAll for admin users!
|
||||
func (s Scope) GetAll(ctx domain.RequestContext) (sp []space.Space, err error) {
|
||||
qry := `
|
||||
SELECT id,refid,label as name,orgid,userid,type,likes,created,revised FROM label
|
||||
SELECT id,refid,label as name,orgid,userid,type,lifecycle,likes,created,revised FROM label
|
||||
WHERE orgid=?
|
||||
ORDER BY name`
|
||||
|
||||
|
@ -130,7 +130,7 @@ func (s Scope) GetAll(ctx domain.RequestContext) (sp []space.Space, err error) {
|
|||
func (s Scope) Update(ctx domain.RequestContext, sp space.Space) (err error) {
|
||||
sp.Revised = time.Now().UTC()
|
||||
|
||||
_, err = ctx.Transaction.NamedExec("UPDATE label SET label=:name, type=:type, userid=:userid, likes=:likes, revised=:revised WHERE orgid=:orgid AND refid=:refid", &sp)
|
||||
_, err = ctx.Transaction.NamedExec("UPDATE label SET label=:name, type=:type, lifecycle=:lifecycle, userid=:userid, likes=:likes, revised=:revised WHERE orgid=:orgid AND refid=:refid", &sp)
|
||||
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, fmt.Sprintf("unable to execute update for label %s", sp.RefID))
|
||||
|
|
|
@ -294,6 +294,14 @@ func (h *Handler) Use(w http.ResponseWriter, r *http.Request) {
|
|||
attachments, _ = h.Store.Attachment.GetAttachmentsWithData(ctx, templateID)
|
||||
}
|
||||
|
||||
// Fetch space where document resides.
|
||||
sp, err := h.Store.Space.Get(ctx, folderID)
|
||||
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)
|
||||
|
@ -308,7 +316,7 @@ func (h *Handler) Use(w http.ResponseWriter, r *http.Request) {
|
|||
d.LabelID = folderID
|
||||
d.UserID = ctx.UserID
|
||||
d.Title = docTitle
|
||||
d.Lifecycle = workflow.LifecycleLive
|
||||
d.Lifecycle = sp.Lifecycle
|
||||
|
||||
err = h.Store.Document.Add(ctx, d)
|
||||
if err != nil {
|
||||
|
|
|
@ -41,7 +41,7 @@ func main() {
|
|||
// product details
|
||||
rt.Product = env.ProdInfo{}
|
||||
rt.Product.Major = "1"
|
||||
rt.Product.Minor = "62"
|
||||
rt.Product.Minor = "63"
|
||||
rt.Product.Patch = "0"
|
||||
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
|
||||
rt.Product.Edition = "Community"
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "documize",
|
||||
"version": "1.62.0",
|
||||
"version": "1.63.0",
|
||||
"description": "The Document IDE",
|
||||
"private": true,
|
||||
"repository": "",
|
||||
|
|
|
@ -18,7 +18,6 @@ Convert test to support i.get() as per toc.js
|
|||
// import { module, test } from 'qunit';
|
||||
// import { setupTest } from 'ember-qunit';
|
||||
// import toc from 'documize/utils/toc';
|
||||
// import models from 'documize/utils/model';
|
||||
|
||||
// module('Unit | Utility | TOC', function (hooks) {
|
||||
// setupTest(hooks);
|
||||
|
|
|
@ -112,6 +112,9 @@ const (
|
|||
// TypeSearched records user performing document keyword search.
|
||||
// Metadata field should contain search terms.
|
||||
TypeSearched Type = 15
|
||||
|
||||
// TypePublished happens when a document is moved from Draft to Live.
|
||||
TypePublished Type = 16
|
||||
)
|
||||
|
||||
// TypeName returns one-work descriptor for activity type
|
||||
|
@ -147,6 +150,8 @@ func TypeName(t Type) string {
|
|||
return "Version"
|
||||
case TypeSearched:
|
||||
return "Search"
|
||||
case TypePublished:
|
||||
return "Publish"
|
||||
}
|
||||
|
||||
return ""
|
||||
|
|
|
@ -13,6 +13,7 @@ package space
|
|||
|
||||
import (
|
||||
"github.com/documize/community/model"
|
||||
"github.com/documize/community/model/workflow"
|
||||
)
|
||||
|
||||
// Space defines a container for documents.
|
||||
|
@ -22,6 +23,10 @@ type Space struct {
|
|||
OrgID string `json:"orgId"`
|
||||
UserID string `json:"userId"`
|
||||
Type Scope `json:"folderType"`
|
||||
|
||||
// Lifecycle stores the default value all new documents are given upon creation.
|
||||
Lifecycle workflow.Lifecycle `json:"lifecycle"`
|
||||
|
||||
// Likes stores the question to ask the user such as 'Did this help you?'.
|
||||
// An empty value tells us liking is not allowed.
|
||||
Likes string `json:"likes"`
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue