1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-24 15:49:44 +02:00

Merge pull request #131 from documize/protection-approvals

Document approval and locking
This commit is contained in:
Harvey Kandola 2018-01-28 18:04:00 +00:00 committed by GitHub
commit c77b5c05ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
347 changed files with 81458 additions and 112104 deletions

View file

@ -10,10 +10,10 @@ Documize is an intelligent document environment (IDE) for creating, securing and
## Why should I care?
Because maybe like us, you might be tired of:
Because maybe like us, you're tired of:
* juggling WYSIWYG editors, wiki software and various document related solutions
* playing document related email tennis with contributions, versions and feedback
* juggling WYSIWYG editors, wiki software and other document related solutions
* playing email tennis with documents, contributions, versions and feedback
* sharing not-so-secure folders with external participants
Sound familiar? Read on.
@ -38,17 +38,21 @@ Reusable templates and content blocks.
Documentation related tasking and delegation.
Integrations for embedding SaaS data within documents.
Integrations for embedding SaaS data within documents, zero add-on/marketplace fees.
## What does it look like?
Here is the home screen showing all spaces.
All spaces.
![Documize](screenshot.png "Documize")
![Documize](screenshot-1.png "Documize")
Space view.
![Documize](screenshot-2.png "Documize")
## Latest version
v1.55.0
v1.56.0
## OS support
@ -62,8 +66,8 @@ Documize runs on the following:
Documize is built with the following technologies:
- EmberJS (v2.17.0)
- Go (v1.9.2)
- EmberJS (v2.18.0)
- Go (v1.9.3)
...and supports the following databases:

View file

