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

Support for document draft-live publication workflows

This commit is contained in:
McMatts 2018-04-20 14:38:35 +01:00
parent bde0091a4a
commit 9235c183c5
14 changed files with 828 additions and 697 deletions

View file

@ -52,9 +52,9 @@ Space view.
## Latest version ## 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 ## OS support

View 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

View file

@ -19,11 +19,8 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/documize/community/model/workflow"
"github.com/documize/community/core/env"
api "github.com/documize/community/core/convapi" api "github.com/documize/community/core/convapi"
"github.com/documize/community/core/env"
"github.com/documize/community/core/request" "github.com/documize/community/core/request"
"github.com/documize/community/core/response" "github.com/documize/community/core/response"
"github.com/documize/community/core/stringutil" "github.com/documize/community/core/stringutil"
@ -37,6 +34,7 @@ import (
"github.com/documize/community/model/audit" "github.com/documize/community/model/audit"
"github.com/documize/community/model/doc" "github.com/documize/community/model/doc"
"github.com/documize/community/model/page" "github.com/documize/community/model/page"
"github.com/documize/community/model/space"
uuid "github.com/nu7hatch/gouuid" uuid "github.com/nu7hatch/gouuid"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -144,7 +142,16 @@ func (h *Handler) convert(w http.ResponseWriter, r *http.Request, job, folderID
return 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 { if err != nil {
ctx.Transaction.Rollback() ctx.Transaction.Rollback()
response.WriteServerError(w, method, err) 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) 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 // Convert into database objects
document := convertFileResult(filename, fileResult) document := convertFileResult(filename, fileResult)
document.Job = job document.Job = job
document.OrgID = ctx.OrgID document.OrgID = ctx.OrgID
document.LabelID = folderID document.LabelID = sp.RefID
document.UserID = ctx.UserID document.UserID = ctx.UserID
documentID := uniqueid.Generate() documentID := uniqueid.Generate()
document.RefID = documentID document.RefID = documentID
document.Lifecycle = workflow.LifecycleLive document.Lifecycle = sp.Lifecycle
err = store.Document.Add(ctx, document) err = store.Document.Add(ctx, document)
if err != nil { if err != nil {

View file

@ -253,22 +253,34 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
} }
} }
// Record document being marked as archived. // Detect change in document status/lifecycle.
if d.Lifecycle != oldDoc.Lifecycle && d.Lifecycle == workflow.LifecycleArchived { if d.Lifecycle != oldDoc.Lifecycle {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{ // Record document being marked as archived.
LabelID: d.LabelID, if d.Lifecycle == workflow.LifecycleArchived {
DocumentID: documentID, h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
SourceType: activity.SourceTypeDocument, LabelID: d.LabelID,
ActivityType: activity.TypeArchived}) DocumentID: documentID,
} SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeArchived})
}
// Record document being marked as draft. // Record document being marked as draft.
if d.Lifecycle != oldDoc.Lifecycle && d.Lifecycle == workflow.LifecycleDraft { if d.Lifecycle == workflow.LifecycleDraft {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{ h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: d.LabelID, LabelID: d.LabelID,
DocumentID: documentID, DocumentID: documentID,
SourceType: activity.SourceTypeDocument, SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDraft}) 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() 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 approval workflow then only approvers can delete page
if doc.Protection == workflow.ProtectionReview { 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 { if err != nil {
response.WriteForbiddenError(w) response.WriteForbiddenError(w)
h.Runtime.Log.Error(method, err) h.Runtime.Log.Error(method, err)

View file

@ -1440,7 +1440,7 @@ func (h *Handler) workflowPermitsChange(doc dm.Document, ctx domain.RequestConte
// If approval workflow then only approvers can delete page // If approval workflow then only approvers can delete page
if doc.Protection == workflow.ProtectionReview { 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 { if err != nil {
h.Runtime.Log.Error("workflowAllowsChange", err) h.Runtime.Log.Error("workflowAllowsChange", err)
return false, err return false, err

View file

@ -227,8 +227,74 @@ func HasPermission(ctx domain.RequestContext, s domain.Store, spaceID string, ac
return false return false
} }
// GetDocumentApprovers returns list of users who can approve given document in given space // // 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) { // 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{} users = []u.User{}
prev := make(map[string]bool) // used to ensure we only process user once 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 { for _, p := range all {
// only approvers // only approvers
if p.Action != pm.DocumentApprove { if p.Action != permissionRequired {
continue continue
} }

View file

@ -35,8 +35,8 @@ func (s Scope) Add(ctx domain.RequestContext, sp space.Space) (err error) {
sp.Created = time.Now().UTC() sp.Created = time.Now().UTC()
sp.Revised = time.Now().UTC() sp.Revised = time.Now().UTC()
_, err = ctx.Transaction.Exec("INSERT INTO label (refid, label, orgid, userid, type, likes, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", _, 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.Likes, sp.Created, sp.Revised) sp.RefID, sp.Name, sp.OrgID, sp.UserID, sp.Type, sp.Lifecycle, sp.Likes, sp.Created, sp.Revised)
if err != nil { if err != nil {
err = errors.Wrap(err, "unable to execute insert for label") 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. // Get returns a space from the store.
func (s Scope) Get(ctx domain.RequestContext, id string) (sp space.Space, err error) { 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) ctx.OrgID, id)
if err != nil { 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. // PublicSpaces returns spaces that anyone can see.
func (s Scope) PublicSpaces(ctx domain.RequestContext, orgID string) (sp []space.Space, err error) { 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) 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. // Also handles which spaces can be seen by anonymous users.
func (s Scope) GetViewable(ctx domain.RequestContext) (sp []space.Space, err error) { func (s Scope) GetViewable(ctx domain.RequestContext) (sp []space.Space, err error) {
q := ` 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=? WHERE orgid=?
AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN ( 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 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! // GetAll for admin users!
func (s Scope) GetAll(ctx domain.RequestContext) (sp []space.Space, err error) { func (s Scope) GetAll(ctx domain.RequestContext) (sp []space.Space, err error) {
qry := ` 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=? WHERE orgid=?
ORDER BY name` 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) { func (s Scope) Update(ctx domain.RequestContext, sp space.Space) (err error) {
sp.Revised = time.Now().UTC() 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 { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute update for label %s", sp.RefID)) err = errors.Wrap(err, fmt.Sprintf("unable to execute update for label %s", sp.RefID))

View file

@ -294,6 +294,14 @@ func (h *Handler) Use(w http.ResponseWriter, r *http.Request) {
attachments, _ = h.Store.Attachment.GetAttachmentsWithData(ctx, templateID) 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() ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil { if err != nil {
response.WriteServerError(w, method, err) response.WriteServerError(w, method, err)
@ -308,7 +316,7 @@ func (h *Handler) Use(w http.ResponseWriter, r *http.Request) {
d.LabelID = folderID d.LabelID = folderID
d.UserID = ctx.UserID d.UserID = ctx.UserID
d.Title = docTitle d.Title = docTitle
d.Lifecycle = workflow.LifecycleLive d.Lifecycle = sp.Lifecycle
err = h.Store.Document.Add(ctx, d) err = h.Store.Document.Add(ctx, d)
if err != nil { if err != nil {

View file

@ -41,7 +41,7 @@ func main() {
// product details // product details
rt.Product = env.ProdInfo{} rt.Product = env.ProdInfo{}
rt.Product.Major = "1" rt.Product.Major = "1"
rt.Product.Minor = "62" rt.Product.Minor = "63"
rt.Product.Patch = "0" rt.Product.Patch = "0"
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch) rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
rt.Product.Edition = "Community" rt.Product.Edition = "Community"

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
{ {
"name": "documize", "name": "documize",
"version": "1.62.0", "version": "1.63.0",
"description": "The Document IDE", "description": "The Document IDE",
"private": true, "private": true,
"repository": "", "repository": "",

View file

@ -18,7 +18,6 @@ Convert test to support i.get() as per toc.js
// import { module, test } from 'qunit'; // import { module, test } from 'qunit';
// import { setupTest } from 'ember-qunit'; // import { setupTest } from 'ember-qunit';
// import toc from 'documize/utils/toc'; // import toc from 'documize/utils/toc';
// import models from 'documize/utils/model';
// module('Unit | Utility | TOC', function (hooks) { // module('Unit | Utility | TOC', function (hooks) {
// setupTest(hooks); // setupTest(hooks);

View file

@ -112,6 +112,9 @@ const (
// TypeSearched records user performing document keyword search. // TypeSearched records user performing document keyword search.
// Metadata field should contain search terms. // Metadata field should contain search terms.
TypeSearched Type = 15 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 // TypeName returns one-work descriptor for activity type
@ -147,6 +150,8 @@ func TypeName(t Type) string {
return "Version" return "Version"
case TypeSearched: case TypeSearched:
return "Search" return "Search"
case TypePublished:
return "Publish"
} }
return "" return ""

View file

@ -13,6 +13,7 @@ package space
import ( import (
"github.com/documize/community/model" "github.com/documize/community/model"
"github.com/documize/community/model/workflow"
) )
// Space defines a container for documents. // Space defines a container for documents.
@ -22,6 +23,10 @@ type Space struct {
OrgID string `json:"orgId"` OrgID string `json:"orgId"`
UserID string `json:"userId"` UserID string `json:"userId"`
Type Scope `json:"folderType"` 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?'. // Likes stores the question to ask the user such as 'Did this help you?'.
// An empty value tells us liking is not allowed. // An empty value tells us liking is not allowed.
Likes string `json:"likes"` Likes string `json:"likes"`