1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-25 16:19:46 +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 ## 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 ## OS support
@ -69,7 +69,7 @@ Documize runs on the following:
Documize is built with the following technologies: Documize is built with the following technologies:
- EmberJS (v2.18.0) - EmberJS (v2.18.0)
- Go (v1.10) - Go (v1.10.1)
...and supports the following databases: ...and supports the following databases:
@ -77,7 +77,7 @@ Documize is built with the following technologies:
- Percona (v5.7.16-10+) - Percona (v5.7.16-10+)
- MariaDB (10.3.0+) - MariaDB (10.3.0+)
Coming soon, PostgreSQL and Microsoft SQL Server support. Coming soon, PostgreSQL and Microsoft SQL Server database support.
## Authentication options ## Authentication options

View file

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

View file

@ -44,6 +44,7 @@ CREATE TABLE IF NOT EXISTS `search` (
FULLTEXT INDEX `idx_search_content` (`content`)) FULLTEXT INDEX `idx_search_content` (`content`))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = MyISAM; ENGINE = MyISAM;
-- FULLTEXT search requires MyISAM and NOT InnoDB
-- migrate page content -- 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; 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), UNIQUE INDEX `idx_permission_id` (`id` ASC),
INDEX `idx_permission_orgid` (`orgid` ASC)) INDEX `idx_permission_orgid` (`orgid` ASC))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 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_1 ON permission(orgid,who,whoid,location);
CREATE INDEX idx_permission_2 ON permission(orgid,who,whoid,location,action); 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_refid` (`refid` ASC),
INDEX `idx_category_orgid` (`orgid` ASC)) INDEX `idx_category_orgid` (`orgid` ASC))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = MyISAM; ENGINE = InnoDB;
CREATE INDEX idx_category_1 ON category(orgid,labelid); 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), UNIQUE INDEX `idx_categorymember_id` (`id` ASC),
INDEX `idx_category_documentid` (`documentid`)) INDEX `idx_category_documentid` (`documentid`))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 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_1 ON categorymember(orgid,documentid);
CREATE INDEX idx_categorymember_2 ON categorymember(orgid,labelid); CREATE INDEX idx_categorymember_2 ON categorymember(orgid,labelid);
-- rolee represent user groups -- rolee represent user groups
DROP TABLE IF EXISTS `role`; DROP TABLE IF EXISTS `role`;
CREATE TABLE IF NOT 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_refid` (`refid` ASC),
INDEX `idx_category_orgid` (`orgid` ASC)) INDEX `idx_category_orgid` (`orgid` ASC))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
ENGINE = MyISAM; ENGINE = InnoDB;
-- role member records user role membership -- role member records user role membership
DROP TABLE IF EXISTS `rolemember`; DROP TABLE IF EXISTS `rolemember`;
@ -93,49 +93,49 @@ CREATE TABLE IF NOT EXISTS `rolemember` (
`userid` CHAR(16) NOT NULL COLLATE utf8_bin, `userid` CHAR(16) NOT NULL COLLATE utf8_bin,
UNIQUE INDEX `idx_category_id` (`id` ASC)) UNIQUE INDEX `idx_category_id` (`id` ASC))
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci 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_1 ON rolemember(roleid,userid);
CREATE INDEX idx_rolemember_2 ON rolemember(orgid,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 -- 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`; ALTER TABLE account ADD COLUMN `users` BOOL NOT NULL DEFAULT 1 AFTER `admin`;
-- migrate space/document permissions -- migrate space/document permissions
-- space own -- 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 SELECT orgid, 'user' as who, userid as whois, 'own' as `action`, 'object' as scope, 'space' as location, refid
FROM label; FROM label;
-- space manage (same as owner) -- 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 SELECT orgid, 'user' as who, userid as whois, 'manage' as `action`, 'object' as scope, 'space' as location, refid
FROM label; FROM label;
-- view space -- 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 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; FROM labelrole WHERE canview=1;
-- edit space => add/edit/delete/move/copy/template documents -- 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 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; 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 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; 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 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; 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 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; 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 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; 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 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; 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.Created = time.Now().UTC()
account.Revised = 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 (?, ?, ?, ?, ?, ?, ?, ?, ?)", _, 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.Active, account.Created, account.Revised) account.RefID, account.OrgID, account.UserID, account.Admin, account.Editor, account.Users, account.Analytics, account.Active, account.Created, account.Revised)
if err != nil { if err != nil {
err = errors.Wrap(err, "unable to execute insert for account") 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. // 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) { func (s Scope) GetUserAccount(ctx domain.RequestContext, userID string) (account account.Account, err error) {
err = s.Runtime.Db.Get(&account, ` 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 b.company, b.title, b.message, b.domain
FROM account a, organization b FROM account a, organization b
WHERE b.refid=a.orgid AND a.orgid=? AND a.userid=?`, ctx.OrgID, userID) 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. // 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) { func (s Scope) GetUserAccounts(ctx domain.RequestContext, userID string) (t []account.Account, err error) {
err = s.Runtime.Db.Select(&t, ` 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 b.company, b.title, b.message, b.domain
FROM account a, organization b FROM account a, organization b
WHERE a.userid=? AND a.orgid=b.refid AND a.active=1 ORDER BY b.title`, userID) 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. // 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) { func (s Scope) GetAccountsByOrg(ctx domain.RequestContext) (t []account.Account, err error) {
err = s.Runtime.Db.Select(&t, 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 b.company, b.title, b.message, b.domain
FROM account a, organization b FROM account a, organization b
WHERE a.orgid=b.refid AND a.orgid=? AND a.active=1`, ctx.OrgID) 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) { func (s Scope) UpdateAccount(ctx domain.RequestContext, account account.Account) (err error) {
account.Revised = time.Now().UTC() 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 { if err != sql.ErrNoRows && err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute update for account %s", account.RefID)) 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.UserID = ctx.UserID
activity.Created = time.Now().UTC() activity.Created = time.Now().UTC()
_, err = ctx.Transaction.Exec("INSERT INTO useractivity (orgid, userid, labelid, documentid, pageid, sourcetype, activitytype, created) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", _, 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.Created) activity.OrgID, activity.UserID, activity.LabelID, activity.DocumentID, activity.PageID, activity.SourceType, activity.ActivityType, activity.Metadata, activity.Created)
if err != nil { if err != nil {
err = errors.Wrap(err, "execute record user activity") 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. // GetDocumentActivity returns the metadata for a specified document.
func (s Scope) GetDocumentActivity(ctx domain.RequestContext, id string) (a []activity.DocumentActivity, err error) { 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(u.firstname, 'Anonymous') AS firstname, IFNULL(u.lastname, 'Viewer') AS lastname,
IFNULL(p.title, '') as pagetitle IFNULL(p.title, '') as pagetitle
FROM useractivity a FROM useractivity a

View file

@ -53,7 +53,7 @@ func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (c []catego
WHERE orgid=? AND labelid=? WHERE orgid=? AND labelid=?
AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='category' AND refid IN ( 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 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') 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) 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. // GetAllBySpace returns all space categories.
func (s Scope) GetAllBySpace(ctx domain.RequestContext, spaceID string) (c []category.Category, err error) { func (s Scope) GetAllBySpace(ctx domain.RequestContext, spaceID string) (c []category.Category, err error) {
c = []category.Category{}
err = s.Runtime.Db.Select(&c, ` err = s.Runtime.Db.Select(&c, `
SELECT id, refid, orgid, labelid, category, created, revised FROM category SELECT id, refid, orgid, labelid, category, created, revised FROM category
WHERE orgid=? AND labelid=? WHERE orgid=? AND labelid=?
AND labelid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN ( 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 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') 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) 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 err = nil
c = []category.Category{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select all categories for space %s", spaceID)) 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. // GetSpaceCategorySummary returns number of documents and users for space categories.
func (s Scope) GetSpaceCategorySummary(ctx domain.RequestContext, spaceID string) (c []category.SummaryModel, err error) { func (s Scope) GetSpaceCategorySummary(ctx domain.RequestContext, spaceID string) (c []category.SummaryModel, err error) {
c = []category.SummaryModel{}
err = s.Runtime.Db.Select(&c, ` err = s.Runtime.Db.Select(&c, `
SELECT 'documents' as type, categoryid, COUNT(*) as count SELECT 'documents' as type, categoryid, COUNT(*) as count
FROM categorymember FROM categorymember
@ -197,14 +200,13 @@ func (s Scope) GetSpaceCategorySummary(ctx domain.RequestContext, spaceID string
UNION ALL UNION ALL
SELECT 'users' as type, refid AS categoryid, count(*) AS count SELECT 'users' as type, refid AS categoryid, count(*) AS count
FROM permission FROM permission
WHERE orgid=? AND location='category' WHERE orgid=? AND location='category'
AND refid IN (SELECT refid FROM category WHERE orgid=? AND labelid=?) AND refid IN (SELECT refid FROM category WHERE orgid=? AND labelid=?)
GROUP BY refid, type`, GROUP BY refid, type`,
ctx.OrgID, spaceID, ctx.OrgID, ctx.OrgID, spaceID /*, ctx.OrgID, ctx.OrgID, spaceID*/) 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 err = nil
c = []category.SummaryModel{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("select category summary for space %s", spaceID)) 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. // GetDocumentCategoryMembership returns all space categories associated with given document.
func (s Scope) GetDocumentCategoryMembership(ctx domain.RequestContext, documentID string) (c []category.Category, err error) { func (s Scope) GetDocumentCategoryMembership(ctx domain.RequestContext, documentID string) (c []category.Category, err error) {
c = []category.Category{}
err = s.Runtime.Db.Select(&c, ` err = s.Runtime.Db.Select(&c, `
SELECT id, refid, orgid, labelid, category, created, revised FROM category 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) 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=? WHERE orgid=? AND labelid=?
AND labelid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN ( 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 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') 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) 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 Guest bool
Editor bool Editor bool
Global bool Global bool
Analytics bool
UserID string UserID string
OrgID string OrgID string
OrgName 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 return
} }

View file

@ -17,6 +17,7 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"sort" "sort"
"strings"
"github.com/documize/community/core/env" "github.com/documize/community/core/env"
"github.com/documize/community/core/request" "github.com/documize/community/core/request"
@ -72,15 +73,15 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
return 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 // draft mode does not record document views
if document.Lifecycle == workflow.LifecycleLive { 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{ err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
LabelID: document.LabelID, LabelID: document.LabelID,
DocumentID: document.RefID, DocumentID: document.RefID,
@ -91,9 +92,9 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
ctx.Transaction.Rollback() ctx.Transaction.Rollback()
h.Runtime.Log.Error(method, err) h.Runtime.Log.Error(method, err)
} }
}
ctx.Transaction.Commit() ctx.Transaction.Commit()
}
h.Store.Audit.Record(ctx, audit.EventTypeDocumentView) 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 { if oldDoc.LabelID != d.LabelID {
h.Store.Category.RemoveDocumentCategories(ctx, d.RefID) 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) err = h.Store.Document.Update(ctx, d)
@ -394,6 +402,8 @@ func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) {
return return
} }
options.Keywords = strings.TrimSpace(options.Keywords)
results, err := h.Store.Search.Documents(ctx, options) results, err := h.Store.Search.Documents(ctx, options)
if err != nil { if err != nil {
h.Runtime.Log.Error(method, err) 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 // Put in slugs for easy UI display of search URL
for key, result := range results { for key, result := range results {
result.DocumentSlug = stringutil.MakeSlug(result.Document) results[key].DocumentSlug = stringutil.MakeSlug(result.Document)
result.SpaceSlug = stringutil.MakeSlug(result.Space) results[key].SpaceSlug = stringutil.MakeSlug(result.Space)
results[key] = result
} }
if len(results) == 0 { // Record user search history
results = []search.QueryResult{} 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) 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) 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. // FetchDocumentData returns all document data in single API call.
func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) { func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
method := "document.FetchDocumentData" 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 // All versions of a document are returned, hence caller must
// decide what to do with them. // decide what to do with them.
func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (documents []doc.Document, err error) { func (s Scope) GetBySpace(ctx domain.RequestContext, spaceID string) (documents []doc.Document, err error) {
documents = []doc.Document{}
err = s.Runtime.Db.Select(&documents, ` err = s.Runtime.Db.Select(&documents, `
SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template, SELECT id, refid, orgid, labelid, userid, job, location, title, excerpt, slug, tags, template,
protection, approval, lifecycle, versioned, versionid, versionorder, groupid, created, revised 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) 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 err = nil
documents = []doc.Document{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, "select documents by space") 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=?", _, err = ctx.Transaction.Exec("UPDATE document SET labelid=? WHERE orgid=? AND labelid=?",
move, ctx.OrgID, id) move, ctx.OrgID, id)
if err == sql.ErrNoRows {
err = nil
}
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute document space move %s", id)) 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 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. // Delete removes the specified document.
// Remove document pages, revisions, attachments, updates the search subsystem. // Remove document pages, revisions, attachments, updates the search subsystem.
func (s Scope) Delete(ctx domain.RequestContext, documentID string) (rows int64, err error) { 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 // All versions of a document are returned, hence caller must
// decide what to do with them. // decide what to do with them.
func (s Scope) GetVersions(ctx domain.RequestContext, groupID string) (v []doc.Version, err error) { func (s Scope) GetVersions(ctx domain.RequestContext, groupID string) (v []doc.Version, err error) {
v = []doc.Version{}
err = s.Runtime.Db.Select(&v, ` err = s.Runtime.Db.Select(&v, `
SELECT versionid, refid as documentid SELECT versionid, refid as documentid
FROM document FROM document
WHERE orgid=? AND groupid=? WHERE orgid=? AND groupid=?
ORDER BY versionorder`, ctx.OrgID, groupID) ORDER BY versionorder`, ctx.OrgID, groupID)
if err == sql.ErrNoRows || len(v) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
v = []doc.Version{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, "document.store.GetVersions") 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. // GetAll returns all user groups for current orgID.
func (s Scope) GetAll(ctx domain.RequestContext) (groups []group.Group, err error) { func (s Scope) GetAll(ctx domain.RequestContext) (groups []group.Group, err error) {
groups = []group.Group{}
err = s.Runtime.Db.Select(&groups, 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 `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 FROM role a
@ -67,9 +69,8 @@ func (s Scope) GetAll(ctx domain.RequestContext) (groups []group.Group, err erro
ORDER BY a.role`, ORDER BY a.role`,
ctx.OrgID) ctx.OrgID)
if err == sql.ErrNoRows || len(groups) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
groups = []group.Group{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, "select groups") 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. // GetGroupMembers returns all user associated with given group.
func (s Scope) GetGroupMembers(ctx domain.RequestContext, groupID string) (members []group.Member, err error) { func (s Scope) GetGroupMembers(ctx domain.RequestContext, groupID string) (members []group.Member, err error) {
members = []group.Member{}
err = s.Runtime.Db.Select(&members, 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 IFNULL(b.firstname, '') as firstname, IFNULL(b.lastname, '') as lastname
FROM rolemember a FROM rolemember a
LEFT JOIN user b ON b.refid=a.userid 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`, ORDER BY b.firstname, b.lastname`,
ctx.OrgID, groupID) ctx.OrgID, groupID)
if err == sql.ErrNoRows || len(members) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
members = []group.Member{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, "select members") 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 // Useful when you need to bulk fetch membership records
// for subsequent processing. // for subsequent processing.
func (s Scope) GetMembers(ctx domain.RequestContext) (r []group.Record, err error) { func (s Scope) GetMembers(ctx domain.RequestContext) (r []group.Record, err error) {
r = []group.Record{}
err = s.Runtime.Db.Select(&r, err = s.Runtime.Db.Select(&r,
`SELECT a.id, a.orgid, a.roleid, a.userid, b.role as name, b.purpose `SELECT a.id, a.orgid, a.roleid, a.userid, b.role as name, b.purpose
FROM rolemember a, role b 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`, ORDER BY a.userid`,
ctx.OrgID) ctx.OrgID)
if err == sql.ErrNoRows || len(r) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
r = []group.Record{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, "select group members") err = errors.Wrap(err, "select group members")

View file

@ -1236,6 +1236,9 @@ func (h *Handler) FetchPages(w http.ResponseWriter, r *http.Request) {
return return
} }
// Who referred user this document (e.g. search page).
source := request.Query(r, "source")
doc, err := h.Store.Document.Get(ctx, documentID) doc, err := h.Store.Document.Get(ctx, documentID)
if err != nil { if err != nil {
response.WriteServerError(w, method, err) 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 // deliver payload
response.WriteJSON(w, model) 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 // Context is used to for userID because must match by userID
// or everyone ID of 0. // or everyone ID of 0.
func (s Scope) GetUserSpacePermissions(ctx domain.RequestContext, spaceID string) (r []permission.Permission, err error) { func (s Scope) GetUserSpacePermissions(ctx domain.RequestContext, spaceID string) (r []permission.Permission, err error) {
r = []permission.Permission{}
err = s.Runtime.Db.Select(&r, ` err = s.Runtime.Db.Select(&r, `
SELECT id, orgid, who, whoid, action, scope, location, refid SELECT id, orgid, who, whoid, action, scope, location, refid
FROM permission 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')`, 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) ctx.OrgID, spaceID, ctx.UserID, ctx.OrgID, spaceID, ctx.UserID)
if err == sql.ErrNoRows || len(r) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
r = []permission.Permission{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select user permissions %s", ctx.UserID)) 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. // GetSpacePermissions returns space permissions for all users.
// We do not filter by userID because we return 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) { func (s Scope) GetSpacePermissions(ctx domain.RequestContext, spaceID string) (r []permission.Permission, err error) {
r = []permission.Permission{}
err = s.Runtime.Db.Select(&r, ` err = s.Runtime.Db.Select(&r, `
SELECT id, orgid, who, whoid, action, scope, location, refid SELECT id, orgid, who, whoid, action, scope, location, refid
FROM permission WHERE orgid=? AND location='space' AND refid=? AND who='user' 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 { if err == sql.ErrNoRows {
err = nil err = nil
r = []permission.Permission{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select space permissions %s", ctx.UserID)) 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. // GetCategoryPermissions returns category permissions for all users.
func (s Scope) GetCategoryPermissions(ctx domain.RequestContext, catID string) (r []permission.Permission, err error) { func (s Scope) GetCategoryPermissions(ctx domain.RequestContext, catID string) (r []permission.Permission, err error) {
r = []permission.Permission{}
err = s.Runtime.Db.Select(&r, ` err = s.Runtime.Db.Select(&r, `
SELECT id, orgid, who, whoid, action, scope, location, refid SELECT id, orgid, who, whoid, action, scope, location, refid
FROM permission 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')`, WHERE p.orgid=? AND p.location='category' AND p.who='role' AND (p.refid=? OR p.refid='0')`,
ctx.OrgID, catID, ctx.OrgID, catID) ctx.OrgID, catID, ctx.OrgID, catID)
if err == sql.ErrNoRows || len(r) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
r = []permission.Permission{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select category permissions %s", catID)) 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. // GetCategoryUsers returns space permissions for all users.
func (s Scope) GetCategoryUsers(ctx domain.RequestContext, catID string) (u []user.User, err error) { func (s Scope) GetCategoryUsers(ctx domain.RequestContext, catID string) (u []user.User, err error) {
u = []user.User{}
err = s.Runtime.Db.Select(&u, ` 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 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 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`, ORDER BY firstname, lastname`,
ctx.OrgID, ctx.OrgID, catID, ctx.OrgID, catID) ctx.OrgID, ctx.OrgID, catID, ctx.OrgID, catID)
if err == sql.ErrNoRows || len(u) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
u = []user.User{} 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. // GetUserCategoryPermissions returns category permissions for given user.
func (s Scope) GetUserCategoryPermissions(ctx domain.RequestContext, userID string) (r []permission.Permission, err error) { func (s Scope) GetUserCategoryPermissions(ctx domain.RequestContext, userID string) (r []permission.Permission, err error) {
r = []permission.Permission{}
err = s.Runtime.Db.Select(&r, ` err = s.Runtime.Db.Select(&r, `
SELECT id, orgid, who, whoid, action, scope, location, refid SELECT id, orgid, who, whoid, action, scope, location, refid
FROM permission WHERE orgid=? AND location='category' AND who='user' AND (whoid=? OR whoid='0') 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')`, WHERE p.orgid=? AND p.location='category' AND p.who='role' AND (r.userid=? OR r.userid='0')`,
ctx.OrgID, userID, ctx.OrgID, userID) ctx.OrgID, userID, ctx.OrgID, userID)
if err == sql.ErrNoRows || len(r) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
r = []permission.Permission{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select category permissions for user %s", userID)) 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...) results = append(results, r4...)
} }
if len(results) == 0 {
results = []search.QueryResult{}
}
return 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 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 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)` 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.template = 0
AND d.labelid IN AND d.labelid IN
( (
SELECT refid FROM label WHERE orgid=? SELECT refid FROM label WHERE orgid=? AND refid IN
AND refid IN (SELECT refid FROM permission WHERE orgid=? AND location='space' AND refid IN ( (
SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space' AND action='view' SELECT refid from permission WHERE orgid=? AND who='user' AND (whoid=? OR whoid='0') AND location='space'
UNION ALL UNION ALL
SELECT p.refid from permission p LEFT JOIN rolemember r ON p.whoid=r.roleid WHERE p.orgid=? AND p.who='role' 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 ?` 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" method := "search.IndexDocument"
var err error var err error
ctx.Transaction, err = m.runtime.Db.Beginx() tx, err := m.runtime.Db.Beginx()
if err != nil { if err != nil {
m.runtime.Log.Error(method, err) m.runtime.Log.Error(method, err)
return 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) err = m.store.Search.IndexDocument(ctx, d, a)
if err != nil { if err != nil {
ctx.Transaction.Rollback() tx.Rollback()
m.runtime.Log.Error(method, err) m.runtime.Log.Error(method, err)
return return
} }
ctx.Transaction.Commit() tx.Commit()
} }
// DeleteDocument removes all search entries for document. // DeleteDocument removes all search entries for document.
@ -45,7 +45,7 @@ func (m *Indexer) DeleteDocument(ctx domain.RequestContext, ID string) {
method := "search.DeleteDocument" method := "search.DeleteDocument"
var err error var err error
ctx.Transaction, err = m.runtime.Db.Beginx() tx, err := m.runtime.Db.Beginx()
if err != nil { if err != nil {
m.runtime.Log.Error(method, err) m.runtime.Log.Error(method, err)
return return
@ -53,12 +53,12 @@ func (m *Indexer) DeleteDocument(ctx domain.RequestContext, ID string) {
err = m.store.Search.DeleteDocument(ctx, ID) err = m.store.Search.DeleteDocument(ctx, ID)
if err != nil { if err != nil {
ctx.Transaction.Rollback() tx.Rollback()
m.runtime.Log.Error(method, err) m.runtime.Log.Error(method, err)
return return
} }
ctx.Transaction.Commit() tx.Commit()
} }
// IndexContent adds search index entry for document context. // 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" method := "search.IndexContent"
var err error var err error
ctx.Transaction, err = m.runtime.Db.Beginx() tx, err := m.runtime.Db.Beginx()
if err != nil { if err != nil {
m.runtime.Log.Error(method, err) m.runtime.Log.Error(method, err)
return return
@ -75,12 +75,12 @@ func (m *Indexer) IndexContent(ctx domain.RequestContext, p page.Page) {
err = m.store.Search.IndexContent(ctx, p) err = m.store.Search.IndexContent(ctx, p)
if err != nil { if err != nil {
ctx.Transaction.Rollback() tx.Rollback()
m.runtime.Log.Error(method, err) m.runtime.Log.Error(method, err)
return return
} }
ctx.Transaction.Commit() tx.Commit()
} }
// DeleteContent removes all search entries for specific document content. // 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" method := "search.DeleteContent"
var err error var err error
ctx.Transaction, err = m.runtime.Db.Beginx() tx, err := m.runtime.Db.Beginx()
if err != nil { if err != nil {
m.runtime.Log.Error(method, err) m.runtime.Log.Error(method, err)
return return
@ -96,10 +96,10 @@ func (m *Indexer) DeleteContent(ctx domain.RequestContext, pageID string) {
err = m.store.Search.DeleteContent(ctx, pageID) err = m.store.Search.DeleteContent(ctx, pageID)
if err != nil { if err != nil {
ctx.Transaction.Rollback() tx.Rollback()
m.runtime.Log.Error(method, err) m.runtime.Log.Error(method, err)
return return
} }
ctx.Transaction.Commit() tx.Commit()
} }

View file

@ -33,6 +33,7 @@ import (
"github.com/documize/community/domain/mail" "github.com/documize/community/domain/mail"
"github.com/documize/community/domain/organization" "github.com/documize/community/domain/organization"
"github.com/documize/community/model/account" "github.com/documize/community/model/account"
"github.com/documize/community/model/activity"
"github.com/documize/community/model/audit" "github.com/documize/community/model/audit"
"github.com/documize/community/model/doc" "github.com/documize/community/model/doc"
"github.com/documize/community/model/page" "github.com/documize/community/model/page"
@ -127,6 +128,15 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
return 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() ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSpaceAdd) h.Store.Audit.Record(ctx, audit.EventTypeSpaceAdd)
@ -338,6 +348,25 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
return 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) response.WriteJSON(w, sp)
} }
@ -510,6 +539,15 @@ func (h *Handler) Remove(w http.ResponseWriter, r *http.Request) {
return 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() ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSpaceDelete) h.Store.Audit.Record(ctx, audit.EventTypeSpaceDelete)
@ -598,6 +636,15 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
return 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() ctx.Transaction.Commit()
h.Store.Audit.Record(ctx, audit.EventTypeSpaceDelete) 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) Delete(ctx RequestContext, documentID string) (rows int64, err error)
DeleteBySpace(ctx RequestContext, spaceID string) (rows int64, err error) DeleteBySpace(ctx RequestContext, spaceID string) (rows int64, err error)
GetVersions(ctx RequestContext, groupID string) (v []doc.Version, 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 // 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.Editor = true
a.Admin = false a.Admin = false
a.Active = true a.Active = true
a.Analytics = false
err = h.Store.Account.Add(ctx, a) err = h.Store.Account.Add(ctx, a)
if err != nil { if err != nil {
@ -481,6 +482,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
a.Admin = u.Admin a.Admin = u.Admin
a.Active = u.Active a.Active = u.Active
a.Users = u.ViewUsers a.Users = u.ViewUsers
a.Analytics = u.Analytics
err = h.Store.Account.UpdateAccount(ctx, a) err = h.Store.Account.UpdateAccount(ctx, a)
if err != nil { if err != nil {
@ -799,6 +801,7 @@ func (h *Handler) BulkImport(w http.ResponseWriter, r *http.Request) {
a.Editor = true a.Editor = true
a.Admin = false a.Admin = false
a.Active = true a.Active = true
a.Analytics = false
err = h.Store.Account.Add(ctx, a) err = h.Store.Account.Add(ctx, a)
if err != nil { 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 // GetActiveUsersForOrganization returns a slice containing of active user records for the organization
// identified in the Persister. // identified in the Persister.
func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []user.User, err error) { func (s Scope) GetActiveUsersForOrganization(ctx domain.RequestContext) (u []user.User, err error) {
u = []user.User{}
err = s.Runtime.Db.Select(&u, 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, `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 FROM user u, account a
WHERE u.refid=a.userid AND a.orgid=? AND a.active=1 WHERE u.refid=a.userid AND a.orgid=? AND a.active=1
ORDER BY u.firstname,u.lastname`, ORDER BY u.firstname,u.lastname`,
ctx.OrgID) ctx.OrgID)
if err == sql.ErrNoRows || len(u) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
u = []user.User{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("get active users by org %s", ctx.OrgID)) 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 // GetUsersForOrganization returns a slice containing all of the user records for the organizaiton
// identified in the Persister. // identified in the Persister.
func (s Scope) GetUsersForOrganization(ctx domain.RequestContext, filter string) (u []user.User, err error) { func (s Scope) GetUsersForOrganization(ctx domain.RequestContext, filter string) (u []user.User, err error) {
u = []user.User{}
filter = strings.TrimSpace(strings.ToLower(filter)) filter = strings.TrimSpace(strings.ToLower(filter))
likeQuery := "" likeQuery := ""
if len(filter) > 0 { if len(filter) > 0 {
@ -140,14 +143,13 @@ func (s Scope) GetUsersForOrganization(ctx domain.RequestContext, filter string)
err = s.Runtime.Db.Select(&u, 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, `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 FROM user u, account a
WHERE u.refid=a.userid AND a.orgid=? `+likeQuery+ WHERE u.refid=a.userid AND a.orgid=? `+likeQuery+
`ORDER BY u.firstname, u.lastname LIMIT 100`, ctx.OrgID) `ORDER BY u.firstname, u.lastname LIMIT 100`, ctx.OrgID)
if err == sql.ErrNoRows || len(u) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
u = []user.User{}
} }
if err != nil { 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. // 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) { func (s Scope) GetSpaceUsers(ctx domain.RequestContext, spaceID string) (u []user.User, err error) {
u = []user.User{}
err = s.Runtime.Db.Select(&u, ` 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, 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 FROM user u, account a
WHERE a.orgid=? AND u.refid = a.userid AND a.active=1 AND u.refid IN ( 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 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 ORDER BY u.firstname, u.lastname
`, ctx.OrgID, ctx.OrgID, spaceID, ctx.OrgID, spaceID) `, ctx.OrgID, ctx.OrgID, spaceID, ctx.OrgID, spaceID)
if err == sql.ErrNoRows || len(u) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
u = []user.User{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("get space users for org %s", ctx.OrgID)) 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. // GetUsersForSpaces returns users with access to specified spaces.
func (s Scope) GetUsersForSpaces(ctx domain.RequestContext, spaces []string) (u []user.User, err error) { func (s Scope) GetUsersForSpaces(ctx domain.RequestContext, spaces []string) (u []user.User, err error) {
u = []user.User{}
if len(spaces) == 0 { if len(spaces) == 0 {
u = []user.User{}
return return
} }
query, args, err := sqlx.In(` 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, 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 FROM user u, account a
WHERE a.orgid=? AND u.refid = a.userid AND a.active=1 AND u.refid IN ( 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 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) query = s.Runtime.Db.Rebind(query)
err = s.Runtime.Db.Select(&u, query, args...) err = s.Runtime.Db.Select(&u, query, args...)
if err == sql.ErrNoRows || len(u) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
u = []user.User{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("get users for spaces for user %s", ctx.UserID)) 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. // 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) { 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)) text = strings.TrimSpace(strings.ToLower(text))
likeQuery := "" likeQuery := ""
if len(text) > 0 { 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, 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, `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 FROM user u, account a
WHERE a.orgid=? AND u.refid=a.userid AND a.active=1 `+likeQuery+ WHERE a.orgid=? AND u.refid=a.userid AND a.active=1 `+likeQuery+
`ORDER BY u.firstname,u.lastname LIMIT `+strconv.Itoa(maxMatches), `ORDER BY u.firstname,u.lastname LIMIT `+strconv.Itoa(maxMatches),
ctx.OrgID) ctx.OrgID)
if err == sql.ErrNoRows || len(u) == 0 { if err == sql.ErrNoRows {
err = nil err = nil
u = []user.User{}
} }
if err != nil { if err != nil {
err = errors.Wrap(err, fmt.Sprintf("matching users for org %s", ctx.OrgID)) 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.Admin = false
u.Active = false u.Active = false
u.ViewUsers = false u.ViewUsers = false
u.Analytics = false
for _, account := range u.Accounts { for _, account := range u.Accounts {
if account.OrgID == orgID { if account.OrgID == orgID {
@ -47,6 +48,7 @@ func AttachUserAccounts(ctx domain.RequestContext, s domain.Store, orgID string,
u.Editor = account.Editor u.Editor = account.Editor
u.Active = account.Active u.Active = account.Active
u.ViewUsers = account.Users u.ViewUsers = account.Users
u.Analytics = account.Analytics
break break
} }
} }

View file

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

File diff suppressed because one or more lines are too long

View file

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

View file

@ -9,10 +9,14 @@
// //
// https://documize.com // https://documize.com
import { computed } from '@ember/object';
import Component from '@ember/component'; import Component from '@ember/component';
export default Component.extend({ export default Component.extend({
resultPhrase: '', resultPhrase: '',
searchQuery: computed('keywords', function() {
return encodeURIComponent(this.get('keywords'));
}),
didReceiveAttrs() { didReceiveAttrs() {
this._super(...arguments); 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 // https://documize.com
import { inject as service } from '@ember/service';
import Component from '@ember/component'; import Component from '@ember/component';
export default Component.extend({ 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 }), editor: attr('boolean', { defaultValue: false }),
admin: attr('boolean', { defaultValue: false }), admin: attr('boolean', { defaultValue: false }),
viewUsers: attr('boolean', { defaultValue: false }), viewUsers: attr('boolean', { defaultValue: false }),
analytics: attr('boolean', { defaultValue: false }),
global: attr('boolean', { defaultValue: false }), global: attr('boolean', { defaultValue: false }),
accounts: attr(), accounts: attr(),
groups: attr(), groups: attr(),

View file

@ -1,14 +1,17 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved. // 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 // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
// //
// You can operate outside the AGPL restrictions by purchasing // You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license // Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>. // by contacting <sales@documize.com>.
// //
// https://documize.com // https://documize.com
import { inject as service } from '@ember/service';
import Controller from '@ember/controller'; import Controller from '@ember/controller';
export default Controller.extend({}); export default Controller.extend({
appMeta: service()
});

View file

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

View file

@ -2,7 +2,6 @@
{{#toolbar/t-toolbar}} {{#toolbar/t-toolbar}}
{{#toolbar/t-links}} {{#toolbar/t-links}}
{{#link-to "folders" class="link" tagName="li" }}Spaces{{/link-to}}
{{/toolbar/t-links}} {{/toolbar/t-links}}
{{#toolbar/t-actions}} {{#toolbar/t-actions}}
{{/toolbar/t-actions}} {{/toolbar/t-actions}}
@ -10,7 +9,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col my-5"> <div class="col my-5 text-center">
<ul class="tabnav-control"> <ul class="tabnav-control">
{{#link-to 'customize.general' activeClass='selected' class="tab" tagName="li" }}General{{/link-to}} {{#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}} {{#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.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.auth' activeClass='selected' class="tab" tagName="li" }}Authentication{{/link-to}}
{{#link-to 'customize.search' activeClass='selected' class="tab" tagName="li" }}Search{{/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}} {{/if}}
{{#link-to 'customize.archive' activeClass='selected' class="tab" tagName="li" }}Archive{{/link-to}}
</ul> </ul>
</div> </div>
</div> </div>

View file

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

View file

@ -20,9 +20,13 @@ export default Route.extend(AuthenticatedRouteMixin, {
folderService: service('folder'), folderService: service('folder'),
userService: service('user'), 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) => { 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); this.set('pages', data);
resolve(); 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 permissions=permissions roles=roles tab=tab versions=versions
onDocumentDelete=(action 'onDocumentDelete') onDocumentDelete=(action 'onDocumentDelete')
onSaveTemplate=(action 'onSaveTemplate') onSaveTemplate=(action 'onSaveTemplate')

View file

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

View file

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

View file

@ -9,64 +9,14 @@
// //
// https://documize.com // https://documize.com
import { debounce } from '@ember/runloop';
import { inject as service } from '@ember/service';
import Controller from '@ember/controller'; import Controller from '@ember/controller';
export default Controller.extend({ export default Controller.extend({
searchService: service('search'), queryParams: ['filter', 'matchDoc', 'matchContent', 'matchTag', 'matchFile', 'slog'],
queryParams: ['filter', 'matchDoc', 'matchContent', 'matchTag', 'matchFile'],
filter: '', filter: '',
onKeywordChange: function () {
debounce(this, this.fetch, 750);
}.observes('filter'),
matchDoc: true, matchDoc: true,
onMatchDoc: function () {
debounce(this, this.fetch, 750);
}.observes('matchDoc'),
matchContent: true, matchContent: true,
onMatchContent: function () {
debounce(this, this.fetch, 750);
}.observes('matchContent'),
matchTag: false, matchTag: false,
onMatchTag: function () {
debounce(this, this.fetch, 750);
}.observes('matchTag'),
matchFile: false, matchFile: false,
onMatchFile: function () { slog: false,
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);
});
},
}); });

View file

@ -1,11 +1,11 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved. // 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 // GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
// //
// You can operate outside the AGPL restrictions by purchasing // You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license // Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>. // by contacting <sales@documize.com>.
// //
// https://documize.com // https://documize.com
@ -15,5 +15,5 @@ import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-rout
export default Route.extend(AuthenticatedRouteMixin, { export default Route.extend(AuthenticatedRouteMixin, {
activate() { activate() {
this.get('browser').setTitle('Search'); this.get('browser').setTitle('Search');
} }
}); });

View file

@ -2,35 +2,9 @@
{{#toolbar/t-toolbar}} {{#toolbar/t-toolbar}}
{{#toolbar/t-links}} {{#toolbar/t-links}}
{{#link-to "folders" class="link" tagName="li" }}Spaces{{/link-to}}
{{/toolbar/t-links}} {{/toolbar/t-links}}
{{#toolbar/t-actions}} {{#toolbar/t-actions}}
{{/toolbar/t-actions}} {{/toolbar/t-actions}}
{{/toolbar/t-toolbar}} {{/toolbar/t-toolbar}}
<div class="container"> {{search/search-view filter=filter matchDoc=matchDoc matchContent=matchContent matchTag=matchTag matchFile=matchFile slog=slog}}
<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>

View file

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

View file

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

View file

@ -375,7 +375,9 @@ export default Service.extend({
// This method bulk fetches data to reduce network chatter. // This method bulk fetches data to reduce network chatter.
// We produce a bunch of calculated boolean's for UI display purposes // We produce a bunch of calculated boolean's for UI display purposes
// that can tell us quickly about pending changes for UI display. // 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 constants = this.get('constants');
let changePending = false; let changePending = false;
let changeAwaitingReview = false; let changeAwaitingReview = false;
@ -384,7 +386,9 @@ export default Service.extend({
let userHasChangeAwaitingReview = false; let userHasChangeAwaitingReview = false;
let userHasChangeRejected = 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' method: 'GET'
}).then((response) => { }).then((response) => {
let data = A([]); let data = A([]);

View file

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

View file

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

View file

@ -1,5 +1,5 @@
<div class="view-customize my-5"> <div class="view-customize my-5">
<div class="my-2"> <div class="my-2">
<span class="font-weight-bold">Spaces</span> <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> <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="font-weight-bold">Admin</span>
<span class="text-muted">&nbsp;&nbsp;&mdash;&nbsp;can manage all aspects of Documize, like this screen</span> <span class="text-muted">&nbsp;&nbsp;&mdash;&nbsp;can manage all aspects of Documize, like this screen</span>
</div> </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"> <div class="mt-2 mb-4">
<span class="font-weight-bold">Active</span> <span class="font-weight-bold">Active</span>
<span class="text-muted">&nbsp;&nbsp;&mdash;&nbsp;can login and use Documize</span> <span class="text-muted">&nbsp;&nbsp;&mdash;&nbsp;can login and use Documize</span>
@ -28,11 +32,12 @@
<th class="text-muted"> <th class="text-muted">
{{#if hasSelectedUsers}} {{#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> <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>
<th class="no-width">Spaces</th> <th class="no-width">Spaces</th>
<th class="no-width">Visible</th> <th class="no-width">Visible</th>
<th class="no-width">Admin</th> <th class="no-width">Admin</th>
<th class="no-width">Analytics</th>
<th class="no-width">Active</th> <th class="no-width">Active</th>
<th class="no-width"> <th class="no-width">
</th> </th>
@ -87,6 +92,13 @@
<i class="material-icons checkbox" {{action 'toggleAdmin' user.id}}>check_box_outline_blank</i> <i class="material-icons checkbox" {{action 'toggleAdmin' user.id}}>check_box_outline_blank</i>
{{/if}} {{/if}}
</td> </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"> <td class="no-width text-center">
{{#if user.me}} {{#if user.me}}
<i class="material-icons color-gray">check_box</i> <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-3 heading">Tags</div>
<div class="col-12 col-sm-9 value"> <div class="col-12 col-sm-9 value">
{{#each tagz as |t index|}} {{#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}} {{concat '#' t}}
{{/link-to}}&nbsp;&nbsp; {{/link-to}}&nbsp;&nbsp;
{{/each}} {{/each}}

View file

@ -3,7 +3,7 @@
<ul class="documents"> <ul class="documents">
{{#each documents key="id" as |result index|}} {{#each documents key="id" as |result index|}}
<li class="document"> <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"> <div class="title">
{{result.document}} {{result.document}}
{{#if (gt result.versionId.length 0)}} {{#if (gt result.versionId.length 0)}}
@ -16,7 +16,7 @@
{{#if result.template}} {{#if result.template}}
<button type="button" class="mt-3 btn btn-warning text-uppercase font-weight-bold">TEMPLATE</button> <button type="button" class="mt-3 btn btn-warning text-uppercase font-weight-bold">TEMPLATE</button>
{{/if}} {{/if}}
</a> {{/link-to}}
</li> </li>
{{/each}} {{/each}}
</ul> </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-toolbar}}
{{#toolbar/t-links}} {{#toolbar/t-links}}
{{#link-to "folders" class="link" tagName="li"}}Spaces{{/link-to}} {{#link-to "folder" space.id space.slug class="link selected" tagName="li"}}{{space.name}}{{/link-to}}
{{#link-to "folder" space.id space.slug class="link" tagName="li"}}{{space.name}}{{/link-to}}
{{#if showDocumentLink}} {{#if showDocumentLink}}
{{#link-to 'document.index' space.id space.slug document.id document.slug class="link"}}{{document.name}}{{/link-to}} {{#link-to 'document.index' space.id space.slug document.id document.slug class="link"}}{{document.name}}{{/link-to}}
{{/if}} {{/if}}

View file

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

View file

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

View file

@ -63,7 +63,8 @@
{{/if}} {{/if}}
{{/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 {{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}} {{#if enableLogout}}
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
{{#link-to 'auth.logout' class="dropdown-item" }}Logout{{/link-to}} {{#link-to 'auth.logout' class="dropdown-item" }}Logout{{/link-to}}
@ -140,7 +141,7 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <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> </div>
</div> </div>

View file

@ -1,5 +1,15 @@
<div class="toolbar"> <div class="toolbar">
<ul class="links"> <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}} {{yield}}
</ul> </ul>
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

@ -30,6 +30,7 @@ type User struct {
Editor bool `json:"editor"` Editor bool `json:"editor"`
Admin bool `json:"admin"` Admin bool `json:"admin"`
ViewUsers bool `json:"viewUsers"` ViewUsers bool `json:"viewUsers"`
Analytics bool `json:"analytics"`
Global bool `json:"global"` Global bool `json:"global"`
Password string `json:"-"` Password string `json:"-"`
Salt 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.Administrator = false
rc.Editor = false rc.Editor = false
rc.Global = false rc.Global = false
rc.Analytics = false
rc.AppURL = r.Host rc.AppURL = r.Host
rc.Subdomain = organization.GetSubdomainFromHost(r) rc.Subdomain = organization.GetSubdomainFromHost(r)
rc.SSL = r.TLS != nil 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.Administrator = u.Admin
rc.Editor = u.Editor rc.Editor = u.Editor
rc.Global = u.Global rc.Global = u.Global
rc.Analytics = u.Analytics
rc.Fullname = u.Fullname() rc.Fullname = u.Fullname()
// We send back with every HTTP request/response cycle the latest // 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"` Active bool `json:"active"`
Admin bool `json:"admin"` Admin bool `json:"admin"`
Editor bool `json:"editor"` Editor bool `json:"editor"`
Analytics bool `json:"analytics"`
ViewUsers bool `json:"viewUsers"` ViewUsers bool `json:"viewUsers"`
} }
state.Active = u.Active state.Active = u.Active
state.Admin = u.Admin state.Admin = u.Admin
state.Editor = u.Editor state.Editor = u.Editor
state.Analytics = u.Analytics
state.ViewUsers = u.ViewUsers state.ViewUsers = u.ViewUsers
sb, err := json.Marshal(state) sb, err := json.Marshal(state)
@ -234,6 +238,7 @@ func (m *middleware) preAuthorizeStaticAssets(rt *env.Runtime, r *http.Request)
ctx.OrgName = org.Title ctx.OrgName = org.Title
ctx.Administrator = false ctx.Administrator = false
ctx.Editor = false ctx.Editor = false
ctx.Analytics = false
ctx.Global = false ctx.Global = false
ctx.AppURL = r.Host ctx.AppURL = r.Host
ctx.SSL = r.TLS != nil ctx.SSL = r.TLS != nil