Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize project activity storage #1220

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions grails-app/conf/UserPositionUrlMappings.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ class UserPositionUrlMappings {
action = [POST:"add"]
}

"/api/imageinstance/$image/position.$format" (controller : "restUserPosition") {
action = [GET:"list"]
}

//Deprecated
"/api/imageinstance/$image/positions.$format" (controller : "restUserPosition") {
action = [GET:"list"]
}

"/api/imageinstance/$id/online.$format"(controller: "restUserPosition"){
action = [GET:"listOnlineUsersByImage"]
}
Expand Down
12 changes: 12 additions & 0 deletions grails-app/domain/be/cytomine/project/ProjectLastActivity.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package be.cytomine.project

class ProjectLastActivity {

Project project

Date lastActivity

static constraints = {
project(unique: true)
}
}
13 changes: 5 additions & 8 deletions grails-app/services/be/cytomine/project/ProjectService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,8 @@ class ProjectService extends ModelService {


if(extended.withLastActivity) {
select += ", activities.max_date "
from += "LEFT OUTER JOIN " +
"( SELECT project_id, MAX(created) max_date " +
" FROM command_history " +
" GROUP BY project_id " +
") activities ON p.id = activities.project_id "
select += ", activities.last_activity "
from += "LEFT OUTER JOIN project_last_activity activities ON p.id = activities.project_id "
}
if(extended.withMembersCount) {
select += ", members.member_count "
Expand Down Expand Up @@ -295,7 +291,7 @@ class ProjectService extends ModelService {
if(extended.withMembersCount) sortColumn="members.member_count"
break
case "lastActivity" :
if(extended.withLastActivity) sortColumn="activities.max_date"
if(extended.withLastActivity) sortColumn="activities.last_activity"
break
case "name":
case "numberOfImages":
Expand Down Expand Up @@ -345,7 +341,7 @@ class ProjectService extends ModelService {
def line = Project.getDataFromDomain(map)

if(extended.withLastActivity) {
line.putAt("lastActivity", map.maxDate)
line.putAt("lastActivity", map.lastActivity)
}
if(extended.withMembersCount) {
if(!map.memberCount) map.memberCount = 0
Expand Down Expand Up @@ -767,6 +763,7 @@ class ProjectService extends ModelService {
}

protected def beforeDelete(Project domain) {
ProjectLastActivity.findByProject(domain).delete()
CommandHistory.findAllByProject(domain).each { it.delete() }
Command.findAllByProject(domain).each {
it
Expand Down
12 changes: 8 additions & 4 deletions grails-app/services/be/cytomine/security/SecUserService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -827,15 +827,19 @@ class SecUserService extends ModelService {
}
if (project) {
log.info "deleteUserFromProject project=" + project?.id + " username=" + user?.username + " ADMIN=" + admin
if(project.ontology) {
removeOntologyRightIfNecessary(project, user, admin)
}

if(admin) {
permissionService.deletePermission(project, user.username, ADMINISTRATION)
}
else {
permissionService.deletePermission(project, user.username, READ)
}

if(!project.hasACLPermission(user, READ) && !project.hasACLPermission(user, ADMINISTRATION) && project.ontology) {
removeOntologyRightIfNecessary(project, (User)user, admin)
}


ProjectRepresentativeUser representative = ProjectRepresentativeUser.findByUserAndProject(user, project)
if(representative) {
projectRepresentativeUserService.delete(representative)
Expand All @@ -845,7 +849,7 @@ class SecUserService extends ModelService {
if (projectRepresentativeUserService.listByProject(project).size()==0) {
if (!securityACLService.getProjectList(cytomineService.currentUser).contains(project)) {
// if current user is not in project (= SUPERADMIN), add to the project
addUserToProject(user, project, true)
addUserToProject(cytomineService.currentUser, project, true)
}
log.info("add current user ${cytomineService.currentUser.id} as representative for project ${project.id}")
def json = JSON.parse(new ProjectRepresentativeUser(project:project, user:cytomineService.currentUser).encodeAsJSON());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import be.cytomine.image.server.Storage
import be.cytomine.image.UploadedFile
import be.cytomine.processing.ImageFilter
import be.cytomine.project.Project
import be.cytomine.project.ProjectLastActivity
import be.cytomine.security.SecRole
import be.cytomine.security.SecUser
import be.cytomine.security.SecUserSecRole
Expand Down Expand Up @@ -111,6 +112,24 @@ class BootstrapOldVersionService {
sql.close()

bootstrapDataService.initImageFilters()

if (ProjectLastActivity.count() == 0) {
log.info "Projects: Populate project last activity table"
def values = []
sql.eachRow("SELECT project_id, MAX(created) AS date FROM command_history WHERE project_id IS NOT NULL GROUP BY project_id") {
values << [id: "nextval('hibernate_sequence')", version: 0, project_id: it.project_id, last_activity: "'${it.date}'"]
}

if (values.size() > 0) {
def batchSize = 100
def fields = ["id", "version", "project_id", "last_activity"]
def groups = values.collate(batchSize)
groups.eachWithIndex { def vals, int i ->
def formatted = vals.collect { v -> "(" + fields.collect { f -> v[f] }.join(",") + ")"}
sql.execute('INSERT INTO project_last_activity (' + fields.join(",") +') VALUES ' + formatted.join(",") + " ON CONFLICT DO NOTHING;")
}
}
}
}

def initv3_2_0() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class TriggerService {

def statement = connection.createStatement()

statement.execute(getProjectCommandHistoryTriggerAfterInsert())

statement.execute(getUserAnnotationTriggerBeforeInsert())
statement.execute(getUserAnnotationTriggerAfterInsert())
statement.execute(getUserAnnotationTriggerAfterUpdate())
Expand Down Expand Up @@ -83,6 +85,35 @@ class TriggerService {
}
}

String getProjectCommandHistoryTriggerAfterInsert() {
String createFunction = """
CREATE OR REPLACE FUNCTION afterInsertCommandHistory() RETURNS TRIGGER AS \$updateLastActivity\$
DECLARE
alreadyExists INTEGER;
BEGIN
IF (NEW.project_id IS NOT NULL) THEN
SELECT count(*) INTO alreadyExists FROM project_last_activity WHERE project_id = NEW.project_id;
IF (alreadyExists = 0) THEN
INSERT INTO project_last_activity(id, version, project_id, last_activity) VALUES(nextval('hibernate_sequence'), 0, NEW.project_id, NEW.created);
END IF;
UPDATE project_last_activity SET last_activity = NEW.created, version = version + 1 WHERE project_id = NEW.project_id;
END IF;

RETURN NEW;
END;
\$updateLastActivity\$ LANGUAGE plpgsql;
"""

String dropTrigger = "DROP TRIGGER IF EXISTS afterInsertCommandHistoryTrigger ON command_history;"

String createTrigger = "CREATE TRIGGER afterInsertCommandHistoryTrigger AFTER INSERT ON command_history FOR EACH ROW EXECUTE PROCEDURE afterInsertCommandHistory();"

log.debug createFunction
log.debug dropTrigger
log.debug createTrigger
return createFunction + dropTrigger + createTrigger
}

String getUserAnnotationTriggerBeforeInsert() {
String createFunction = """
CREATE OR REPLACE FUNCTION beforeInsertUserAnnotation() RETURNS TRIGGER AS \$incUserAnnBefore\$
Expand Down
6 changes: 6 additions & 0 deletions src/groovy/be/cytomine/CytomineDomain.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package be.cytomine
*/

import be.cytomine.security.SecUser
import be.cytomine.security.User
import grails.converters.JSON
import grails.util.Holders
import groovy.sql.Sql
Expand Down Expand Up @@ -189,6 +190,11 @@ abstract class CytomineDomain implements Comparable,Serializable{
}


boolean hasACLPermission(User user, Permission permission) {
def masks = getPermissionInACL(this,user)
return masks.max() >= permission.mask
}

List getPermissionInACL(def domain, def user = null) {
try {
String request = "SELECT mask FROM acl_object_identity aoi, acl_sid sid, acl_entry ae " +
Expand Down