@ -171,7 +171,7 @@ func setupAccount(rt *env.Runtime, completion onboardRequest, serial string) (er
}
// assign permissions to space
perms := []string{"view", "manage", "own", "doc-add", "doc-edit", "doc-delete", "doc-move", "doc-copy", "doc-template"}
perms := []string{"view", "manage", "own", "doc-add", "doc-edit", "doc-delete", "doc-move", "doc-copy", "doc-template", "doc-approve"}
for _, p := range perms {
sql = fmt.Sprintf("insert into permission (orgid, who, whoid, action, scope, location, refid) values (\"%s\", 'user', \"%s\", \"%s\", 'object', 'space', \"%s\")", orgID, userID, p, labelID)
_, err = runSQL(rt, sql)

View file

@ -1,8 +1,7 @@
## TODO
## PENDING REMOVALS
1. Remove audit table
2. Remove document.layout field ?
NONE
## MYSQL ENCODING

View file

@ -0,0 +1,32 @@
/* enterprise edition */
-- document needs proection and approval columns
ALTER TABLE document ADD COLUMN `protection` INT NOT NULL DEFAULT 0 AFTER `template`;
ALTER TABLE document ADD COLUMN `approval` INT NOT NULL DEFAULT 0 AFTER `protection`;
-- page workflow status
ALTER TABLE page ADD COLUMN `status` INT NOT NULL DEFAULT 0 AFTER `revisions`;
-- links pending changes to another page
ALTER TABLE page ADD COLUMN `relativeid` CHAR(16) DEFAULT '' NOT NULL COLLATE utf8_bin AFTER `status`;
-- useraction captures what is being actioned
ALTER TABLE useraction ADD COLUMN `reftype` CHAR(1) DEFAULT 'D' NOT NULL COLLATE utf8_bin AFTER `iscomplete`;
ALTER TABLE useraction ADD COLUMN `reftypeid` CHAR(16) NOT NULL COLLATE utf8_bin AFTER `reftype`;
-- useractivity usage expansion
ALTER TABLE useractivity ADD COLUMN `documentid` CHAR(16) DEFAULT '' NOT NULL COLLATE utf8_bin AFTER `sourceid`;
ALTER TABLE useractivity ADD COLUMN `pageid` CHAR(16) DEFAULT '' NOT NULL COLLATE utf8_bin AFTER `documentid`;
UPDATE useractivity SET documentid=sourceid WHERE sourcetype=2;
ALTER TABLE useractivity DROP COLUMN `sourceid`;
CREATE INDEX idx_useractivity_1 ON useractivity(orgid,documentid,sourcetype);
CREATE INDEX idx_useractivity_2 ON useractivity(orgid,documentid,userid,sourcetype);
-- clean-up
DELETE FROM categorymember WHERE documentid NOT IN (SELECT refid FROM document);
UPDATE page SET level=1 WHERE level=0;
-- deprecations
DROP TABLE IF EXISTS `audit`;
DROP TABLE IF EXISTS `search_old`;
ALTER TABLE document DROP COLUMN `layout`;

1
core/env/logger.go vendored
View file

@ -15,6 +15,7 @@ package env
// Logger provides the interface for Documize compatible loggers.
type Logger interface {
Info(message string)
Trace(message string)
Error(message string, err error)
// SetDB(l Logger, db *sqlx.DB) Logger
}

View file

@ -32,8 +32,8 @@ func (s Scope) RecordUserActivity(ctx domain.RequestContext, activity activity.U
activity.UserID = ctx.UserID
activity.Created = time.Now().UTC()
_, err = ctx.Transaction.Exec("INSERT INTO useractivity (orgid, userid, labelid, sourceid, sourcetype, activitytype, created) VALUES (?, ?, ?, ?, ?, ?, ?)",
activity.OrgID, activity.UserID, activity.LabelID, activity.SourceID, activity.SourceType, activity.ActivityType, activity.Created)
_, err = ctx.Transaction.Exec("INSERT INTO useractivity (orgid, userid, labelid, documentid, pageid, sourcetype, activitytype, created) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
activity.OrgID, activity.UserID, activity.LabelID, activity.DocumentID, activity.PageID, activity.SourceType, activity.ActivityType, activity.Created)
if err != nil {
err = errors.Wrap(err, "execute record user activity")
@ -44,24 +44,28 @@ func (s Scope) RecordUserActivity(ctx domain.RequestContext, activity activity.U
// GetDocumentActivity returns the metadata for a specified document.
func (s Scope) GetDocumentActivity(ctx domain.RequestContext, id string) (a []activity.DocumentActivity, err error) {
qry := `SELECT a.id, a.created, a.orgid, IFNULL(a.userid, '') AS userid, a.labelid, a.sourceid as documentid, a.activitytype,
IFNULL(u.firstname, 'Anonymous') AS firstname, IFNULL(u.lastname, 'Viewer') AS lastname
qry := `SELECT a.id, DATE(a.created) as created, a.orgid, IFNULL(a.userid, '') AS userid, a.labelid, a.documentid, a.pageid, a.activitytype,
IFNULL(u.firstname, 'Anonymous') AS firstname, IFNULL(u.lastname, 'Viewer') AS lastname,
IFNULL(p.title, '') as pagetitle
FROM useractivity a
LEFT JOIN user u ON a.userid=u.refid
WHERE a.orgid=? AND a.sourceid=? AND a.sourcetype=2
LEFT JOIN page p ON a.pageid=p.refid
WHERE a.orgid=? AND a.documentid=?
AND a.userid != '0' AND a.userid != ''
ORDER BY a.created DESC`
err = s.Runtime.Db.Select(&a, qry, ctx.OrgID, id)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, "select document user activity")
}
if len(a) == 0 {
a = []activity.DocumentActivity{}
}
if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, "select document user activity")
return
}
return
}

View file

@ -234,9 +234,9 @@ func (s Scope) GetSpaceCategoryMembership(ctx domain.RequestContext, spaceID str
SELECT id, refid, orgid, labelid, categoryid, documentid, created, revised FROM categorymember
WHERE orgid=? AND labelid=?
AND labelid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN (
SELECT refid from permission WHERE orgid=? AND who='user' AND whoid=? AND location='space' UNION ALL
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' 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=?
AND p.action='view' AND (r.userid=? OR r.userid='0')
))
ORDER BY documentid`, ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)

View file

@ -222,7 +222,7 @@ func processDocument(ctx domain.RequestContext, r *env.Runtime, store *domain.St
store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: newDocument.LabelID,
SourceID: newDocument.RefID,
DocumentID: newDocument.RefID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeCreated})

View file

@ -16,6 +16,7 @@ import (
"encoding/json"
"io/ioutil"
"net/http"
"sort"
"github.com/documize/community/core/env"
"github.com/documize/community/core/request"
@ -32,6 +33,8 @@ import (
pm "github.com/documize/community/model/permission"
"github.com/documize/community/model/search"
"github.com/documize/community/model/space"
"github.com/documize/community/model/user"
"github.com/documize/community/model/workflow"
)
// Handler contains the runtime information such as logging and database.
@ -77,7 +80,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: document.LabelID,
SourceID: document.RefID,
DocumentID: document.RefID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeRead})
@ -92,27 +95,6 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, document)
}
// Activity is an endpoint returning the activity logs for specified document.
func (h *Handler) Activity(w http.ResponseWriter, r *http.Request) {
method := "document.activity"
ctx := domain.GetRequestContext(r)
id := request.Param(r, "documentID")
if len(id) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
a, err := h.Store.Activity.GetDocumentActivity(ctx, id)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
response.WriteJSON(w, a)
}
// DocumentLinks is an endpoint returning the links for a document.
func (h *Handler) DocumentLinks(w http.ResponseWriter, r *http.Request) {
method := "document.links"
@ -155,7 +137,7 @@ func (h *Handler) BySpace(w http.ResponseWriter, r *http.Request) {
// get complete list of documents
documents, err := h.Store.Document.GetBySpace(ctx, spaceID)
if err != nil && err != sql.ErrNoRows {
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
@ -164,6 +146,8 @@ func (h *Handler) BySpace(w http.ResponseWriter, r *http.Request) {
documents = []doc.Document{}
}
sort.Sort(doc.ByTitle(documents))
// remove documents that cannot be seen due to lack of
// category view/access permission
filtered := []doc.Document{}
@ -279,11 +263,6 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if !permission.CanDeleteDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
@ -291,6 +270,35 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
return
}
// If locked document then no can do
if doc.Protection == workflow.ProtectionLock {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted action on locked document")
return
}
// 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)
if err != nil {
response.WriteForbiddenError(w)
h.Runtime.Log.Error(method, err)
return
}
if !user.Exists(approvers, ctx.UserID) {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted action on document when not approver")
return
}
}
// permission check
if !permission.CanDeleteDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
@ -319,7 +327,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
SourceID: documentID,
DocumentID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDeleted})
@ -411,9 +419,18 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
if len(perms) == 0 {
perms = []pm.Permission{}
}
record := pm.DecodeUserPermissions(perms)
roles, err := h.Store.Permission.GetUserDocumentPermissions(ctx, document.RefID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(roles) == 0 {
roles = []pm.Permission{}
}
rolesRecord := pm.DecodeUserDocumentPermissions(roles)
// links
l, err := h.Store.Link.GetDocumentOutboundLinks(ctx, id)
if len(l) == 0 {
@ -436,9 +453,10 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
sp = []space.Space{}
}
data := documentData{}
data := BulkDocumentData{}
data.Document = document
data.Permissions = record
data.Roles = rolesRecord
data.Links = l
data.Spaces = sp
@ -451,7 +469,7 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: document.LabelID,
SourceID: document.RefID,
DocumentID: document.RefID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeRead})
@ -466,11 +484,12 @@ func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, data)
}
// documentData represents all data associated for a single document.
// BulkDocumentData represents all data associated for a single document.
// Used by FetchDocumentData() bulk data load call.
type documentData struct {
Document doc.Document `json:"document"`
Permissions pm.Record `json:"permissions"`
Spaces []space.Space `json:"folders"`
Links []link.Link `json:"link"`
type BulkDocumentData struct {
Document doc.Document `json:"document"`
Permissions pm.Record `json:"permissions"`
Roles pm.DocumentRecord `json:"roles"`
Spaces []space.Space `json:"folders"`
Links []link.Link `json:"links"`
}

View file

@ -34,8 +34,8 @@ func (s Scope) Add(ctx domain.RequestContext, document doc.Document) (err error)
document.Created = time.Now().UTC()
document.Revised = document.Created // put same time in both fields
_, err = ctx.Transaction.Exec("INSERT INTO document (refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
document.RefID, document.OrgID, document.LabelID, document.UserID, document.Job, document.Location, document.Title, document.Excerpt, document.Slug, document.Tags, document.Template, document.Created, document.Revised)
_, err = ctx.Transaction.Exec("INSERT INTO document (refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
document.RefID, document.OrgID, document.LabelID, document.UserID, document.Job, document.Location, document.Title, document.Excerpt, document.Slug, document.Tags, document.Template, document.Protection, document.Approval, document.Created, document.Revised)
if err != nil {
err = errors.Wrap(err, "execuet insert document")
@ -46,7 +46,7 @@ func (s Scope) Add(ctx domain.RequestContext, document doc.Document) (err error)
// Get fetches the document record with the given id fromt the document table and audits that it has been got.
func (s Scope) Get(ctx domain.RequestContext, id string) (document doc.Document, err error) {
err = s.Runtime.Db.Get(&document, "SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, layout, created, revised FROM document WHERE orgid=? and refid=?",
err = s.Runtime.Db.Get(&document, "SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised FROM document WHERE orgid=? and refid=?",
ctx.OrgID, id)
if err != nil {
@ -90,9 +90,9 @@ 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, with the most recient first.
// 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, layout, created, revised FROM document WHERE orgid=? AND template=0 ORDER BY revised DESC", ctx.OrgID)
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")
@ -106,18 +106,21 @@ func (s Scope) GetAll() (ctx domain.RequestContext, documents []doc.Document, er
// by category permissions -- caller must filter as required.
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, layout, created, revised
SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised
FROM document
WHERE orgid=? AND template=0 AND labelid IN (
SELECT refid FROM label WHERE orgid=? AND refid IN
(SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid=? AND refid IN (
SELECT refid from permission WHERE orgid=? AND who='user' AND whoid=? AND location='space' UNION ALL
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' 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.refid=? AND p.action='view' AND r.userid=?
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)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, "select documents by space")
}
@ -128,14 +131,14 @@ func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (documents
// 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, layout, created, revised FROM document WHERE orgid=? AND template=1
`SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised FROM document WHERE orgid=? AND template=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=? AND location='space'
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space'
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=?
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)
@ -150,14 +153,14 @@ func (s Scope) Templates(ctx domain.RequestContext) (documents []doc.Document, e
// 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,
`SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, layout, created, revised FROM document WHERE orgid=? AND labelid=? AND template=1
`SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised FROM document WHERE orgid=? AND labelid=? AND template=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=? AND location='space'
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space'
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=?
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, spaceID, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)
@ -193,14 +196,14 @@ func (s Scope) PublicDocuments(ctx domain.RequestContext, orgID string) (documen
// 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, layout, created, revised FROM document WHERE orgid=? AND template=0
`SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, protection, approval, created, revised FROM document WHERE orgid=? AND template=0
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=? AND location='space'
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space'
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=?
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)
@ -221,7 +224,7 @@ func (s Scope) DocumentList(ctx domain.RequestContext) (documents []doc.Document
func (s Scope) Update(ctx domain.RequestContext, document doc.Document) (err error) {
document.Revised = time.Now().UTC()
_, err = ctx.Transaction.NamedExec("UPDATE document SET labelid=:labelid, userid=:userid, job=:job, location=:location, title=:title, excerpt=:excerpt, slug=:slug, tags=:tags, template=:template, layout=:layout, revised=:revised WHERE orgid=:orgid AND refid=:refid",
_, err = ctx.Transaction.NamedExec("UPDATE document SET labelid=:labelid, userid=:userid, job=:job, location=:location, title=:title, excerpt=:excerpt, slug=:slug, tags=:tags, template=:template, protection=:protection, approval=:approval, revised=:revised WHERE orgid=:orgid AND refid=:refid",
&document)
if err != nil {
@ -277,6 +280,11 @@ func (s Scope) Delete(ctx domain.RequestContext, documentID string) (rows int64,
return
}
_, err = b.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE from categorymember WHERE documentid=\"%s\" AND orgid=\"%s\"", documentID, ctx.OrgID))
if err != nil {
return
}
return b.DeleteConstrained(ctx.Transaction, "document", ctx.OrgID, documentID)
}

View file

@ -164,9 +164,10 @@ func (s Scope) SearchCandidates(ctx domain.RequestContext, keywords string) (doc
(
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=? AND location='space'
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space'
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=?
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)
@ -201,9 +202,10 @@ func (s Scope) SearchCandidates(ctx domain.RequestContext, keywords string) (doc
(
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=? AND location='space'
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space'
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=?
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 p.title`, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)
@ -238,9 +240,10 @@ func (s Scope) SearchCandidates(ctx domain.RequestContext, keywords string) (doc
(
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=? AND location='space'
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space'
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=?
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 a.filename`, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)

View file

@ -0,0 +1,98 @@
<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{{.Subject}}</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
h1 {
font-weight: 600 !important; margin: 20px 0 5px !important;
}
h2 {
font-weight: 600 !important; margin: 20px 0 5px !important;
}
h3 {
font-weight: 600 !important; margin: 20px 0 5px !important;
}
h4 {
font-weight: 600 !important; margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
width: 100% !important;
}
.content {
padding: 10px !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6; background: #f6f6f6; margin: 0; padding: 0;">
<table class="body-wrap" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background: #f6f6f6; margin: 0; padding: 0;">
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0;" valign="top"></td>
<td class="container" width="600" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto; padding: 0;" valign="top">
<div class="content" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background: #fff; margin: 0; padding: 0; border: 1px solid #e9e9e9;">
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<td class="alert alert-warning" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; color: #fff; font-weight: 500; text-align: center; border-radius: 3px 3px 0 0; background: #1b75bb; margin: 0; padding: 20px;" align="center" valign="top">
Document Approval Role Granted
</td>
</tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; margin: 0; padding: 0;">
<td class="content-wrap" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<p>You are requested to approve all changes to the following document:</p>
<p style="font-weight: bold;">{{.Document}}</p>
<p>{{.Inviter}}</p>
</td>
</tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
<a href="{{.Url}}" class="btn-primary" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; background: #4ccb6a; margin: 0; padding: 0; border-color: #4ccb6a; border-style: solid; border-width: 10px 20px;">View document</a>
</td>
</tr>
<tr style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0; padding: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px; color: #7a8184;" valign="top">
Have any questions? <a href="mailto:team@documize.com" style="color: #7a8184;">Contact Documize</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</td>
<td style="font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0;" valign="top"></td>
</tr>
</table>
</body>
</html>

70
domain/mail/document.go Normal file
View file

@ -0,0 +1,70 @@
// 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
// jshint ignore:start
package mail
import (
"bytes"
"fmt"
"html/template"
"github.com/documize/community/server/web"
)
// DocumentApprover notifies user who has just been granted document approval rights.
func (m *Mailer) DocumentApprover(recipient, inviter, url, document string) {
method := "DocumentApprover"
m.LoadCredentials()
file, err := web.ReadFile("mail/document-approver.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name
if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague"
}
subject := fmt.Sprintf("%s has granted you document approval", inviter)
e := NewEmail()
e.From = m.Credentials.SMTPsender
e.To = []string{recipient}
e.Subject = subject
parameters := struct {
Subject string
Inviter string
Url string
Document string
}{
subject,
inviter,
url,
document,
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
t.Execute(buffer, &parameters)
e.HTML = buffer.Bytes()
err = e.Send(m.GetHost(), m.GetAuth())
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
}
}

View file

@ -14,15 +14,11 @@
package mail
import (
"bytes"
"fmt"
"html/template"
"net/smtp"
"strings"
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
"github.com/documize/community/server/web"
)
// Mailer provides emailing facilities
@ -33,241 +29,6 @@ type Mailer struct {
Credentials Credentials
}
// InviteNewUser invites someone new providing credentials, explaining the product and stating who is inviting them.
func (m *Mailer) InviteNewUser(recipient, inviter, url, username, password string) {
method := "InviteNewUser"
m.LoadCredentials()
file, err := web.ReadFile("mail/invite-new-user.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name
if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague"
}
subject := fmt.Sprintf("%s has invited you to Documize", inviter)
e := NewEmail()
e.From = m.Credentials.SMTPsender
e.To = []string{recipient}
e.Subject = subject
parameters := struct {
Subject string
Inviter string
Url string
Username string
Password string
}{
subject,
inviter,
url,
recipient,
password,
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
t.Execute(buffer, &parameters)
e.HTML = buffer.Bytes()
err = e.Send(m.GetHost(), m.GetAuth())
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
}
}
// InviteExistingUser invites a known user to an organization.
func (m *Mailer) InviteExistingUser(recipient, inviter, url string) {
method := "InviteExistingUser"
m.LoadCredentials()
file, err := web.ReadFile("mail/invite-existing-user.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name
if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague"
}
subject := fmt.Sprintf("%s has invited you to their Documize account", inviter)
e := NewEmail()
e.From = m.Credentials.SMTPsender
e.To = []string{recipient}
e.Subject = subject
parameters := struct {
Subject string
Inviter string
Url string
}{
subject,
inviter,
url,
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
t.Execute(buffer, &parameters)
e.HTML = buffer.Bytes()
err = e.Send(m.GetHost(), m.GetAuth())
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
}
}
// PasswordReset sends a reset email with an embedded token.
func (m *Mailer) PasswordReset(recipient, url string) {
method := "PasswordReset"
m.LoadCredentials()
file, err := web.ReadFile("mail/password-reset.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
subject := "Documize password reset request"
e := NewEmail()
e.From = m.Credentials.SMTPsender //e.g. "Documize <hello@documize.com>"
e.To = []string{recipient}
e.Subject = subject
parameters := struct {
Subject string
Url string
}{
subject,
url,
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
t.Execute(buffer, &parameters)
e.HTML = buffer.Bytes()
err = e.Send(m.GetHost(), m.GetAuth())
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
}
}
// ShareSpaceExistingUser provides an existing user with a link to a newly shared space.
func (m *Mailer) ShareSpaceExistingUser(recipient, inviter, url, folder, intro string) {
method := "ShareSpaceExistingUser"
m.LoadCredentials()
file, err := web.ReadFile("mail/share-space-existing-user.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name
if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague"
}
subject := fmt.Sprintf("%s has shared %s with you", inviter, folder)
e := NewEmail()
e.From = m.Credentials.SMTPsender
e.To = []string{recipient}
e.Subject = subject
parameters := struct {
Subject string
Inviter string
Url string
Folder string
Intro string
}{
subject,
inviter,
url,
folder,
intro,
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
t.Execute(buffer, &parameters)
e.HTML = buffer.Bytes()
err = e.Send(m.GetHost(), m.GetAuth())
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
}
}
// ShareSpaceNewUser invites new user providing Credentials, explaining the product and stating who is inviting them.
func (m *Mailer) ShareSpaceNewUser(recipient, inviter, url, space, invitationMessage string) {
method := "ShareSpaceNewUser"
m.LoadCredentials()
file, err := web.ReadFile("mail/share-space-new-user.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name
if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague"
}
subject := fmt.Sprintf("%s has shared %s with you on Documize", inviter, space)
e := NewEmail()
e.From = m.Credentials.SMTPsender
e.To = []string{recipient}
e.Subject = subject
parameters := struct {
Subject string
Inviter string
Url string
Invitation string
Folder string
}{
subject,
inviter,
url,
invitationMessage,
space,
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
t.Execute(buffer, &parameters)
e.HTML = buffer.Bytes()
err = e.Send(m.GetHost(), m.GetAuth())
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
}
}
// Credentials holds SMTP endpoint and authentication methods
type Credentials struct {
SMTPuserid string

122
domain/mail/space.go Normal file
View file

@ -0,0 +1,122 @@
// 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
// jshint ignore:start
package mail
import (
"bytes"
"fmt"
"html/template"
"github.com/documize/community/server/web"
)
// ShareSpaceExistingUser provides an existing user with a link to a newly shared space.
func (m *Mailer) ShareSpaceExistingUser(recipient, inviter, url, folder, intro string) {
method := "ShareSpaceExistingUser"
m.LoadCredentials()
file, err := web.ReadFile("mail/share-space-existing-user.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name
if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague"
}
subject := fmt.Sprintf("%s has shared %s with you", inviter, folder)
e := NewEmail()
e.From = m.Credentials.SMTPsender
e.To = []string{recipient}
e.Subject = subject
parameters := struct {
Subject string
Inviter string
Url string
Folder string
Intro string
}{
subject,
inviter,
url,
folder,
intro,
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
t.Execute(buffer, &parameters)
e.HTML = buffer.Bytes()
err = e.Send(m.GetHost(), m.GetAuth())
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
}
}
// ShareSpaceNewUser invites new user providing Credentials, explaining the product and stating who is inviting them.
func (m *Mailer) ShareSpaceNewUser(recipient, inviter, url, space, invitationMessage string) {
method := "ShareSpaceNewUser"
m.LoadCredentials()
file, err := web.ReadFile("mail/share-space-new-user.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name
if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague"
}
subject := fmt.Sprintf("%s has shared %s with you on Documize", inviter, space)
e := NewEmail()
e.From = m.Credentials.SMTPsender
e.To = []string{recipient}
e.Subject = subject
parameters := struct {
Subject string
Inviter string
Url string
Invitation string
Folder string
}{
subject,
inviter,
url,
invitationMessage,
space,
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
t.Execute(buffer, &parameters)
e.HTML = buffer.Bytes()
err = e.Send(m.GetHost(), m.GetAuth())
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
}
}

157
domain/mail/user.go Normal file
View file

@ -0,0 +1,157 @@
// 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
// jshint ignore:start
package mail
import (
"bytes"
"fmt"
"html/template"
"github.com/documize/community/server/web"
)
// InviteNewUser invites someone new providing credentials, explaining the product and stating who is inviting them.
func (m *Mailer) InviteNewUser(recipient, inviter, url, username, password string) {
method := "InviteNewUser"
m.LoadCredentials()
file, err := web.ReadFile("mail/invite-new-user.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name
if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague"
}
subject := fmt.Sprintf("%s has invited you to Documize", inviter)
e := NewEmail()
e.From = m.Credentials.SMTPsender
e.To = []string{recipient}
e.Subject = subject
parameters := struct {
Subject string
Inviter string
Url string
Username string
Password string
}{
subject,
inviter,
url,
recipient,
password,
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
t.Execute(buffer, &parameters)
e.HTML = buffer.Bytes()
err = e.Send(m.GetHost(), m.GetAuth())
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
}
}
// InviteExistingUser invites a known user to an organization.
func (m *Mailer) InviteExistingUser(recipient, inviter, url string) {
method := "InviteExistingUser"
m.LoadCredentials()
file, err := web.ReadFile("mail/invite-existing-user.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
// check inviter name
if inviter == "Hello You" || len(inviter) == 0 {
inviter = "Your colleague"
}
subject := fmt.Sprintf("%s has invited you to their Documize account", inviter)
e := NewEmail()
e.From = m.Credentials.SMTPsender
e.To = []string{recipient}
e.Subject = subject
parameters := struct {
Subject string
Inviter string
Url string
}{
subject,
inviter,
url,
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
t.Execute(buffer, &parameters)
e.HTML = buffer.Bytes()
err = e.Send(m.GetHost(), m.GetAuth())
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
}
}
// PasswordReset sends a reset email with an embedded token.
func (m *Mailer) PasswordReset(recipient, url string) {
method := "PasswordReset"
m.LoadCredentials()
file, err := web.ReadFile("mail/password-reset.html")
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to load email template", method), err)
return
}
emailTemplate := string(file)
subject := "Documize password reset request"
e := NewEmail()
e.From = m.Credentials.SMTPsender //e.g. "Documize <hello@documize.com>"
e.To = []string{recipient}
e.Subject = subject
parameters := struct {
Subject string
Url string
}{
subject,
url,
}
buffer := new(bytes.Buffer)
t := template.Must(template.New("emailTemplate").Parse(emailTemplate))
t.Execute(buffer, &parameters)
e.HTML = buffer.Bytes()
err = e.Send(m.GetHost(), m.GetAuth())
if err != nil {
m.Runtime.Log.Error(fmt.Sprintf("%s - unable to send email", method), err)
}
}

View file

@ -31,8 +31,11 @@ import (
"github.com/documize/community/domain/section/provider"
"github.com/documize/community/model/activity"
"github.com/documize/community/model/audit"
"github.com/documize/community/model/doc"
dm "github.com/documize/community/model/doc"
"github.com/documize/community/model/page"
pm "github.com/documize/community/model/permission"
"github.com/documize/community/model/user"
"github.com/documize/community/model/workflow"
htmldiff "github.com/documize/html-diff"
)
@ -53,17 +56,14 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
return
}
// check param
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
// read payload
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
@ -90,6 +90,39 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
return
}
// Check protection and approval process
document, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
// Protect locked
if document.Protection == workflow.ProtectionLock {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted write to locked document")
return
}
// Check edit permission
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
// if document review process then we must mark page as pending
if document.Protection == workflow.ProtectionReview {
if model.Page.RelativeID == "" {
model.Page.Status = workflow.ChangePendingNew
} else {
model.Page.Status = workflow.ChangePending
}
} else {
model.Page.RelativeID = ""
model.Page.Status = workflow.ChangePublished
}
pageID := uniqueid.Generate()
model.Page.RefID = pageID
model.Meta.PageID = pageID
@ -97,7 +130,6 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
model.Meta.UserID = ctx.UserID // required for Render call below
model.Page.SetDefaults()
model.Meta.SetDefaults()
// page.Title = template.HTMLEscapeString(page.Title)
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
@ -134,8 +166,9 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
SourceID: model.Page.DocumentID,
SourceType: activity.SourceTypeDocument,
DocumentID: model.Page.DocumentID,
PageID: model.Page.RefID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeCreated})
h.Store.Audit.Record(ctx, audit.EventTypeSectionAdd)
@ -229,16 +262,11 @@ func (h *Handler) GetPages(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, pages)
}
// Delete deletes a page.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
method := "page.delete"
// GetMeta gets page meta data for specified document page.
func (h *Handler) GetMeta(w http.ResponseWriter, r *http.Request) {
method := "page.meta"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
@ -251,156 +279,31 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
return
}
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
if !permission.CanViewDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
meta, err := h.Store.Page.GetPageMeta(ctx, pageID)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, pageID)
h.Runtime.Log.Info(method + " no record")
meta = page.Meta{}
response.WriteJSON(w, meta)
return
}
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)
if meta.DocumentID != documentID {
response.WriteBadRequestError(w, method, "documentID mismatch")
h.Runtime.Log.Error(method, err)
return
}
page, err := h.Store.Page.Get(ctx, pageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(page.BlockID) > 0 {
h.Store.Block.DecrementUsage(ctx, page.BlockID)
}
_, err = h.Store.Page.Delete(ctx, documentID, pageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
SourceID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDeleted})
h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)
go h.Indexer.DeleteContent(ctx, pageID)
h.Store.Link.DeleteSourcePageLinks(ctx, pageID)
h.Store.Link.MarkOrphanPageLink(ctx, pageID)
h.Store.Page.DeletePageRevisions(ctx, pageID)
ctx.Transaction.Commit()
response.WriteEmpty(w)
}
// DeletePages batch deletes pages.
func (h *Handler) DeletePages(w http.ResponseWriter, r *http.Request) {
method := "page.delete.pages"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, "Bad body")
return
}
model := new([]page.LevelRequest)
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteBadRequestError(w, method, "JSON marshal")
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
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)
h.Runtime.Log.Error(method, err)
return
}
for _, page := range *model {
pageData, err := h.Store.Page.Get(ctx, page.PageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(pageData.BlockID) > 0 {
h.Store.Block.DecrementUsage(ctx, pageData.BlockID)
}
_, err = h.Store.Page.Delete(ctx, documentID, page.PageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
go h.Indexer.DeleteContent(ctx, page.PageID)
h.Store.Link.DeleteSourcePageLinks(ctx, page.PageID)
h.Store.Link.MarkOrphanPageLink(ctx, page.PageID)
h.Store.Page.DeletePageRevisions(ctx, page.PageID)
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
SourceID: documentID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeDeleted})
h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)
ctx.Transaction.Commit()
response.WriteEmpty(w)
response.WriteJSON(w, meta)
}
// Update will persist changed page and note the fact
@ -415,11 +318,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
// if !ctx.Editor {
// response.WriteForbiddenError(w)
// return
// }
// Check params
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
@ -432,11 +331,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
// Read payload
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
@ -458,13 +353,26 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
return
}
// Check protection and approval process
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
if doc.Protection == workflow.ProtectionLock {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted write to locked document")
return
}
// Check edit permission
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
@ -492,6 +400,11 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
skipRevision := false
skipRevision, err = strconv.ParseBool(request.Query(r, "r"))
// We don't track revisions for non-published pages
if model.Page.Status != workflow.ChangePublished {
skipRevision = true
}
err = h.Store.Page.Update(ctx, model.Page, refID, ctx.UserID, skipRevision)
if err != nil {
response.WriteServerError(w, method, err)
@ -510,8 +423,9 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
SourceID: model.Page.DocumentID,
SourceType: activity.SourceTypeDocument,
DocumentID: model.Page.DocumentID,
PageID: model.Page.RefID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeEdited})
h.Store.Audit.Record(ctx, audit.EventTypeSectionUpdate)
@ -562,6 +476,211 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, updatedPage)
}
// Delete a page.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
method := "page.delete"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
pageID := request.Param(r, "pageID")
if len(pageID) == 0 {
response.WriteMissingDataError(w, method, "pageID")
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
p, err := h.Store.Page.Get(ctx, pageID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// you can delete your own pending page
ownPending := false
if (p.Status == workflow.ChangePending || p.Status == workflow.ChangePendingNew) && p.UserID == ctx.UserID {
ownPending = true
}
// if not own page then check permission
if !ownPending {
ok, _ := h.workflowPermitsChange(doc, ctx)
if !ok {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted delete section on locked document")
return
}
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(p.BlockID) > 0 {
h.Store.Block.DecrementUsage(ctx, p.BlockID)
}
_, err = h.Store.Page.Delete(ctx, documentID, pageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: documentID,
PageID: pageID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeDeleted})
go h.Indexer.DeleteContent(ctx, pageID)
h.Store.Link.DeleteSourcePageLinks(ctx, pageID)
h.Store.Link.MarkOrphanPageLink(ctx, pageID)
h.Store.Page.DeletePageRevisions(ctx, pageID)
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)
// Re-level all pages in document
h.LevelizeDocument(ctx, documentID)
response.WriteEmpty(w)
}
// DeletePages batch deletes pages.
func (h *Handler) DeletePages(w http.ResponseWriter, r *http.Request) {
method := "page.delete.pages"
ctx := domain.GetRequestContext(r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, "Bad body")
return
}
model := new([]page.LevelRequest)
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteBadRequestError(w, method, "JSON marshal")
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
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)
h.Runtime.Log.Error(method, err)
return
}
for _, page := range *model {
pageData, err := h.Store.Page.Get(ctx, page.PageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
ownPending := false
if (pageData.Status == workflow.ChangePending || pageData.Status == workflow.ChangePendingNew) && pageData.UserID == ctx.UserID {
ownPending = true
}
// if not own page then check permission
if !ownPending {
ok, _ := h.workflowPermitsChange(doc, ctx)
if !ok {
ctx.Transaction.Rollback()
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted delete section on locked document")
return
}
}
if len(pageData.BlockID) > 0 {
h.Store.Block.DecrementUsage(ctx, pageData.BlockID)
}
_, err = h.Store.Page.Delete(ctx, documentID, page.PageID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
go h.Indexer.DeleteContent(ctx, page.PageID)
h.Store.Link.DeleteSourcePageLinks(ctx, page.PageID)
h.Store.Link.MarkOrphanPageLink(ctx, page.PageID)
h.Store.Page.DeletePageRevisions(ctx, page.PageID)
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: documentID,
PageID: page.PageID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeDeleted})
}
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSectionDelete)
// Re-level all pages in document
h.LevelizeDocument(ctx, documentID)
response.WriteEmpty(w)
}
//**************************************************
// Table of Contents
//**************************************************
// ChangePageSequence will swap page sequence for a given number of pages.
func (h *Handler) ChangePageSequence(w http.ResponseWriter, r *http.Request) {
method := "page.sequence"
@ -578,8 +697,17 @@ func (h *Handler) ChangePageSequence(w http.ResponseWriter, r *http.Request) {
return
}
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
ok, err := h.workflowPermitsChange(doc, ctx)
if !ok {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted to chaneg page sequence on protected document")
return
}
@ -638,9 +766,17 @@ func (h *Handler) ChangePageLevel(w http.ResponseWriter, r *http.Request) {
response.WriteMissingDataError(w, method, "documentID")
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
ok, err := h.workflowPermitsChange(doc, ctx)
if !ok {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted to chaneg page level on protected document")
return
}
@ -684,75 +820,10 @@ func (h *Handler) ChangePageLevel(w http.ResponseWriter, r *http.Request) {
response.WriteEmpty(w)
}
// GetMeta gets page meta data for specified document page.
func (h *Handler) GetMeta(w http.ResponseWriter, r *http.Request) {
method := "page.meta"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
pageID := request.Param(r, "pageID")
if len(pageID) == 0 {
response.WriteMissingDataError(w, method, "pageID")
return
}
if !permission.CanViewDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
return
}
meta, err := h.Store.Page.GetPageMeta(ctx, pageID)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, pageID)
h.Runtime.Log.Info(method + " no record")
meta = page.Meta{}
response.WriteJSON(w, meta)
return
}
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if meta.DocumentID != documentID {
response.WriteBadRequestError(w, method, "documentID mismatch")
h.Runtime.Log.Error(method, err)
return
}
response.WriteJSON(w, meta)
}
//**************************************************
// Copy Move Page
//**************************************************
// GetMoveCopyTargets returns available documents for page copy/move axction.
func (h *Handler) GetMoveCopyTargets(w http.ResponseWriter, r *http.Request) {
method := "page.targets"
ctx := domain.GetRequestContext(r)
var d []doc.Document
var err error
d, err = h.Store.Document.DocumentList(ctx)
if len(d) == 0 {
d = []doc.Document{}
}
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
response.WriteJSON(w, d)
}
// Copy copies page to either same or different document.
func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
method := "page.targets"
@ -790,6 +861,12 @@ func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
return
}
// workflow check
if doc.Protection == workflow.ProtectionLock || doc.Protection == workflow.ProtectionReview {
response.WriteForbiddenError(w)
return
}
p, err := h.Store.Page.Get(ctx, pageID)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, documentID)
@ -850,9 +927,10 @@ func (h *Handler) Copy(w http.ResponseWriter, r *http.Request) {
// Log action against target document
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
SourceID: targetID,
SourceType: activity.SourceTypeDocument,
ActivityType: activity.TypeEdited})
DocumentID: targetID,
PageID: newPageID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeCreated})
h.Store.Audit.Record(ctx, audit.EventTypeSectionCopy)
@ -993,6 +1071,7 @@ func (h *Handler) GetDiff(w http.ResponseWriter, r *http.Request) {
}
res, err := cfg.HTMLdiff([]string{latestHTML, previousHTML})
// res, err := cfg.HTMLdiff([]string{previousHTML, latestHTML})
if err != nil {
response.WriteServerError(w, method, err)
return
@ -1025,12 +1104,20 @@ func (h *Handler) Rollback(w http.ResponseWriter, r *http.Request) {
return
}
if !permission.CanChangeDocument(ctx, *h.Store, documentID) {
response.WriteForbiddenError(w)
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
ok, err := h.workflowPermitsChange(doc, ctx)
if !ok {
response.WriteForbiddenError(w)
h.Runtime.Log.Info("attempted to chaneg page sequence on protected document")
return
}
var err error
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
@ -1058,13 +1145,6 @@ func (h *Handler) Rollback(w http.ResponseWriter, r *http.Request) {
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// roll back page
p.Body = revision.Body
refID := uniqueid.Generate()
@ -1091,8 +1171,9 @@ func (h *Handler) Rollback(w http.ResponseWriter, r *http.Request) {
h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
SourceID: p.DocumentID,
SourceType: activity.SourceTypeDocument,
DocumentID: p.DocumentID,
PageID: p.RefID,
SourceType: activity.SourceTypePage,
ActivityType: activity.TypeReverted})
h.Store.Audit.Record(ctx, audit.EventTypeSectionRollback)
@ -1101,3 +1182,216 @@ func (h *Handler) Rollback(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, p)
}
//**************************************************
// Bulk data fetching (reduce network traffic)
//**************************************************
// FetchPages returns all page data for given document: page, meta data, pending changes.
func (h *Handler) FetchPages(w http.ResponseWriter, r *http.Request) {
method := "page.FetchPages"
ctx := domain.GetRequestContext(r)
model := []page.BulkRequest{}
// check params
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// published pages and new pages awaiting approval
pages, err := h.Store.Page.GetPages(ctx, documentID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(pages) == 0 {
pages = []page.Page{}
}
// unpublished pages
unpublished, err := h.Store.Page.GetUnpublishedPages(ctx, documentID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(unpublished) == 0 {
unpublished = []page.Page{}
}
// meta for all pages
meta, err := h.Store.Page.GetDocumentPageMeta(ctx, documentID, false)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
if len(meta) == 0 {
meta = []page.Meta{}
}
// permissions
perms, err := h.Store.Permission.GetUserSpacePermissions(ctx, doc.LabelID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(perms) == 0 {
perms = []pm.Permission{}
}
permissions := pm.DecodeUserPermissions(perms)
roles, err := h.Store.Permission.GetUserDocumentPermissions(ctx, doc.RefID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(roles) == 0 {
roles = []pm.Permission{}
}
docRoles := pm.DecodeUserDocumentPermissions(roles)
// check document view permissions
if !permissions.SpaceView && !permissions.SpaceManage && !permissions.SpaceOwner {
response.WriteForbiddenError(w)
return
}
// process published pages
for _, p := range pages {
// only send back pages that user can see
process := false
forcePending := false
if process == false && p.Status == workflow.ChangePublished {
process = true
}
if process == false && p.Status == workflow.ChangePendingNew && p.RelativeID == "" && p.UserID == ctx.UserID {
process = true
forcePending = true // user has newly created page which should be treated as pending
}
if process == false && p.Status == workflow.ChangeUnderReview && p.RelativeID == "" && p.UserID == ctx.UserID {
process = true
forcePending = true // user has newly created page which should be treated as pending
}
if process == false && p.Status == workflow.ChangeUnderReview && p.RelativeID == "" && (permissions.DocumentApprove || docRoles.DocumentRoleApprove) {
process = true
forcePending = true // user has newly created page which should be treated as pending
}
if process {
d := page.BulkRequest{}
d.ID = fmt.Sprintf("container-%s", p.RefID)
d.Page = p
for _, m := range meta {
if p.RefID == m.PageID {
d.Meta = m
break
}
}
d.Pending = []page.PendingPage{}
// process pending pages
for _, up := range unpublished {
if up.RelativeID == p.RefID {
ud := page.PendingPage{}
ud.Page = up
for _, m := range meta {
if up.RefID == m.PageID {
ud.Meta = m
break
}
}
owner, err := h.Store.User.Get(ctx, up.UserID)
if err == nil {
ud.Owner = owner.Fullname()
}
d.Pending = append(d.Pending, ud)
}
}
// Handle situation where we need approval, and user has created new page
if forcePending && len(d.Pending) == 0 && doc.Protection == workflow.ProtectionReview {
ud := page.PendingPage{}
ud.Page = d.Page
ud.Meta = d.Meta
owner, err := h.Store.User.Get(ctx, d.Page.UserID)
if err == nil {
ud.Owner = owner.Fullname()
}
d.Pending = append(d.Pending, ud)
}
model = append(model, d)
}
}
// Attach numbers to pages, 1.1, 2.1.1 etc.
t := []page.Page{}
for _, i := range model {
t = append(t, i.Page)
}
page.Numberize(t)
for i, j := range t {
model[i].Page = j
// pending pages get same numbering
for k := range model[i].Pending {
model[i].Pending[k].Page.Numbering = j.Numbering
}
}
// deliver payload
response.WriteJSON(w, model)
}
func (h *Handler) workflowPermitsChange(doc dm.Document, ctx domain.RequestContext) (ok bool, err error) {
if doc.Protection == workflow.ProtectionNone {
if !permission.CanChangeDocument(ctx, *h.Store, doc.RefID) {
h.Runtime.Log.Info("attempted forbidden action on document")
return false, nil
}
return true, nil
}
// If locked document then no can do
if doc.Protection == workflow.ProtectionLock {
h.Runtime.Log.Info("attempted action on locked document")
return false, err
}
// 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)
if err != nil {
h.Runtime.Log.Error("workflowAllowsChange", err)
return false, err
}
if user.Exists(approvers, ctx.UserID) {
h.Runtime.Log.Info("attempted action on document when not approver")
return true, nil
}
return false, nil
}
return true, nil
}

View file

@ -28,6 +28,10 @@ type Scope struct {
Runtime *env.Runtime
}
//**************************************************
// Page Revisions
//**************************************************
// Add inserts the given page into the page table, adds that page to the queue of pages to index and audits that the page has been added.
func (s Scope) Add(ctx domain.RequestContext, model page.NewPage) (err error) {
model.Page.OrgID = ctx.OrgID
@ -54,8 +58,8 @@ func (s Scope) Add(ctx domain.RequestContext, model page.NewPage) (err error) {
model.Page.Sequence = maxSeq * 2
}
_, err = ctx.Transaction.Exec("INSERT INTO page (refid, orgid, documentid, userid, contenttype, pagetype, level, title, body, revisions, sequence, blockid, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
model.Page.RefID, model.Page.OrgID, model.Page.DocumentID, model.Page.UserID, model.Page.ContentType, model.Page.PageType, model.Page.Level, model.Page.Title, model.Page.Body, model.Page.Revisions, model.Page.Sequence, model.Page.BlockID, model.Page.Created, model.Page.Revised)
_, err = ctx.Transaction.Exec("INSERT INTO page (refid, orgid, documentid, userid, contenttype, pagetype, level, title, body, revisions, sequence, blockid, status, relativeid, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
model.Page.RefID, model.Page.OrgID, model.Page.DocumentID, model.Page.UserID, model.Page.ContentType, model.Page.PageType, model.Page.Level, model.Page.Title, model.Page.Body, model.Page.Revisions, model.Page.Sequence, model.Page.BlockID, model.Page.Status, model.Page.RelativeID, model.Page.Created, model.Page.Revised)
_, err = ctx.Transaction.Exec("INSERT INTO pagemeta (pageid, orgid, userid, documentid, rawbody, config, externalsource, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
model.Meta.PageID, model.Meta.OrgID, model.Meta.UserID, model.Meta.DocumentID, model.Meta.RawBody, model.Meta.Config, model.Meta.ExternalSource, model.Meta.Created, model.Meta.Revised)
@ -69,7 +73,7 @@ func (s Scope) Add(ctx domain.RequestContext, model page.NewPage) (err error) {
// Get returns the pageID page record from the page table.
func (s Scope) Get(ctx domain.RequestContext, pageID string) (p page.Page, err error) {
err = s.Runtime.Db.Get(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.refid=?",
err = s.Runtime.Db.Get(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.status, a.relativeid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.refid=?",
ctx.OrgID, pageID)
if err != nil {
@ -79,9 +83,9 @@ func (s Scope) Get(ctx domain.RequestContext, pageID string) (p page.Page, err e
return
}
// GetPages returns a slice containing all the page records for a given documentID, in presentation sequence.
// GetPages returns a slice containing all published page records for a given documentID, in presentation sequence.
func (s Scope) GetPages(ctx domain.RequestContext, documentID string) (p []page.Page, err error) {
err = s.Runtime.Db.Select(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? ORDER BY a.sequence", ctx.OrgID, documentID)
err = s.Runtime.Db.Select(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.status, a.relativeid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? AND (a.status=0 OR ((a.status=4 OR a.status=2) AND a.relativeid='')) ORDER BY a.sequence", ctx.OrgID, documentID)
if err != nil {
err = errors.Wrap(err, "execute get pages")
@ -90,10 +94,21 @@ func (s Scope) GetPages(ctx domain.RequestContext, documentID string) (p []page.
return
}
// GetUnpublishedPages returns a slice containing all published page records for a given documentID, in presentation sequence.
func (s Scope) GetUnpublishedPages(ctx domain.RequestContext, documentID string) (p []page.Page, err error) {
err = s.Runtime.Db.Select(&p, "SELECT a.id, a.refid, a.orgid, a.documentid, a.userid, a.contenttype, a.pagetype, a.level, a.sequence, a.title, a.body, a.revisions, a.blockid, a.status, a.relativeid, a.created, a.revised FROM page a WHERE a.orgid=? AND a.documentid=? AND a.status!=0 AND a.relativeid!='' ORDER BY a.sequence", ctx.OrgID, documentID)
if err != nil {
err = errors.Wrap(err, "execute get unpublished pages")
}
return
}
// GetPagesWithoutContent returns a slice containing all the page records for a given documentID, in presentation sequence,
// but without the body field (which holds the HTML content).
func (s Scope) GetPagesWithoutContent(ctx domain.RequestContext, documentID string) (pages []page.Page, err error) {
err = s.Runtime.Db.Select(&pages, "SELECT id, refid, orgid, documentid, userid, contenttype, pagetype, sequence, level, title, revisions, blockid, created, revised FROM page WHERE orgid=? AND documentid=? ORDER BY sequence", ctx.OrgID, documentID)
err = s.Runtime.Db.Select(&pages, "SELECT id, refid, orgid, documentid, userid, contenttype, pagetype, sequence, level, title, revisions, blockid, status, relativeid, created, revised FROM page WHERE orgid=? AND documentid=? AND status=0 ORDER BY sequence", ctx.OrgID, documentID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("Unable to execute select pages for org %s and document %s", ctx.OrgID, documentID))
@ -119,7 +134,7 @@ func (s Scope) Update(ctx domain.RequestContext, page page.Page, refID, userID s
}
// Update page
_, err = ctx.Transaction.NamedExec("UPDATE page SET documentid=:documentid, level=:level, title=:title, body=:body, revisions=:revisions, sequence=:sequence, revised=:revised WHERE orgid=:orgid AND refid=:refid",
_, err = ctx.Transaction.NamedExec("UPDATE page SET documentid=:documentid, level=:level, title=:title, body=:body, revisions=:revisions, sequence=:sequence, status=:status, relativeid=:relativeid, revised=:revised WHERE orgid=:orgid AND refid=:refid",
&page)
if err != nil {
@ -139,6 +154,27 @@ func (s Scope) Update(ctx domain.RequestContext, page page.Page, refID, userID s
return
}
// Delete deletes the pageID page in the document.
// It then propagates that change into the search table, adds a delete the page revisions history, and audits that the page has been removed.
func (s Scope) Delete(ctx domain.RequestContext, documentID, pageID string) (rows int64, err error) {
b := mysql.BaseQuery{}
rows, err = b.DeleteConstrained(ctx.Transaction, "page", ctx.OrgID, pageID)
if err == nil {
_, _ = b.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM pagemeta WHERE orgid='%s' AND pageid='%s'", ctx.OrgID, pageID))
}
if err == nil {
_, _ = b.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM useraction WHERE orgid='%s' AND reftypeid='%s' AND reftype='P'", ctx.OrgID, pageID))
}
return
}
//**************************************************
// Page Meta
//**************************************************
// UpdateMeta persists meta information associated with a document page.
func (s Scope) UpdateMeta(ctx domain.RequestContext, meta page.Meta, updateUserID bool) (err error) {
meta.Revised = time.Now().UTC()
@ -157,43 +193,6 @@ func (s Scope) UpdateMeta(ctx domain.RequestContext, meta page.Meta, updateUserI
return
}
// UpdateSequence changes the presentation sequence of the pageID page in the document.
// It then propagates that change into the search table and audits that it has occurred.
func (s Scope) UpdateSequence(ctx domain.RequestContext, documentID, pageID string, sequence float64) (err error) {
_, err = ctx.Transaction.Exec("UPDATE page SET sequence=? WHERE orgid=? AND refid=?", sequence, ctx.OrgID, pageID)
if err != nil {
err = errors.Wrap(err, "execute page sequence update")
}
return
}
// UpdateLevel changes the heading level of the pageID page in the document.
// It then propagates that change into the search table and audits that it has occurred.
func (s Scope) UpdateLevel(ctx domain.RequestContext, documentID, pageID string, level int) (err error) {
_, err = ctx.Transaction.Exec("UPDATE page SET level=? WHERE orgid=? AND refid=?", level, ctx.OrgID, pageID)
if err != nil {
err = errors.Wrap(err, "execute page level update")
}
return
}
// Delete deletes the pageID page in the document.
// It then propagates that change into the search table, adds a delete the page revisions history, and audits that the page has been removed.
func (s Scope) Delete(ctx domain.RequestContext, documentID, pageID string) (rows int64, err error) {
b := mysql.BaseQuery{}
rows, err = b.DeleteConstrained(ctx.Transaction, "page", ctx.OrgID, pageID)
if err == nil {
_, _ = b.DeleteWhere(ctx.Transaction, fmt.Sprintf("DELETE FROM pagemeta WHERE orgid='%s' AND pageid='%s'", ctx.OrgID, pageID))
}
return
}
// GetPageMeta returns the meta information associated with the page.
func (s Scope) GetPageMeta(ctx domain.RequestContext, pageID string) (meta page.Meta, err error) {
err = s.Runtime.Db.Get(&meta, "SELECT id, pageid, orgid, userid, documentid, rawbody, coalesce(config,JSON_UNQUOTE('{}')) as config, externalsource, created, revised FROM pagemeta WHERE orgid=? AND pageid=?",
@ -222,9 +221,63 @@ func (s Scope) GetDocumentPageMeta(ctx domain.RequestContext, documentID string,
return
}
/********************
* Page Revisions
********************/
//**************************************************
// Table of contents
//**************************************************
// UpdateSequence changes the presentation sequence of the pageID page in the document.
// It then propagates that change into the search table and audits that it has occurred.
func (s Scope) UpdateSequence(ctx domain.RequestContext, documentID, pageID string, sequence float64) (err error) {
_, err = ctx.Transaction.Exec("UPDATE page SET sequence=? WHERE orgid=? AND refid=?", sequence, ctx.OrgID, pageID)
if err != nil {
err = errors.Wrap(err, "execute page sequence update")
}
return
}
// UpdateLevel changes the heading level of the pageID page in the document.
// It then propagates that change into the search table and audits that it has occurred.
func (s Scope) UpdateLevel(ctx domain.RequestContext, documentID, pageID string, level int) (err error) {
_, err = ctx.Transaction.Exec("UPDATE page SET level=? WHERE orgid=? AND refid=?", level, ctx.OrgID, pageID)
if err != nil {
err = errors.Wrap(err, "execute page level update")
}
return
}
// UpdateLevelSequence changes page level and sequence numbers.
func (s Scope) UpdateLevelSequence(ctx domain.RequestContext, documentID, pageID string, level int, sequence float64) (err error) {
_, err = ctx.Transaction.Exec("UPDATE page SET level=?, sequence=? WHERE orgid=? AND refid=?",
level, sequence, ctx.OrgID, pageID)
if err != nil {
err = errors.Wrap(err, "execute page level/sequence update")
}
return
}
// GetNextPageSequence returns the next sequence numbner to use for a page in given document.
func (s Scope) GetNextPageSequence(ctx domain.RequestContext, documentID string) (maxSeq float64, err error) {
row := s.Runtime.Db.QueryRow("SELECT max(sequence) FROM page WHERE orgid=? AND documentid=?", ctx.OrgID, documentID)
err = row.Scan(&maxSeq)
if err != nil {
maxSeq = 2048
}
maxSeq = maxSeq * 2
return
}
//**************************************************
// Page Revisions
//**************************************************
// GetPageRevision returns the revisionID page revision record.
func (s Scope) GetPageRevision(ctx domain.RequestContext, revisionID string) (revision page.Revision, err error) {
@ -273,17 +326,3 @@ func (s Scope) DeletePageRevisions(ctx domain.RequestContext, pageID string) (ro
return
}
// GetNextPageSequence returns the next sequence numbner to use for a page in given document.
func (s Scope) GetNextPageSequence(ctx domain.RequestContext, documentID string) (maxSeq float64, err error) {
row := s.Runtime.Db.QueryRow("SELECT max(sequence) FROM page WHERE orgid=? AND documentid=?", ctx.OrgID, documentID)
err = row.Scan(&maxSeq)
if err != nil {
maxSeq = 2048
}
maxSeq = maxSeq * 2
return
}

52
domain/page/page.go Normal file
View file

@ -0,0 +1,52 @@
// 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 page
import (
"github.com/documize/community/domain"
"github.com/documize/community/model/page"
)
// LevelizeDocument generates level and sequence numbers for all document sections
func (h *Handler) LevelizeDocument(ctx domain.RequestContext, documentID string) {
method := "page.Levelize"
var err error
ctx.Transaction.Commit()
// Re-level all pages in document
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
h.Runtime.Log.Error(method, err)
return
}
p2, err := h.Store.Page.GetPages(ctx, documentID)
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
return
}
page.Levelize(p2)
page.Sequenize(p2)
for _, i := range p2 {
err = h.Store.Page.UpdateLevelSequence(ctx, documentID, i.RefID, int(i.Level), i.Sequence)
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
return
}
}
ctx.Transaction.Commit()
}

View file

@ -69,7 +69,7 @@ func (h *Handler) SetSpacePermissions(w http.ResponseWriter, r *http.Request) {
return
}
var model = permission.PermissionsModel{}
var model = permission.SpaceRequestModel{}
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteServerError(w, method, err)
@ -398,3 +398,192 @@ func (h *Handler) SetCategoryPermissions(w http.ResponseWriter, r *http.Request)
response.WriteEmpty(w)
}
// GetDocumentPermissions returns permissions for all users for given document.
func (h *Handler) GetDocumentPermissions(w http.ResponseWriter, r *http.Request) {
method := "space.GetDocumentPermissions"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
perms, err := h.Store.Permission.GetDocumentPermissions(ctx, documentID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(perms) == 0 {
perms = []permission.Permission{}
}
userPerms := make(map[string][]permission.Permission)
for _, p := range perms {
userPerms[p.WhoID] = append(userPerms[p.WhoID], p)
}
records := []permission.DocumentRecord{}
for _, up := range userPerms {
records = append(records, permission.DecodeUserDocumentPermissions(up))
}
response.WriteJSON(w, records)
}
// GetUserDocumentPermissions returns permissions for the requested document, for current user.
func (h *Handler) GetUserDocumentPermissions(w http.ResponseWriter, r *http.Request) {
method := "space.GetUserDocumentPermissions"
ctx := domain.GetRequestContext(r)
documentID := request.Param(r, "documentID")
if len(documentID) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
perms, err := h.Store.Permission.GetUserDocumentPermissions(ctx, documentID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(perms) == 0 {
perms = []permission.Permission{}
}
record := permission.DecodeUserDocumentPermissions(perms)
response.WriteJSON(w, record)
}
// SetDocumentPermissions persists specified document permissions
// These permissions override document permissions
func (h *Handler) SetDocumentPermissions(w http.ResponseWriter, r *http.Request) {
method := "space.SetDocumentPermissions"
ctx := domain.GetRequestContext(r)
id := request.Param(r, "documentID")
if len(id) == 0 {
response.WriteMissingDataError(w, method, "documentID")
return
}
doc, err := h.Store.Document.Get(ctx, id)
if err != nil {
response.WriteNotFoundError(w, method, "document not found")
return
}
sp, err := h.Store.Space.Get(ctx, doc.LabelID)
if err != nil {
response.WriteNotFoundError(w, method, "space not found")
return
}
// if !HasPermission(ctx, *h.Store, doc.LabelID, permission.SpaceManage, permission.SpaceOwner) {
// response.WriteForbiddenError(w)
// return
// }
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
h.Runtime.Log.Error(method, err)
return
}
var model = []permission.DocumentRecord{}
err = json.Unmarshal(body, &model)
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)
h.Runtime.Log.Error(method, err)
return
}
// We compare new permisions to what we had before.
// Why? So we can send out space invitation emails.
previousRoles, err := h.Store.Permission.GetDocumentPermissions(ctx, id)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// Store all previous approval roles as map for easy querying
previousRoleUsers := make(map[string]bool)
for _, v := range previousRoles {
if v.Action == permission.DocumentApprove {
previousRoleUsers[v.WhoID] = true
}
}
// Get user who is setting document permissions so we can send out emails with context
inviter, err := h.Store.User.Get(ctx, ctx.UserID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// Nuke all previous permissions for this document
_, err = h.Store.Permission.DeleteDocumentPermissions(ctx, id)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s/d/%s/%s",
sp.RefID, stringutil.MakeSlug(sp.Name), doc.RefID, stringutil.MakeSlug(doc.Title)))
for _, perm := range model {
perm.OrgID = ctx.OrgID
perm.DocumentID = id
// Only persist if there is a role!
if permission.HasAnyDocumentPermission(perm) {
r := permission.EncodeUserDocumentPermissions(perm)
for _, p := range r {
err = h.Store.Permission.AddPermission(ctx, p)
if err != nil {
h.Runtime.Log.Error("set document permission", err)
}
}
// Send email notification to users who have been given document approver role
if _, isExisting := previousRoleUsers[perm.UserID]; !isExisting {
// we skip 'everyone' (user id != empty string)
if perm.UserID != "0" && perm.UserID != "" && perm.DocumentRoleApprove {
existingUser, err := h.Store.User.Get(ctx, perm.UserID)
if err != nil {
response.WriteServerError(w, method, err)
break
}
mailer := mail.Mailer{Runtime: h.Runtime, Store: h.Store, Context: ctx}
go mailer.DocumentApprover(existingUser.Email, inviter.Fullname(), url, doc.Title)
h.Runtime.Log.Info(fmt.Sprintf("%s has made %s document approver for: %s", inviter.Email, existingUser.Email, doc.Title))
}
}
}
}
h.Store.Audit.Record(ctx, audit.EventTypeDocumentPermission)
ctx.Transaction.Commit()
response.WriteEmpty(w)
}

View file

@ -64,7 +64,7 @@ func (s Scope) GetUserSpacePermissions(ctx domain.RequestContext, spaceID string
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
FROM permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.location='space' AND refid=?
AND p.who='role' AND (r.userid=? OR r.userid='0')`,
ctx.OrgID, spaceID, ctx.UserID, ctx.OrgID, spaceID, ctx.OrgID)
ctx.OrgID, spaceID, ctx.UserID, ctx.OrgID, spaceID, ctx.UserID)
if err == sql.ErrNoRows {
err = nil
@ -200,7 +200,7 @@ func (s Scope) GetUserCategoryPermissions(ctx domain.RequestContext, userID stri
UNION ALL
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
FROM permission p LEFT JOIN rolemember r ON p.whoid=r.roleid
WHERE p.orgid=? AND p.location='category' AND p.who='role'`,
WHERE p.orgid=? AND p.location='category' AND p.who='role'`,
ctx.OrgID, userID, ctx.OrgID)
if err == sql.ErrNoRows {
@ -212,3 +212,55 @@ func (s Scope) GetUserCategoryPermissions(ctx domain.RequestContext, userID stri
return
}
// GetUserDocumentPermissions returns document permissions for user.
// Context is used to for user ID.
func (s Scope) GetUserDocumentPermissions(ctx domain.RequestContext, documentID string) (r []permission.Permission, err error) {
err = s.Runtime.Db.Select(&r, `
SELECT id, orgid, who, whoid, action, scope, location, refid
FROM permission WHERE orgid=? AND location='document' AND refid=? AND who='user' AND (whoid=? OR whoid='0')
UNION ALL
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
FROM permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.location='document' AND refid=?
AND p.who='role' AND (r.userid=? OR r.userid='0')`,
ctx.OrgID, documentID, ctx.UserID, ctx.OrgID, documentID, ctx.OrgID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select user document permissions %s", ctx.UserID))
}
return
}
// GetDocumentPermissions returns documents permissions for all users.
func (s Scope) GetDocumentPermissions(ctx domain.RequestContext, documentID string) (r []permission.Permission, err error) {
err = s.Runtime.Db.Select(&r, `
SELECT id, orgid, who, whoid, action, scope, location, refid
FROM permission WHERE orgid=? AND location='document' AND refid=? AND who='user'
UNION ALL
SELECT p.id, p.orgid, p.who, p.whoid, p.action, p.scope, p.location, p.refid
FROM permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.location='document' AND p.refid=?
AND p.who='role'`,
ctx.OrgID, documentID, ctx.OrgID, documentID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select document permissions %s", ctx.UserID))
}
return
}
// DeleteDocumentPermissions removes records from permissions table for given document.
func (s Scope) DeleteDocumentPermissions(ctx domain.RequestContext, documentID string) (rows int64, err error) {
b := mysql.BaseQuery{}
sql := fmt.Sprintf("DELETE FROM permission WHERE orgid='%s' AND location='document' AND refid='%s'", ctx.OrgID, documentID)
return b.DeleteWhere(ctx.Transaction, sql)
}

View file

@ -16,6 +16,7 @@ import (
"github.com/documize/community/domain"
pm "github.com/documize/community/model/permission"
u "github.com/documize/community/model/user"
)
// CanViewSpaceDocument returns if the user has permission to view a document within the specified folder.
@ -153,7 +154,6 @@ func CanViewSpace(ctx domain.RequestContext, s domain.Store, spaceID string) boo
if err != nil {
return false
}
for _, role := range roles {
if role.RefID == spaceID && role.Location == "space" && role.Scope == "object" &&
pm.ContainsPermission(role.Action, pm.SpaceView, pm.SpaceManage, pm.SpaceOwner) {
@ -187,3 +187,40 @@ 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) {
users = []u.User{}
prev := make(map[string]bool) // used to ensure we only process user once
// check space permissions
sp, err := s.Permission.GetSpacePermissions(ctx, spaceID)
for _, p := range sp {
if p.Action == pm.DocumentApprove {
user, err := s.User.Get(ctx, p.WhoID)
if err == nil {
prev[user.RefID] = true
users = append(users, user)
} else {
return users, err
}
}
}
// check document permissions
dp, err := s.Permission.GetDocumentPermissions(ctx, documentID)
for _, p := range dp {
if p.Action == pm.DocumentApprove {
user, err := s.User.Get(ctx, p.WhoID)
if err == nil {
if _, isExisting := prev[user.RefID]; !isExisting {
users = append(users, user)
}
} else {
return users, err
}
}
}
return
}

View file

@ -24,6 +24,7 @@ import (
"github.com/documize/community/model/doc"
"github.com/documize/community/model/page"
"github.com/documize/community/model/search"
"github.com/documize/community/model/workflow"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
@ -93,6 +94,11 @@ func (s Scope) DeleteDocument(ctx domain.RequestContext, ID string) (err error)
// IndexContent adds search index entry for document context.
// Any existing document entries are removed.
func (s Scope) IndexContent(ctx domain.RequestContext, p page.Page) (err error) {
// we do not index pending pages
if p.Status == workflow.ChangePending || p.Status == workflow.ChangePendingNew {
return
}
// remove previous search entries
_, err = ctx.Transaction.Exec("DELETE FROM search WHERE orgid=? AND documentid=? AND itemid=? AND itemtype='page'",
ctx.OrgID, p.DocumentID, p.RefID)
@ -216,7 +222,7 @@ func (s Scope) matchFullText(ctx domain.RequestContext, keywords, itemType strin
(
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=? AND location='space'
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space'
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=?
))
@ -273,9 +279,10 @@ func (s Scope) matchLike(ctx domain.RequestContext, keywords, itemType string) (
(
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=? AND location='space'
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space'
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=?
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')
))
)
AND s.content LIKE ?`

View file

@ -118,7 +118,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
err = h.Store.Permission.AddPermissions(ctx, perm, permission.SpaceOwner, permission.SpaceManage, permission.SpaceView,
permission.DocumentAdd, permission.DocumentCopy, permission.DocumentDelete, permission.DocumentEdit, permission.DocumentMove,
permission.DocumentTemplate)
permission.DocumentTemplate, permission.DocumentApprove)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
@ -642,7 +642,7 @@ func (h *Handler) AcceptInvitation(w http.ResponseWriter, r *http.Request) {
u, err := h.Store.User.GetBySerial(ctx, model.Serial)
if err != nil && err == sql.ErrNoRows {
response.WriteDuplicateError(w, method, "user")
response.WriteNotFoundError(w, method, "user")
h.Runtime.Log.Error(method, err)
return
}
@ -650,6 +650,7 @@ func (h *Handler) AcceptInvitation(w http.ResponseWriter, r *http.Request) {
// AcceptShare does not authenticate the user hence the context needs to set up
ctx.UserID = u.RefID
// Prepare user data
u.Firstname = model.Firstname
u.Lastname = model.Lastname
u.Initials = stringutil.MakeInitials(u.Firstname, u.Lastname)
@ -670,7 +671,6 @@ func (h *Handler) AcceptInvitation(w http.ResponseWriter, r *http.Request) {
}
salt := secrets.GenerateSalt()
err = h.Store.User.UpdateUserPassword(ctx, u.RefID, salt, secrets.GeneratePassword(model.Password, salt))
if err != nil {
ctx.Transaction.Rollback()
@ -679,10 +679,12 @@ func (h *Handler) AcceptInvitation(w http.ResponseWriter, r *http.Request) {
return
}
h.Store.Audit.Record(ctx, audit.EventTypeSpaceJoin)
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSpaceJoin)
// We send back POJO and not fully authenticated user object as
// SSO should take place thereafter
response.WriteJSON(w, u)
}
@ -704,10 +706,10 @@ func (h *Handler) Invite(w http.ResponseWriter, r *http.Request) {
return
}
if sp.UserID != ctx.UserID {
response.WriteForbiddenError(w)
return
}
// if sp.UserID != ctx.UserID {
// response.WriteForbiddenError(w)
// return
// }
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)

View file

@ -94,6 +94,9 @@ type PermissionStorer interface {
GetCategoryPermissions(ctx RequestContext, catID string) (r []permission.Permission, err error)
GetCategoryUsers(ctx RequestContext, catID string) (u []user.User, err error)
GetUserCategoryPermissions(ctx RequestContext, userID string) (r []permission.Permission, err error)
GetUserDocumentPermissions(ctx RequestContext, documentID string) (r []permission.Permission, err error)
GetDocumentPermissions(ctx RequestContext, documentID string) (r []permission.Permission, err error)
DeleteDocumentPermissions(ctx RequestContext, documentID string) (rows int64, err error)
}
// UserStorer defines required methods for user management
@ -245,17 +248,19 @@ type PageStorer interface {
Add(ctx RequestContext, model page.NewPage) (err error)
Get(ctx RequestContext, pageID string) (p page.Page, err error)
GetPages(ctx RequestContext, documentID string) (p []page.Page, err error)
GetUnpublishedPages(ctx RequestContext, documentID string) (p []page.Page, err error)
GetPagesWithoutContent(ctx RequestContext, documentID string) (pages []page.Page, err error)
Update(ctx RequestContext, page page.Page, refID, userID string, skipRevision bool) (err error)
Delete(ctx RequestContext, documentID, pageID string) (rows int64, err error)
GetPageMeta(ctx RequestContext, pageID string) (meta page.Meta, err error)
GetDocumentPageMeta(ctx RequestContext, documentID string, externalSourceOnly bool) (meta []page.Meta, err error)
UpdateMeta(ctx RequestContext, meta page.Meta, updateUserID bool) (err error)
UpdateSequence(ctx RequestContext, documentID, pageID string, sequence float64) (err error)
UpdateLevel(ctx RequestContext, documentID, pageID string, level int) (err error)
Delete(ctx RequestContext, documentID, pageID string) (rows int64, err error)
GetPageMeta(ctx RequestContext, pageID string) (meta page.Meta, err error)
UpdateLevelSequence(ctx RequestContext, documentID, pageID string, level int, sequence float64) (err error)
GetNextPageSequence(ctx RequestContext, documentID string) (maxSeq float64, err error)
GetPageRevision(ctx RequestContext, revisionID string) (revision page.Revision, err error)
GetPageRevisions(ctx RequestContext, pageID string) (revisions []page.Revision, err error)
GetDocumentRevisions(ctx RequestContext, documentID string) (revisions []page.Revision, err error)
GetDocumentPageMeta(ctx RequestContext, documentID string, externalSourceOnly bool) (meta []page.Meta, err error)
DeletePageRevisions(ctx RequestContext, pageID string) (rows int64, err error)
GetNextPageSequence(ctx RequestContext, documentID string) (maxSeq float64, err error)
}

View file

@ -502,7 +502,7 @@ func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
newPassword := string(body)
// can only update your own account unless you are an admin
if !ctx.Administrator || (!ctx.Administrator && userID != ctx.UserID) {
if !ctx.Administrator && userID != ctx.UserID {
response.WriteForbiddenError(w)
return
}

View file

@ -97,7 +97,7 @@ func (s Scope) GetByToken(ctx domain.RequestContext, token string) (u user.User,
// This occurs when we you share a folder with a new user and they have to complete
// the onboarding process.
func (s Scope) GetBySerial(ctx domain.RequestContext, serial string) (u user.User, err error) {
err = s.Runtime.Db.Get("SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE salt=?", serial)
err = s.Runtime.Db.Get(&u, "SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE salt=?", serial)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute user select by serial %s", serial))

View file

@ -33,7 +33,7 @@ func main() {
rt := env.Runtime{}
// wire up logging implementation
rt.Log = logging.NewLogger()
rt.Log = logging.NewLogger(false)
// wire up embedded web assets handler
web.Embed = embed.NewEmbedder()
@ -41,7 +41,7 @@ func main() {
// product details
rt.Product = env.ProdInfo{}
rt.Product.Major = "1"
rt.Product.Minor = "55"
rt.Product.Minor = "56"
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"

View file

@ -25,8 +25,9 @@ import (
// Logger is how we log.
type Logger struct {
db *sqlx.DB
log *log.Logger
db *sqlx.DB
log *log.Logger
trace bool // shows Info() entries
}
// Info logs message.
@ -34,6 +35,13 @@ func (l Logger) Info(message string) {
l.log.Println(message)
}
// Trace logs message if tracing enabled.
func (l Logger) Trace(message string) {
if l.trace {
l.log.Println(message)
}
}
// Error logs error with message.
func (l Logger) Error(message string, err error) {
l.log.Println(message)
@ -56,13 +64,14 @@ func (l Logger) SetDB(logger env.Logger, db *sqlx.DB) env.Logger {
}
// NewLogger returns initialized logging instance.
func NewLogger() env.Logger {
func NewLogger(trace bool) env.Logger {
l := log.New(os.Stdout, "", 0)
l.SetOutput(os.Stdout)
// log.SetOutput(os.Stdout)
var logger Logger
logger.log = l
logger.trace = trace
return logger
}

File diff suppressed because one or more lines are too long

View file

@ -4,17 +4,46 @@ module.exports = {
ecmaVersion: 2017,
sourceType: 'module'
},
extends: 'eslint:recommended',
plugins: [
'ember'
],
extends: [
'eslint:recommended',
'plugin:ember/recommended'
],
env: {
browser: true,
jquery: true,
qunit: true,
embertest: true
browser: true
},
rules: {
},
overrides: [
// node files
{
files: [
'testem.js',
'ember-cli-build.js',
'config/**/*.js'
],
parserOptions: {
sourceType: 'script',
ecmaVersion: 2015
},
env: {
browser: false,
node: true
}
},
// test files
{
files: ['tests/**/*.js'],
excludedFiles: ['tests/dummy/**/*.js'],
env: {
embertest: true
}
}
],
globals: {
"$": true,
"is": true,
"_": true,
"tinymce": true,
@ -31,5 +60,5 @@ module.exports = {
"Keycloak": true,
"slug": true,
"interact": true
}
}
};

View file

@ -4,18 +4,26 @@ node_js:
- "6"
sudo: false
dist: trusty
addons:
chrome: stable
cache:
directories:
- $HOME/.npm
yarn: true
env:
global:
# See https://git.io/vdao3 for details.
- JOBS=1
before_install:
- npm config set spin false
- npm install -g phantomjs-prebuilt
- phantomjs --version
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH=$HOME/.yarn/bin:$PATH
install:
- npm install
- yarn install --non-interactive
script:
- npm test
- yarn lint:js
- yarn test

View file

@ -27,16 +27,20 @@ export default Component.extend({
KeycloakPublicKeyError: empty('keycloakConfig.publicKey'),
KeycloakAdminUserError: empty('keycloakConfig.adminUser'),
KeycloakAdminPasswordError: empty('keycloakConfig.adminPassword'),
keycloakConfig: {
url: '',
realm: '',
clientId: '',
publicKey: '',
adminUser: '',
adminPassword: '',
group: '',
disableLogout: false,
defaultPermissionAddSpace: false
init() {
this._super(...arguments);
this.keycloakConfig = {
url: '',
realm: '',
clientId: '',
publicKey: '',
adminUser: '',
adminPassword: '',
group: '',
disableLogout: false,
defaultPermissionAddSpace: false
};
},
didReceiveAttrs() {
@ -139,7 +143,7 @@ export default Component.extend({
});
} else {
if (data.authProvider === this.get('appMeta.authProvider')) {
// this.showNotification(response.message);
this.showNotification(response.message);
} else {
this.get('onChange')(data);
}

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import { empty } from '@ember/object/computed';
import Component from '@ember/component';

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import Component from '@ember/component';
import { schedule, debounce } from '@ember/runloop';
import AuthProvider from '../../mixins/auth';
@ -17,13 +18,17 @@ import ModalMixin from '../../mixins/modal';
export default Component.extend(AuthProvider, ModalMixin, {
editUser: null,
deleteUser: null,
password: {},
filter: '',
filteredUsers: [],
selectedUsers: [],
hasSelectedUsers: false,
showDeleteDialog: false,
init() {
this._super(...arguments);
this.password = {};
this.filteredUsers = [];
this.selectedUsers = [];
},
didReceiveAttrs() {
this._super(...arguments);
@ -74,25 +79,29 @@ export default Component.extend(AuthProvider, ModalMixin, {
toggleActive(id) {
let user = this.users.findBy("id", id);
user.set('active', !user.get('active'));
this.attrs.onSave(user);
let cb = this.get('onSave');
cb(user);
},
toggleEditor(id) {
let user = this.users.findBy("id", id);
user.set('editor', !user.get('editor'));
this.attrs.onSave(user);
let cb = this.get('onSave');
cb(user);
},
toggleAdmin(id) {
let user = this.users.findBy("id", id);
user.set('admin', !user.get('admin'));
this.attrs.onSave(user);
let cb = this.get('onSave');
cb(user);
},
toggleUsers(id) {
let user = this.users.findBy("id", id);
user.set('viewUsers', !user.get('viewUsers'));
this.attrs.onSave(user);
let cb = this.get('onSave');
cb(user);
},
onShowEdit(id) {
@ -135,11 +144,14 @@ export default Component.extend(AuthProvider, ModalMixin, {
$('#edit-user-modal').modal('hide');
$('#edit-user-modal').modal('dispose');
this.attrs.onSave(user);
let cb = this.get('onSave');
cb(user);
if (is.not.empty(password.password) && is.not.empty(password.confirmation) &&
password.password === password.confirmation) {
this.attrs.onPassword(user, password.password);
let pw = this.get('onPassword');
pw(user, password.password);
}
},
@ -153,7 +165,9 @@ export default Component.extend(AuthProvider, ModalMixin, {
this.set('selectedUsers', []);
this.set('hasSelectedUsers', false);
this.attrs.onDelete(this.get('deleteUser.id'));
let cb = this.get('onDelete');
cb(this.get('deleteUser.id'));
return true;
},
@ -162,7 +176,8 @@ export default Component.extend(AuthProvider, ModalMixin, {
let su = this.get('selectedUsers');
su.forEach(userId => {
this.attrs.onDelete(userId);
let cb = this.get('onDelete');
cb(userId);
});
this.set('selectedUsers', []);

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import { empty, and } from '@ember/object/computed';
import Component from '@ember/component';
import { isEmpty } from '@ember/utils';
@ -16,7 +17,6 @@ import { get, set } from '@ember/object';
import AuthProvider from '../../mixins/auth';
export default Component.extend(AuthProvider, {
newUser: { firstname: "", lastname: "", email: "", active: true },
firstnameEmpty: empty('newUser.firstname'),
lastnameEmpty: empty('newUser.lastname'),
emailEmpty: empty('newUser.email'),
@ -24,6 +24,11 @@ export default Component.extend(AuthProvider, {
hasLastnameEmptyError: and('lastnameEmpty', 'lastnameError'),
hasEmailEmptyError: and('emailEmpty', 'emailError'),
init() {
this._super(...arguments);
this.newUser = { firstname: "", lastname: "", email: "", active: true };
},
actions: {
add() {
if (isEmpty(this.get('newUser.firstname'))) {

View file

@ -44,11 +44,13 @@ export default Component.extend({
actions: {
onCancel() {
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onAction(page, meta) {
this.attrs.onAction(page, meta);
let cb = this.get('onAction');
cb(page, meta);
}
}
});

View file

@ -20,25 +20,17 @@ export default Component.extend(ModalMixin, TooltipMixin, {
link: service(),
linkName: '',
selection: null,
tab1Selected: true,
tab2Selected: false,
tab3Selected: false,
showSections: computed('tab1Selected', function() { return this.get('tab1Selected'); }),
showAttachments: computed('tab2Selected', function() { return this.get('tab2Selected'); }),
showSearch: computed('tab3Selected', function() { return this.get('tab3Selected'); }),
keywords: '',
matches: {
documents: [],
pages: [],
attachments: []
},
hasMatches: computed('matches', function () {
let m = this.get('matches');
return m.documents.length || m.pages.length || m.attachments.length;
}),
modalId: computed('page', function() { return '#content-linker-modal-' + this.get('page.id'); }),
showModal: false,
onToggle: function() {
@ -63,17 +55,23 @@ export default Component.extend(ModalMixin, TooltipMixin, {
this.modalOpen(modalId, {show: true});
}.observes('showModal'),
init() {
this._super(...arguments);
this.matches = {
documents: [],
pages: [],
attachments: []
};
},
didRender() {
this._super(...arguments);
this.renderTooltips();
},
willDestroyElement() {
this._super(...arguments);
this.removeTooltips();
this.modalClose(this.get('modalId'));
},

View file

@ -18,11 +18,13 @@ export default Component.extend({
actions: {
onCancel() {
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onAction(page, meta) {
this.attrs.onAction(page, meta);
let cb = this.get('onAction');
cb(page, meta);
}
}
});

View file

@ -9,7 +9,9 @@
//
// https://documize.com
import $ from 'jquery';
import { empty } from '@ember/object/computed';
import { computed } from '@ember/object';
import { schedule } from '@ember/runloop';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
@ -22,6 +24,19 @@ export default Component.extend({
hasNameError: empty('docName'),
hasExcerptError: empty('docExcerpt'),
canEdit: computed('permssions', 'document', function() {
let constants = this.get('constants');
let permissions = this.get('permissions');
if (permissions.get('documentEdit') && this.get('document.protection') === constants.ProtectionType.None) {
return true;
} else if (permissions.get('documentApprove') && this.get('document.protection') === constants.ProtectionType.Review) {
return true;
}
return false;
}),
keyUp(e) {
if (e.keyCode === 27) { // escape key
this.send('onCancel');
@ -48,7 +63,8 @@ export default Component.extend({
this.set('document.excerpt', this.get('docExcerpt'));
this.set('editMode', false);
this.attrs.onSaveDocument(this.get('document'));
let cb = this.get('onSaveDocument');
cb(this.get('document'));
},
onCancel() {

View file

@ -9,7 +9,9 @@
//
// https://documize.com
import $ from 'jquery';
import { computed } from '@ember/object';
import { notEmpty } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { A } from "@ember/array"
@ -19,12 +21,9 @@ export default Component.extend({
documentService: service('document'),
categoryService: service('category'),
sessionService: service('session'),
maxTags: 3,
categories: A([]),
newCategory: '',
tagz: A([]),
tagzModal: A([]),
newTag: '',
showCategoryModal: false,
hasCategories: computed('categories', function() {
return this.get('categories').length > 0;
@ -36,9 +35,63 @@ export default Component.extend({
return this.get('permissions.spaceOwner') || this.get('permissions.spaceManage');
}),
maxTags: 3,
tagz: A([]),
tagzModal: A([]),
newTag: '',
contributorMsg: '',
approverMsg: '',
userChanges: notEmpty('contributorMsg'),
isApprover: computed('permissions', function() {
return this.get('permissions.documentApprove');
}),
changeControlMsg: computed('document.protection', function() {
let p = this.get('document.protection');
let constants = this.get('constants');
let msg = '';
switch (p) {
case constants.ProtectionType.None:
msg = constants.ProtectionType.NoneLabel;
break;
case constants.ProtectionType.Lock:
msg = constants.ProtectionType.LockLabel;
break;
case constants.ProtectionType.Review:
msg = constants.ProtectionType.ReviewLabel;
break;
}
return msg;
}),
approvalMsg: computed('document.{protection,approval}', function() {
let p = this.get('document.protection');
let a = this.get('document.approval');
let constants = this.get('constants');
let msg = '';
if (p === constants.ProtectionType.Review) {
switch (a) {
case constants.ApprovalType.Anybody:
msg = constants.ApprovalType.AnybodyLabel;
break;
case constants.ApprovalType.Majority:
msg = constants.ApprovalType.MajorityLabel;
break;
case constants.ApprovalType.Unanimous:
msg = constants.ApprovalType.UnanimousLabel;
break;
}
}
return msg;
}),
didReceiveAttrs() {
this._super(...arguments);
this.load();
this.workflowStatus();
},
didInsertElement() {
@ -100,6 +153,39 @@ export default Component.extend({
this.set('tagz', A(tagz));
},
workflowStatus() {
let pages = this.get('pages');
let contributorMsg = '';
let userPendingCount = 0;
let userReviewCount = 0;
let userRejectedCount = 0;
let approverMsg = '';
let approverPendingCount = 0;
let approverReviewCount = 0;
let approverRejectedCount = 0;
pages.forEach((item) => {
if (item.get('userHasChangePending')) userPendingCount+=1;
if (item.get('userHasChangeAwaitingReview')) userReviewCount+=1;
if (item.get('userHasChangeRejected')) userRejectedCount+=1;
if (item.get('changePending')) approverPendingCount+=1;
if (item.get('changeAwaitingReview')) approverReviewCount+=1;
if (item.get('changeRejected')) approverRejectedCount+=1;
});
if (userPendingCount > 0 || userReviewCount > 0 || userRejectedCount > 0) {
let label = userPendingCount === 1 ? 'change' : 'changes';
contributorMsg = `${userPendingCount} ${label} progressing, ${userReviewCount} awaiting review, ${userRejectedCount} rejected`;
}
this.set('contributorMsg', contributorMsg);
if (approverPendingCount > 0 || approverReviewCount > 0 || approverRejectedCount > 0) {
let label = approverPendingCount === 1 ? 'change' : 'changes';
approverMsg = `${approverPendingCount} ${label} progressing, ${approverReviewCount} awaiting review, ${approverRejectedCount} rejected`;
}
this.set('approverMsg', approverMsg);
},
actions: {
onShowCategoryModal() {
this.set('showCategoryModal', true);
@ -179,13 +265,15 @@ export default Component.extend({
let doc = this.get('document');
doc.set('tags', save);
this.attrs.onSaveDocument(doc);
let cb = this.get('onSaveDocument');
cb(doc);
this.load();
this.set('newTag', '');
$('#document-tags-modal').modal('hide');
$('#document-tags-modal').modal('dispose');
},
}
}
});

View file

@ -10,55 +10,63 @@
// https://documize.com
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import NotifierMixin from '../../mixins/notifier';
import TooltipMixin from '../../mixins/tooltip';
export default Component.extend(NotifierMixin, TooltipMixin, {
export default Component.extend(TooltipMixin, {
documentService: service('document'),
sectionService: service('section'),
editMode: false,
editPage: null,
editMeta: null,
didReceiveAttrs() {
this._super(...arguments);
if (this.get('isDestroyed') || this.get('isDestroying')) return;
if (this.get('isDestroyed') || this.get('isDestroying')) {
return;
if (this.get('session.authenticated')) {
this.workflow();
}
let page = this.get('page');
if (this.get('toEdit') === this.get('page.id') && this.get('permissions.documentEdit')) this.send('onEdit');
},
this.get('documentService').getPageMeta(page.get('documentId'), page.get('id')).then((meta) => {
if (this.get('isDestroyed') || this.get('isDestroying')) {
return;
}
this.set('meta', meta);
if (this.get('toEdit') === this.get('page.id') && this.get('permissions.documentEdit')) {
this.send('onEdit');
}
});
workflow() {
this.set('editPage', this.get('page'));
this.set('editMeta', this.get('meta'));
},
actions: {
onSavePage(page, meta) {
this.set('page', page);
this.set('meta', meta);
let constants = this.get('constants');
if (this.get('document.protection') === constants.ProtectionType.Review) {
if (this.get('page.status') === constants.ChangeState.Published) {
page.set('relativeId', this.get('page.id'));
}
if (this.get('page.status') === constants.ChangeState.PendingNew) {
page.set('relativeId', '');
}
}
this.set('editMode', false);
this.get('onSavePage')(page, meta);
let cb = this.get('onSavePage');
cb(page, meta);
},
onSavePageAsBlock(block) {
this.attrs.onSavePageAsBlock(block);
let cb = this.get('onSavePageAsBlock');
cb(block);
},
onCopyPage(documentId) {
this.attrs.onCopyPage(this.get('page.id'), documentId);
let cb = this.get('onCopyPage');
cb(this.get('page.id'), documentId);
},
onMovePage(documentId) {
this.attrs.onMovePage(this.get('page.id'), documentId);
let cb = this.get('onMovePage');
cb(this.get('page.id'), documentId);
},
onDeletePage(deleteChildren) {
@ -74,16 +82,14 @@ export default Component.extend(NotifierMixin, TooltipMixin, {
children: deleteChildren
};
this.attrs.onDeletePage(params);
let cb = this.get('onDeletePage');
cb(params);
},
// Calculate if user is editing page or a pending change as per approval process
onEdit() {
if (this.get('editMode')) {
return;
}
if (this.get('editMode')) return;
this.get('toEdit', '');
// this.set('pageId', this.get('page.id'));
this.set('editMode', true);
},

View file

@ -9,101 +9,118 @@
//
// https://documize.com
import $ from 'jquery';
import { computed } from '@ember/object';
import { schedule } from '@ember/runloop'
import { schedule } from '@ember/runloop';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import tocUtil from '../../utils/toc';
import TooltipMixin from '../../mixins/tooltip';
export default Component.extend({
export default Component.extend(TooltipMixin, {
documentService: service('document'),
document: {},
folder: {},
pages: [],
currentPageId: '',
state: {
actionablePage: false,
upDisabled: true,
downDisabled: true,
indentDisabled: true,
outdentDisabled: true
},
isDesktop: false,
emptyState: computed('pages', function () {
return this.get('pages.length') === 0;
}),
isDesktop: false,
canEdit: computed('permssions', 'document', function() {
let constants = this.get('constants');
let permissions = this.get('permissions');
let authenticated = this.get('session.authenticated');
let notEmpty = this.get('pages.length') > 0;
if (notEmpty && authenticated && permissions.get('documentEdit') && this.get('document.protection') === constants.ProtectionType.None) return true;
if (notEmpty && authenticated && permissions.get('documentApprove') && this.get('document.protection') === constants.ProtectionType.Review) return true;
return false;
}),
init() {
this._super(...arguments);
this.state = {
actionablePage: false,
upDisabled: true,
downDisabled: true,
indentDisabled: true,
outdentDisabled: true,
pageId: ''
};
},
didReceiveAttrs() {
this._super(...arguments);
this.setState(this.get('currentPageId'));
let cp = this.get('currentPageId');
this.setState(is.empty(cp) ? '' : cp);
},
didInsertElement() {
this._super(...arguments);
this.setSize();
this.eventBus.subscribe('documentPageAdded', this, 'onDocumentPageAdded');
this.eventBus.subscribe('resized', this, 'onResize');
this.attachResizer();
this.eventBus.subscribe('resized', this, 'setSize');
this.setSize();
this.renderTooltips();
},
willDestroyElement() {
this._super(...arguments);
this.eventBus.unsubscribe('documentPageAdded');
this.eventBus.unsubscribe('resized');
let t = '#doc-toc';
if (interact.isSet(t)) {
interact(t).unset();
}
if (interact.isSet(t)) interact(t).unset();
this.removeTooltips();
},
onDocumentPageAdded(pageId) {
this.send('onEntryClick', pageId);
this.setSize();
},
onResize() {
onDocumentPageAdded(pageId) { // eslint-disable-line no-unused-vars
this.setSize();
},
setSize() {
let isDesktop = $(window).width() >= 1800;
this.set('isDesktop', isDesktop);
schedule('afterRender', () => {
let isDesktop = $(window).width() >= 1800;
this.set('isDesktop', isDesktop);
if (isDesktop) {
let h = $(window).height() - $("#nav-bar").height() - 140;
$("#doc-toc").css('max-height', h);
let i = $("#doc-view").offset();
if (is.not.undefined(i)) {
let l = i.left - 100;
if (l > 350) l = 350;
$("#doc-toc").width(l);
if (isDesktop) {
let h = $(window).height() - $("#nav-bar").height() - 140;
$("#doc-toc").css('max-height', h);
let i = $("#doc-view").offset();
if (is.not.undefined(i)) {
let l = i.left - 100;
if (l > 350) l = 350;
$("#doc-toc").width(l);
$("#doc-toc").css({
'display': 'inline-block',
'position': 'fixed',
'width': l+'px',
'height': 'auto',
'transform': '',
});
this.attachResizer();
}
} else {
$("#doc-toc").css({
'display': 'inline-block',
'position': 'fixed',
'width': l+'px',
'height': 'auto',
'transform': '',
'display': 'block',
'position': 'relative',
'width': '100%',
'height': '500px',
'transform': 'none',
});
}
} else {
$("#doc-toc").css({
'display': 'block',
'position': 'relative',
'width': '100%',
'height': '500px',
'transform': 'none',
});
}
});
},
attachResizer() {
schedule('afterRender', () => {
let t = '#doc-toc';
if (interact.isSet(t)) {
interact(t).unset();
}
interact('#doc-toc')
.draggable({
autoScroll: true,
@ -166,15 +183,12 @@ export default Component.extend({
},
// Controls what user can do with the toc (left sidebar)
// Identifies the target pages
setState(pageId) {
this.set('currentPageId', pageId);
let toc = this.get('pages');
let page = _.findWhere(toc, { id: pageId });
let state = tocUtil.getState(toc, page);
let page = _.find(toc, function(i) { return i.get('page.id') === pageId; });
let state = tocUtil.getState(toc, is.not.undefined(page) ? page.get('page') : page);
if (!this.get('permissions.documentEdit') || is.empty(pageId)) {
if (!this.get('canEdit')) {
state.actionablePage = false;
state.upDisabled = state.downDisabled = state.indentDisabled = state.outdentDisabled = true;
}
@ -185,77 +199,77 @@ export default Component.extend({
actions: {
// Page up -- above pages shunt down
pageUp() {
if (this.get('state.upDisabled')) {
let state = this.get('state');
if (state.upDisabled || this.get('document.protection') !== this.get('constants').ProtectionType.None) {
return;
}
let state = this.get('state');
let pages = this.get('pages');
let page = _.findWhere(pages, { id: this.get('currentPageId') });
let pendingChanges = tocUtil.moveUp(state, pages, page);
let page = _.find(pages, function(i) { return i.get('page.id') === state.pageId; });
if (is.not.undefined(page)) page = page.get('page');
let pendingChanges = tocUtil.moveUp(state, pages, page);
if (pendingChanges.length > 0) {
this.attrs.onPageSequenceChange(pendingChanges);
let cb = this.get('onPageSequenceChange');
cb(state.pageId, pendingChanges);
}
},
// Move down -- pages below shift up
pageDown() {
if (this.get('state.downDisabled')) {
return;
}
if (!this.get('canEdit')) return;
let state = this.get('state');
var pages = this.get('pages');
var page = _.findWhere(pages, { id: this.get('currentPageId') });
let pendingChanges = tocUtil.moveDown(state, pages, page);
let pages = this.get('pages');
let page = _.find(pages, function(i) { return i.get('page.id') === state.pageId; });
if (is.not.undefined(page)) page = page.get('page');
let pendingChanges = tocUtil.moveDown(state, pages, page);
if (pendingChanges.length > 0) {
this.attrs.onPageSequenceChange(pendingChanges);
let cb = this.get('onPageSequenceChange');
cb(state.pageId, pendingChanges);
}
},
// Indent -- changes a page from H2 to H3, etc.
pageIndent() {
if (this.get('state.indentDisabled')) {
return;
}
if (!this.get('canEdit')) return;
let state = this.get('state');
var pages = this.get('pages');
var page = _.findWhere(pages, { id: this.get('currentPageId') });
let pendingChanges = tocUtil.indent(state, pages, page);
let pages = this.get('pages');
let page = _.find(pages, function(i) { return i.get('page.id') === state.pageId; });
if (is.not.undefined(page)) page = page.get('page');
let pendingChanges = tocUtil.indent(state, pages, page);
if (pendingChanges.length > 0) {
this.attrs.onPageLevelChange(pendingChanges);
let cb = this.get('onPageLevelChange');
cb(state.pageId, pendingChanges);
}
},
// Outdent -- changes a page from H3 to H2, etc.
pageOutdent() {
if (this.get('state.outdentDisabled')) {
return;
}
if (!this.get('canEdit')) return;
let state = this.get('state');
var pages = this.get('pages');
var page = _.findWhere(pages, { id: this.get('currentPageId') });
let pendingChanges = tocUtil.outdent(state, pages, page);
let pages = this.get('pages');
let page = _.find(pages, function(i) { return i.get('page.id') === state.pageId; });
if (is.not.undefined(page)) page = page.get('page');
let pendingChanges = tocUtil.outdent(state, pages, page);
if (pendingChanges.length > 0) {
this.attrs.onPageLevelChange(pendingChanges);
let cb = this.get('onPageLevelChange');
cb(state.pageId, pendingChanges);
}
},
onEntryClick(id) {
if (id !== '') {
let jumpTo = "#page-" + id;
this.set('tab', 'content');
if (!$(jumpTo).inView()) {
$(jumpTo).velocity("scroll", { duration: 250, offset: -100 });
}
this.setState(id);
}
onGotoPage(id) {
if (id === '') return;
this.setState(id);
let cb = this.get('onShowPage');
cb(id);
}
}
});

View file

@ -9,66 +9,76 @@
//
// https://documize.com
import Component from '@ember/component';
import $ from 'jquery';
import { computed } from '@ember/object';
import { debounce } from '@ember/runloop';
import { inject as service } from '@ember/service';
import { A } from "@ember/array"
import ModalMixin from '../../mixins/modal';
import Component from '@ember/component';
export default Component.extend(ModalMixin, {
documentService: service('document'),
searchService: service('search'),
router: service(),
deleteChildren: false,
blockTitle: "",
blockExcerpt: "",
documentList: A([]), //includes the current document
documentListOthers: A([]), //excludes the current document
hasMenuPermissions: computed('permissions', function() {
let permissions = this.get('permissions');
return permissions.get('documentDelete') || permissions.get('documentCopy') ||
permissions.get('documentMove') || permissions.get('documentTemplate');
canEdit: false,
canDelete: false,
canMove: false,
docSearchFilter: '',
onKeywordChange: function () {
debounce(this, this.searchDocs, 750);
}.observes('docSearchFilter'),
emptySearch: computed('docSearchResults', function() {
return this.get('docSearchResults.length') === 0;
}),
hasMenuPermissions: computed('permissions', 'userPendingItem', 'canEdit', 'canMove', 'canDelete', function() {
let permissions = this.get('permissions');
return permissions.get('documentCopy') || permissions.get('documentTemplate') ||
this.get('canEdit') || this.get('canMove') || this.get('canDelete');
}),
init() {
this._super(...arguments);
this.docSearchResults = [];
},
didReceiveAttrs() {
this._super(...arguments);
// Fetch document targets once
if (this.get('documentList').length > 0) {
return;
}
this.modalInputFocus('#publish-page-modal-' + this.get('page.id'), '#block-title-' + this.get('page.id'));
this.load();
},
load() {
let permissions = this.get('permissions');
if (permissions.get('documentMove') || permissions.get('documentCopy')) {
this.get('documentService').getPageMoveCopyTargets().then((d) => {
let me = this.get('document');
this.set('canEdit', permissions.get('documentEdit'));
this.set('canDelete', permissions.get('documentDelete'));
this.set('canMove', permissions.get('documentMove'));
},
d.forEach((i) => {
i.set('selected', false);
});
if (this.get('isDestroyed') || this.get('isDestroying')) {
return;
}
this.set('documentList', A(d));
this.set('documentListOthers', A(d.filter((item) => item.get('id') !== me.get('id'))));
});
}
searchDocs() {
let payload = { keywords: this.get('docSearchFilter').trim(), doc: true };
if (payload.keywords.length == 0) return;
this.get('searchService').find(payload).then((response)=> {
this.set('docSearchResults', response);
});
},
actions: {
onEdit() {
this.attrs.onEdit();
let page = this.get('page');
if (page.get('pageType') == this.get('constants').PageType.Tab) {
this.get('router').transitionTo('document.section', page.get('id'));
} else {
let cb = this.get('onEdit');
cb();
}
},
onDeletePage() {
this.attrs.onDeletePage(this.get('deleteChildren'));
this.load();
let cb = this.get('onDeletePage');
cb(this.get('deleteChildren'));
this.modalClose('#delete-page-modal-' + this.get('page.id'));
},
@ -103,57 +113,60 @@ export default Component.extend(ModalMixin, {
externalSource: pm.get('externalSource')
};
this.attrs.onSavePageAsBlock(block);
let cb = this.get('onSavePageAsBlock');
cb(block);
this.set('blockTitle', '');
this.set('blockExcerpt', '');
$(titleElem).removeClass('is-invalid');
$(excerptElem).removeClass('is-invalid');
this.load();
this.modalClose('#publish-page-modal-' + this.get('page.id'));
let refresh = this.get('refresh');
refresh();
});
},
onSelectSearchResult(documentId) {
let results = this.get('docSearchResults');
results.forEach((d) => {
d.set('selected', d.get('documentId') === documentId);
});
this.set('docSearchResults', results);
},
onCopyPage() {
// can't proceed if no data
if (this.get('documentList.length') === 0) {
return;
}
let item = this.get('docSearchResults').findBy('selected', true);
let documentId = is.not.undefined(item) ? item.get('documentId') : '';
let targetDocumentId = this.get('documentList').findBy('selected', true).get('id');
// fall back to self
if (is.null(targetDocumentId)) {
targetDocumentId = this.get('document.id');
}
this.attrs.onCopyPage(targetDocumentId);
this.load();
if (is.empty(documentId)) return;
this.modalClose('#copy-page-modal-' + this.get('page.id'));
let cb = this.get('onCopyPage');
cb(documentId);
let refresh = this.get('refresh');
refresh();
},
onMovePage() {
// can't proceed if no data
if (this.get('documentListOthers.length') === 0) {
return;
}
let item = this.get('docSearchResults').findBy('selected', true);
let documentId = is.not.undefined(item) ? item.get('documentId') : '';
let targetDocumentId = this.get('documentListOthers').findBy('selected', true).get('id');
if (is.empty(documentId)) return;
// fall back to first document
if (is.null(targetDocumentId)) {
targetDocumentId = this.get('documentListOthers')[0].get('id');
}
this.attrs.onMovePage(targetDocumentId);
this.load();
// can't move into self
if (documentId === this.get('document.id')) return;
this.modalClose('#move-page-modal-' + this.get('page.id'));
}
let cb = this.get('onMovePage');
cb(documentId);
let refresh = this.get('refresh');
refresh();
},
}
});

View file

@ -18,24 +18,25 @@ export default Component.extend({
documentService: service('document'),
appMeta: service(),
hasAttachments: notEmpty('files'),
deleteAttachment: {
id: "",
name: "",
},
canShow: computed('permissions', 'files', function() {
return this.get('files.length') > 0 || this.get('permissions.documentEdit');
canEdit: computed('permissions', 'document.protection', function() {
return this.get('document.protection') !== this.get('constants').ProtectionType.Lock && this.get('permissions.documentEdit');
}),
showDialog: false,
init() {
this._super(...arguments);
this.getAttachments();
this.deleteAttachment = {
id: "",
name: "",
};
},
didInsertElement() {
this._super(...arguments);
if (!this.get('permissions.documentEdit')) {
if (!this.get('permissions.documentEdit') || this.get('document.protection') === this.get('constants').ProtectionType.Lock) {
return;
}
@ -78,10 +79,6 @@ export default Component.extend({
this.set('drop', dzone);
},
willDestroyElement() {
this._super(...arguments);
},
getAttachments() {
this.get('documentService').getAttachments(this.get('document.id')).then((files) => {
this.set('files', files);

View file

@ -9,11 +9,14 @@
//
// https://documize.com
import $ from 'jquery';
import { notEmpty, empty } from '@ember/object/computed';
import { schedule } from '@ember/runloop';
import { inject as service } from '@ember/service';
import { computed } from '@ember/object';
import Component from '@ember/component';
import TooltipMixin from '../../mixins/tooltip';
import models from '../../utils/model';
export default Component.extend(TooltipMixin, {
documentService: service('document'),
@ -28,11 +31,15 @@ export default Component.extend(TooltipMixin, {
toEdit: '',
showDeleteBlockDialog: false,
deleteBlockId: '',
canEdit: computed('permissions', 'document.protection', function() {
let canEdit = this.get('document.protection') !== this.get('constants').ProtectionType.Lock && this.get('permissions.documentEdit');
didReceiveAttrs() {
this._super(...arguments);
this.loadBlocks();
},
if (canEdit) this.setupAddWizard();
return canEdit;
}),
hasBlocks: computed('blocks', function() {
return this.get('blocks.length') > 0;
}),
didRender() {
this._super(...arguments);
@ -41,20 +48,22 @@ export default Component.extend(TooltipMixin, {
didInsertElement() {
this._super(...arguments);
this.setupAddWizard();
if (this.attrs.onGotoPage !== null) {
this.attrs.onGotoPage(this.get('pageId'));
if (this.get('session.authenticated')) {
this.setupAddWizard();
this.renderTooltips();
}
this.renderTooltips();
this.jumpToSection();
},
willDestroyElement() {
this._super(...arguments);
$('.start-section:not(.start-section-empty-state)').off('.hoverIntent');
this.removeTooltips();
if (this.get('session.authenticated')) {
$('.start-section:not(.start-section-empty-state)').off('.hoverIntent');
this.removeTooltips();
}
},
contentLinkHandler() {
@ -73,7 +82,9 @@ export default Component.extend(TooltipMixin, {
link.orphan = true;
} else {
if (link.linkType === "section") {
self.attrs.onGotoPage(link.targetId);
self.get('currentPageId', link.targetId)
self.jumpToSection();
// self.get('browser').scrollTo(`#page-${link.targetId}`);
}
}
}
@ -105,80 +116,112 @@ export default Component.extend(TooltipMixin, {
},
addSection(model) {
// calculate sequence of page (position in document)
let sequence = 0;
let level = 1;
let beforePage = this.get('beforePage');
let constants = this.get('constants');
// calculate sequence of page (position in document)
if (is.not.null(beforePage)) {
level = beforePage.get('level');
// get any page before the beforePage so we can insert this new section between them
let index = _.findIndex(this.get('pages'), function(p) { return p.get('id') === beforePage.get('id'); });
let index = _.findIndex(this.get('pages'), function(item) { return item.get('page.id') === beforePage.get('id'); });
if (index !== -1) {
let beforeBeforePage = this.get('pages')[index-1];
if (is.not.undefined(beforeBeforePage)) {
sequence = (beforePage.get('sequence') + beforeBeforePage.get('sequence')) / 2;
sequence = (beforePage.get('sequence') + beforeBeforePage.get('page.sequence')) / 2;
} else {
sequence = beforePage.get('sequence') / 2;
}
model.page.set('sequence', sequence);
model.page.set('level', level);
}
}
model.page.sequence = sequence;
model.page.level = level;
if (this.get('document.protection') === constants.ProtectionType.Review) {
model.page.set('status', model.page.get('relativeId') === '' ? constants.ChangeState.PendingNew : constants.ChangeState.Pending);
}
this.send('onHideSectionWizard');
return this.get('onInsertSection')(model);
},
loadBlocks() {
if (is.not.undefined(this.get('folder'))) {
this.get('sectionService').getSpaceBlocks(this.get('folder.id')).then((blocks) => {
if (this.get('isDestroyed') || this.get('isDestroying')) {
return;
}
this.set('blocks', blocks);
this.set('hasBlocks', blocks.get('length') > 0);
});
jumpToSection() {
let cp = this.get('currentPageId');
if (is.not.empty(cp) && is.not.undefined(cp) && is.not.null(cp)) {
this.get('browser').scrollTo(`#page-${cp}`)
}
},
actions: {
onSavePageAsBlock(block) {
const promise = this.attrs.onSavePageAsBlock(block);
let cb = this.get('onSavePageAsBlock');
const promise = cb(block);
promise.then(() => {
this.loadBlocks();
let refresh = this.get('refresh');
refresh();
});
},
onCopyPage(pageId, documentId) {
this.attrs.onCopyPage(pageId, documentId);
let cb = this.get('onCopyPage');
cb(pageId, documentId);
},
onMovePage(pageId, documentId) {
this.attrs.onMovePage(pageId, documentId);
let cb = this.get('onMovePage');
cb(pageId, documentId);
},
onDeletePage(params) {
this.attrs.onDeletePage(params);
let cb = this.get('onDeletePage');
cb(params);
},
onSavePage(page, meta) {
this.set('toEdit', '');
this.attrs.onSavePage(page, meta);
let document = this.get('document');
let constants = this.get('constants');
switch (document.get('protection')) {
case constants.ProtectionType.Lock:
break;
case constants.ProtectionType.Review:
// detect edits to newly created pending page
if (page.get('relativeId') === '' && page.get('status') === constants.ChangeState.PendingNew) {
// new page, edits
this.set('toEdit', '');
let cb = this.get('onSavePage');
cb(page, meta);
} else if (page.get('relativeId') !== '' && page.get('status') === constants.ChangeState.Published) {
// existing page, first edit
const promise = this.addSection({ page: page, meta: meta });
promise.then((/*id*/) => { this.set('toEdit', ''); });
} else if (page.get('relativeId') !== '' && page.get('status') === constants.ChangeState.Pending) {
// existing page, subsequent edits
this.set('toEdit', '');
let cb = this.get('onSavePage');
cb(page, meta);
}
break;
case constants.ProtectionType.None:
// for un-protected documents, edits welcome!
this.set('toEdit', '');
// let cb2 = this.get('onSavePage');
// cb2(page, meta);
this.attrs.onSavePage(page, meta); // eslint-disable-line ember/no-attrs-in-components
break;
}
},
onShowSectionWizard(page) {
if (is.undefined(page)) {
page = { id: '0' };
}
this.set('pageId', '');
if (is.undefined(page)) page = { id: '0' };
let beforePage = this.get('beforePage');
if (is.not.null(beforePage) && $("#new-section-wizard").is(':visible') && beforePage.get('id') === page.id) {
@ -217,15 +260,11 @@ export default Component.extend(TooltipMixin, {
return;
}
let page = {
documentId: this.get('document.id'),
title: sectionName,
level: 1,
sequence: 0, // calculated elsewhere
body: "",
contentType: section.get('contentType'),
pageType: section.get('pageType')
};
let page = models.PageModel.create();
page.set('documentId', this.get('document.id'));
page.set('title', sectionName);
page.set('contentType', section.get('contentType'));
page.set('pageType', section.get('pageType'));
let meta = {
documentId: this.get('document.id'),
@ -240,14 +279,7 @@ export default Component.extend(TooltipMixin, {
const promise = this.addSection(model);
promise.then((id) => {
this.set('pageId', id);
if (model.page.pageType === 'section') {
this.set('toEdit', id);
} else {
this.set('toEdit', '');
}
this.set('toEdit', model.page.pageType === 'section' ? id: '');
this.setupAddWizard();
});
},
@ -259,16 +291,13 @@ export default Component.extend(TooltipMixin, {
return;
}
let page = {
documentId: this.get('document.id'),
title: `${block.get('title')}`,
level: 1,
sequence: 0, // calculated elsewhere
body: block.get('body'),
contentType: block.get('contentType'),
pageType: block.get('pageType'),
blockId: block.get('id')
};
let page = models.PageModel.create();
page.set('documentId', this.get('document.id'));
page.set('title', `${block.get('title')}`);
page.set('body', block.get('body'));
page.set('contentType', block.get('contentType'));
page.set('pageType', block.get('pageType'));
page.set('blockId', block.get('id'));
let meta = {
documentId: this.get('document.id'),
@ -283,9 +312,7 @@ export default Component.extend(TooltipMixin, {
};
const promise = this.addSection(model);
promise.then((id) => {
this.set('pageId', id);
promise.then((id) => { // eslint-disable-line no-unused-vars
this.setupAddWizard();
});
},
@ -299,11 +326,14 @@ export default Component.extend(TooltipMixin, {
this.set('showDeleteBlockDialog', false);
let id = this.get('deleteBlockId');
const promise = this.attrs.onDeleteBlock(id);
let cb = this.get('onDeleteBlock');
let promise = cb(id);
promise.then(() => {
this.set('deleteBlockId', '');
this.loadBlocks();
let refresh = this.get('refresh');
refresh();
});
return true;

View file

@ -16,7 +16,6 @@ import ModalMixin from '../../mixins/modal';
export default Component.extend(ModalMixin, {
documentService: service('document'),
revisions: [],
revision: null,
diff: '',
hasRevisions: computed('revisions', function() {
@ -26,6 +25,11 @@ export default Component.extend(ModalMixin, {
return this.get('diff').length > 0;
}),
init() {
this._super(...arguments);
this.revisions = [];
},
didReceiveAttrs() {
this._super(...arguments);
this.fetchRevisions();
@ -65,7 +69,8 @@ export default Component.extend(ModalMixin, {
onRollback() {
let revision = this.get('revision');
this.attrs.onRollback(revision.pageId, revision.id);
let cb = this.get('onRollback');
cb(revision.pageId, revision.id);
this.modalClose('#document-rollback-modal');
}

View file

@ -12,7 +12,8 @@
import TextField from '@ember/component/text-field';
export default TextField.extend({
becomeFocused: function() {
this.$().focus();
}.on('didInsertElement')
didInsertElement() {
this._super(...arguments);
this.$().focus();
}
});

View file

@ -12,7 +12,8 @@
import TextArea from '@ember/component/text-area';
export default TextArea.extend({
becomeFocused: function() {
didInsertElement() {
this._super(...arguments);
this.$().focus();
}.on('didInsertElement')
}
});

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import TooltipMixin from '../../mixins/tooltip';
@ -22,7 +23,11 @@ export default Component.extend(ModalMixin, TooltipMixin, {
newCategory: '',
deleteId: '',
dropdown: null,
users: [],
init() {
this._super(...arguments);
this.users = [];
},
didReceiveAttrs() {
this._super(...arguments);

View file

@ -12,9 +12,6 @@
import Component from '@ember/component';
export default Component.extend({
documentTags: [],
tagz: [],
init() {
this._super(...arguments);
let tagz = [];

View file

@ -19,13 +19,13 @@ export default Component.extend({
selectedDocuments: A([]),
selectedCaption: 'document',
showAdd: computed('permissions', 'documents', function() {
showAdd: computed('permissions.documentAdd', 'documents', function() {
return this.get('documents.length') === 0 && this.get('permissions.documentAdd');
}),
showLockout: computed('permissions', 'documents', function() {
showLockout: computed('permissions.documentAdd', 'documents', function() {
return this.get('documents.length') === 0 && !this.get('permissions.documentAdd');
}),
hasDocumentActions: computed('permissions', function() {
hasDocumentActions: computed('permissions.{documentDelete,documentMove}', function() {
return this.get('permissions.documentDelete') || this.get('permissions.documentMove');
}),
@ -48,7 +48,8 @@ export default Component.extend({
this.set('selectedDocuments', A([]));
this.set('showDeleteDialog', false);
this.attrs.onDeleteDocument(list);
let cb = this.get('onDeleteDocument');
cb(list);
return true;
},
@ -72,7 +73,9 @@ export default Component.extend({
this.set('showMoveDialog', false);
this.set('selectedDocuments', A([]));
this.attrs.onMoveDocument(list, moveSpaceId);
let cb = this.get('onMoveDocument');
cb(list, moveSpaceId);
return true;
},

View file

@ -10,12 +10,11 @@
// https://documize.com
import { setProperties } from '@ember/object';
import Component from '@ember/component';
import { inject as service } from '@ember/service';
import NotifierMixin from '../../mixins/notifier';
import ModalMixin from '../../mixins/modal';
export default Component.extend(NotifierMixin, {
export default Component.extend(ModalMixin, {
folderService: service('folder'),
userService: service('user'),
appMeta: service(),
@ -42,7 +41,8 @@ export default Component.extend(NotifierMixin, {
documentDelete: false,
documentMove: false,
documentCopy: false,
documentTemplate: false
documentTemplate: false,
documentApprove: false,
};
let data = this.get('store').normalize('space-permission', u)
@ -63,8 +63,9 @@ export default Component.extend(NotifierMixin, {
documentDelete: false,
documentMove: false,
documentCopy: false,
documentTemplate: false
};
documentTemplate: false,
documentApprove: false,
};
let data = this.get('store').normalize('space-permission', u)
folderPermissions.pushObject(this.get('store').push(data));
@ -96,7 +97,7 @@ export default Component.extend(NotifierMixin, {
let hasEveryone = _.find(permissions, function (permission) {
return permission.get('userId') === "0" &&
(permission.get('spaceView') || permission.get('documentAdd') || permission.get('documentEdit') || permission.get('documentDelete') ||
permission.get('documentMove') || permission.get('documentCopy') || permission.get('documentTemplate'));
permission.get('documentMove') || permission.get('documentCopy') || permission.get('documentTemplate') || permission.get('documentApprove'));
});
// see if more than oen user is granted access to space (excluding everyone)
@ -104,28 +105,23 @@ export default Component.extend(NotifierMixin, {
permissions.forEach((permission) => {
if (permission.get('userId') !== "0" &&
(permission.get('spaceView') || permission.get('documentAdd') || permission.get('documentEdit') || permission.get('documentDelete') ||
permission.get('documentMove') || permission.get('documentCopy') || permission.get('documentTemplate'))) {
permission.get('documentMove') || permission.get('documentCopy') || permission.get('documentTemplate') || permission.get('documentApprove'))) {
roleCount += 1;
}
});
if (is.not.undefined(hasEveryone)) {
folder.markAsPublic();
this.showNotification('Marked space as public');
} else {
if (roleCount > 1) {
folder.markAsRestricted();
this.showNotification('Marked space as protected');
} else {
folder.markAsPrivate();
this.showNotification('Marked space as private');
}
}
this.get('folderService').savePermissions(folder.get('id'), payload).then(() => {
this.showNotification('Saved permissions');
$('#space-permission-modal').modal('hide');
$('#space-permission-modal').modal('dispose');
this.modalClose('#space-permission-modal');
});
}
}

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import { empty } from '@ember/object/computed';
import { schedule } from '@ember/runloop';
import { inject as service } from '@ember/service';

View file

@ -23,12 +23,16 @@ export default Component.extend(AuthMixin, {
folderService: service('folder'),
localStorage: service('localStorage'),
hasCategories: gt('categories.length', 0),
filteredDocs: [],
categoryLinkName: 'Manage',
spaceSettings: computed('permissions', function() {
return this.get('permissions.spaceOwner') || this.get('permissions.spaceManage');
}),
init() {
this._super(...arguments);
this.filteredDocs = [];
},
didReceiveAttrs() {
this._super(...arguments);
this.setup();
@ -108,7 +112,8 @@ export default Component.extend(AuthMixin, {
});
this.set('documents', documents);
this.attrs.onRefresh();
let cb = this.get('onRefresh');
cb();
});
},

View file

@ -9,8 +9,8 @@
//
// https://documize.com
import $ from 'jquery';
import { empty, and } from '@ember/object/computed';
import { set } from '@ember/object';
import Component from '@ember/component';
import { isEmpty } from '@ember/utils';

View file

@ -9,8 +9,8 @@
//
// https://documize.com
import $ from 'jquery';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
export default Component.extend({
@ -114,7 +114,7 @@ export default Component.extend({
$(".stage-3").fadeIn();
// $("#spinner-1").show();
var payload = '{ "Password": "' + $("#stage-2-password").val() + '", "Serial": "' + self.serial + '", "Firstname": "' + $("#stage-1-firstname").val() + '", "Lastname": "' + $("#stage-1-lastname").val() + '" }';
var payload = '{ "password": "' + $("#stage-2-password").val() + '", "serial": "' + self.serial + '", "firstname": "' + $("#stage-1-firstname").val() + '", "lastname": "' + $("#stage-1-lastname").val() + '" }';
var password = $("#stage-2-password").val();
self.get('folderService').onboard(self.folderId, payload).then(function(user) {

View file

@ -9,8 +9,8 @@
//
// https://documize.com
import $ from 'jquery';
import { empty, and } from '@ember/object/computed';
import Component from '@ember/component';
import { isEqual, isEmpty } from '@ember/utils';
import { set } from '@ember/object';

View file

@ -12,9 +12,13 @@
import Component from '@ember/component';
export default Component.extend({
results: [],
resultPhrase: "",
init() {
this._super(...arguments);
this.results = [];
},
didReceiveAttrs() {
let docs = this.get('results');
let duped = [];

View file

@ -24,7 +24,8 @@ export default Component.extend({
},
onCancel() {
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onAction(title) {
@ -33,7 +34,8 @@ export default Component.extend({
page.set('title', title);
meta.set('rawBody', this.get("data"));
this.attrs.onAction(page, meta);
let cb = this.get('onAction');
cb(page, meta);
}
}
});

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import { empty } from '@ember/object/computed';
import { computed } from '@ember/object';
import TooltipMixin from '../../mixins/tooltip';
@ -75,21 +76,25 @@ export default Component.extend(TooltipMixin, ModalMixin, {
return;
}
this.attrs.onAction(this.get('page.title'));
let cb = this.get('onAction');
cb(this.get('page.title'));
},
onCancel() {
if (this.attrs.isDirty() !== null && this.attrs.isDirty()) {
let isDirty = this.get('isDirty');
if (isDirty() !== null && isDirty()) {
this.modalOpen('#discard-modal-' + this.get('page.id'), {show: true});
return;
}
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onDiscard() {
this.modalClose('#discard-modal-' + this.get('page.id'));
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onPreview() {

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import { empty } from '@ember/object/computed';
import Component from '@ember/component';
import ModalMixin from '../../mixins/modal';
@ -44,19 +45,22 @@ export default Component.extend(ModalMixin, {
actions: {
onCancel() {
if (this.attrs.isDirty() !== null && this.attrs.isDirty()) {
let isDirty = this.get('isDirty');
if (isDirty() !== null && isDirty()) {
this.modalOpen('#discard-modal', {show: true});
return;
}
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onDiscard() {
this.modalClose('#discard-modal');
this.attrs.onCancel();
},
let cb = this.get('onCancel');
cb();
},
onAction() {
if (this.get('busy')) {
@ -73,7 +77,8 @@ export default Component.extend(ModalMixin, {
return;
}
this.attrs.onAction(this.get('page.title'));
let cb = this.get('onAction');
cb(this.get('page.title'));
}
}
});

View file

@ -16,7 +16,7 @@ import TooltipMixin from '../../../mixins/tooltip';
export default Component.extend(TooltipMixin, {
isDirty: false,
pageBody: "",
syntaxOptions: [],
codeSyntax: null,
codeEditor: null,
editorId: computed('page', function () {
@ -29,7 +29,8 @@ export default Component.extend(TooltipMixin, {
}),
init() {
this._super(...arguments);
this._super(...arguments);
this.syntaxOptions = [];
let self = this;
let rawBody = this.get('meta.rawBody');
@ -128,7 +129,8 @@ export default Component.extend(TooltipMixin, {
},
onCancel() {
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onAction(title) {
@ -138,7 +140,8 @@ export default Component.extend(TooltipMixin, {
page.set('title', title);
page.set('body', meta.get('rawBody'));
this.attrs.onAction(page, meta);
let cb = this.get('onAction');
cb(page, meta);
}
}
});

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import { set } from '@ember/object';
import { schedule } from '@ember/runloop';
import { inject as service } from '@ember/service';
@ -21,9 +22,13 @@ export default Component.extend(SectionMixin, TooltipMixin, {
isDirty: false,
waiting: false,
authenticated: false,
user: {},
workspaces: [],
config: {},
init() {
this._super(...arguments);
this.user = {};
this.workspaces = [];
this.config = {};
},
didReceiveAttrs() {
let config = {};
@ -195,7 +200,8 @@ export default Component.extend(SectionMixin, TooltipMixin, {
},
onCancel() {
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onAction(title) {
@ -206,7 +212,8 @@ export default Component.extend(SectionMixin, TooltipMixin, {
meta.set('config', JSON.stringify(this.get('config')));
meta.set('externalSource', true);
this.attrs.onAction(page, meta);
let cb = this.get('onAction');
cb(page, meta);
}
}
});

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import EmberObject from '@ember/object';
import { A } from '@ember/array';
import { inject as service } from '@ember/service';
@ -21,9 +22,13 @@ export default Component.extend(SectionMixin, NotifierMixin, {
isDirty: false,
busy: false,
authenticated: false,
config: {},
owners: null,
init() {
this._super(...arguments);
this.config = {};
},
didReceiveAttrs() {
let self = this;
let page = this.get('page');
@ -227,7 +232,8 @@ export default Component.extend(SectionMixin, NotifierMixin, {
},
onCancel() {
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onAction(title) {

View file

@ -111,7 +111,8 @@ export default Component.extend({
},
onCancel() {
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onAction(title) {
@ -120,7 +121,8 @@ export default Component.extend({
page.set('title', title);
meta.set('rawBody', this.getBody());
this.attrs.onAction(page, meta);
let cb = this.get('onAction');
cb(page, meta);
}
}
});

View file

@ -21,8 +21,12 @@ export default Component.extend(SectionMixin, NotifierMixin, {
isDirty: false,
waiting: false,
authenticated: false,
config: {},
items: {},
init() {
this._super(...arguments);
this.config = {};
this.items = {};
},
didReceiveAttrs() {
let config = {};
@ -48,9 +52,9 @@ export default Component.extend(SectionMixin, NotifierMixin, {
displayError(reason) {
if (netUtil.isAjaxAccessError(reason)) {
this.showNotification(`Unable to authenticate`);
// this.showNotification(`Unable to authenticate`);
} else {
this.showNotification(`Something went wrong, try again!`);
// this.showNotification(`Something went wrong, try again!`);
}
},
@ -140,7 +144,8 @@ export default Component.extend(SectionMixin, NotifierMixin, {
},
onCancel() {
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onAction(title) {
@ -176,7 +181,7 @@ export default Component.extend(SectionMixin, NotifierMixin, {
}, function (reason) { // eslint-disable-line no-unused-vars
self.set('authenticated', false);
self.set('waiting', false);
self.showNotification(`Something went wrong, try again!`);
// self.showNotification(`Something went wrong, try again!`);
});
}
}

View file

@ -9,8 +9,9 @@
//
// https://documize.com
import $ from 'jquery';
import { schedule } from '@ember/runloop';
import { computed } from '@ember/object';
import Component from '@ember/component';
export default Component.extend({
@ -38,8 +39,10 @@ export default Component.extend({
tableResizerOffset: 10
});
$(id).on('froalaEditor.contentChanged', () => {
this.set('isDirty', true);
schedule('afterRender', function() {
$(id).on('froalaEditor.contentChanged', () => {
this.set('isDirty', true); // eslint-disable-line ember/jquery-ember-run
});
});
},
@ -53,7 +56,8 @@ export default Component.extend({
},
onCancel() {
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onAction(title) {
@ -69,7 +73,8 @@ export default Component.extend({
meta.set('rawBody', body);
this.attrs.onAction(page, meta);
let cb = this.get('onAction');
cb(page, meta);
}
}
});

View file

@ -11,7 +11,6 @@
/*global Trello*/
import $ from 'jquery';
import { htmlSafe } from '@ember/string';
import { computed, set } from '@ember/object';
import { inject as service } from '@ember/service';
@ -25,11 +24,9 @@ export default Component.extend(SectionMixin, NotifierMixin, TooltipMixin, {
isDirty: false,
busy: false,
authenticated: false,
config: {},
boards: null,
noBoards: false,
appKey: "",
boardStyle: computed('config.board', function () {
let board = this.get('config.board');
@ -41,6 +38,11 @@ export default Component.extend(SectionMixin, NotifierMixin, TooltipMixin, {
return htmlSafe("background-color: " + color);
}),
init() {
this._super(...arguments);
this.config = {};
},
didReceiveAttrs() {
let page = this.get('page');
let config = {};
@ -219,7 +221,8 @@ export default Component.extend(SectionMixin, NotifierMixin, TooltipMixin, {
},
onCancel() {
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onAction(title) {

View file

@ -9,8 +9,8 @@
//
// https://documize.com
import $ from 'jquery';
import { computed, set } from '@ember/object';
import Component from '@ember/component';
import { inject as service } from '@ember/service';
@ -31,8 +31,8 @@ export default Component.extend({
let options = {
selector: "#" + this.get('editorId'),
relative_urls: false,
cache_suffix: "?v=443",
browser_spellcheck: false,
cache_suffix: "?v=475",
browser_spellcheck: true,
gecko_spellcheck: false,
theme: "modern",
skin: 'documize',
@ -116,7 +116,8 @@ export default Component.extend({
},
onCancel() {
this.attrs.onCancel();
let cb = this.get('onCancel');
cb();
},
onAction(title) {
@ -127,7 +128,8 @@ export default Component.extend({
page.set('title', title);
meta.set('rawBody', editor.getContent());
this.attrs.onAction(page, meta);
let cb = this.get('onAction');
cb(page, meta);
}
}
});

View file

@ -16,13 +16,17 @@ import NotifierMixin from '../../mixins/notifier';
import AuthMixin from '../../mixins/auth';
export default Component.extend(TooltipMixin, NotifierMixin, AuthMixin, {
publicFolders: [],
protectedFolders: [],
privateFolders: [],
hasPublicFolders: false,
hasProtectedFolders: false,
hasPrivateFolders: false,
init() {
this._super(...arguments);
this.publicFolders = [];
this.protectedFolders = [];
this.privateFolders = [];
},
didReceiveAttrs() {
let folders = this.get('spaces');
let publicFolders = [];

View file

@ -9,29 +9,36 @@
//
// https://documize.com
import Component from '@ember/component';
import $ from 'jquery';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
import AuthMixin from '../../mixins/auth';
import TooltipMixin from '../../mixins/tooltip';
import ModalMixin from '../../mixins/modal';
export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, {
spaceService: service('folder'),
userSvc: service('user'),
store: service(),
spaceSvc: service('folder'),
session: service(),
appMeta: service(),
pinned: service(),
pinState : {
isPinned: false,
pinId: '',
newName: ''
},
saveTemplate: {
name: '',
description: ''
},
showTools: true, // show document related tools? favourite, delete, make template...
showDocumentLink: false, // show link to document in breadcrumbs
init() {
this._super(...arguments);
this.pinState = {
isPinned: false,
pinId: '',
newName: ''
};
this.saveTemplate = {
name: '',
description: ''
};
},
didReceiveAttrs() {
this._super(...arguments);
@ -50,7 +57,6 @@ export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, {
didInsertElement() {
this._super(...arguments);
this.modalInputFocus('#document-template-modal', '#new-template-name');
},
@ -62,8 +68,9 @@ export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, {
actions: {
onDocumentDelete() {
this.modalClose('#document-delete-modal');
this.attrs.onDocumentDelete();
let cb = this.get('onDocumentDelete');
cb();
},
onPrintDocument() {
@ -118,7 +125,8 @@ export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, {
this.set('saveTemplate.name', '');
this.set('saveTemplate.description', '');
this.attrs.onSaveTemplate(name, excerpt);
let cb = this.get('onSaveTemplate');
cb(name, excerpt);
this.modalClose('#document-template-modal');

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { schedule } from '@ember/runloop';
@ -29,12 +30,7 @@ export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, {
copyTemplate: true,
copyPermission: true,
copyDocument: false,
clonedSpace: { id: '' },
pinState : {
isPinned: false,
pinId: '',
newName: ''
},
spaceSettings: computed('permissions', function() {
return this.get('permissions.spaceOwner') || this.get('permissions.spaceManage');
}),
@ -49,10 +45,21 @@ export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, {
templateDocName: '',
templateDocNameError: false,
selectedTemplate: '',
importedDocuments: [],
importStatus: [],
dropzone: null,
init() {
this._super(...arguments);
this.importedDocuments = [];
this.importStatus = [];
this.clonedSpace = { id: '' };
this.pinState = {
isPinned: false,
pinId: '',
newName: ''
};
},
didReceiveAttrs() {
this._super(...arguments);
@ -226,8 +233,8 @@ export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, {
this.set('deleteSpaceName', '');
$("#delete-space-name").removeClass("is-invalid");
this.attrs.onDeleteSpace(this.get('space.id'));
let cb = this.get('onDeleteSpace');
cb(this.get('space.id'));
this.modalClose('#space-delete-modal');
},
@ -258,17 +265,17 @@ export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, {
onShowTemplateDocModal() {
let t = this.get('templates');
if (t.length > 0) {
t[0].set('selected', true);
this.modalOpen("#template-doc-modal", {"show": true}, '#template-doc-name');
}
t.forEach((t) => {
t.set('selected', false);
});
this.modalOpen("#template-doc-modal", {"show": true}, '#template-doc-name');
},
onSelectTemplate(i) {
let t = this.get('templates');
t.forEach((t) => {
t.set('selected', false);
})
});
i.set('selected', true);
this.set('selectedTemplate', i.id);
},
@ -281,16 +288,17 @@ export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, {
this.set('templateDocNameError', true);
$('#template-doc-name').focus();
return;
} else {
this.set('templateDocNameError', false);
this.set('templateDocName', '');
}
let id = this.get('selectedTemplate');
if (is.empty(id)) {
$('#widget-list-picker').addClass('is-invalid');
return;
}
this.set('templateDocNameError', false);
this.set('templateDocName', '');
this.modalClose("#template-doc-modal");
this.get('templateService').importSavedTemplate(this.get('space.id'), id, docName).then((document) => {
@ -330,8 +338,9 @@ export default Component.extend(ModalMixin, TooltipMixin, AuthMixin, {
this.set('importedDocuments', documents);
if (documents.length === 0) {
this.modalClose("#import-doc-modal");
this.attrs.onRefresh();
this.modalClose("#import-doc-modal");
let cb = this.get('onRefresh');
cb();
}
},

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import Component from '@ember/component';
import { schedule } from '@ember/runloop';
import { notEmpty } from '@ember/object/computed';
@ -20,8 +21,13 @@ export default Component.extend(NotifierMixin, AuthMixin, {
copyTemplate: true,
copyPermission: true,
copyDocument: false,
clonedSpace: { id: '' },
hasClone: notEmpty('clonedSpace.id'),
clonedSpace: null,
init() {
this._super(...arguments);
// this.clonedSpace = { id: '' };
},
didInsertElement() {
this._super(...arguments);
@ -58,13 +64,13 @@ export default Component.extend(NotifierMixin, AuthMixin, {
}
this.set('spaceName', '');
this.set('clonedSpace.id', '');
this.set('clonedSpace', null);
$("#new-space-name").removeClass("is-invalid");
$('#add-space-modal').modal('hide');
$('#add-space-modal').modal('dispose');
this.attrs.onAddSpace(payload);
let cb = this.get('onAddSpace');
cb(payload);
}
}
});

View file

@ -21,13 +21,13 @@ export default Component.extend({
store: service(),
pinned: service(),
enableLogout: true,
pins: [],
hasPins: notEmpty('pins'),
hasSpacePins: notEmpty('spacePins'),
hasDocumentPins: notEmpty('documentPins'),
init() {
this._super(...arguments);
this.pins = [];
if (this.get('appMeta.authProvider') === constants.AuthProvider.Keycloak) {
let config = this.get('appMeta.authConfig');

View file

@ -15,7 +15,6 @@ import Component from '@ember/component';
export default Component.extend({
cssClass: "",
content: [],
prompt: null,
optionValuePath: 'id',
optionLabelPath: 'name',

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import Component from '@ember/component';
import stringUtil from '../../utils/string';
@ -56,7 +57,8 @@ export default Component.extend({
return;
}
let result = this.attrs.onAction();
let cb = this.get('onAction');
let result = cb();
if (result) {
this.set('show', false);
}

View file

@ -16,7 +16,6 @@ import { computed } from '@ember/object';
export default Component.extend({
nameField: 'category',
singleSelect: false,
items: [],
maxHeight: 0,
onSelect: null,
styleCss: computed('maxHeight', function () {
@ -29,13 +28,18 @@ export default Component.extend({
}
}),
init() {
this._super(...arguments);
},
actions: {
onToggle(item) {
// callback takes precedence
// caller sets item to 'selected'
let cb = this.get('onSelect');
if (cb !== null) {
this.attrs.onSelect(item);
let cb = this.get('onSelect');
cb(item);
return;
}

View file

@ -19,7 +19,8 @@ export default Component.extend({
actions: {
onCheck() {
if (this.get('onClick') !== null) {
this.attrs.onClick(this.get('value'));
let cb = this.get('onClick');
cb(this.get('value'));
} else {
this.set('selected', !this.get('selected'));
}

View file

@ -10,12 +10,13 @@
// https://documize.com
import { run } from '@ember/runloop';
import Component from '@ember/component';
import miscUtil from '../utils/misc';
export default Component.extend({
notifications: [],
init() {
this._super(...arguments);
},
didInsertElement() {
this.eventBus.subscribe('notifyUser', this, 'showNotification');

View file

@ -9,15 +9,14 @@
//
// https://documize.com
import $ from 'jquery';
import { empty } from '@ember/object/computed';
import Component from '@ember/component';
import { computed, set } from '@ember/object';
import { isPresent, isEqual, isEmpty } from '@ember/utils';
import AuthProvider from '../mixins/auth';
export default Component.extend(AuthProvider, {
password: { password: "", confirmation: "" },
hasFirstnameError: empty('model.firstname'),
hasLastnameError: empty('model.lastname'),
hasEmailError: computed('model.email', function() {
@ -45,6 +44,11 @@ export default Component.extend(AuthProvider, {
}
}),
init() {
this._super(...arguments);
this.password = { password: "", confirmation: "" };
},
actions: {
save() {
let password = this.get('password.password');

View file

@ -0,0 +1,54 @@
// 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
import EmberObject from "@ember/object";
let constants = EmberObject.extend({
// Document
ProtectionType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
None: 0,
Lock: 1,
Review: 2,
NoneLabel: 'Changes permitted without approval',
LockLabel: 'Locked, changes not permitted',
ReviewLabel: 'Changes require approval before publication'
},
// Document
ApprovalType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
None: 0,
Anybody: 1,
Majority: 2,
Unanimous: 3,
AnybodyLabel: 'Approval required from any approver',
MajorityLabel: 'Majority approval required from approvers',
UnanimousLabel: 'Unanimous approval required from all approvers'
},
// Section
ChangeState: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
Published: 0,
Pending: 1,
UnderReview: 2,
Rejected: 3,
PendingNew: 4,
},
// Section
PageType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
Tab: 'tab',
Section: 'section'
}
});
export default { constants }

View file

@ -0,0 +1,26 @@
// 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
import EmberObject from "@ember/object";
let econstants = EmberObject.extend({
// Document
ActionType: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects
Read: 1,
Feedback: 2,
Contribute: 3,
ApprovalRequest: 4,
Approved: 5,
Rejected: 6,
},
});
export default { econstants }

View file

@ -0,0 +1,27 @@
// 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
import { helper } from '@ember/component/helper';
export function isOneOf(params/*, hash*/) {
if (is.not.undefined(params) || is.not.null(params)) {
if (params.length >= 2) {
let value = params[0];
for (let i=1; i < params.length; i++) {
if (params[i] == value) return true;
}
}
}
return false;
}
export default helper(isOneOf);

View file

@ -9,7 +9,11 @@
//
// https://documize.com
export function initialize( /*application*/ ) {
import $ from 'jquery';
import constants from '../constants/constants';
import econstants from '../constants/econstants';
export function initialize(application) {
// address insecure jquery defaults (kudos: @nathanhammond)
$.globalEval = function() {};
$.ajaxSetup({
@ -19,6 +23,11 @@ export function initialize( /*application*/ ) {
}
});
let cs = constants.constants;
let ec = econstants.econstants;
application.register('constants:main', cs);
application.register('econstants:main', ec);
Dropzone.autoDiscover = false;
CodeMirror.modeURL = "/codemirror/mode/%N/%N.js";
}

View file

@ -0,0 +1,25 @@
// 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
export function initialize(application) {
application.inject('route', 'constants', 'constants:main');
application.inject('controller', 'constants', 'constants:main');
application.inject('component', 'constants', 'constants:main');
application.inject('template', 'constants', 'constants:main');
application.inject('service', 'constants', 'constants:main');
application.inject('model', 'constants', 'constants:main');
}
export default {
name: 'constants',
after: "application",
initialize: initialize
};

View file

@ -0,0 +1,24 @@
// 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
export function initialize(application) {
application.inject('route', 'econstants', 'econstants:main');
application.inject('controller', 'econstants', 'econstants:main');
application.inject('component', 'econstants', 'econstants:main');
application.inject('template', 'econstants', 'econstants:main');
application.inject('service', 'econstants', 'econstants:main');
}
export default {
name: 'econstants',
after: "application",
initialize: initialize
};

View file

@ -9,6 +9,7 @@
//
// https://documize.com
import $ from 'jquery';
import Mixin from '@ember/object/mixin';
// ID values expected format:

View file

@ -12,15 +12,15 @@
import Mixin from '@ember/object/mixin';
export default Mixin.create({
isReadonly: function () {
if (this.get('page.userId') === this.get('session.session.authenticated.user.id')) {
return undefined;
} else {
return "readonly";
}
}.property('page'),
// isReadonly() {
// if (this.get('page.userId') === this.get('session.session.authenticated.user.id')) {
// return undefined;
// } else {
// return "readonly";
// }
// }.property('page'),
isMine: function () {
return this.get('page.userId') === this.get('session.session.authenticated.user.id');
}.property('page')
// isMine() {
// return this.get('page.userId') === this.get('session.session.authenticated.user.id');
// }.property('page')
});

View file

@ -9,12 +9,11 @@
//
// https://documize.com
import $ from 'jquery';
import Mixin from '@ember/object/mixin';
import { schedule } from '@ember/runloop';
export default Mixin.create({
tooltips: [],
renderTooltips() {
schedule('afterRender', () => {
$('[data-toggle="tooltip"]').tooltip('dispose');
@ -24,5 +23,17 @@ export default Mixin.create({
removeTooltips() {
$('[data-toggle="tooltip"]').tooltip('dispose');
},
renderPopovers() {
schedule('afterRender', () => {
$('[data-toggle="popover"]').popover('dispose');
$('body').popover({selector: '[data-toggle="popover"]', delay: 250});
});
},
removePopovers() {
$('[data-toggle="tooltip"]').popover('dispose');
}
});

View file

@ -0,0 +1,28 @@
// 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
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
orgId: attr(),
document: attr(),
documentId: attr(),
documentSlug: attr(),
tags: attr(),
excerpt: attr(),
itemId: attr(),
itemType: attr(),
space: attr(),
spaceId: attr(),
spaceSlug: attr(),
selected: attr()
});

View file

@ -18,6 +18,8 @@ export default Model.extend({
orgId: attr('string'),
folderId: attr('string'),
documentId: attr('string'),
pageId: attr('string'),
pageTitle: attr('string'),
userId: attr('string'),
firstname: attr('string'),
lastname: attr('string'),
@ -55,6 +57,9 @@ export default Model.extend({
case constants.UserActivityType.PublishedBlock:
label = 'Published Block';
break;
case constants.UserActivityType.Rejected:
label = 'Rejected';
break;
default:
break;
}
@ -93,11 +98,13 @@ export default Model.extend({
case constants.UserActivityType.PublishedBlock:
color = 'color-blue';
break;
case constants.UserActivityType.Rejected:
color = 'color-red';
break;
default:
break;
}
return color;
})
});

View file

@ -0,0 +1,25 @@
// 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
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
orgId: attr('string'),
documentId: attr('string'),
userId: attr('string'),
fullname: attr('string'), // client-side usage only, not from API
documentEdit: attr('boolean'), // space level setting
documentApprove: attr('boolean'), // space level setting
documentRoleEdit: attr('boolean'), // document level setting
documentRoleApprove: attr('boolean') // document level setting
});

View file

@ -25,7 +25,8 @@ export default Model.extend({
userId: attr('string'),
tags: attr('string'),
template: attr('boolean'),
layout: attr('string'),
protection: attr('number', { defaultValue: 0 }),
approval: attr('number', { defaultValue: 0 }),
// client-side property
selected: attr('boolean', { defaultValue: false }),

View file

@ -0,0 +1,30 @@
// 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
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
// import { belongsTo, hasMany } from 'ember-data/relationships';
export default Model.extend({
// page: belongsTo('page', { inverse: null }),
// meta: belongsTo('page-meta', { inverse: null }),
// pending: hasMany('page-pending', { inverse: null }),
page: attr(),
meta: attr(),
pending: attr(),
changePending: attr('boolean'),
changeAwaitingReview: attr('boolean'),
changeRejected: attr('boolean'),
userHasChangePending: attr('boolean'),
userHasChangeAwaitingReview: attr('boolean'),
userHasChangeRejected: attr('boolean'),
userHasNewPagePending: attr('boolean')
});

View file

@ -0,0 +1,25 @@
// 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
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
page: attr(),
meta: attr(),
owner: attr('string'),
changePending: attr('boolean'),
changeAwaitingReview: attr('boolean'),
changeRejected: attr('boolean'),
userHasChangePending: attr('boolean'),
userHasChangeAwaitingReview: attr('boolean'),
userHasChangeRejected: attr('boolean')
});

View file

@ -12,7 +12,6 @@
import { computed } from '@ember/object';
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
// import { hasMany } from 'ember-data/relationships';
export default Model.extend({
documentId: attr('string'),
@ -20,7 +19,7 @@ export default Model.extend({
contentType: attr('string'),
pageType: attr('string'),
level: attr('number', { defaultValue: 1 }),
sequence: attr('number', { defaultValue: 0 }),
sequence: attr('number', { defaultValue: 1024 }),
numbering: attr('string'),
revisions: attr('number', { defaultValue: 0 }),
blockId: attr('string'),
@ -28,10 +27,12 @@ export default Model.extend({
body: attr('string'),
rawBody: attr('string'),
meta: attr(),
status: attr('number', { defaultValue: 0 }),
relativeId: attr('string'),
userId: attr('string'),
tagName: computed('level', function () {
return "h2";
// return "h" + (this.get('level') + 1);
}),
tocIndent: computed('level', function () {
@ -48,5 +49,16 @@ export default Model.extend({
}),
created: attr(),
revised: attr()
revised: attr(),
// is this a new page that is pending and belongs to the user?
isNewPageUserPending(userId) {
return this.get('relativeId') === '' && this.get('userId') === userId && (
this.get('status') === this.get('constants').ChangeState.PendingNew || this.get('status') === this.get('constants').ChangeState.UnderReview);
},
// is this new page ready for review?
isNewPageReviewReady() {
return this.get('relativeId') === '' && this.get('status') === this.get('constants').ChangeState.UnderReview;
}
});

View file

@ -27,5 +27,6 @@ export default Model.extend({
documentDelete: attr('boolean'),
documentMove: attr('boolean'),
documentCopy: attr('boolean'),
documentTemplate: attr('boolean')
documentTemplate: attr('boolean'),
documentApprove: attr('boolean')
});

View file

@ -15,10 +15,13 @@ import attr from 'ember-data/attr';
export default Model.extend({
documentName: attr('string'),
documentId: attr('string'),
folderId: attr('string'),
contributed: attr('string'),
viewed: attr('string'),
created: attr('string'),
approved: attr('string'),
rejected: attr('string'),
hasContributed: computed('contributed', function () {
return this.get('contributed').length > 0;
@ -28,5 +31,11 @@ export default Model.extend({
}),
hasCreated: computed('created', function () {
return this.get('created').length > 0;
})
}),
hasApproved: computed('approved', function () {
return this.get('approved').length > 0;
}),
hasRejected: computed('rejected', function () {
return this.get('rejected').length > 0;
})
});

View file

@ -9,8 +9,8 @@
//
// https://documize.com
import $ from 'jquery';
import { inject as service } from '@ember/service';
import Route from '@ember/routing/route';
import constants from '../../../utils/constants';
@ -29,6 +29,7 @@ export default Route.extend({
},
activate() {
this.get('browser').setTitleAsPhrase('Forgot Password');
$('body').addClass('background-color-theme-light');
},

Some files were not shown because too many files have changed in this diff Show more