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

Merge pull request #144 from documize/content-analytics

Content analytics and reporting
This commit is contained in:
Saul S 2018-04-09 10:26:59 +01:00 committed by GitHub
commit 2dce8a89a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1300 additions and 924 deletions

View file

@ -52,9 +52,9 @@ Space view.
## Latest version
[Community edition: v1.60.0](https://github.com/documize/community/releases)
[Community edition: v1.61.0](https://github.com/documize/community/releases)
[Enterprise edition: v1.62.0](https://documize.com/downloads)
[Enterprise edition: v1.63.0](https://documize.com/downloads)
## OS support
@ -69,7 +69,7 @@ Documize runs on the following:
Documize is built with the following technologies:
- EmberJS (v2.18.0)
- Go (v1.10)
- Go (v1.10.1)
...and supports the following databases:
@ -77,7 +77,7 @@ Documize is built with the following technologies:
- Percona (v5.7.16-10+)
- MariaDB (10.3.0+)
Coming soon, PostgreSQL and Microsoft SQL Server support.
Coming soon, PostgreSQL and Microsoft SQL Server database support.
## Authentication options

View file

@ -227,6 +227,8 @@ CREATE TABLE IF NOT EXISTS `search` (
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin
ENGINE = MyISAM;
-- FULLTEXT search requires MyISAM and NOT InnoDB
DROP TABLE IF EXISTS `revision`;
CREATE TABLE IF NOT EXISTS `revision` (
@ -258,7 +260,8 @@ CREATE TABLE IF NOT EXISTS `config` (
`key` CHAR(255) NOT NULL,
`config` JSON,
UNIQUE INDEX `idx_config_area` (`key` ASC))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin
ENGINE = InnoDB;
INSERT INTO `config` VALUES ('SMTP','{\"userid\": \"\",\"password\": \"\",\"host\": \"\",\"port\": \"\",\"sender\": \"\"}');
INSERT INTO `config` VALUES ('FILEPLUGINS',

View file

@ -44,6 +44,7 @@ CREATE TABLE IF NOT EXISTS `search` (
FULLTEXT INDEX `idx_search_content` (`content`))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = MyISAM;
-- FULLTEXT search requires MyISAM and NOT InnoDB
-- migrate page content
INSERT INTO search (orgid, documentid, itemid, itemtype, content) SELECT orgid, documentid, id AS itemid, 'page' AS itemtype, TRIM(body) AS content FROM search_old;

View file

@ -22,7 +22,7 @@ CREATE TABLE IF NOT EXISTS `permission` (
UNIQUE INDEX `idx_permission_id` (`id` ASC),
INDEX `idx_permission_orgid` (`orgid` ASC))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = MyISAM;
ENGINE = InnoDB;
CREATE INDEX idx_permission_1 ON permission(orgid,who,whoid,location);
CREATE INDEX idx_permission_2 ON permission(orgid,who,whoid,location,action);
@ -44,7 +44,7 @@ CREATE TABLE IF NOT EXISTS `category` (
INDEX `idx_category_refid` (`refid` ASC),
INDEX `idx_category_orgid` (`orgid` ASC))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = MyISAM;
ENGINE = InnoDB;
CREATE INDEX idx_category_1 ON category(orgid,labelid);
@ -63,12 +63,12 @@ CREATE TABLE IF NOT EXISTS `categorymember` (
UNIQUE INDEX `idx_categorymember_id` (`id` ASC),
INDEX `idx_category_documentid` (`documentid`))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = MyISAM;
ENGINE = InnoDB;
CREATE INDEX idx_categorymember_1 ON categorymember(orgid,documentid);
CREATE INDEX idx_categorymember_2 ON categorymember(orgid,labelid);
-- rolee represent user groups
-- rolee represent user groups
DROP TABLE IF EXISTS `role`;
CREATE TABLE IF NOT EXISTS `role` (
@ -81,7 +81,7 @@ CREATE TABLE IF NOT EXISTS `role` (
INDEX `idx_category_refid` (`refid` ASC),
INDEX `idx_category_orgid` (`orgid` ASC))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = MyISAM;
ENGINE = InnoDB;
-- role member records user role membership
DROP TABLE IF EXISTS `rolemember`;
@ -93,49 +93,49 @@ CREATE TABLE IF NOT EXISTS `rolemember` (
`userid` CHAR(16) NOT NULL COLLATE utf8_bin,
UNIQUE INDEX `idx_category_id` (`id` ASC))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = MyISAM;
ENGINE = InnoDB;
CREATE INDEX idx_rolemember_1 ON rolemember(roleid,userid);
CREATE INDEX idx_rolemember_2 ON rolemember(orgid,roleid,userid);
-- user account can have global permssion to state if user can see all other users
-- provides granular control for external users
-- provides granular control for external users
ALTER TABLE account ADD COLUMN `users` BOOL NOT NULL DEFAULT 1 AFTER `admin`;
-- migrate space/document permissions
-- space own
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
SELECT orgid, 'user' as who, userid as whois, 'own' as `action`, 'object' as scope, 'space' as location, refid
FROM label;
-- space manage (same as owner)
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
SELECT orgid, 'user' as who, userid as whois, 'manage' as `action`, 'object' as scope, 'space' as location, refid
FROM label;
-- view space
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
SELECT orgid, 'user' as who, userid as whois, 'view' as `action`, 'object' as scope, 'space' as location, labelid as refid
FROM labelrole WHERE canview=1;
-- edit space => add/edit/delete/move/copy/template documents
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
SELECT orgid, 'user' as who, userid as whois, 'doc-add' as `action`, 'object' as scope, 'space' as location, labelid as refid
FROM labelrole WHERE canedit=1;
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
SELECT orgid, 'user' as who, userid as whois, 'doc-edit' as `action`, 'object' as scope, 'space' as location, labelid as refid
FROM labelrole WHERE canedit=1;
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
SELECT orgid, 'user' as who, userid as whois, 'doc-delete' as `action`, 'object' as scope, 'space' as location, labelid as refid
FROM labelrole WHERE canedit=1;
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
SELECT orgid, 'user' as who, userid as whois, 'doc-move' as `action`, 'object' as scope, 'space' as location, labelid as refid
FROM labelrole WHERE canedit=1;
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
SELECT orgid, 'user' as who, userid as whois, 'doc-copy' as `action`, 'object' as scope, 'space' as location, labelid as refid
FROM labelrole WHERE canedit=1;
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
INSERT INTO permission (orgid, who, whoid, `action`, scope, location, refid)
SELECT orgid, 'user' as who, userid as whois, 'doc-template' as `action`, 'object' as scope, 'space' as location, labelid as refid
FROM labelrole WHERE canedit=1;

View file

@ -0,0 +1,40 @@
/* enterprise edition */
-- consistency of table engines
ALTER TABLE config ENGINE = InnoDB;
ALTER TABLE permission ENGINE = InnoDB;
ALTER TABLE category ENGINE = InnoDB;
ALTER TABLE categorymember ENGINE = InnoDB;
ALTER TABLE role ENGINE = InnoDB;
ALTER TABLE rolemember ENGINE = InnoDB;
-- content analytics
ALTER TABLE useractivity ADD COLUMN `metadata` VARCHAR(1000) NOT NULL DEFAULT '' AFTER `activitytype`;
-- new role for viewing content analytics
ALTER TABLE account ADD COLUMN `analytics` BOOL NOT NULL DEFAULT 0 AFTER `users`;
UPDATE account SET analytics=1 WHERE admin=1;
-- content likes/feedback
-- DROP TABLE IF EXISTS `vote`;
-- CREATE TABLE IF NOT EXISTS `vote` (
-- `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
-- `refid` CHAR(16) NOT NULL COLLATE utf8_bin,
-- `orgid` CHAR(16) NOT NULL COLLATE utf8_bin,
-- `documentid` CHAR(16) NOT NULL COLLATE utf8_bin,
-- `userid` CHAR(16) NOT NULL DEFAULT '' COLLATE utf8_bin,
-- `vote` INT NOT NULL DEFAULT 0,
-- `comment` VARCHAR(300) NOT NULL DEFAULT '',
-- `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- `revised` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- UNIQUE INDEX `idx_vote_id` (`id` ASC),
-- INDEX `idx_vote_refid` (`refid` ASC),
-- INDEX `idx_vote_documentid` (`documentid` ASC),
-- INDEX `idx_vote_orgid` (`orgid` ASC))
-- DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
-- ENGINE = InnoDB;
-- CREATE INDEX idx_vote_1 ON vote(orgid,documentid);
-- deprecations

View file

@ -33,8 +33,8 @@ func (s Scope) Add(ctx domain.RequestContext, account account.Account) (err erro
account.Created = time.Now().UTC()
account.Revised = time.Now().UTC()
_, err = ctx.Transaction.Exec("INSERT INTO account (refid, orgid, userid, admin, editor, users, active, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
account.RefID, account.OrgID, account.UserID, account.Admin, account.Editor, account.Users, account.Active, account.Created, account.Revised)
_, err = ctx.Transaction.Exec("INSERT INTO account (refid, orgid, userid, admin, editor, users, analytics, active, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
account.RefID, account.OrgID, account.UserID, account.Admin, account.Editor, account.Users, account.Analytics, account.Active, account.Created, account.Revised)
if err != nil {
err = errors.Wrap(err, "unable to execute insert for account")
@ -46,7 +46,7 @@ func (s Scope) Add(ctx domain.RequestContext, account account.Account) (err erro
// GetUserAccount returns the database account record corresponding to the given userID, using the client's current organizaion.
func (s Scope) GetUserAccount(ctx domain.RequestContext, userID string) (account account.Account, err error) {
err = s.Runtime.Db.Get(&account, `
SELECT a.id, a.refid, a.orgid, a.userid, a.editor, a.admin, a.users, a.active, a.created, a.revised,
SELECT a.id, a.refid, a.orgid, a.userid, a.editor, a.admin, a.users, a.analytics, a.active, a.created, a.revised,
b.company, b.title, b.message, b.domain
FROM account a, organization b
WHERE b.refid=a.orgid AND a.orgid=? AND a.userid=?`, ctx.OrgID, userID)
@ -61,8 +61,8 @@ func (s Scope) GetUserAccount(ctx domain.RequestContext, userID string) (account
// GetUserAccounts returns a slice of database account records, for all organizations that the userID is a member of, in organization title order.
func (s Scope) GetUserAccounts(ctx domain.RequestContext, userID string) (t []account.Account, err error) {
err = s.Runtime.Db.Select(&t, `
SELECT a.id, a.refid, a.orgid, a.userid, a.editor, a.admin, a.users, a.active, a.created, a.revised,
b.company, b.title, b.message, b.domain
SELECT a.id, a.refid, a.orgid, a.userid, a.editor, a.admin, a.users, a.analytics, a.active, a.created, a.revised,
b.company, b.title, b.message, b.domain
FROM account a, organization b
WHERE a.userid=? AND a.orgid=b.refid AND a.active=1 ORDER BY b.title`, userID)
@ -76,7 +76,7 @@ func (s Scope) GetUserAccounts(ctx domain.RequestContext, userID string) (t []ac
// GetAccountsByOrg returns a slice of database account records, for all users in the client's organization.
func (s Scope) GetAccountsByOrg(ctx domain.RequestContext) (t []account.Account, err error) {
err = s.Runtime.Db.Select(&t,
`SELECT a.id, a.refid, a.orgid, a.userid, a.editor, a.admin, a.users, a.active, a.created, a.revised,
`SELECT a.id, a.refid, a.orgid, a.userid, a.editor, a.admin, a.users, a.analytics, a.active, a.created, a.revised,
b.company, b.title, b.message, b.domain
FROM account a, organization b
WHERE a.orgid=b.refid AND a.orgid=? AND a.active=1`, ctx.OrgID)
@ -109,7 +109,7 @@ func (s Scope) CountOrgAccounts(ctx domain.RequestContext) (c int) {
func (s Scope) UpdateAccount(ctx domain.RequestContext, account account.Account) (err error) {
account.Revised = time.Now().UTC()
_, err = ctx.Transaction.NamedExec("UPDATE account SET userid=:userid, admin=:admin, editor=:editor, users=:users, active=:active, revised=:revised WHERE orgid=:orgid AND refid=:refid", &account)
_, err = ctx.Transaction.NamedExec("UPDATE account SET userid=:userid, admin=:admin, editor=:editor, users=:users, analytics=:analytics, active=:active, revised=:revised WHERE orgid=:orgid AND refid=:refid", &account)
if err != sql.ErrNoRows && err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute update for account %s", account.RefID))

View file

@ -34,8 +34,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, documentid, pageid, sourcetype, activitytype, created) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
activity.OrgID, activity.UserID, activity.LabelID, activity.DocumentID, activity.PageID, activity.SourceType, activity.ActivityType, activity.Created)
_, err = ctx.Transaction.Exec("INSERT INTO useractivity (orgid, userid, labelid, documentid, pageid, sourcetype, activitytype, metadata, created) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
activity.OrgID, activity.UserID, activity.LabelID, activity.DocumentID, activity.PageID, activity.SourceType, activity.ActivityType, activity.Metadata, activity.Created)
if err != nil {
err = errors.Wrap(err, "execute record user activity")
@ -46,7 +46,7 @@ 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, DATE(a.created) as created, a.orgid, IFNULL(a.userid, '') AS userid, a.labelid, a.documentid, a.pageid, a.activitytype,
qry := `SELECT a.id, DATE(a.created) as created, a.orgid, IFNULL(a.userid, '') AS userid, a.labelid, a.documentid, a.pageid, a.activitytype, a.metadata,
IFNULL(u.firstname, 'Anonymous') AS firstname, IFNULL(u.lastname, 'Viewer') AS lastname,
IFNULL(p.title, '') as pagetitle
FROM useractivity a

View file

@ -53,7 +53,7 @@ func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (c []catego
WHERE orgid=? AND labelid=?
AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='category' AND refid IN (
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='category' UNION ALL
SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid
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='category' AND (r.userid=? OR r.userid='0')
))
ORDER BY category`, ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)
@ -70,19 +70,20 @@ func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (c []catego
// GetAllBySpace returns all space categories.
func (s Scope) GetAllBySpace(ctx domain.RequestContext, spaceID string) (c []category.Category, err error) {
c = []category.Category{}
err = s.Runtime.Db.Select(&c, `
SELECT id, refid, orgid, labelid, category, created, revised FROM category
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=? OR whoid='0') AND location='space' AND action='view' 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'
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 category`, ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)
if err == sql.ErrNoRows || len(c) == 0 {
if err == sql.ErrNoRows {
err = nil
c = []category.Category{}
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select all categories for space %s", spaceID))
@ -190,6 +191,8 @@ func (s Scope) DeleteBySpace(ctx domain.RequestContext, spaceID string) (rows in
// GetSpaceCategorySummary returns number of documents and users for space categories.
func (s Scope) GetSpaceCategorySummary(ctx domain.RequestContext, spaceID string) (c []category.SummaryModel, err error) {
c = []category.SummaryModel{}
err = s.Runtime.Db.Select(&c, `
SELECT 'documents' as type, categoryid, COUNT(*) as count
FROM categorymember
@ -197,14 +200,13 @@ func (s Scope) GetSpaceCategorySummary(ctx domain.RequestContext, spaceID string
UNION ALL
SELECT 'users' as type, refid AS categoryid, count(*) AS count
FROM permission
WHERE orgid=? AND location='category'
WHERE orgid=? AND location='category'
AND refid IN (SELECT refid FROM category WHERE orgid=? AND labelid=?)
GROUP BY refid, type`,
ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, spaceID /*, ctx.OrgID, ctx.OrgID, spaceID*/)
if err == sql.ErrNoRows || len(c) == 0 {
if err == sql.ErrNoRows {
err = nil
c = []category.SummaryModel{}
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("select category summary for space %s", spaceID))
@ -215,6 +217,8 @@ func (s Scope) GetSpaceCategorySummary(ctx domain.RequestContext, spaceID string
// GetDocumentCategoryMembership returns all space categories associated with given document.
func (s Scope) GetDocumentCategoryMembership(ctx domain.RequestContext, documentID string) (c []category.Category, err error) {
c = []category.Category{}
err = s.Runtime.Db.Select(&c, `
SELECT id, refid, orgid, labelid, category, created, revised FROM category
WHERE orgid=? AND refid IN (SELECT categoryid FROM categorymember WHERE orgid=? AND documentid=?)`, ctx.OrgID, ctx.OrgID, documentID)
@ -236,7 +240,7 @@ func (s Scope) GetSpaceCategoryMembership(ctx domain.RequestContext, spaceID str
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=? OR whoid='0') AND location='space' AND action='view' 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'
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 documentid`, ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, ctx.UserID, ctx.OrgID, ctx.UserID)

View file

@ -29,6 +29,7 @@ type RequestContext struct {
Guest bool
Editor bool
Global bool
Analytics bool
UserID string
OrgID string
OrgName string

View file

@ -142,6 +142,25 @@ func CopyDocument(ctx domain.RequestContext, s domain.Store, documentID string)
}
}
cats, err := s.Category.GetDocumentCategoryMembership(ctx, documentID)
if err != nil {
err = errors.Wrap(err, "unable to add copied page")
return
}
for ci := range cats {
cm := category.Member{}
cm.DocumentID = newDocumentID
cm.CategoryID = cats[ci].RefID
cm.OrgID = ctx.OrgID
cm.RefID = uniqueid.Generate()
s.Category.AssociateDocument(ctx, cm)
if err != nil {
err = errors.Wrap(err, "unable to add copied page")
return
}
}
return
}

View file

@ -17,6 +17,7 @@ import (
"io/ioutil"
"net/http"
"sort"
"strings"
"github.com/documize/community/core/env"
"github.com/documize/community/core/request"
@ -72,15 +73,15 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
// draft mode does not record document views
if document.Lifecycle == workflow.LifecycleLive {
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: document.LabelID,
DocumentID: document.RefID,
@ -91,9 +92,9 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
}
ctx.Transaction.Commit()
ctx.Transaction.Commit()
}
h.Store.Audit.Record(ctx, audit.EventTypeDocumentView)
@ -220,6 +221,13 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
if oldDoc.LabelID != d.LabelID {
h.Store.Category.RemoveDocumentCategories(ctx, d.RefID)
err = h.Store.Document.MoveActivity(ctx, documentID, oldDoc.LabelID, d.LabelID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
}
err = h.Store.Document.Update(ctx, d)
@ -394,6 +402,8 @@ func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) {
return
}
options.Keywords = strings.TrimSpace(options.Keywords)
results, err := h.Store.Search.Documents(ctx, options)
if err != nil {
h.Runtime.Log.Error(method, err)
@ -401,13 +411,35 @@ func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) {
// Put in slugs for easy UI display of search URL
for key, result := range results {
result.DocumentSlug = stringutil.MakeSlug(result.Document)
result.SpaceSlug = stringutil.MakeSlug(result.Space)
results[key] = result
results[key].DocumentSlug = stringutil.MakeSlug(result.Document)
results[key].SpaceSlug = stringutil.MakeSlug(result.Space)
}
if len(results) == 0 {
results = []search.QueryResult{}
// Record user search history
if !options.SkipLog {
if len(results) > 0 {
go h.recordSearchActivity(ctx, results, options.Keywords)
} else {
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
h.Runtime.Log.Error(method, err)
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: "",
DocumentID: "",
Metadata: options.Keywords,
SourceType: activity.SourceTypeSearch,
ActivityType: activity.TypeSearched})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
ctx.Transaction.Commit()
}
}
h.Store.Audit.Record(ctx, audit.EventTypeSearch)
@ -415,6 +447,50 @@ func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) {
response.WriteJSON(w, results)
}
// Record search request once per document.
// But only if document is partof shared space at the time of the search.
func (h *Handler) recordSearchActivity(ctx domain.RequestContext, q []search.QueryResult, keywords string) {
method := "recordSearchActivity"
var err error
prev := make(map[string]bool)
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
h.Runtime.Log.Error(method, err)
return
}
for i := range q {
// Empty space ID usually signals private document
// hence search activity should not be visible to others.
if len(q[i].SpaceID) == 0 {
continue
}
sp, err := h.Store.Space.Get(ctx, q[i].SpaceID)
if err != nil || len(sp.RefID) == 0 || sp.Type == space.ScopePrivate {
continue
}
if _, isExisting := prev[q[i].DocumentID]; !isExisting {
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: q[i].SpaceID,
DocumentID: q[i].DocumentID,
Metadata: keywords,
SourceType: activity.SourceTypeSearch,
ActivityType: activity.TypeSearched})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
prev[q[i].DocumentID] = true
}
}
ctx.Transaction.Commit()
}
// FetchDocumentData returns all document data in single API call.
func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
method := "document.FetchDocumentData"

View file

@ -105,6 +105,8 @@ func (s Scope) DocumentMeta(ctx domain.RequestContext, id string) (meta doc.Docu
// All versions of a document are returned, hence caller must
// decide what to do with them.
func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (documents []doc.Document, err error) {
documents = []doc.Document{}
err = s.Runtime.Db.Select(&documents, `
SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template,
protection, approval, lifecycle, versioned, versionid, versionorder, groupid, created, revised
@ -120,9 +122,8 @@ func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (documents
)
ORDER BY title, versionorder`, ctx.OrgID, ctx.OrgID, ctx.OrgID, spaceID, ctx.OrgID, ctx.UserID, ctx.OrgID, spaceID, ctx.UserID)
if err == sql.ErrNoRows || len(documents) == 0 {
if err == sql.ErrNoRows {
err = nil
documents = []doc.Document{}
}
if err != nil {
err = errors.Wrap(err, "select documents by space")
@ -237,6 +238,9 @@ func (s Scope) MoveDocumentSpace(ctx domain.RequestContext, id, move string) (er
_, err = ctx.Transaction.Exec("UPDATE document SET labelid=? WHERE orgid=? AND labelid=?",
move, ctx.OrgID, id)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute document space move %s", id))
}
@ -244,6 +248,18 @@ func (s Scope) MoveDocumentSpace(ctx domain.RequestContext, id, move string) (er
return
}
// MoveActivity changes the space for all document activity records.
func (s Scope) MoveActivity(ctx domain.RequestContext, documentID, oldSpaceID, newSpaceID string) (err error) {
_, err = ctx.Transaction.Exec("UPDATE useractivity SET labelid=? WHERE orgid=? AND labelid=? AND documentid=?",
newSpaceID, ctx.OrgID, oldSpaceID, documentID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute document activity move %s", documentID))
}
return
}
// Delete removes the specified document.
// Remove document pages, revisions, attachments, updates the search subsystem.
func (s Scope) Delete(ctx domain.RequestContext, documentID string) (rows int64, err error) {
@ -303,15 +319,16 @@ func (s Scope) DeleteBySpace(ctx domain.RequestContext, spaceID string) (rows in
// All versions of a document are returned, hence caller must
// decide what to do with them.
func (s Scope) GetVersions(ctx domain.RequestContext, groupID string) (v []doc.Version, err error) {
v = []doc.Version{}
err = s.Runtime.Db.Select(&v, `
SELECT versionid, refid as documentid
FROM document
WHERE orgid=? AND groupid=?
ORDER BY versionorder`, ctx.OrgID, groupID)
if err == sql.ErrNoRows || len(v) == 0 {
if err == sql.ErrNoRows {
err = nil
v = []doc.Version{}
}
if err != nil {
err = errors.Wrap(err, "document.store.GetVersions")

View file

@ -58,6 +58,8 @@ func (s Scope) Get(ctx domain.RequestContext, refID string) (g group.Group, err
// GetAll returns all user groups for current orgID.
func (s Scope) GetAll(ctx domain.RequestContext) (groups []group.Group, err error) {
groups = []group.Group{}
err = s.Runtime.Db.Select(&groups,
`SELECT a.id, a.refid, a.orgid, a.role as name, a.purpose, a.created, a.revised, COUNT(b.roleid) AS members
FROM role a
@ -67,9 +69,8 @@ func (s Scope) GetAll(ctx domain.RequestContext) (groups []group.Group, err erro
ORDER BY a.role`,
ctx.OrgID)
if err == sql.ErrNoRows || len(groups) == 0 {
if err == sql.ErrNoRows {
err = nil
groups = []group.Group{}
}
if err != nil {
err = errors.Wrap(err, "select groups")
@ -101,8 +102,10 @@ func (s Scope) Delete(ctx domain.RequestContext, refID string) (rows int64, err
// GetGroupMembers returns all user associated with given group.
func (s Scope) GetGroupMembers(ctx domain.RequestContext, groupID string) (members []group.Member, err error) {
members = []group.Member{}
err = s.Runtime.Db.Select(&members,
`SELECT a.id, a.orgid, a.roleid, a.userid,
`SELECT a.id, a.orgid, a.roleid, a.userid,
IFNULL(b.firstname, '') as firstname, IFNULL(b.lastname, '') as lastname
FROM rolemember a
LEFT JOIN user b ON b.refid=a.userid
@ -110,9 +113,8 @@ func (s Scope) GetGroupMembers(ctx domain.RequestContext, groupID string) (membe
ORDER BY b.firstname, b.lastname`,
ctx.OrgID, groupID)
if err == sql.ErrNoRows || len(members) == 0 {
if err == sql.ErrNoRows {
err = nil
members = []group.Member{}
}
if err != nil {
err = errors.Wrap(err, "select members")
@ -146,16 +148,17 @@ func (s Scope) LeaveGroup(ctx domain.RequestContext, groupID, userID string) (er
// Useful when you need to bulk fetch membership records
// for subsequent processing.
func (s Scope) GetMembers(ctx domain.RequestContext) (r []group.Record, err error) {
r = []group.Record{}
err = s.Runtime.Db.Select(&r,
`SELECT a.id, a.orgid, a.roleid, a.userid, b.role as name, b.purpose
FROM rolemember a, role b
WHERE a.orgid=? AND a.roleid=b.refid
WHERE a.orgid=? AND a.roleid=b.refid
ORDER BY a.userid`,
ctx.OrgID)
if err == sql.ErrNoRows || len(r) == 0 {
if err == sql.ErrNoRows {
err = nil
r = []group.Record{}
}
if err != nil {
err = errors.Wrap(err, "select group members")

View file

@ -1236,6 +1236,9 @@ func (h *Handler) FetchPages(w http.ResponseWriter, r *http.Request) {
return
}
// Who referred user this document (e.g. search page).
source := request.Query(r, "source")
doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil {
response.WriteServerError(w, method, err)
@ -1393,6 +1396,28 @@ func (h *Handler) FetchPages(w http.ResponseWriter, r *http.Request) {
}
}
// If we have source, record document access via source.
if len(source) > 0 {
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
h.Runtime.Log.Error(method, err)
} else {
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: doc.LabelID,
DocumentID: doc.RefID,
Metadata: source, // deliberate
SourceType: activity.SourceTypeSearch, // deliberate
ActivityType: activity.TypeRead})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
ctx.Transaction.Commit()
}
}
// deliver payload
response.WriteJSON(w, model)
}

View file

@ -62,6 +62,8 @@ func (s Scope) AddPermissions(ctx domain.RequestContext, r permission.Permission
// Context is used to for userID because must match by userID
// or everyone ID of 0.
func (s Scope) GetUserSpacePermissions(ctx domain.RequestContext, spaceID string) (r []permission.Permission, err error) {
r = []permission.Permission{}
err = s.Runtime.Db.Select(&r, `
SELECT id, orgid, who, whoid, action, scope, location, refid
FROM permission
@ -73,9 +75,8 @@ func (s Scope) GetUserSpacePermissions(ctx domain.RequestContext, spaceID string
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.UserID)
if err == sql.ErrNoRows || len(r) == 0 {
if err == sql.ErrNoRows {
err = nil
r = []permission.Permission{}
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select user permissions %s", ctx.UserID))
@ -87,6 +88,8 @@ func (s Scope) GetUserSpacePermissions(ctx domain.RequestContext, spaceID string
// GetSpacePermissions returns space permissions for all users.
// We do not filter by userID because we return permissions for all users.
func (s Scope) GetSpacePermissions(ctx domain.RequestContext, spaceID string) (r []permission.Permission, err error) {
r = []permission.Permission{}
err = s.Runtime.Db.Select(&r, `
SELECT id, orgid, who, whoid, action, scope, location, refid
FROM permission WHERE orgid=? AND location='space' AND refid=? AND who='user'
@ -99,7 +102,6 @@ func (s Scope) GetSpacePermissions(ctx domain.RequestContext, spaceID string) (r
if err == sql.ErrNoRows {
err = nil
r = []permission.Permission{}
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select space permissions %s", ctx.UserID))
@ -110,6 +112,8 @@ func (s Scope) GetSpacePermissions(ctx domain.RequestContext, spaceID string) (r
// GetCategoryPermissions returns category permissions for all users.
func (s Scope) GetCategoryPermissions(ctx domain.RequestContext, catID string) (r []permission.Permission, err error) {
r = []permission.Permission{}
err = s.Runtime.Db.Select(&r, `
SELECT id, orgid, who, whoid, action, scope, location, refid
FROM permission
@ -121,9 +125,8 @@ func (s Scope) GetCategoryPermissions(ctx domain.RequestContext, catID string) (
WHERE p.orgid=? AND p.location='category' AND p.who='role' AND (p.refid=? OR p.refid='0')`,
ctx.OrgID, catID, ctx.OrgID, catID)
if err == sql.ErrNoRows || len(r) == 0 {
if err == sql.ErrNoRows {
err = nil
r = []permission.Permission{}
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select category permissions %s", catID))
@ -134,6 +137,8 @@ func (s Scope) GetCategoryPermissions(ctx domain.RequestContext, catID string) (
// GetCategoryUsers returns space permissions for all users.
func (s Scope) GetCategoryUsers(ctx domain.RequestContext, catID string) (u []user.User, err error) {
u = []user.User{}
err = s.Runtime.Db.Select(&u, `
SELECT u.id, IFNULL(u.refid, '') AS refid, IFNULL(u.firstname, '') AS firstname, IFNULL(u.lastname, '') as lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised
FROM user u LEFT JOIN account a ON u.refid = a.userid
@ -149,7 +154,7 @@ func (s Scope) GetCategoryUsers(ctx domain.RequestContext, catID string) (u []us
ORDER BY firstname, lastname`,
ctx.OrgID, ctx.OrgID, catID, ctx.OrgID, catID)
if err == sql.ErrNoRows || len(u) == 0 {
if err == sql.ErrNoRows {
err = nil
u = []user.User{}
}
@ -162,6 +167,8 @@ func (s Scope) GetCategoryUsers(ctx domain.RequestContext, catID string) (u []us
// GetUserCategoryPermissions returns category permissions for given user.
func (s Scope) GetUserCategoryPermissions(ctx domain.RequestContext, userID string) (r []permission.Permission, err error) {
r = []permission.Permission{}
err = s.Runtime.Db.Select(&r, `
SELECT id, orgid, who, whoid, action, scope, location, refid
FROM permission WHERE orgid=? AND location='category' AND who='user' AND (whoid=? OR whoid='0')
@ -172,9 +179,8 @@ func (s Scope) GetUserCategoryPermissions(ctx domain.RequestContext, userID stri
WHERE p.orgid=? AND p.location='category' AND p.who='role' AND (r.userid=? OR r.userid='0')`,
ctx.OrgID, userID, ctx.OrgID, userID)
if err == sql.ErrNoRows || len(r) == 0 {
if err == sql.ErrNoRows {
err = nil
r = []permission.Permission{}
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select category permissions for user %s", userID))

View file

@ -204,6 +204,10 @@ func (s Scope) Documents(ctx domain.RequestContext, q search.QueryOptions) (resu
results = append(results, r4...)
}
if len(results) == 0 {
results = []search.QueryResult{}
}
return
}
@ -227,9 +231,10 @@ 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 who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view'
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 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 (r.userid=? OR r.userid='0')
)
)
AND MATCH(s.content) AGAINST(? IN BOOLEAN MODE)`
@ -280,13 +285,13 @@ func (s Scope) matchLike(ctx domain.RequestContext, keywords, itemType string) (
-- AND d.template = 0
AND d.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=? OR whoid='0') AND location='space' AND action='view'
SELECT refid FROM label WHERE orgid=? AND refid IN
(
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=? OR r.userid='0')
))
AND p.location='space' AND (r.userid=? OR r.userid='0')
)
)
AND s.content LIKE ?`

View file

@ -24,7 +24,7 @@ func (m *Indexer) IndexDocument(ctx domain.RequestContext, d doc.Document, a []a
method := "search.IndexDocument"
var err error
ctx.Transaction, err = m.runtime.Db.Beginx()
tx, err := m.runtime.Db.Beginx()
if err != nil {
m.runtime.Log.Error(method, err)
return
@ -32,12 +32,12 @@ func (m *Indexer) IndexDocument(ctx domain.RequestContext, d doc.Document, a []a
err = m.store.Search.IndexDocument(ctx, d, a)
if err != nil {
ctx.Transaction.Rollback()
tx.Rollback()
m.runtime.Log.Error(method, err)
return
}
ctx.Transaction.Commit()
tx.Commit()
}
// DeleteDocument removes all search entries for document.
@ -45,7 +45,7 @@ func (m *Indexer) DeleteDocument(ctx domain.RequestContext, ID string) {
method := "search.DeleteDocument"
var err error
ctx.Transaction, err = m.runtime.Db.Beginx()
tx, err := m.runtime.Db.Beginx()
if err != nil {
m.runtime.Log.Error(method, err)
return
@ -53,12 +53,12 @@ func (m *Indexer) DeleteDocument(ctx domain.RequestContext, ID string) {
err = m.store.Search.DeleteDocument(ctx, ID)
if err != nil {
ctx.Transaction.Rollback()
tx.Rollback()
m.runtime.Log.Error(method, err)
return
}
ctx.Transaction.Commit()
tx.Commit()
}
// IndexContent adds search index entry for document context.
@ -67,7 +67,7 @@ func (m *Indexer) IndexContent(ctx domain.RequestContext, p page.Page) {
method := "search.IndexContent"
var err error
ctx.Transaction, err = m.runtime.Db.Beginx()
tx, err := m.runtime.Db.Beginx()
if err != nil {
m.runtime.Log.Error(method, err)
return
@ -75,12 +75,12 @@ func (m *Indexer) IndexContent(ctx domain.RequestContext, p page.Page) {
err = m.store.Search.IndexContent(ctx, p)
if err != nil {
ctx.Transaction.Rollback()
tx.Rollback()
m.runtime.Log.Error(method, err)
return
}
ctx.Transaction.Commit()
tx.Commit()
}
// DeleteContent removes all search entries for specific document content.
@ -88,7 +88,7 @@ func (m *Indexer) DeleteContent(ctx domain.RequestContext, pageID string) {
method := "search.DeleteContent"
var err error
ctx.Transaction, err = m.runtime.Db.Beginx()
tx, err := m.runtime.Db.Beginx()
if err != nil {
m.runtime.Log.Error(method, err)
return
@ -96,10 +96,10 @@ func (m *Indexer) DeleteContent(ctx domain.RequestContext, pageID string) {
err = m.store.Search.DeleteContent(ctx, pageID)
if err != nil {
ctx.Transaction.Rollback()
tx.Rollback()
m.runtime.Log.Error(method, err)
return
}
ctx.Transaction.Commit()
}
tx.Commit()
}

View file

@ -33,6 +33,7 @@ import (
"github.com/documize/community/domain/mail"
"github.com/documize/community/domain/organization"
"github.com/documize/community/model/account"
"github.com/documize/community/model/activity"
"github.com/documize/community/model/audit"
"github.com/documize/community/model/doc"
"github.com/documize/community/model/page"
@ -127,6 +128,15 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: sp.RefID,
SourceType: activity.SourceTypeSpace,
ActivityType: activity.TypeCreated})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSpaceAdd)
@ -338,6 +348,25 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
h.Runtime.Log.Error(method, err)
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: sp.RefID,
SourceType: activity.SourceTypeSpace,
ActivityType: activity.TypeRead})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
ctx.Transaction.Commit()
response.WriteJSON(w, sp)
}
@ -510,6 +539,15 @@ func (h *Handler) Remove(w http.ResponseWriter, r *http.Request) {
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: id,
SourceType: activity.SourceTypeSpace,
ActivityType: activity.TypeDeleted})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSpaceDelete)
@ -598,6 +636,15 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
return
}
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: id,
SourceType: activity.SourceTypeSpace,
ActivityType: activity.TypeDeleted})
if err != nil {
ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err)
}
ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSpaceDelete)

View file

@ -180,6 +180,7 @@ type DocumentStorer interface {
Delete(ctx RequestContext, documentID string) (rows int64, err error)
DeleteBySpace(ctx RequestContext, spaceID string) (rows int64, err error)
GetVersions(ctx RequestContext, groupID string) (v []doc.Version, err error)
MoveActivity(ctx RequestContext, documentID, oldSpaceID, newSpaceID string) (err error)
}
// SettingStorer defines required methods for persisting global and user level settings

View file

@ -162,6 +162,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
a.Editor = true
a.Admin = false
a.Active = true
a.Analytics = false
err = h.Store.Account.Add(ctx, a)
if err != nil {
@ -481,6 +482,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
a.Admin = u.Admin
a.Active = u.Active
a.Users = u.ViewUsers
a.Analytics = u.Analytics
err = h.Store.Account.UpdateAccount(ctx, a)
if err != nil {
@ -799,6 +801,7 @@ func (h *Handler) BulkImport(w http.ResponseWriter, r *http.Request) {
a.Editor = true
a.Admin = false
a.Active = true
a.Analytics = false
err = h.Store.Account.Add(ctx, a)
if err != nil {

View file

@ -110,17 +110,18 @@ func (s Scope) GetBySerial(ctx domain.RequestContext, serial string) (u user.Use
// GetActiveUsersForOrganization returns a slice containing of active user records for the organization
// identified in the Persister.
func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []user.User, err error) {
u = []user.User{}
err = s.Runtime.Db.Select(&u,
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.lastversion, u.created, u.revised,
u.global, a.active, a.editor, a.admin, a.users as viewusers
u.global, a.active, a.editor, a.admin, a.users as viewusers, a.analytics
FROM user u, account a
WHERE u.refid=a.userid AND a.orgid=? AND a.active=1
ORDER BY u.firstname,u.lastname`,
ctx.OrgID)
if err == sql.ErrNoRows || len(u) == 0 {
if err == sql.ErrNoRows {
err = nil
u = []user.User{}
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("get active users by org %s", ctx.OrgID))
@ -132,6 +133,8 @@ func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []use
// GetUsersForOrganization returns a slice containing all of the user records for the organizaiton
// identified in the Persister.
func (s Scope) GetUsersForOrganization(ctx domain.RequestContext, filter string) (u []user.User, err error) {
u = []user.User{}
filter = strings.TrimSpace(strings.ToLower(filter))
likeQuery := ""
if len(filter) > 0 {
@ -140,14 +143,13 @@ func (s Scope) GetUsersForOrganization(ctx domain.RequestContext, filter string)
err = s.Runtime.Db.Select(&u,
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.lastversion, u.created, u.revised,
u.global, a.active, a.editor, a.admin, a.users as viewusers
u.global, a.active, a.editor, a.admin, a.users as viewusers, a.analytics
FROM user u, account a
WHERE u.refid=a.userid AND a.orgid=? `+likeQuery+
`ORDER BY u.firstname, u.lastname LIMIT 100`, ctx.OrgID)
if err == sql.ErrNoRows || len(u) == 0 {
if err == sql.ErrNoRows {
err = nil
u = []user.User{}
}
if err != nil {
@ -159,9 +161,11 @@ func (s Scope) GetUsersForOrganization(ctx domain.RequestContext, filter string)
// GetSpaceUsers returns a slice containing all user records for given space.
func (s Scope) GetSpaceUsers(ctx domain.RequestContext, spaceID string) (u []user.User, err error) {
u = []user.User{}
err = s.Runtime.Db.Select(&u, `
SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.lastversion, u.revised, u.global,
a.active, a.users AS viewusers, a.editor, a.admin
a.active, a.users AS viewusers, a.editor, a.admin, a.analytics
FROM user u, account a
WHERE a.orgid=? AND u.refid = a.userid AND a.active=1 AND u.refid IN (
SELECT whoid from permission WHERE orgid=? AND who='user' AND scope='object' AND location='space' AND refid=? UNION ALL
@ -170,9 +174,8 @@ func (s Scope) GetSpaceUsers(ctx domain.RequestContext, spaceID string) (u []use
ORDER BY u.firstname, u.lastname
`, ctx.OrgID, ctx.OrgID, spaceID, ctx.OrgID, spaceID)
if err == sql.ErrNoRows || len(u) == 0 {
if err == sql.ErrNoRows {
err = nil
u = []user.User{}
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("get space users for org %s", ctx.OrgID))
@ -183,14 +186,15 @@ func (s Scope) GetSpaceUsers(ctx domain.RequestContext, spaceID string) (u []use
// GetUsersForSpaces returns users with access to specified spaces.
func (s Scope) GetUsersForSpaces(ctx domain.RequestContext, spaces []string) (u []user.User, err error) {
u = []user.User{}
if len(spaces) == 0 {
u = []user.User{}
return
}
query, args, err := sqlx.In(`
SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.lastversion, u.created, u.revised, u.global,
a.active, a.users AS viewusers, a.editor, a.admin
a.active, a.users AS viewusers, a.editor, a.admin, a.analytics
FROM user u, account a
WHERE a.orgid=? AND u.refid = a.userid AND a.active=1 AND u.refid IN (
SELECT whoid from permission WHERE orgid=? AND who='user' AND scope='object' AND location='space' AND refid IN(?) UNION ALL
@ -202,9 +206,8 @@ func (s Scope) GetUsersForSpaces(ctx domain.RequestContext, spaces []string) (u
query = s.Runtime.Db.Rebind(query)
err = s.Runtime.Db.Select(&u, query, args...)
if err == sql.ErrNoRows || len(u) == 0 {
if err == sql.ErrNoRows {
err = nil
u = []user.User{}
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("get users for spaces for user %s", ctx.UserID))
@ -282,6 +285,8 @@ func (s Scope) CountActiveUsers() (c int) {
// MatchUsers returns users that have match to either firstname, lastname or email.
func (s Scope) MatchUsers(ctx domain.RequestContext, text string, maxMatches int) (u []user.User, err error) {
u = []user.User{}
text = strings.TrimSpace(strings.ToLower(text))
likeQuery := ""
if len(text) > 0 {
@ -290,15 +295,14 @@ func (s Scope) MatchUsers(ctx domain.RequestContext, text string, maxMatches int
err = s.Runtime.Db.Select(&u,
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.lastversion, u.created, u.revised,
u.global, a.active, a.editor, a.admin, a.users as viewusers
u.global, a.active, a.editor, a.admin, a.users as viewusers, a.analytics
FROM user u, account a
WHERE a.orgid=? AND u.refid=a.userid AND a.active=1 `+likeQuery+
`ORDER BY u.firstname,u.lastname LIMIT `+strconv.Itoa(maxMatches),
ctx.OrgID)
if err == sql.ErrNoRows || len(u) == 0 {
if err == sql.ErrNoRows {
err = nil
u = []user.User{}
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("matching users for org %s", ctx.OrgID))

View file

@ -40,6 +40,7 @@ func AttachUserAccounts(ctx domain.RequestContext, s domain.Store, orgID string,
u.Admin = false
u.Active = false
u.ViewUsers = false
u.Analytics = false
for _, account := range u.Accounts {
if account.OrgID == orgID {
@ -47,6 +48,7 @@ func AttachUserAccounts(ctx domain.RequestContext, s domain.Store, orgID string,
u.Editor = account.Editor
u.Active = account.Active
u.ViewUsers = account.Users
u.Analytics = account.Analytics
break
}
}

View file

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

File diff suppressed because one or more lines are too long

View file

@ -27,7 +27,7 @@ export default Component.extend(AuthProvider, ModalMixin, {
init() {
this._super(...arguments);
this.password = {};
this.selectedUsers = [];
this.selectedUsers = [];
},
didReceiveAttrs() {
@ -91,6 +91,13 @@ export default Component.extend(AuthProvider, ModalMixin, {
cb(user);
},
toggleAnalytics(id) {
let user = this.users.findBy("id", id);
user.set('analytics', !user.get('analytics'));
let cb = this.get('onSave');
cb(user);
},
toggleUsers(id) {
let user = this.users.findBy("id", id);
user.set('viewUsers', !user.get('viewUsers'));
@ -208,7 +215,7 @@ export default Component.extend(AuthProvider, ModalMixin, {
this.get('groupSvc').leave(groupId, userId).then(() => {
this.filterUsers();
});
});
},
onJoinGroup(groupId) {
@ -222,7 +229,7 @@ export default Component.extend(AuthProvider, ModalMixin, {
this.get('groupSvc').join(groupId, userId).then(() => {
this.filterUsers();
});
}
});
}
}
});

View file

@ -9,10 +9,14 @@
//
// https://documize.com
import { computed } from '@ember/object';
import Component from '@ember/component';
export default Component.extend({
resultPhrase: '',
searchQuery: computed('keywords', function() {
return encodeURIComponent(this.get('keywords'));
}),
didReceiveAttrs() {
this._super(...arguments);

View file

@ -0,0 +1,56 @@
// 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 { A } from '@ember/array';
import { inject as service } from '@ember/service';
import Component from '@ember/component';
export default Component.extend({
searchSvc: service('search'),
results: A([]),
init() {
this._super(...arguments);
this.fetch();
},
fetch() {
let payload = {
keywords: this.get('filter'),
doc: this.get('matchDoc'),
attachment: this.get('matchFile'),
tag: this.get('matchTag'),
content: this.get('matchContent'),
slog: this.get('slog')
};
payload.keywords = payload.keywords.trim();
if (payload.keywords.length == 0) {
this.set('results', A([]));
return;
}
if (!payload.doc && !payload.tag && !payload.content && !payload.attachment) {
this.set('results', A([]));
return;
}
this.get('searchSvc').find(payload).then( (response) => {
this.set('results', response);
});
},
actions: {
onSearch() {
this.fetch();
}
}
});

View file

@ -9,8 +9,11 @@
//
// https://documize.com
import { inject as service } from '@ember/service';
import Component from '@ember/component';
export default Component.extend({
classNames: ['col', 'col-sm-8']
appMeta: service(),
classNames: ['col', 'col-sm-8'],
selectItem: '',
});

View file

@ -22,6 +22,7 @@ export default Model.extend({
editor: attr('boolean', { defaultValue: false }),
admin: attr('boolean', { defaultValue: false }),
viewUsers: attr('boolean', { defaultValue: false }),
analytics: attr('boolean', { defaultValue: false }),
global: attr('boolean', { defaultValue: false }),
accounts: attr(),
groups: attr(),

View file

@ -1,14 +1,17 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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>.
// by contacting <sales@documize.com>.
//
// https://documize.com
import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
export default Controller.extend({});
export default Controller.extend({
appMeta: service()
});

View file

@ -9,8 +9,8 @@
//
// https://documize.com
import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';
import Route from '@ember/routing/route';
export default Route.extend(AuthenticatedRouteMixin, {
beforeModel: function (transition) {

View file

@ -2,7 +2,6 @@
{{#toolbar/t-toolbar}}
{{#toolbar/t-links}}
{{#link-to "folders" class="link" tagName="li" }}Spaces{{/link-to}}
{{/toolbar/t-links}}
{{#toolbar/t-actions}}
{{/toolbar/t-actions}}
@ -10,7 +9,7 @@
<div class="container">
<div class="row">
<div class="col my-5">
<div class="col my-5 text-center">
<ul class="tabnav-control">
{{#link-to 'customize.general' activeClass='selected' class="tab" tagName="li" }}General{{/link-to}}
{{#link-to 'customize.folders' activeClass='selected' class="tab" tagName="li" }}Spaces{{/link-to}}
@ -21,8 +20,13 @@
{{#link-to 'customize.license' activeClass='selected' class="tab" tagName="li" }}License{{/link-to}}
{{#link-to 'customize.auth' activeClass='selected' class="tab" tagName="li" }}Authentication{{/link-to}}
{{#link-to 'customize.search' activeClass='selected' class="tab" tagName="li" }}Search{{/link-to}}
{{#if (eq appMeta.edition 'Enterprise')}}
{{#link-to 'customize.audit' activeClass='selected' class="tab" tagName="li" }}Audit{{/link-to}}
{{/if}}
{{/if}}
{{#if (eq appMeta.edition 'Enterprise')}}
{{#link-to 'customize.archive' activeClass='selected' class="tab" tagName="li" }}Archive{{/link-to}}
{{/if}}
{{#link-to 'customize.archive' activeClass='selected' class="tab" tagName="li" }}Archive{{/link-to}}
</ul>
</div>
</div>

View file

@ -214,12 +214,16 @@ export default Controller.extend(TooltipMixin, {
},
refresh() {
return new EmberPromise((resolve) => {
this.get('documentService').fetchPages(this.get('document.id'), this.get('session.user.id')).then((data) => {
this.set('pages', data);
});
this.get('sectionService').getSpaceBlocks(this.get('folder.id')).then((data) => {
this.set('blocks', data);
});
resolve();
});
});
}
}

View file

@ -20,9 +20,13 @@ export default Route.extend(AuthenticatedRouteMixin, {
folderService: service('folder'),
userService: service('user'),
beforeModel() {
beforeModel(transition) {
// Note the source that sent user to this document.
let source = transition.queryParams.source;
if (is.null(source) || is.undefined(source)) source = "";
return new EmberPromise((resolve) => {
this.get('documentService').fetchPages(this.paramsFor('document').document_id, this.get('session.user.id')).then((data) => {
this.get('documentService').fetchPages(this.paramsFor('document').document_id, this.get('session.user.id'), source).then((data) => {
this.set('pages', data);
resolve();
});

View file

@ -1,4 +1,6 @@
{{toolbar/nav-bar}} {{toolbar/for-document document=document spaces=folders space=folder
{{toolbar/nav-bar}}
{{toolbar/for-document document=document spaces=folders space=folder
permissions=permissions roles=roles tab=tab versions=versions
onDocumentDelete=(action 'onDocumentDelete')
onSaveTemplate=(action 'onSaveTemplate')

View file

@ -2,8 +2,7 @@
{{#toolbar/t-toolbar}}
{{#toolbar/t-links}}
{{#link-to "folders" class="link" tagName="li"}}Spaces{{/link-to}}
{{#link-to "folder" model.folder.id model.folder.slug class="link" tagName="li"}}{{model.folder.name}}{{/link-to}}
{{#link-to "folder" model.folder.id model.folder.slug class="link selected" tagName="li"}}{{model.folder.name}}{{/link-to}}
{{/toolbar/t-links}}
{{/toolbar/t-toolbar}}

View file

@ -2,7 +2,6 @@
{{#toolbar/t-toolbar}}
{{#toolbar/t-links}}
{{#link-to "folders" class="link" tagName="li"}}Spaces{{/link-to}}
{{/toolbar/t-links}}
{{#toolbar/t-actions}}
{{/toolbar/t-actions}}

View file

@ -9,64 +9,14 @@
//
// https://documize.com
import { debounce } from '@ember/runloop';
import { inject as service } from '@ember/service';
import Controller from '@ember/controller';
export default Controller.extend({
searchService: service('search'),
queryParams: ['filter', 'matchDoc', 'matchContent', 'matchTag', 'matchFile'],
queryParams: ['filter', 'matchDoc', 'matchContent', 'matchTag', 'matchFile', 'slog'],
filter: '',
onKeywordChange: function () {
debounce(this, this.fetch, 750);
}.observes('filter'),
matchDoc: true,
onMatchDoc: function () {
debounce(this, this.fetch, 750);
}.observes('matchDoc'),
matchContent: true,
onMatchContent: function () {
debounce(this, this.fetch, 750);
}.observes('matchContent'),
matchTag: false,
onMatchTag: function () {
debounce(this, this.fetch, 750);
}.observes('matchTag'),
matchFile: false,
onMatchFile: function () {
debounce(this, this.fetch, 750);
}.observes('matchFile'),
init() {
this._super(...arguments);
this.results = [];
},
fetch() {
let self = this;
let payload = {
keywords: this.get('filter'),
doc: this.get('matchDoc'),
attachment: this.get('matchFile'),
tag: this.get('matchTag'),
content: this.get('matchContent')
};
payload.keywords = payload.keywords.trim();
if (payload.keywords.length == 0) {
return;
}
if (!payload.doc && !payload.tag && !payload.content && !payload.attachment) {
return;
}
this.get('searchService').find(payload).then(function(response) {
self.set('results', response);
});
},
slog: false,
});

View file

@ -1,11 +1,11 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// 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>.
// by contacting <sales@documize.com>.
//
// https://documize.com
@ -15,5 +15,5 @@ import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-rout
export default Route.extend(AuthenticatedRouteMixin, {
activate() {
this.get('browser').setTitle('Search');
}
});
}
});

View file

@ -2,35 +2,9 @@
{{#toolbar/t-toolbar}}
{{#toolbar/t-links}}
{{#link-to "folders" class="link" tagName="li" }}Spaces{{/link-to}}
{{/toolbar/t-links}}
{{#toolbar/t-actions}}
{{/toolbar/t-actions}}
{{/toolbar/t-toolbar}}
<div class="container">
<div class="view-search mt-5">
<div class="heading">Search</div>
<div class="form-group mt-4">
{{focus-input type="text" value=filter class="form-control mb-4" placeholder='a OR b, x AND y, "phrase mat*"'}}
<div class="form-check form-check-inline">
{{input type="checkbox" id="search-1" class="form-check-input" checked=matchDoc}}
<label class="form-check-label" for="search-1">&nbsp;document title</label>
</div>
<div class="form-check form-check-inline">
{{input type="checkbox" id="search-2" class="form-check-input" checked=matchContent}}
<label class="form-check-label" for="search-2">&nbsp;content</label>
</div>
<div class="form-check form-check-inline">
{{input type="checkbox" id="search-3" class="form-check-input" checked=matchTag}}
<label class="form-check-label" for="search-3">&nbsp;tag name</label>
</div>
<div class="form-check form-check-inline">
{{input type="checkbox" id="search-4" class="form-check-input" checked=matchFile}}
<label class="form-check-label" for="search-4">&nbsp;attachment name</label>
</div>
</div>
{{search/search-results results=results}}
</div>
</div>
{{search/search-view filter=filter matchDoc=matchDoc matchContent=matchContent matchTag=matchTag matchFile=matchFile slog=slog}}

View file

@ -25,6 +25,10 @@ export default Router.map(function () {
path: 'dashboard'
});
this.route('analytics', {
path: 'analytics'
});
this.route(
'folder',
{

View file

@ -40,15 +40,15 @@ export default AjaxService.extend({
// when unauthorized on local API AJAX calls, redirect to app root
if (status === 401 && is.not.undefined(appVersion) && is.not.includes(window.location.href, '/auth')) {
this.get('localStorage').clearAll();
this.get('localStorage').clearAll();
window.location.href = 'auth/login';
}
if (is.not.empty(userUpdate)) {
let latest = JSON.parse(userUpdate);
if (!latest.active || user.editor !== latest.editor || user.admin !== latest.admin || user.viewUsers !== latest.viewUsers) {
this.get('localStorage').clearAll();
if (!latest.active || user.editor !== latest.editor || user.admin !== latest.admin || user.analytics !== latest.analytics || user.viewUsers !== latest.viewUsers) {
this.get('localStorage').clearAll();
window.location.href = 'auth/login';
}
}

View file

@ -375,7 +375,9 @@ export default Service.extend({
// This method bulk fetches data to reduce network chatter.
// We produce a bunch of calculated boolean's for UI display purposes
// that can tell us quickly about pending changes for UI display.
fetchPages(documentId, currentUserId) {
// Source - optional identifier of (document) referrer.
fetchPages(documentId, currentUserId, source) {
let constants = this.get('constants');
let changePending = false;
let changeAwaitingReview = false;
@ -384,7 +386,9 @@ export default Service.extend({
let userHasChangeAwaitingReview = false;
let userHasChangeRejected = false;
return this.get('ajax').request(`fetch/page/${documentId}`, {
if (is.null(source) || is.undefined(source)) source = "";
return this.get('ajax').request(`fetch/page/${documentId}?source=${source}`, {
method: 'GET'
}).then((response) => {
let data = A([]);

View file

@ -30,6 +30,7 @@ $color-dark: #434343;
$color-gray: #8b9096;
$color-gray-light: #d8d8d8;
$color-gray-light2: #eaeaea;
$color-gray-light3: #f9f9f9;
$color-border: #d3d3d3;
// colors

View file

@ -4,13 +4,14 @@
> .links {
display: inlne-block;
> .link {
> .link, div > .link {
color: $color-gray;
font-size: 1.1rem;
font-weight: bold;
display: inline-block;
margin-right: 30px;
cursor: pointer;
text-transform: uppercase;
@include ease-in();
// border-bottom: 2px solid $color-gray-light;
@ -19,7 +20,7 @@
}
}
> .selected {
> .selected, div > .link {
color: $color-link;
}
}

View file

@ -1,5 +1,5 @@
<div class="view-customize my-5">
<div class="my-2">
<span class="font-weight-bold">Spaces</span>
<span class="text-muted">&nbsp;&nbsp;&mdash;&nbsp;can add spaces, both personal and shared with others</span>
@ -12,6 +12,10 @@
<span class="font-weight-bold">Admin</span>
<span class="text-muted">&nbsp;&nbsp;&mdash;&nbsp;can manage all aspects of Documize, like this screen</span>
</div>
<div class="my-2">
<span class="font-weight-bold">Analytics</span>
<span class="text-muted">&nbsp;&nbsp;&mdash;&nbsp;can view analytical reports</span>
</div>
<div class="mt-2 mb-4">
<span class="font-weight-bold">Active</span>
<span class="text-muted">&nbsp;&nbsp;&mdash;&nbsp;can login and use Documize</span>
@ -28,11 +32,12 @@
<th class="text-muted">
{{#if hasSelectedUsers}}
<button id="bulk-delete-users" type="button" class="btn btn-danger" data-toggle="modal" data-target="#admin-user-delete-modal" data-backdrop="static">Delete selected users</button>
{{/if}}
{{/if}}
</th>
<th class="no-width">Spaces</th>
<th class="no-width">Visible</th>
<th class="no-width">Admin</th>
<th class="no-width">Analytics</th>
<th class="no-width">Active</th>
<th class="no-width">
</th>
@ -87,6 +92,13 @@
<i class="material-icons checkbox" {{action 'toggleAdmin' user.id}}>check_box_outline_blank</i>
{{/if}}
</td>
<td class="no-width text-center">
{{#if user.analytics}}
<i class="material-icons checkbox" {{action 'toggleAnalytics' user.id}}>check_box</i>
{{else}}
<i class="material-icons checkbox" {{action 'toggleAnalytics' user.id}}>check_box_outline_blank</i>
{{/if}}
</td>
<td class="no-width text-center">
{{#if user.me}}
<i class="material-icons color-gray">check_box</i>

View file

@ -30,7 +30,7 @@
<div class="col-12 col-sm-3 heading">Tags</div>
<div class="col-12 col-sm-9 value">
{{#each tagz as |t index|}}
{{#link-to 'search' (query-params filter=t matchTag=true)}}
{{#link-to 'search' (query-params filter=t matchTag=true matchDoc=false matchContent=false matchFile=false)}}
{{concat '#' t}}
{{/link-to}}&nbsp;&nbsp;
{{/each}}

View file

@ -3,7 +3,7 @@
<ul class="documents">
{{#each documents key="id" as |result index|}}
<li class="document">
<a class="link" href="s/{{result.spaceId}}/{{result.spaceSlug}}/d/{{ result.documentId }}/{{result.documentSlug}}?page={{ result.itemId }}">
{{#link-to 'document.index' result.spaceId result.spaceSlug result.documentId result.documentSlug (query-params currentPageId=result.itemId source=searchQuery) class="link"}}
<div class="title">
{{result.document}}
{{#if (gt result.versionId.length 0)}}
@ -16,7 +16,7 @@
{{#if result.template}}
<button type="button" class="mt-3 btn btn-warning text-uppercase font-weight-bold">TEMPLATE</button>
{{/if}}
</a>
{{/link-to}}
</li>
{{/each}}
</ul>

View file

@ -0,0 +1,31 @@
<div class="container">
<div class="view-search mt-5">
<div class="heading">Search</div>
<form onsubmit={{action 'onSearch'}}>
<div class="form-group mt-4">
{{focus-input type="text" value=filter class="form-control mb-4" placeholder='a OR b, x AND y, "phrase mat*"'}}
<div class="form-check form-check-inline">
{{input type="checkbox" id="search-1" class="form-check-input" checked=matchDoc}}
<label class="form-check-label" for="search-1">&nbsp;document title</label>
</div>
<div class="form-check form-check-inline">
{{input type="checkbox" id="search-2" class="form-check-input" checked=matchContent}}
<label class="form-check-label" for="search-2">&nbsp;content</label>
</div>
<div class="form-check form-check-inline">
{{input type="checkbox" id="search-3" class="form-check-input" checked=matchTag}}
<label class="form-check-label" for="search-3">&nbsp;tag name</label>
</div>
<div class="form-check form-check-inline">
{{input type="checkbox" id="search-4" class="form-check-input" checked=matchFile}}
<label class="form-check-label" for="search-4">&nbsp;attachment name</label>
</div>
</div>
<div class="form-group">
<button class="btn btn-success" {{action 'onSearch'}}>Search</button>
</div>
</form>
{{search/search-results results=results keywords=filter}}
</div>
</div>

View file

@ -1,8 +1,7 @@
{{#toolbar/t-toolbar}}
{{#toolbar/t-links}}
{{#link-to "folders" class="link" tagName="li"}}Spaces{{/link-to}}
{{#link-to "folder" space.id space.slug class="link" tagName="li"}}{{space.name}}{{/link-to}}
{{#link-to "folder" space.id space.slug class="link selected" tagName="li"}}{{space.name}}{{/link-to}}
{{#if showDocumentLink}}
{{#link-to 'document.index' space.id space.slug document.id document.slug class="link"}}{{document.name}}{{/link-to}}
{{/if}}

View file

@ -1,7 +1,6 @@
{{#toolbar/t-toolbar}}
{{#toolbar/t-links}}
{{#link-to "folders" class="link" tagName="li"}}Spaces{{/link-to}}
{{#toolbar/t-links selectItem="spaces"}}
{{/toolbar/t-links}}
{{#toolbar/t-actions}}

View file

@ -1,6 +1,5 @@
{{#toolbar/t-toolbar}}
{{#toolbar/t-links}}
{{#link-to "folders" class="link selected" tagName="li"}}Spaces{{/link-to}}
{{#toolbar/t-links selectItem="spaces"}}
{{/toolbar/t-links}}
{{#toolbar/t-actions}}
{{#if session.isEditor}}

View file

@ -63,7 +63,8 @@
{{/if}}
{{/if}}
<a href="#" class="dropdown-item {{if hasWhatsNew 'color-whats-new font-weight-bold'}}" {{action 'onShowWhatsNewModal'}}>What's New</a>
<a href="#" class="dropdown-item" data-toggle="modal" data-target="#about-documize-modal" data-backdrop="static">About Documize</a>
<a href="https://docs.documize.com" target="_blank" class="dropdown-item">Help</a>
<a href="#" class="dropdown-item" data-toggle="modal" data-target="#about-documize-modal" data-backdrop="static">About</a>
{{#if enableLogout}}
<div class="dropdown-divider"></div>
{{#link-to 'auth.logout' class="dropdown-item" }}Logout{{/link-to}}
@ -140,7 +141,7 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>

View file

@ -1,5 +1,15 @@
<div class="toolbar">
<ul class="links">
{{#if (eq appMeta.edition 'Community')}}
{{#link-to "folders" class=(if (eq selectItem 'spaces') 'link selected' 'link') tagName="li"}}Spaces{{/link-to}}
{{/if}}
{{#if (eq appMeta.edition 'Enterprise')}}
{{#if session.isEditor}}
{{#link-to "dashboard" class=(if (eq selectItem 'dashboard') 'link selected' 'link') tagName="li"}}Actions{{/link-to}}
{{#link-to "analytics" class=(if (eq selectItem 'analytics') 'link selected' 'link') tagName="li"}}Insights{{/link-to}}
{{/if}}
{{#link-to "folders" class=(if (eq selectItem 'spaces') 'link selected' 'link') tagName="li"}}Spaces{{/link-to}}
{{/if}}
{{yield}}
</ul>
</div>

View file

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

View file

@ -16,14 +16,15 @@ import "github.com/documize/community/model"
// Account links a User to an Organization.
type Account struct {
model.BaseEntity
Admin bool `json:"admin"`
Editor bool `json:"editor"`
Users bool `json:"viewUsers"` // either view all users or just users in your space
UserID string `json:"userId"`
OrgID string `json:"orgId"`
Company string `json:"company"`
Title string `json:"title"`
Message string `json:"message"`
Domain string `json:"domain"`
Active bool `json:"active"`
Admin bool `json:"admin"`
Editor bool `json:"editor"`
Users bool `json:"viewUsers"` // either view all users or just users in your space
Analytics bool `json:"analytics"` // view content analytics
UserID string `json:"userId"`
OrgID string `json:"orgId"`
Company string `json:"company"`
Title string `json:"title"`
Message string `json:"message"`
Domain string `json:"domain"`
Active bool `json:"active"`
}

View file

@ -23,8 +23,11 @@ type UserActivity struct {
PageID string `json:"pageId"`
ActivityType Type `json:"activityType"`
SourceType SourceType `json:"sourceType"`
SourceName string `json:"sourceName"`
Metadata string `json:"metadata"`
Created time.Time `json:"created"`
// Read-only outbound fields (e.g. for UI display)
SourceName string `json:"sourceName"`
}
// DocumentActivity represents an activity taken against a document.
@ -39,6 +42,7 @@ type DocumentActivity struct {
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
ActivityType int `json:"activityType"`
Metadata string `json:"metadata"`
Created time.Time `json:"created"`
}
@ -57,50 +61,57 @@ const (
// SourceTypePage indicates activity against a document page.
SourceTypePage SourceType = 3
// SourceTypeSearch indicates activity on search page.
SourceTypeSearch SourceType = 4
)
const (
// TypeCreated records user document creation
// TypeCreated records user object creation (document or space).
TypeCreated Type = 1
// TypeRead states user has read document
// TypeRead states user has consumed object (document or space).
TypeRead Type = 2
// TypeEdited states user has editing document
// TypeEdited states user has editing document.
TypeEdited Type = 3
// TypeDeleted records user deleting space/document
// TypeDeleted records user deleting space/document.
TypeDeleted Type = 4
// TypeArchived records user archiving space/document
// TypeArchived records user archiving space/document.
TypeArchived Type = 5
// TypeApproved records user approval of document
// TypeApproved records user approval of document.
TypeApproved Type = 6
// TypeReverted records user content roll-back to previous version
// TypeReverted records user content roll-back to previous document version.
TypeReverted Type = 7
// TypePublishedTemplate records user creating new document template
// TypePublishedTemplate records user creating new document template.
TypePublishedTemplate Type = 8
// TypePublishedBlock records user creating reusable content block
// TypePublishedBlock records user creating reusable content block.
TypePublishedBlock Type = 9
// TypeCommented records user providing document feedback
// TypeCommented records user providing document feedback.
TypeCommented Type = 10
// TypeRejected records user rejecting document
// TypeRejected records user rejecting document.
TypeRejected Type = 11
// TypeSentSecureLink records user sending secure document link to email address(es)
// TypeSentSecureLink records user sending secure document link via email.
TypeSentSecureLink Type = 12
// TypeDraft records user marking space/document as draft
// TypeDraft records user marking space/document as draft.
TypeDraft Type = 13
// TypeVersioned records user creating new document version
// TypeVersioned records user creating new document version.
TypeVersioned Type = 14
// TypeSearched records user performing document keyword search.
// Metadata field should contain search terms.
TypeSearched Type = 15
)
// TypeName returns one-work descriptor for activity type
@ -130,6 +141,12 @@ func TypeName(t Type) string {
return "Reject"
case TypeSentSecureLink:
return "Share"
case TypeDraft:
return "Draft"
case TypeVersioned:
return "Version"
case TypeSearched:
return "Search"
}
return ""

View file

@ -18,6 +18,7 @@ type QueryOptions struct {
Tag bool `json:"tag"`
Attachment bool `json:"attachment"`
Content bool `json:"content"`
SkipLog bool `json:"slog"`
}
// QueryResult represents 'presentable' search results.

View file

@ -30,6 +30,7 @@ type User struct {
Editor bool `json:"editor"`
Admin bool `json:"admin"`
ViewUsers bool `json:"viewUsers"`
Analytics bool `json:"analytics"`
Global bool `json:"global"`
Password string `json:"-"`
Salt string `json:"-"`

View file

@ -141,6 +141,7 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http
rc.Administrator = false
rc.Editor = false
rc.Global = false
rc.Analytics = false
rc.AppURL = r.Host
rc.Subdomain = organization.GetSubdomainFromHost(r)
rc.SSL = r.TLS != nil
@ -170,6 +171,7 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http
rc.Administrator = u.Admin
rc.Editor = u.Editor
rc.Global = u.Global
rc.Analytics = u.Analytics
rc.Fullname = u.Fullname()
// We send back with every HTTP request/response cycle the latest
@ -179,12 +181,14 @@ func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http
Active bool `json:"active"`
Admin bool `json:"admin"`
Editor bool `json:"editor"`
Analytics bool `json:"analytics"`
ViewUsers bool `json:"viewUsers"`
}
state.Active = u.Active
state.Admin = u.Admin
state.Editor = u.Editor
state.Analytics = u.Analytics
state.ViewUsers = u.ViewUsers
sb, err := json.Marshal(state)
@ -234,6 +238,7 @@ func (m *middleware) preAuthorizeStaticAssets(rt *env.Runtime, r *http.Request)
ctx.OrgName = org.Title
ctx.Administrator = false
ctx.Editor = false
ctx.Analytics = false
ctx.Global = false
ctx.AppURL = r.Host
ctx.SSL = r.TLS != nil